Skip to main content

saola_psl_core/configuration/
datasource.rs

1use schema_ast::ast::WithSpan;
2use serde::Deserialize;
3
4use crate::{
5    configuration::StringFromEnvVar,
6    datamodel_connector::{Connector, ConnectorCapabilities, RelationMode},
7    diagnostics::{DatamodelError, Diagnostics, Span},
8    set_config_dir,
9};
10use std::{any::Any, borrow::Cow, path::Path, sync::Arc};
11
12#[derive(Debug, Clone, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct DatasourceUrls {
15    pub url: String,
16    pub shadow_database_url: Option<String>,
17    pub direct_url: Option<String>,
18}
19
20/// a `datasource` from the prisma schema.
21#[derive(Clone)]
22pub struct Datasource {
23    pub name: String,
24    /// Span of the whole datasource block (including `datasource` keyword and braces)
25    pub span: Span,
26    /// The provider string
27    pub provider: String,
28    /// The provider that was selected as active from all specified providers
29    pub active_provider: &'static str,
30    pub documentation: Option<String>,
31    /// the connector of the active provider
32    pub active_connector: &'static dyn Connector,
33    /// In which layer referential actions are handled.
34    pub relation_mode: Option<RelationMode>,
35    /// _Sorted_ vec of schemas defined in the schemas property.
36    pub namespaces: Vec<(String, Span)>,
37    pub schemas_span: Option<Span>,
38    pub connector_data: DatasourceConnectorData,
39
40    /// URL-related fields will no longer be parsed from Prisma Schemas in
41    /// Prisma 7.0.0.
42    pub url: StringFromEnvVar,
43    pub url_span: Span,
44    pub direct_url: Option<StringFromEnvVar>,
45    pub direct_url_span: Option<Span>,
46    /// An optional user-defined shadow database URL.
47    pub shadow_database_url: Option<(StringFromEnvVar, Span)>,
48}
49
50pub enum UrlValidationError {
51    EmptyUrlValue,
52    EmptyEnvValue(String),
53    NoEnvValue(String),
54    NoUrlOrEnv,
55}
56
57#[derive(Clone, Default)]
58pub struct DatasourceConnectorData {
59    data: Option<Arc<dyn Any + Send + Sync + 'static>>,
60}
61
62impl DatasourceConnectorData {
63    pub fn new(data: Arc<dyn Any + Send + Sync + 'static>) -> Self {
64        Self { data: Some(data) }
65    }
66
67    #[track_caller]
68    pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
69        self.data.as_ref().map(|data| data.downcast_ref().unwrap())
70    }
71}
72
73impl std::fmt::Debug for Datasource {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.debug_struct("Datasource")
76            .field("name", &self.name)
77            .field("provider", &self.provider)
78            .field("active_provider", &self.active_provider)
79            .field("url", &"<url>")
80            .field("documentation", &self.documentation)
81            .field("active_connector", &&"...")
82            .field("shadow_database_url", &"<shadow_database_url>")
83            .field("relation_mode", &self.relation_mode)
84            .field("namespaces", &self.namespaces)
85            .finish()
86    }
87}
88
89impl Datasource {
90    pub fn override_urls(&mut self, datasource_urls_override: DatasourceUrls) {
91        self.url = StringFromEnvVar {
92            value: Some(datasource_urls_override.url),
93            from_env_var: None,
94        };
95        self.direct_url = datasource_urls_override.direct_url.map(|url| StringFromEnvVar {
96            value: Some(url),
97            from_env_var: None,
98        });
99        self.shadow_database_url = datasource_urls_override.shadow_database_url.map(|url| {
100            (
101                StringFromEnvVar {
102                    value: Some(url),
103                    from_env_var: None,
104                },
105                self.url_span,
106            )
107        });
108    }
109
110    /// Extract connector-specific constructs. The type parameter must be the right one.
111    #[track_caller]
112    pub fn downcast_connector_data<T: 'static>(&self) -> Option<&T> {
113        self.connector_data.downcast_ref()
114    }
115
116    pub(crate) fn has_schema(&self, name: &str) -> bool {
117        self.namespaces.binary_search_by_key(&name, |(s, _)| s).is_ok()
118    }
119
120    pub fn capabilities(&self) -> ConnectorCapabilities {
121        self.active_connector.capabilities()
122    }
123
124    /// The applicable relation mode for this datasource.
125    #[allow(clippy::or_fun_call)] // not applicable in this case
126    pub fn relation_mode(&self) -> RelationMode {
127        self.relation_mode
128            .unwrap_or(self.active_connector.default_relation_mode())
129    }
130
131    /// Load the database URL, validating it and resolving env vars in the
132    /// process. Also see `load_url_with_config_dir()`.
133    pub fn load_url<F>(&self, env: F) -> Result<String, Diagnostics>
134    where
135        F: Fn(&str) -> Option<String>,
136    {
137        let url = self.load_url_no_validation(env)?;
138
139        self.active_connector.validate_url(&url).map_err(|err_str| {
140            let err_str = if url.starts_with("prisma") {
141                let s = indoc::formatdoc! {"
142                    {err_str}
143
144                    To use a URL with protocol `prisma://`, you need to either enable Accelerate or the Data Proxy.
145                    Enable Accelerate via `prisma generate --accelerate` or the Data Proxy via `prisma generate --data-proxy.`
146
147                    More information about Data Proxy: https://pris.ly/d/data-proxy
148                "};
149
150                Cow::from(s)
151            } else {
152                Cow::from(err_str)
153            };
154
155            DatamodelError::new_source_validation_error(&format!("the URL {}", &err_str), &self.name, self.url_span)
156        })?;
157
158        Ok(url)
159    }
160
161    /// Load the database URL, without validating it and resolve env vars in the
162    /// process.
163    pub fn load_url_no_validation<F>(&self, env: F) -> Result<String, Diagnostics>
164    where
165        F: Fn(&str) -> Option<String>,
166    {
167        from_url(&self.url, env).map_err(|err| match err {
168            UrlValidationError::EmptyUrlValue => {
169                let msg = "You must provide a nonempty URL";
170                DatamodelError::new_source_validation_error(msg, &self.name, self.url_span).into()
171            }
172            UrlValidationError::EmptyEnvValue(env_var) => DatamodelError::new_source_validation_error(
173                &format!(
174                    "You must provide a nonempty URL. The environment variable `{env_var}` resolved to an empty string."
175                ),
176                &self.name,
177                self.url_span,
178            )
179            .into(),
180            UrlValidationError::NoEnvValue(env_var) => {
181                DatamodelError::new_environment_functional_evaluation_error(env_var, self.url_span).into()
182            }
183            UrlValidationError::NoUrlOrEnv => unreachable!("Missing url in datasource"),
184        })
185    }
186
187    /// Load the direct database URL, validating it and resolving env vars in the
188    /// process. If there is no `directUrl` passed, it will default to `load_url()`.
189    ///
190    pub fn load_direct_url<F>(&self, env: F) -> Result<String, Diagnostics>
191    where
192        F: Fn(&str) -> Option<String>,
193    {
194        let validate_direct_url = |(url, span)| {
195            let handle_err = |err| match err {
196                UrlValidationError::EmptyUrlValue => {
197                    let msg = "You must provide a nonempty direct URL";
198                    Err(DatamodelError::new_source_validation_error(msg, &self.name, span).into())
199                }
200                UrlValidationError::EmptyEnvValue(env_var) => {
201                    let msg = format!(
202                        "You must provide a nonempty direct URL. The environment variable `{env_var}` resolved to an empty string."
203                    );
204
205                    Err(DatamodelError::new_source_validation_error(&msg, &self.name, span).into())
206                }
207                UrlValidationError::NoEnvValue(env_var) => {
208                    let e = DatamodelError::new_environment_functional_evaluation_error(env_var, span);
209                    Err(e.into())
210                }
211                UrlValidationError::NoUrlOrEnv => self.load_url(&env),
212            };
213
214            let url = from_url(&url, &env).map_or_else(handle_err, Result::Ok)?;
215
216            if url.starts_with("prisma://") {
217                let msg = "You must provide a direct URL that points directly to the database. Using `prisma` in URL scheme is not allowed.";
218                let e = DatamodelError::new_source_validation_error(msg, &self.name, span);
219
220                Err(e.into())
221            } else {
222                Ok(url)
223            }
224        };
225
226        self.direct_url
227            .clone()
228            .and_then(|url| self.direct_url_span.map(|span| (url, span)))
229            .map_or_else(|| self.load_url(&env), validate_direct_url)
230    }
231
232    /// Same as `load_url()`, with the following difference.
233    ///
234    /// By default we treat relative paths (in the connection string and
235    /// datasource url value) as relative to the CWD. This does not work in all
236    /// cases, so we need a way to prefix these relative paths with a
237    /// config_dir.
238    ///
239    /// This is, at the time of this writing (2021-05-05), only used in the
240    /// context of Node-API integration.
241    ///
242    /// P.S. Don't forget to add new parameters here if needed!
243    pub fn load_url_with_config_dir<F>(&self, config_dir: &Path, env: F) -> Result<String, Diagnostics>
244    where
245        F: Fn(&str) -> Option<String>,
246    {
247        //CHECKUP
248        let url = self.load_url(env)?;
249        let url = set_config_dir(self.active_connector.flavour(), config_dir, &url);
250
251        Ok(url.into_owned())
252    }
253
254    /// Load the shadow database URL, validating it and resolving env vars in the process.
255    pub fn load_shadow_database_url(&self) -> Result<Option<String>, Diagnostics> {
256        let (url, url_span) = match self
257            .shadow_database_url
258            .as_ref()
259            .map(|(url, span)| (&url.value, &url.from_env_var, span))
260        {
261            None => return Ok(None),
262            Some((Some(lit), _, span)) => (lit.clone(), span),
263            Some((None, Some(env_var), span)) => match std::env::var(env_var) {
264                // We explicitly ignore empty and missing env vars, because the same schema (with the same env function) has to be usable for dev and deployment alike.
265                Ok(var) if var.trim().is_empty() => return Ok(None),
266                Err(_) => return Ok(None),
267
268                Ok(var) => (var, span),
269            },
270            Some((None, None, _span)) => unreachable!("Missing url in datasource"),
271        };
272
273        if !url.trim().is_empty() {
274            self.active_connector.validate_url(&url).map_err(|err_str| {
275                DatamodelError::new_source_validation_error(
276                    &format!("the shadow database URL {}", &err_str),
277                    &self.name,
278                    *url_span,
279                )
280            })?;
281        }
282
283        Ok(Some(url))
284    }
285
286    // Validation for property existence
287    pub fn provider_defined(&self) -> bool {
288        !self.provider.is_empty()
289    }
290
291    pub fn url_defined(&self) -> bool {
292        self.url_span.end > self.url_span.start
293    }
294
295    pub fn direct_url_defined(&self) -> bool {
296        self.direct_url.is_some()
297    }
298
299    pub fn shadow_url_defined(&self) -> bool {
300        self.shadow_database_url.is_some()
301    }
302
303    pub fn relation_mode_defined(&self) -> bool {
304        self.relation_mode.is_some()
305    }
306
307    pub fn schemas_defined(&self) -> bool {
308        self.schemas_span.is_some()
309    }
310}
311
312impl WithSpan for Datasource {
313    fn span(&self) -> Span {
314        self.span
315    }
316}
317
318pub(crate) fn from_url<F>(url: &StringFromEnvVar, env: F) -> Result<String, UrlValidationError>
319where
320    F: Fn(&str) -> Option<String>,
321{
322    let url = match (&url.value, &url.from_env_var) {
323        (Some(lit), _) if lit.trim().is_empty() => {
324            return Err(UrlValidationError::EmptyUrlValue);
325        }
326        (Some(lit), _) => lit.clone(),
327        (None, Some(env_var)) => match env(env_var) {
328            Some(var) if var.trim().is_empty() => {
329                return Err(UrlValidationError::EmptyEnvValue(env_var.clone()));
330            }
331            Some(var) => var,
332            None => return Err(UrlValidationError::NoEnvValue(env_var.clone())),
333        },
334        (None, None) => return Err(UrlValidationError::NoUrlOrEnv),
335    };
336
337    Ok(url)
338}