1use std::{collections::BTreeMap, path::PathBuf};
7
8use anyhow::{Context, Result};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Default, Deserialize, Serialize)]
33#[serde(default)]
34pub struct DomainDiscovery {
35 pub enabled: bool,
37 pub root_dir: String,
39}
40
41#[derive(Debug, Clone)]
43pub struct Domain {
44 pub name: String,
46 pub path: PathBuf,
48}
49
50impl DomainDiscovery {
51 pub fn resolve_domains(&self) -> Result<Vec<Domain>> {
53 if !self.enabled {
54 return Ok(Vec::new());
55 }
56
57 let root = PathBuf::from(&self.root_dir);
58 if !root.is_dir() {
59 anyhow::bail!("Domain discovery root not found: {}", self.root_dir);
60 }
61
62 let mut domains = Vec::new();
63
64 for entry in std::fs::read_dir(&root)
65 .context(format!("Failed to read domain root: {}", self.root_dir))?
66 {
67 let entry = entry.context("Failed to read directory entry")?;
68 let path = entry.path();
69
70 if path.is_dir() {
71 let name = path
72 .file_name()
73 .and_then(|n| n.to_str())
74 .map(std::string::ToString::to_string)
75 .ok_or_else(|| anyhow::anyhow!("Invalid domain name: {}", path.display()))?;
76
77 domains.push(Domain { name, path });
78 }
79 }
80
81 domains.sort_by(|a, b| a.name.cmp(&b.name));
83
84 Ok(domains)
85 }
86}
87
88#[derive(Debug, Clone, Default, Deserialize, Serialize)]
98#[serde(default)]
99pub struct SchemaIncludes {
100 pub types: Vec<String>,
102 pub queries: Vec<String>,
104 pub mutations: Vec<String>,
106}
107
108impl SchemaIncludes {
109 pub fn is_empty(&self) -> bool {
111 self.types.is_empty() && self.queries.is_empty() && self.mutations.is_empty()
112 }
113
114 pub fn resolve_globs(&self) -> Result<ResolvedIncludes> {
119 use glob::glob as glob_pattern;
120
121 let mut type_paths = Vec::new();
122 let mut query_paths = Vec::new();
123 let mut mutation_paths = Vec::new();
124
125 for pattern in &self.types {
127 for entry in glob_pattern(pattern)
128 .context(format!("Invalid glob pattern for types: {pattern}"))?
129 {
130 match entry {
131 Ok(path) => type_paths.push(path),
132 Err(e) => {
133 anyhow::bail!("Error resolving type glob pattern '{pattern}': {e}");
134 },
135 }
136 }
137 }
138
139 for pattern in &self.queries {
141 for entry in glob_pattern(pattern)
142 .context(format!("Invalid glob pattern for queries: {pattern}"))?
143 {
144 match entry {
145 Ok(path) => query_paths.push(path),
146 Err(e) => {
147 anyhow::bail!("Error resolving query glob pattern '{pattern}': {e}");
148 },
149 }
150 }
151 }
152
153 for pattern in &self.mutations {
155 for entry in glob_pattern(pattern)
156 .context(format!("Invalid glob pattern for mutations: {pattern}"))?
157 {
158 match entry {
159 Ok(path) => mutation_paths.push(path),
160 Err(e) => {
161 anyhow::bail!("Error resolving mutation glob pattern '{pattern}': {e}");
162 },
163 }
164 }
165 }
166
167 type_paths.sort();
169 query_paths.sort();
170 mutation_paths.sort();
171
172 type_paths.dedup();
174 query_paths.dedup();
175 mutation_paths.dedup();
176
177 Ok(ResolvedIncludes {
178 types: type_paths,
179 queries: query_paths,
180 mutations: mutation_paths,
181 })
182 }
183}
184
185#[derive(Debug, Clone)]
187pub struct ResolvedIncludes {
188 pub types: Vec<PathBuf>,
190 pub queries: Vec<PathBuf>,
192 pub mutations: Vec<PathBuf>,
194}
195
196#[derive(Debug, Clone, Default, Deserialize, Serialize)]
198#[serde(default)]
199pub struct TomlSchema {
200 #[serde(rename = "schema")]
202 pub schema: SchemaMetadata,
203
204 #[serde(rename = "database")]
206 pub database: DatabaseConfig,
207
208 #[serde(rename = "types")]
210 pub types: BTreeMap<String, TypeDefinition>,
211
212 #[serde(rename = "queries")]
214 pub queries: BTreeMap<String, QueryDefinition>,
215
216 #[serde(rename = "mutations")]
218 pub mutations: BTreeMap<String, MutationDefinition>,
219
220 #[serde(rename = "federation")]
222 pub federation: FederationConfig,
223
224 #[serde(rename = "security")]
226 pub security: SecuritySettings,
227
228 #[serde(rename = "observers")]
230 pub observers: ObserversConfig,
231
232 #[serde(rename = "caching")]
234 pub caching: CachingConfig,
235
236 #[serde(rename = "analytics")]
238 pub analytics: AnalyticsConfig,
239
240 #[serde(rename = "observability")]
242 pub observability: ObservabilityConfig,
243
244 #[serde(default)]
246 pub includes: SchemaIncludes,
247
248 #[serde(default)]
250 pub domain_discovery: DomainDiscovery,
251}
252
253#[derive(Debug, Clone, Deserialize, Serialize)]
255#[serde(default)]
256pub struct SchemaMetadata {
257 pub name: String,
259 pub version: String,
261 pub description: Option<String>,
263 pub database_target: String,
265}
266
267impl Default for SchemaMetadata {
268 fn default() -> Self {
269 Self {
270 name: "myapp".to_string(),
271 version: "1.0.0".to_string(),
272 description: None,
273 database_target: "postgresql".to_string(),
274 }
275 }
276}
277
278#[derive(Debug, Clone, Deserialize, Serialize)]
280#[serde(default)]
281pub struct DatabaseConfig {
282 pub url: String,
284 pub pool_size: u32,
286 pub ssl_mode: String,
288 pub timeout_seconds: u32,
290}
291
292impl Default for DatabaseConfig {
293 fn default() -> Self {
294 Self {
295 url: "postgresql://localhost/mydb".to_string(),
296 pool_size: 10,
297 ssl_mode: "prefer".to_string(),
298 timeout_seconds: 30,
299 }
300 }
301}
302
303#[derive(Debug, Clone, Deserialize, Serialize)]
305#[serde(default)]
306pub struct TypeDefinition {
307 pub sql_source: String,
309 pub description: Option<String>,
311 pub fields: BTreeMap<String, FieldDefinition>,
313}
314
315impl Default for TypeDefinition {
316 fn default() -> Self {
317 Self {
318 sql_source: "v_entity".to_string(),
319 description: None,
320 fields: BTreeMap::new(),
321 }
322 }
323}
324
325#[derive(Debug, Clone, Deserialize, Serialize)]
327pub struct FieldDefinition {
328 #[serde(rename = "type")]
330 pub field_type: String,
331 #[serde(default)]
333 pub nullable: bool,
334 pub description: Option<String>,
336}
337
338#[derive(Debug, Clone, Deserialize, Serialize)]
340#[serde(default)]
341pub struct QueryDefinition {
342 pub return_type: String,
344 #[serde(default)]
346 pub return_array: bool,
347 pub sql_source: String,
349 pub description: Option<String>,
351 pub args: Vec<ArgumentDefinition>,
353}
354
355impl Default for QueryDefinition {
356 fn default() -> Self {
357 Self {
358 return_type: "String".to_string(),
359 return_array: false,
360 sql_source: "v_entity".to_string(),
361 description: None,
362 args: vec![],
363 }
364 }
365}
366
367#[derive(Debug, Clone, Deserialize, Serialize)]
369#[serde(default)]
370pub struct MutationDefinition {
371 pub return_type: String,
373 pub sql_source: String,
375 pub operation: String,
377 pub description: Option<String>,
379 pub args: Vec<ArgumentDefinition>,
381}
382
383impl Default for MutationDefinition {
384 fn default() -> Self {
385 Self {
386 return_type: "String".to_string(),
387 sql_source: "fn_operation".to_string(),
388 operation: "CREATE".to_string(),
389 description: None,
390 args: vec![],
391 }
392 }
393}
394
395#[derive(Debug, Clone, Deserialize, Serialize)]
397pub struct ArgumentDefinition {
398 pub name: String,
400 #[serde(rename = "type")]
402 pub arg_type: String,
403 #[serde(default)]
405 pub required: bool,
406 pub default: Option<serde_json::Value>,
408 pub description: Option<String>,
410}
411
412#[derive(Debug, Clone, Deserialize, Serialize)]
414#[serde(default)]
415pub struct FederationConfig {
416 #[serde(default)]
418 pub enabled: bool,
419 pub apollo_version: Option<u32>,
421 pub entities: Vec<FederationEntity>,
423}
424
425impl Default for FederationConfig {
426 fn default() -> Self {
427 Self {
428 enabled: false,
429 apollo_version: Some(2),
430 entities: vec![],
431 }
432 }
433}
434
435#[derive(Debug, Clone, Deserialize, Serialize)]
437pub struct FederationEntity {
438 pub name: String,
440 pub key_fields: Vec<String>,
442}
443
444#[derive(Debug, Clone, Deserialize, Serialize)]
446#[serde(default)]
447pub struct SecuritySettings {
448 pub default_policy: Option<String>,
450 pub rules: Vec<AuthorizationRule>,
452 pub policies: Vec<AuthorizationPolicy>,
454 pub field_auth: Vec<FieldAuthRule>,
456 pub enterprise: EnterpriseSecurityConfig,
458}
459
460impl Default for SecuritySettings {
461 fn default() -> Self {
462 Self {
463 default_policy: Some("authenticated".to_string()),
464 rules: vec![],
465 policies: vec![],
466 field_auth: vec![],
467 enterprise: EnterpriseSecurityConfig::default(),
468 }
469 }
470}
471
472#[derive(Debug, Clone, Deserialize, Serialize)]
474pub struct AuthorizationRule {
475 pub name: String,
477 pub rule: String,
479 pub description: Option<String>,
481 #[serde(default)]
483 pub cacheable: bool,
484 pub cache_ttl_seconds: Option<u32>,
486}
487
488#[derive(Debug, Clone, Deserialize, Serialize)]
490pub struct AuthorizationPolicy {
491 pub name: String,
493 #[serde(rename = "type")]
495 pub policy_type: String,
496 pub rule: Option<String>,
498 pub roles: Vec<String>,
500 pub strategy: Option<String>,
502 #[serde(default)]
504 pub attributes: Vec<String>,
505 pub description: Option<String>,
507 pub cache_ttl_seconds: Option<u32>,
509}
510
511#[derive(Debug, Clone, Deserialize, Serialize)]
513pub struct FieldAuthRule {
514 pub type_name: String,
516 pub field_name: String,
518 pub policy: String,
520}
521
522#[derive(Debug, Clone, Deserialize, Serialize)]
524#[serde(default)]
525pub struct EnterpriseSecurityConfig {
526 pub rate_limiting_enabled: bool,
528 pub auth_endpoint_max_requests: u32,
530 pub auth_endpoint_window_seconds: u64,
532 pub audit_logging_enabled: bool,
534 pub audit_log_backend: String,
536 pub audit_retention_days: u32,
538 pub error_sanitization: bool,
540 pub hide_implementation_details: bool,
542 pub constant_time_comparison: bool,
544 pub pkce_enabled: bool,
546}
547
548impl Default for EnterpriseSecurityConfig {
549 fn default() -> Self {
550 Self {
551 rate_limiting_enabled: true,
552 auth_endpoint_max_requests: 100,
553 auth_endpoint_window_seconds: 60,
554 audit_logging_enabled: true,
555 audit_log_backend: "postgresql".to_string(),
556 audit_retention_days: 365,
557 error_sanitization: true,
558 hide_implementation_details: true,
559 constant_time_comparison: true,
560 pkce_enabled: true,
561 }
562 }
563}
564
565#[derive(Debug, Clone, Deserialize, Serialize)]
567#[serde(default)]
568pub struct ObserversConfig {
569 #[serde(default)]
571 pub enabled: bool,
572 pub backend: String,
574 pub redis_url: Option<String>,
576 pub handlers: Vec<EventHandler>,
578}
579
580impl Default for ObserversConfig {
581 fn default() -> Self {
582 Self {
583 enabled: false,
584 backend: "redis".to_string(),
585 redis_url: None,
586 handlers: vec![],
587 }
588 }
589}
590
591#[derive(Debug, Clone, Deserialize, Serialize)]
593pub struct EventHandler {
594 pub name: String,
596 pub event: String,
598 pub action: String,
600 pub webhook_url: Option<String>,
602 pub retry_strategy: Option<String>,
604 pub max_retries: Option<u32>,
606 pub description: Option<String>,
608}
609
610#[derive(Debug, Clone, Deserialize, Serialize)]
612#[serde(default)]
613pub struct CachingConfig {
614 #[serde(default)]
616 pub enabled: bool,
617 pub backend: String,
619 pub redis_url: Option<String>,
621 pub rules: Vec<CacheRule>,
623}
624
625impl Default for CachingConfig {
626 fn default() -> Self {
627 Self {
628 enabled: false,
629 backend: "redis".to_string(),
630 redis_url: None,
631 rules: vec![],
632 }
633 }
634}
635
636#[derive(Debug, Clone, Deserialize, Serialize)]
638pub struct CacheRule {
639 pub query: String,
641 pub ttl_seconds: u32,
643 pub invalidation_triggers: Vec<String>,
645}
646
647#[derive(Debug, Clone, Default, Deserialize, Serialize)]
649#[serde(default)]
650pub struct AnalyticsConfig {
651 #[serde(default)]
653 pub enabled: bool,
654 pub queries: Vec<AnalyticsQuery>,
656}
657
658#[derive(Debug, Clone, Deserialize, Serialize)]
660pub struct AnalyticsQuery {
661 pub name: String,
663 pub sql_source: String,
665 pub description: Option<String>,
667}
668
669#[derive(Debug, Clone, Deserialize, Serialize)]
671#[serde(default)]
672pub struct ObservabilityConfig {
673 pub prometheus_enabled: bool,
675 pub prometheus_port: u16,
677 pub otel_enabled: bool,
679 pub otel_exporter: String,
681 pub otel_jaeger_endpoint: Option<String>,
683 pub health_check_enabled: bool,
685 pub health_check_interval_seconds: u32,
687 pub log_level: String,
689 pub log_format: String,
691}
692
693impl Default for ObservabilityConfig {
694 fn default() -> Self {
695 Self {
696 prometheus_enabled: false,
697 prometheus_port: 9090,
698 otel_enabled: false,
699 otel_exporter: "jaeger".to_string(),
700 otel_jaeger_endpoint: None,
701 health_check_enabled: true,
702 health_check_interval_seconds: 30,
703 log_level: "info".to_string(),
704 log_format: "json".to_string(),
705 }
706 }
707}
708
709impl TomlSchema {
710 pub fn from_file(path: &str) -> Result<Self> {
712 let content =
713 std::fs::read_to_string(path).context(format!("Failed to read TOML file: {path}"))?;
714 Self::parse_toml(&content)
715 }
716
717 pub fn parse_toml(content: &str) -> Result<Self> {
719 toml::from_str(content).context("Failed to parse TOML schema")
720 }
721
722 pub fn validate(&self) -> Result<()> {
724 for (query_name, query_def) in &self.queries {
726 if !self.types.contains_key(&query_def.return_type) {
727 anyhow::bail!(
728 "Query '{query_name}' references undefined type '{}'",
729 query_def.return_type
730 );
731 }
732 }
733
734 for (mut_name, mut_def) in &self.mutations {
736 if !self.types.contains_key(&mut_def.return_type) {
737 anyhow::bail!(
738 "Mutation '{mut_name}' references undefined type '{}'",
739 mut_def.return_type
740 );
741 }
742 }
743
744 for field_auth in &self.security.field_auth {
746 let policy_exists = self.security.policies.iter().any(|p| p.name == field_auth.policy);
747 if !policy_exists {
748 anyhow::bail!("Field auth references undefined policy '{}'", field_auth.policy);
749 }
750 }
751
752 for entity in &self.federation.entities {
754 if !self.types.contains_key(&entity.name) {
755 anyhow::bail!("Federation entity '{}' references undefined type", entity.name);
756 }
757 }
758
759 Ok(())
760 }
761
762 pub fn to_intermediate_schema(&self) -> serde_json::Value {
764 let mut types_json = serde_json::Map::new();
765
766 for (type_name, type_def) in &self.types {
767 let mut fields_json = serde_json::Map::new();
768
769 for (field_name, field_def) in &type_def.fields {
770 fields_json.insert(
771 field_name.clone(),
772 serde_json::json!({
773 "type": field_def.field_type,
774 "nullable": field_def.nullable,
775 "description": field_def.description,
776 }),
777 );
778 }
779
780 types_json.insert(
781 type_name.clone(),
782 serde_json::json!({
783 "name": type_name,
784 "sql_source": type_def.sql_source,
785 "description": type_def.description,
786 "fields": fields_json,
787 }),
788 );
789 }
790
791 let mut queries_json = serde_json::Map::new();
792
793 for (query_name, query_def) in &self.queries {
794 let args: Vec<serde_json::Value> = query_def
795 .args
796 .iter()
797 .map(|arg| {
798 serde_json::json!({
799 "name": arg.name,
800 "type": arg.arg_type,
801 "required": arg.required,
802 "default": arg.default,
803 "description": arg.description,
804 })
805 })
806 .collect();
807
808 queries_json.insert(
809 query_name.clone(),
810 serde_json::json!({
811 "name": query_name,
812 "return_type": query_def.return_type,
813 "return_array": query_def.return_array,
814 "sql_source": query_def.sql_source,
815 "description": query_def.description,
816 "args": args,
817 }),
818 );
819 }
820
821 serde_json::json!({
822 "types": types_json,
823 "queries": queries_json,
824 })
825 }
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831
832 #[test]
833 fn test_parse_toml_schema() {
834 let toml = r#"
835[schema]
836name = "myapp"
837version = "1.0.0"
838database_target = "postgresql"
839
840[types.User]
841sql_source = "v_user"
842
843[types.User.fields.id]
844type = "ID"
845nullable = false
846
847[types.User.fields.name]
848type = "String"
849nullable = false
850
851[queries.users]
852return_type = "User"
853return_array = true
854sql_source = "v_user"
855"#;
856 let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
857 assert_eq!(schema.schema.name, "myapp");
858 assert!(schema.types.contains_key("User"));
859 }
860
861 #[test]
862 fn test_validate_schema() {
863 let schema = TomlSchema::default();
864 assert!(schema.validate().is_ok());
865 }
866}