fraiseql_cli/config/
mod.rs1pub mod runtime;
7pub mod security;
8pub mod toml_schema;
9
10use std::path::Path;
11
12use anyhow::{Context, Result};
13use fraiseql_core::schema::NamingConvention;
14pub use runtime::{DatabaseRuntimeConfig, ServerRuntimeConfig};
15pub use security::SecurityConfig;
16use serde::{Deserialize, Serialize};
17pub use toml_schema::TomlSchema;
18use tracing::info;
19
20#[derive(Debug, Clone, Default, Deserialize, Serialize)]
22#[serde(default, deny_unknown_fields)]
23pub struct TomlProjectConfig {
24 #[serde(rename = "project")]
26 pub project: ProjectConfig,
27
28 #[serde(rename = "fraiseql")]
30 pub fraiseql: FraiseQLSettings,
31
32 #[serde(default)]
34 pub server: ServerRuntimeConfig,
35
36 #[serde(default)]
38 pub database: DatabaseRuntimeConfig,
39}
40
41#[derive(Debug, Clone, Deserialize, Serialize)]
43#[serde(default, deny_unknown_fields)]
44pub struct ProjectConfig {
45 pub name: String,
47 pub version: String,
49 pub description: Option<String>,
51 pub database_target: Option<String>,
53}
54
55impl Default for ProjectConfig {
56 fn default() -> Self {
57 Self {
58 name: "my-fraiseql-app".to_string(),
59 version: "1.0.0".to_string(),
60 description: None,
61 database_target: None,
62 }
63 }
64}
65
66#[derive(Debug, Clone, Deserialize, Serialize)]
68#[serde(default, deny_unknown_fields)]
69pub struct FraiseQLSettings {
70 pub schema_file: String,
72 pub output_file: String,
74 #[serde(rename = "security")]
76 pub security: SecurityConfig,
77 #[serde(default)]
79 pub tenancy: security::TenancyTomlConfig,
80 #[serde(default)]
82 pub mutations: MutationsConfig,
83 #[serde(default)]
85 pub naming: NamingTomlConfig,
86 #[serde(default)]
90 pub cost_weights: std::collections::HashMap<String, usize>,
91}
92
93impl Default for FraiseQLSettings {
94 fn default() -> Self {
95 Self {
96 schema_file: "schema.json".to_string(),
97 output_file: "schema.compiled.json".to_string(),
98 security: SecurityConfig::default(),
99 tenancy: security::TenancyTomlConfig::default(),
100 mutations: MutationsConfig::default(),
101 naming: NamingTomlConfig::default(),
102 cost_weights: std::collections::HashMap::new(),
103 }
104 }
105}
106
107#[derive(Debug, Clone, Deserialize, Serialize)]
109#[serde(default, deny_unknown_fields)]
110pub struct NamingTomlConfig {
111 pub convention: NamingConvention,
120 pub acronyms: Vec<String>,
125}
126
127impl Default for NamingTomlConfig {
128 fn default() -> Self {
129 Self {
130 convention: NamingConvention::CamelCase,
133 acronyms: Vec::new(),
134 }
135 }
136}
137
138#[derive(Debug, Clone, Default, Deserialize, Serialize)]
140#[serde(default, deny_unknown_fields)]
141pub struct MutationsConfig {
142 pub auto_error_union: bool,
148}
149
150impl TomlProjectConfig {
151 pub fn from_file(path: &str) -> Result<Self> {
160 info!("Loading configuration from {path}");
161
162 let path = Path::new(path);
163 if !path.exists() {
164 anyhow::bail!("Configuration file not found: {}", path.display());
165 }
166
167 let raw = std::fs::read_to_string(path).context("Failed to read fraiseql.toml")?;
168 let toml_content = expand_env_vars(&raw)?;
169
170 let config: TomlProjectConfig = toml::from_str(&toml_content)
171 .map_err(|e| anyhow::anyhow!("Failed to parse fraiseql.toml: {e}"))?;
172
173 Ok(config)
174 }
175
176 pub fn validate(&self) -> Result<()> {
183 info!("Validating configuration");
184 self.fraiseql.security.validate()?;
185 self.fraiseql.tenancy.validate()?;
186 self.server.validate()?;
187 self.database.validate()?;
188 Ok(())
189 }
190}
191
192#[allow(clippy::expect_used)] pub(crate) fn expand_env_vars(content: &str) -> Result<String> {
197 use std::sync::LazyLock;
198
199 static ENV_VAR_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
200 regex::Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}").expect("env var regex is valid")
201 });
202
203 let mut result = String::with_capacity(content.len());
204 let mut last_end = 0;
205
206 for cap in ENV_VAR_REGEX.captures_iter(content) {
207 let m = cap.get(0).expect("INVARIANT: Regex captures group 0 is always present");
210 result.push_str(&content[last_end..m.start()]);
211 let var_name = &cap[1];
212 match std::env::var(var_name) {
213 Ok(val) => {
214 validate_env_var_value(var_name, &val)?;
215 result.push_str(&val);
216 },
217 Err(_) => {
218 result.push_str(&format!("${{{}}}", var_name));
219 },
220 }
221 last_end = m.end();
222 }
223 result.push_str(&content[last_end..]);
224 Ok(result)
225}
226
227fn validate_env_var_value(var_name: &str, value: &str) -> Result<()> {
228 if value.contains('\n') {
229 anyhow::bail!("Environment variable {} contains newline character", var_name);
230 }
231 if value.contains('\r') {
232 anyhow::bail!("Environment variable {} contains carriage return character", var_name);
233 }
234 if value.contains('\0') {
235 anyhow::bail!("Environment variable {} contains null character", var_name);
236 }
237 if value.contains('"')
239 || value.contains('\'')
240 || value.contains('\\')
241 || value.contains(']')
242 || value.contains('[')
243 || value.contains('{')
244 || value.contains('}')
245 {
246 anyhow::bail!(
247 "Environment variable {} contains TOML metacharacter that could break TOML parsing",
248 var_name
249 );
250 }
251 Ok(())
252}
253
254#[cfg(test)]
255mod tests;