1use std::fs;
10
11use anyhow::{Context, Result};
12use serde_json::{Value, json};
13
14use crate::{
15 config::TomlSchema,
16 schema::{
17 IntermediateSchema,
18 intermediate::IntermediateQueryDefaults,
19 },
20};
21
22pub struct SchemaMerger;
24
25impl SchemaMerger {
26 pub fn merge_files(types_path: &str, toml_path: &str) -> Result<IntermediateSchema> {
35 let types_json = fs::read_to_string(types_path)
37 .context(format!("Failed to read types.json from {types_path}"))?;
38 let types_value: Value =
39 serde_json::from_str(&types_json).context("Failed to parse types.json")?;
40
41 let toml_schema = TomlSchema::from_file(toml_path)
43 .context(format!("Failed to load TOML from {toml_path}"))?;
44
45 Self::merge_values(&types_value, &toml_schema)
50 }
51
52 pub fn merge_toml_only(toml_path: &str) -> Result<IntermediateSchema> {
60 let toml_schema = TomlSchema::from_file(toml_path)
61 .context(format!("Failed to load TOML from {toml_path}"))?;
62
63 toml_schema.validate()?;
64
65 let types_value = toml_schema.to_intermediate_schema();
67 Self::merge_values(&types_value, &toml_schema)
68 }
69
70 pub fn merge_from_directory(toml_path: &str, schema_dir: &str) -> Result<IntermediateSchema> {
79 let toml_schema = TomlSchema::from_file(toml_path)
80 .context(format!("Failed to load TOML from {toml_path}"))?;
81
82 toml_schema.validate()?;
83
84 let types_value = crate::schema::MultiFileLoader::load_from_directory(schema_dir)
86 .context(format!("Failed to load schema from directory {schema_dir}"))?;
87
88 Self::merge_values(&types_value, &toml_schema)
90 }
91
92 fn load_section(files: &[String], key: &str) -> Result<Option<serde_json::Value>> {
94 if files.is_empty() {
95 return Ok(None);
96 }
97 let paths: Vec<std::path::PathBuf> = files.iter().map(std::path::PathBuf::from).collect();
98 let loaded = crate::schema::MultiFileLoader::load_from_paths(&paths)
99 .with_context(|| format!("Failed to load {key} files"))?;
100 Ok(loaded.get(key).cloned())
101 }
102
103 fn extend_from_json_file(
106 path: &std::path::Path,
107 all_types: &mut Vec<Value>,
108 all_queries: &mut Vec<Value>,
109 all_mutations: &mut Vec<Value>,
110 ) -> Result<()> {
111 let content = fs::read_to_string(path)
112 .with_context(|| format!("Failed to read {}", path.display()))?;
113 let value: Value = serde_json::from_str(&content)
114 .with_context(|| format!("Failed to parse {}", path.display()))?;
115 for (vec, key) in [
116 (all_types as &mut Vec<Value>, "types"),
117 (all_queries, "queries"),
118 (all_mutations, "mutations"),
119 ] {
120 if let Some(Value::Array(items)) = value.get(key) {
121 vec.extend(items.iter().cloned());
122 }
123 }
124 Ok(())
125 }
126
127 fn enrich_type_from_toml(enriched_type: &mut Value, toml_type: &crate::config::toml_schema::TypeDefinition) {
129 enriched_type["sql_source"] = json!(toml_type.sql_source);
130 if let Some(desc) = &toml_type.description {
131 enriched_type["description"] = json!(desc);
132 }
133 }
134
135 pub fn merge_explicit_files(
146 toml_path: &str,
147 type_files: &[String],
148 query_files: &[String],
149 mutation_files: &[String],
150 ) -> Result<IntermediateSchema> {
151 let toml_schema = TomlSchema::from_file(toml_path)
152 .context(format!("Failed to load TOML from {toml_path}"))?;
153
154 toml_schema.validate()?;
155
156 let mut types_value = serde_json::json!({
157 "types": [],
158 "queries": [],
159 "mutations": []
160 });
161
162 if let Some(v) = Self::load_section(type_files, "types")? {
163 types_value["types"] = v;
164 }
165 if let Some(v) = Self::load_section(query_files, "queries")? {
166 types_value["queries"] = v;
167 }
168 if let Some(v) = Self::load_section(mutation_files, "mutations")? {
169 types_value["mutations"] = v;
170 }
171
172 Self::merge_values(&types_value, &toml_schema)
173 }
174
175 pub fn merge_from_domains(toml_path: &str) -> Result<IntermediateSchema> {
183 let toml_schema = TomlSchema::from_file(toml_path)
184 .context(format!("Failed to load TOML from {toml_path}"))?;
185
186 toml_schema.validate()?;
187
188 let domains = toml_schema
190 .domain_discovery
191 .resolve_domains()
192 .context("Failed to discover domains")?;
193
194 if domains.is_empty() {
195 let empty_value = serde_json::json!({
197 "types": [],
198 "queries": [],
199 "mutations": []
200 });
201 return Self::merge_values(&empty_value, &toml_schema);
202 }
203
204 let mut all_types = Vec::new();
205 let mut all_queries = Vec::new();
206 let mut all_mutations = Vec::new();
207
208 for domain in domains {
209 for filename in ["types.json", "queries.json", "mutations.json"] {
210 let path = domain.path.join(filename);
211 if path.exists() {
212 Self::extend_from_json_file(
213 &path,
214 &mut all_types,
215 &mut all_queries,
216 &mut all_mutations,
217 )?;
218 }
219 }
220 }
221
222 let types_value = serde_json::json!({
223 "types": all_types,
224 "queries": all_queries,
225 "mutations": all_mutations,
226 });
227
228 Self::merge_values(&types_value, &toml_schema)
230 }
231
232 pub fn merge_with_includes(toml_path: &str) -> Result<IntermediateSchema> {
240 let toml_schema = TomlSchema::from_file(toml_path)
241 .context(format!("Failed to load TOML from {toml_path}"))?;
242
243 toml_schema.validate()?;
244
245 let types_value = if toml_schema.includes.is_empty() {
247 serde_json::json!({
249 "types": [],
250 "queries": [],
251 "mutations": []
252 })
253 } else {
254 let resolved = toml_schema
255 .includes
256 .resolve_globs()
257 .context("Failed to resolve glob patterns in schema.includes")?;
258
259 let type_files: Vec<std::path::PathBuf> = resolved.types;
261 let mut merged_types = if type_files.is_empty() {
262 serde_json::json!({
263 "types": [],
264 "queries": [],
265 "mutations": []
266 })
267 } else {
268 crate::schema::MultiFileLoader::load_from_paths(&type_files)
269 .context("Failed to load type files")?
270 };
271
272 if !resolved.queries.is_empty() {
274 let loaded = crate::schema::MultiFileLoader::load_from_paths(&resolved.queries)
275 .context("Failed to load query files")?;
276 let new_items =
277 loaded.get("queries").and_then(Value::as_array).cloned().unwrap_or_default();
278 if let Some(Value::Array(existing)) = merged_types.get_mut("queries") {
279 existing.extend(new_items);
280 }
281 }
282
283 if !resolved.mutations.is_empty() {
285 let loaded = crate::schema::MultiFileLoader::load_from_paths(&resolved.mutations)
286 .context("Failed to load mutation files")?;
287 let new_items = loaded
288 .get("mutations")
289 .and_then(Value::as_array)
290 .cloned()
291 .unwrap_or_default();
292 if let Some(Value::Array(existing)) = merged_types.get_mut("mutations") {
293 existing.extend(new_items);
294 }
295 }
296
297 merged_types
298 };
299
300 Self::merge_values(&types_value, &toml_schema)
302 }
303
304 fn merge_values(types_value: &Value, toml_schema: &TomlSchema) -> Result<IntermediateSchema> {
306 if toml_schema.queries.contains_key("defaults") {
308 anyhow::bail!(
309 "Found a query definition named 'defaults' under [queries.defaults]. \
310 Did you mean [query_defaults] to set global auto-param defaults?\n\
311 If you intended a query called 'defaults', rename it to avoid confusion."
312 );
313 }
314
315 let mut types_array: Vec<Value> = Vec::new();
318 let mut queries_array: Vec<Value> = Vec::new();
319 let mut mutations_array: Vec<Value> = Vec::new();
320
321 if let Some(types_obj) = types_value.get("types") {
323 match types_obj {
324 Value::Array(types_list) => {
326 for type_item in types_list {
327 if let Some(type_name) = type_item.get("name").and_then(|v| v.as_str()) {
328 let mut enriched_type = type_item.clone();
329 if let Some(toml_type) = toml_schema.types.get(type_name) {
330 Self::enrich_type_from_toml(&mut enriched_type, toml_type);
331 }
332 types_array.push(enriched_type);
333 }
334 }
335 },
336 Value::Object(types_map) => {
338 for (type_name, type_value) in types_map {
339 let mut enriched_type = type_value.clone();
340 enriched_type["name"] = json!(type_name);
341
342 if let Some(Value::Object(fields_map)) = enriched_type.get("fields") {
344 let fields_array: Vec<Value> = fields_map
345 .iter()
346 .map(|(field_name, field_value)| {
347 let mut field = field_value.clone();
348 field["name"] = json!(field_name);
349 field
350 })
351 .collect();
352 enriched_type["fields"] = json!(fields_array);
353 }
354
355 if let Some(toml_type) = toml_schema.types.get(type_name) {
356 Self::enrich_type_from_toml(&mut enriched_type, toml_type);
357 }
358
359 types_array.push(enriched_type);
360 }
361 },
362 _ => {},
363 }
364 }
365
366 let existing_type_names: std::collections::HashSet<_> = types_array
368 .iter()
369 .filter_map(|t| {
370 t.get("name").and_then(|v| v.as_str()).map(str::to_string)
371 })
372 .collect();
373
374 for (type_name, toml_type) in &toml_schema.types {
375 if !existing_type_names.contains(type_name) {
376 types_array.push(json!({
377 "name": type_name,
378 "sql_source": toml_type.sql_source,
379 "description": toml_type.description,
380 "fields": toml_type.fields.iter().map(|(fname, fdef)| json!({
381 "name": fname,
382 "type": fdef.field_type,
383 "nullable": fdef.nullable,
384 "description": fdef.description,
385 })).collect::<Vec<_>>(),
386 }));
387 }
388 }
389
390 if let Some(Value::Array(queries_list)) = types_value.get("queries") {
391 queries_array.clone_from(queries_list);
392 }
393
394 for (query_name, toml_query) in &toml_schema.queries {
396 queries_array.push(json!({
397 "name": query_name,
398 "return_type": toml_query.return_type,
399 "return_array": toml_query.return_array,
400 "sql_source": toml_query.sql_source,
401 "description": toml_query.description,
402 "args": toml_query.args.iter().map(|arg| json!({
403 "name": arg.name,
404 "type": arg.arg_type,
405 "required": arg.required,
406 "default": arg.default,
407 "description": arg.description,
408 })).collect::<Vec<_>>(),
409 }));
410 }
411
412 if let Some(Value::Array(mutations_list)) = types_value.get("mutations") {
413 mutations_array.clone_from(mutations_list);
414 }
415
416 for (mutation_name, toml_mutation) in &toml_schema.mutations {
418 mutations_array.push(json!({
419 "name": mutation_name,
420 "return_type": toml_mutation.return_type,
421 "sql_source": toml_mutation.sql_source,
422 "operation": toml_mutation.operation,
423 "description": toml_mutation.description,
424 "args": toml_mutation.args.iter().map(|arg| json!({
425 "name": arg.name,
426 "type": arg.arg_type,
427 "required": arg.required,
428 "default": arg.default,
429 "description": arg.description,
430 })).collect::<Vec<_>>(),
431 }));
432 }
433
434 let mut merged = serde_json::json!({
436 "version": "2.0.0",
437 "types": types_array,
438 "queries": queries_array,
439 "mutations": mutations_array,
440 });
441
442 if let Some(pkce) = &toml_schema.security.pkce {
444 if pkce.enabled {
445 let enc_enabled = toml_schema
446 .security
447 .state_encryption
448 .as_ref()
449 .is_some_and(|e| e.enabled);
450 if !enc_enabled {
451 tracing::warn!(
452 "pkce.enabled = true but state_encryption.enabled = false. \
453 PKCE state will be stored unencrypted. \
454 Set [security.state_encryption] enabled = true for production."
455 );
456 }
457 }
458 }
459
460 merged["security"] = json!({
462 "default_policy": toml_schema.security.default_policy,
463 "rules": toml_schema.security.rules.iter().map(|r| json!({
464 "name": r.name,
465 "rule": r.rule,
466 "description": r.description,
467 "cacheable": r.cacheable,
468 "cache_ttl_seconds": r.cache_ttl_seconds,
469 })).collect::<Vec<_>>(),
470 "policies": toml_schema.security.policies.iter().map(|p| json!({
471 "name": p.name,
472 "type": p.policy_type,
473 "rule": p.rule,
474 "roles": p.roles,
475 "strategy": p.strategy,
476 "attributes": p.attributes,
477 "description": p.description,
478 "cache_ttl_seconds": p.cache_ttl_seconds,
479 })).collect::<Vec<_>>(),
480 "field_auth": toml_schema.security.field_auth.iter().map(|fa| json!({
481 "type_name": fa.type_name,
482 "field_name": fa.field_name,
483 "policy": fa.policy,
484 })).collect::<Vec<_>>(),
485 "enterprise": json!({
486 "rate_limiting_enabled": toml_schema.security.enterprise.rate_limiting_enabled,
487 "auth_endpoint_max_requests": toml_schema.security.enterprise.auth_endpoint_max_requests,
488 "auth_endpoint_window_seconds": toml_schema.security.enterprise.auth_endpoint_window_seconds,
489 "audit_logging_enabled": toml_schema.security.enterprise.audit_logging_enabled,
490 "audit_log_backend": toml_schema.security.enterprise.audit_log_backend,
491 "audit_retention_days": toml_schema.security.enterprise.audit_retention_days,
492 "error_sanitization": toml_schema.security.enterprise.error_sanitization,
493 "hide_implementation_details": toml_schema.security.enterprise.hide_implementation_details,
494 "constant_time_comparison": toml_schema.security.enterprise.constant_time_comparison,
495 "pkce_enabled": toml_schema.security.enterprise.pkce_enabled,
496 }),
497 "error_sanitization": toml_schema.security.error_sanitization,
498 "rate_limiting": toml_schema.security.rate_limiting,
499 "state_encryption": toml_schema.security.state_encryption,
500 "pkce": toml_schema.security.pkce,
501 "api_keys": toml_schema.security.api_keys,
502 "token_revocation": toml_schema.security.token_revocation,
503 "trusted_documents": toml_schema.security.trusted_documents,
504 });
505
506 if toml_schema.observers.enabled
508 || toml_schema.observers.redis_url.is_some()
509 || toml_schema.observers.nats_url.is_some()
510 {
511 if toml_schema.observers.backend == "nats" && toml_schema.observers.nats_url.is_none() {
512 tracing::warn!(
513 "observers.backend is \"nats\" but observers.nats_url is not set; \
514 the runtime will require FRAISEQL_NATS_URL to be configured"
515 );
516 }
517 merged["observers_config"] = json!({
518 "enabled": toml_schema.observers.enabled,
519 "backend": toml_schema.observers.backend,
520 "redis_url": toml_schema.observers.redis_url,
521 "nats_url": toml_schema.observers.nats_url,
522 "handlers": toml_schema.observers.handlers.iter().map(|h| json!({
523 "name": h.name,
524 "event": h.event,
525 "action": h.action,
526 "webhook_url": h.webhook_url,
527 "retry_strategy": h.retry_strategy,
528 "max_retries": h.max_retries,
529 "description": h.description,
530 })).collect::<Vec<_>>(),
531 });
532 }
533
534 if toml_schema.federation.enabled {
536 merged["federation_config"] = serde_json::to_value(&toml_schema.federation)
537 .unwrap_or_default();
538 }
539
540 let subs_json = serde_json::to_value(&toml_schema.subscriptions).unwrap_or_default();
542 if subs_json != serde_json::json!({}) {
543 merged["subscriptions_config"] = subs_json;
544 }
545
546 let val_json = serde_json::to_value(&toml_schema.validation).unwrap_or_default();
548 if val_json != serde_json::json!({}) {
549 merged["validation_config"] = val_json;
550 }
551
552 if toml_schema.debug.enabled {
554 let debug_json = serde_json::to_value(&toml_schema.debug).unwrap_or_default();
555 merged["debug_config"] = debug_json;
556 }
557
558 if toml_schema.mcp.enabled {
560 merged["mcp_config"] = serde_json::to_value(&toml_schema.mcp)
561 .unwrap_or_default();
562 }
563
564 let mut schema = serde_json::from_value::<IntermediateSchema>(merged)
566 .context("Failed to convert merged schema to IntermediateSchema")?;
567
568 schema.query_defaults = Some(IntermediateQueryDefaults {
571 where_clause: toml_schema.query_defaults.where_clause,
572 order_by: toml_schema.query_defaults.order_by,
573 limit: toml_schema.query_defaults.limit,
574 offset: toml_schema.query_defaults.offset,
575 });
576
577 Ok(schema)
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use std::fs;
584
585 use tempfile::TempDir;
586
587 use super::*;
588
589 #[test]
590 fn test_merge_toml_only() {
591 let toml_content = r#"
592[schema]
593name = "test"
594version = "1.0.0"
595database_target = "postgresql"
596
597[database]
598url = "postgresql://localhost/test"
599
600[types.User]
601sql_source = "v_user"
602
603[types.User.fields.id]
604type = "ID"
605
606[types.User.fields.name]
607type = "String"
608
609[queries.users]
610return_type = "User"
611return_array = true
612sql_source = "v_user"
613"#;
614
615 let temp_path = "/tmp/test_fraiseql.toml";
617 std::fs::write(temp_path, toml_content).unwrap();
618
619 let result = SchemaMerger::merge_toml_only(temp_path);
621 assert!(result.is_ok());
622
623 let _ = std::fs::remove_file(temp_path);
625 }
626
627 #[test]
628 fn test_merge_with_includes() -> Result<()> {
629 let temp_dir = TempDir::new()?;
630
631 let user_types = serde_json::json!({
633 "types": [{"name": "User", "fields": []}],
634 "queries": [],
635 "mutations": []
636 });
637 fs::write(temp_dir.path().join("user.json"), user_types.to_string())?;
638
639 let post_types = serde_json::json!({
640 "types": [{"name": "Post", "fields": []}],
641 "queries": [],
642 "mutations": []
643 });
644 fs::write(temp_dir.path().join("post.json"), post_types.to_string())?;
645
646 let toml_content = format!(
648 r#"
649[schema]
650name = "test"
651version = "1.0.0"
652database_target = "postgresql"
653
654[database]
655url = "postgresql://localhost/test"
656
657[includes]
658types = ["{}/*.json"]
659queries = []
660mutations = []
661"#,
662 temp_dir.path().to_string_lossy()
663 );
664
665 let toml_path = temp_dir.path().join("fraiseql.toml");
666 fs::write(&toml_path, toml_content)?;
667
668 let result = SchemaMerger::merge_with_includes(toml_path.to_str().unwrap());
670 assert!(result.is_ok());
671
672 let schema = result?;
673 assert_eq!(schema.types.len(), 2);
674
675 Ok(())
676 }
677
678 #[test]
679 fn test_merge_with_includes_missing_files() -> Result<()> {
680 let temp_dir = TempDir::new()?;
681
682 let toml_content = r#"
683[schema]
684name = "test"
685version = "1.0.0"
686database_target = "postgresql"
687
688[database]
689url = "postgresql://localhost/test"
690
691[includes]
692types = ["/nonexistent/path/*.json"]
693queries = []
694mutations = []
695"#;
696
697 let toml_path = temp_dir.path().join("fraiseql.toml");
698 fs::write(&toml_path, toml_content)?;
699
700 let result = SchemaMerger::merge_with_includes(toml_path.to_str().unwrap());
702 assert!(result.is_ok());
703
704 let schema = result?;
705 assert_eq!(schema.types.len(), 0);
706
707 Ok(())
708 }
709
710 #[test]
711 fn test_merge_from_domains() -> Result<()> {
712 let temp_dir = TempDir::new()?;
713 let schema_dir = temp_dir.path().join("schema");
714 fs::create_dir(&schema_dir)?;
715
716 fs::create_dir(schema_dir.join("auth"))?;
718 fs::create_dir(schema_dir.join("products"))?;
719
720 let auth_types = serde_json::json!({
721 "types": [{"name": "User", "fields": []}],
722 "queries": [{"name": "getUser", "return_type": "User"}],
723 "mutations": []
724 });
725 fs::write(schema_dir.join("auth/types.json"), auth_types.to_string())?;
726
727 let product_types = serde_json::json!({
728 "types": [{"name": "Product", "fields": []}],
729 "queries": [{"name": "getProduct", "return_type": "Product"}],
730 "mutations": []
731 });
732 fs::write(schema_dir.join("products/types.json"), product_types.to_string())?;
733
734 let schema_dir_str = schema_dir.to_string_lossy().to_string();
736 let toml_content = format!(
737 r#"
738[schema]
739name = "test"
740version = "1.0.0"
741database_target = "postgresql"
742
743[database]
744url = "postgresql://localhost/test"
745
746[domain_discovery]
747enabled = true
748root_dir = "{schema_dir_str}"
749"#
750 );
751
752 let toml_path = temp_dir.path().join("fraiseql.toml");
753 fs::write(&toml_path, toml_content)?;
754
755 let result = SchemaMerger::merge_from_domains(toml_path.to_str().unwrap());
757
758 assert!(result.is_ok());
759 let schema = result?;
760
761 assert_eq!(schema.types.len(), 2);
763 assert_eq!(schema.queries.len(), 2);
765
766 Ok(())
767 }
768
769 #[test]
770 fn test_merge_from_domains_alphabetical_order() -> Result<()> {
771 let temp_dir = TempDir::new()?;
772 let schema_dir = temp_dir.path().join("schema");
773 fs::create_dir(&schema_dir)?;
774
775 fs::create_dir(schema_dir.join("zebra"))?;
777 fs::create_dir(schema_dir.join("alpha"))?;
778 fs::create_dir(schema_dir.join("middle"))?;
779
780 for domain in &["zebra", "alpha", "middle"] {
781 let types = serde_json::json!({
782 "types": [{"name": domain.to_uppercase(), "fields": []}],
783 "queries": [],
784 "mutations": []
785 });
786 fs::write(schema_dir.join(format!("{domain}/types.json")), types.to_string())?;
787 }
788
789 let schema_dir_str = schema_dir.to_string_lossy().to_string();
790 let toml_content = format!(
791 r#"
792[schema]
793name = "test"
794version = "1.0.0"
795database_target = "postgresql"
796
797[database]
798url = "postgresql://localhost/test"
799
800[domain_discovery]
801enabled = true
802root_dir = "{schema_dir_str}"
803"#
804 );
805
806 let toml_path = temp_dir.path().join("fraiseql.toml");
807 fs::write(&toml_path, toml_content)?;
808
809 let result = SchemaMerger::merge_from_domains(toml_path.to_str().unwrap());
810
811 assert!(result.is_ok());
812 let schema = result?;
813
814 let type_names: Vec<String> = schema.types.iter().map(|t| t.name.clone()).collect();
816
817 assert_eq!(type_names[0], "ALPHA");
818 assert_eq!(type_names[1], "MIDDLE");
819 assert_eq!(type_names[2], "ZEBRA");
820
821 Ok(())
822 }
823
824 #[test]
825 fn test_merge_toml_only_with_validation_config() {
826 let toml_content = r#"
827[schema]
828name = "test"
829version = "1.0.0"
830database_target = "postgresql"
831
832[database]
833url = "postgresql://localhost/test"
834
835[types.User]
836sql_source = "v_user"
837
838[types.User.fields.id]
839type = "ID"
840
841[validation]
842max_query_depth = 3
843max_query_complexity = 25
844"#;
845
846 let temp_path = "/tmp/test_fraiseql_validation.toml";
847 std::fs::write(temp_path, toml_content).unwrap();
848
849 let result = SchemaMerger::merge_toml_only(temp_path);
850 assert!(result.is_ok());
851 let schema = result.unwrap();
852
853 let vc = schema.validation_config.as_ref().expect("validation_config should be set");
855 assert_eq!(vc.get("max_query_depth").and_then(serde_json::Value::as_u64), Some(3));
856 assert_eq!(vc.get("max_query_complexity").and_then(serde_json::Value::as_u64), Some(25));
857
858 let _ = std::fs::remove_file(temp_path);
859 }
860
861 #[test]
862 fn test_merge_toml_only_without_validation_config() {
863 let toml_content = r#"
864[schema]
865name = "test"
866version = "1.0.0"
867database_target = "postgresql"
868
869[database]
870url = "postgresql://localhost/test"
871
872[types.User]
873sql_source = "v_user"
874
875[types.User.fields.id]
876type = "ID"
877"#;
878
879 let temp_path = "/tmp/test_fraiseql_no_validation.toml";
880 std::fs::write(temp_path, toml_content).unwrap();
881
882 let result = SchemaMerger::merge_toml_only(temp_path);
883 assert!(result.is_ok());
884 let schema = result.unwrap();
885
886 assert!(schema.validation_config.is_none());
888
889 let _ = std::fs::remove_file(temp_path);
890 }
891}