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