1pub mod advanced_types;
7pub mod analytics;
8pub mod fragments;
9pub mod operations;
10pub mod subscriptions;
11pub mod types;
12
13pub use advanced_types::{
14 IntermediateInputField, IntermediateInputObject, IntermediateInterface, IntermediateUnion,
15};
16pub use analytics::{
17 IntermediateAggregateQuery, IntermediateDimensionPath, IntermediateDimensions,
18 IntermediateFactTable, IntermediateFilter, IntermediateMeasure,
19};
20pub use fragments::{
21 IntermediateAppliedDirective, IntermediateDirective, IntermediateFragment,
22 IntermediateFragmentField, IntermediateFragmentFieldDef,
23};
24use fraiseql_core::schema::{
25 DebugConfig, McpConfig, NamingConvention, SessionVariablesConfig, SubscriptionsConfig,
26 ValidationConfig,
27};
28pub use operations::{
29 IntermediateArgument, IntermediateAutoParams, IntermediateMutation, IntermediateQuery,
30 IntermediateQueryDefaults,
31};
32use serde::{Deserialize, Serialize};
33pub use subscriptions::{
34 IntermediateFilterCondition, IntermediateObserver, IntermediateObserverAction,
35 IntermediateRetryConfig, IntermediateSubscription, IntermediateSubscriptionFilter,
36};
37pub use types::{
38 IntermediateDeprecation, IntermediateEnum, IntermediateEnumValue, IntermediateField,
39 IntermediateScalar, IntermediateType,
40};
41
42#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
44pub struct IntermediateSchema {
45 #[serde(default = "default_version")]
47 pub version: String,
48
49 #[serde(default)]
51 pub types: Vec<IntermediateType>,
52
53 #[serde(default)]
55 pub enums: Vec<IntermediateEnum>,
56
57 #[serde(default)]
59 pub input_types: Vec<IntermediateInputObject>,
60
61 #[serde(default)]
63 pub interfaces: Vec<IntermediateInterface>,
64
65 #[serde(default)]
67 pub unions: Vec<IntermediateUnion>,
68
69 #[serde(default)]
71 pub queries: Vec<IntermediateQuery>,
72
73 #[serde(default)]
75 pub mutations: Vec<IntermediateMutation>,
76
77 #[serde(default)]
79 pub subscriptions: Vec<IntermediateSubscription>,
80
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub fragments: Option<Vec<IntermediateFragment>>,
84
85 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub directives: Option<Vec<IntermediateDirective>>,
88
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub fact_tables: Option<Vec<IntermediateFactTable>>,
92
93 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub aggregate_queries: Option<Vec<IntermediateAggregateQuery>>,
96
97 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub observers: Option<Vec<IntermediateObserver>>,
100
101 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub custom_scalars: Option<Vec<IntermediateScalar>>,
108
109 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub security: Option<serde_json::Value>,
114
115 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub observers_config: Option<serde_json::Value>,
121
122 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub federation_config: Option<serde_json::Value>,
128
129 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub subscriptions_config: Option<SubscriptionsConfig>,
135
136 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub validation_config: Option<ValidationConfig>,
142
143 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub debug_config: Option<DebugConfig>,
149
150 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub mcp_config: Option<McpConfig>,
156
157 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub query_defaults: Option<IntermediateQueryDefaults>,
163
164 #[serde(default)]
168 pub naming_convention: NamingConvention,
169
170 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub session_variables: Option<SessionVariablesConfig>,
179}
180
181fn default_version() -> String {
182 "2.0.0".to_string()
183}
184
185#[cfg(test)]
186#[allow(clippy::unwrap_used)] mod tests {
188 use super::*;
189
190 #[test]
191 fn test_parse_minimal_schema() {
192 let json = r#"{
193 "types": [],
194 "queries": [],
195 "mutations": []
196 }"#;
197
198 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
199 assert_eq!(schema.version, "2.0.0");
200 assert_eq!(schema.types.len(), 0);
201 assert_eq!(schema.queries.len(), 0);
202 assert_eq!(schema.mutations.len(), 0);
203 }
204
205 #[test]
206 fn test_parse_type_with_type_field() {
207 let json = r#"{
208 "types": [{
209 "name": "User",
210 "fields": [
211 {
212 "name": "id",
213 "type": "Int",
214 "nullable": false
215 },
216 {
217 "name": "name",
218 "type": "String",
219 "nullable": false
220 }
221 ]
222 }],
223 "queries": [],
224 "mutations": []
225 }"#;
226
227 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
228 assert_eq!(schema.types.len(), 1);
229 assert_eq!(schema.types[0].name, "User");
230 assert_eq!(schema.types[0].fields.len(), 2);
231 assert_eq!(schema.types[0].fields[0].name, "id");
232 assert_eq!(schema.types[0].fields[0].field_type, "Int");
233 assert!(!schema.types[0].fields[0].nullable);
234 }
235
236 #[test]
237 fn test_parse_query_with_arguments() {
238 let json = r#"{
239 "types": [],
240 "queries": [{
241 "name": "users",
242 "return_type": "User",
243 "returns_list": true,
244 "nullable": false,
245 "arguments": [
246 {
247 "name": "limit",
248 "type": "Int",
249 "nullable": false,
250 "default": 10
251 }
252 ],
253 "sql_source": "v_user"
254 }],
255 "mutations": []
256 }"#;
257
258 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
259 assert_eq!(schema.queries.len(), 1);
260 assert_eq!(schema.queries[0].arguments.len(), 1);
261 assert_eq!(schema.queries[0].arguments[0].arg_type, "Int");
262 assert_eq!(schema.queries[0].arguments[0].default, Some(serde_json::json!(10)));
263 }
264
265 #[test]
266 fn test_parse_fragment_simple() {
267 let json = r#"{
268 "types": [],
269 "queries": [],
270 "mutations": [],
271 "fragments": [{
272 "name": "UserFields",
273 "on": "User",
274 "fields": ["id", "name", "email"]
275 }]
276 }"#;
277
278 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
279 assert!(schema.fragments.is_some());
280 let fragments = schema.fragments.unwrap();
281 assert_eq!(fragments.len(), 1);
282 assert_eq!(fragments[0].name, "UserFields");
283 assert_eq!(fragments[0].type_condition, "User");
284 assert_eq!(fragments[0].fields.len(), 3);
285
286 match &fragments[0].fields[0] {
288 IntermediateFragmentField::Simple(name) => assert_eq!(name, "id"),
289 IntermediateFragmentField::Complex(_) => panic!("Expected simple field"),
290 }
291 }
292
293 #[test]
294 fn test_parse_fragment_with_nested_fields() {
295 let json = r#"{
296 "types": [],
297 "queries": [],
298 "mutations": [],
299 "fragments": [{
300 "name": "PostFields",
301 "on": "Post",
302 "fields": [
303 "id",
304 "title",
305 {
306 "name": "author",
307 "alias": "writer",
308 "fields": ["id", "name"]
309 }
310 ]
311 }]
312 }"#;
313
314 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
315 let fragments = schema.fragments.unwrap();
316 assert_eq!(fragments[0].fields.len(), 3);
317
318 match &fragments[0].fields[2] {
320 IntermediateFragmentField::Complex(def) => {
321 assert_eq!(def.name, "author");
322 assert_eq!(def.alias, Some("writer".to_string()));
323 assert!(def.fields.is_some());
324 assert_eq!(def.fields.as_ref().unwrap().len(), 2);
325 },
326 IntermediateFragmentField::Simple(_) => panic!("Expected complex field"),
327 }
328 }
329
330 #[test]
331 fn test_parse_directive_definition() {
332 let json = r#"{
333 "types": [],
334 "queries": [],
335 "mutations": [],
336 "directives": [{
337 "name": "auth",
338 "locations": ["FIELD_DEFINITION", "OBJECT"],
339 "arguments": [
340 {"name": "role", "type": "String", "nullable": false}
341 ],
342 "description": "Requires authentication"
343 }]
344 }"#;
345
346 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
347 assert!(schema.directives.is_some());
348 let directives = schema.directives.unwrap();
349 assert_eq!(directives.len(), 1);
350 assert_eq!(directives[0].name, "auth");
351 assert_eq!(directives[0].locations, vec!["FIELD_DEFINITION", "OBJECT"]);
352 assert_eq!(directives[0].arguments.len(), 1);
353 assert_eq!(directives[0].description, Some("Requires authentication".to_string()));
354 }
355
356 #[test]
357 fn test_parse_field_with_directive() {
358 let json = r#"{
359 "types": [{
360 "name": "User",
361 "fields": [
362 {
363 "name": "oldId",
364 "type": "Int",
365 "nullable": false,
366 "directives": [
367 {"name": "deprecated", "arguments": {"reason": "Use 'id' instead"}}
368 ]
369 }
370 ]
371 }],
372 "queries": [],
373 "mutations": []
374 }"#;
375
376 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
377 let field = &schema.types[0].fields[0];
378 assert_eq!(field.name, "oldId");
379 assert!(field.directives.is_some());
380 let directives = field.directives.as_ref().unwrap();
381 assert_eq!(directives.len(), 1);
382 assert_eq!(directives[0].name, "deprecated");
383 assert_eq!(
384 directives[0].arguments,
385 Some(serde_json::json!({"reason": "Use 'id' instead"}))
386 );
387 }
388
389 #[test]
390 fn test_parse_fragment_with_spread() {
391 let json = r#"{
392 "types": [],
393 "queries": [],
394 "mutations": [],
395 "fragments": [
396 {
397 "name": "UserFields",
398 "on": "User",
399 "fields": ["id", "name"]
400 },
401 {
402 "name": "PostWithAuthor",
403 "on": "Post",
404 "fields": [
405 "id",
406 "title",
407 {
408 "name": "author",
409 "spread": "UserFields"
410 }
411 ]
412 }
413 ]
414 }"#;
415
416 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
417 let fragments = schema.fragments.unwrap();
418 assert_eq!(fragments.len(), 2);
419
420 match &fragments[1].fields[2] {
422 IntermediateFragmentField::Complex(def) => {
423 assert_eq!(def.name, "author");
424 assert_eq!(def.spread, Some("UserFields".to_string()));
425 },
426 IntermediateFragmentField::Simple(_) => panic!("Expected complex field"),
427 }
428 }
429
430 #[test]
431 fn test_parse_enum() {
432 let json = r#"{
433 "types": [],
434 "queries": [],
435 "mutations": [],
436 "enums": [{
437 "name": "OrderStatus",
438 "values": [
439 {"name": "PENDING"},
440 {"name": "PROCESSING", "description": "Currently being processed"},
441 {"name": "SHIPPED"},
442 {"name": "DELIVERED"}
443 ],
444 "description": "Possible states of an order"
445 }]
446 }"#;
447
448 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
449 assert_eq!(schema.enums.len(), 1);
450 let enum_def = &schema.enums[0];
451 assert_eq!(enum_def.name, "OrderStatus");
452 assert_eq!(enum_def.description, Some("Possible states of an order".to_string()));
453 assert_eq!(enum_def.values.len(), 4);
454 assert_eq!(enum_def.values[0].name, "PENDING");
455 assert_eq!(enum_def.values[1].description, Some("Currently being processed".to_string()));
456 }
457
458 #[test]
459 fn test_parse_enum_with_deprecated_value() {
460 let json = r#"{
461 "types": [],
462 "queries": [],
463 "mutations": [],
464 "enums": [{
465 "name": "UserRole",
466 "values": [
467 {"name": "ADMIN"},
468 {"name": "USER"},
469 {"name": "GUEST", "deprecated": {"reason": "Use USER with limited permissions instead"}}
470 ]
471 }]
472 }"#;
473
474 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
475 let enum_def = &schema.enums[0];
476 assert_eq!(enum_def.values.len(), 3);
477
478 let guest = &enum_def.values[2];
480 assert_eq!(guest.name, "GUEST");
481 assert!(guest.deprecated.is_some());
482 assert_eq!(
483 guest.deprecated.as_ref().unwrap().reason,
484 Some("Use USER with limited permissions instead".to_string())
485 );
486 }
487
488 #[test]
489 fn test_parse_input_object() {
490 let json = r#"{
491 "types": [],
492 "queries": [],
493 "mutations": [],
494 "input_types": [{
495 "name": "UserFilter",
496 "fields": [
497 {"name": "name", "type": "String", "nullable": true},
498 {"name": "email", "type": "String", "nullable": true},
499 {"name": "active", "type": "Boolean", "nullable": true, "default": true}
500 ],
501 "description": "Filter criteria for users"
502 }]
503 }"#;
504
505 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
506 assert_eq!(schema.input_types.len(), 1);
507 let input = &schema.input_types[0];
508 assert_eq!(input.name, "UserFilter");
509 assert_eq!(input.description, Some("Filter criteria for users".to_string()));
510 assert_eq!(input.fields.len(), 3);
511
512 assert_eq!(input.fields[0].name, "name");
514 assert_eq!(input.fields[0].field_type, "String");
515 assert!(input.fields[0].nullable);
516
517 assert_eq!(input.fields[2].name, "active");
519 assert_eq!(input.fields[2].default, Some(serde_json::json!(true)));
520 }
521
522 #[test]
523 fn test_parse_interface() {
524 let json = r#"{
525 "types": [],
526 "queries": [],
527 "mutations": [],
528 "interfaces": [{
529 "name": "Node",
530 "fields": [
531 {"name": "id", "type": "ID", "nullable": false}
532 ],
533 "description": "An object with a globally unique ID"
534 }]
535 }"#;
536
537 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
538 assert_eq!(schema.interfaces.len(), 1);
539 let interface = &schema.interfaces[0];
540 assert_eq!(interface.name, "Node");
541 assert_eq!(interface.description, Some("An object with a globally unique ID".to_string()));
542 assert_eq!(interface.fields.len(), 1);
543 assert_eq!(interface.fields[0].name, "id");
544 assert_eq!(interface.fields[0].field_type, "ID");
545 assert!(!interface.fields[0].nullable);
546 }
547
548 #[test]
549 fn test_parse_type_implements_interface() {
550 let json = r#"{
551 "types": [{
552 "name": "User",
553 "fields": [
554 {"name": "id", "type": "ID", "nullable": false},
555 {"name": "name", "type": "String", "nullable": false}
556 ],
557 "implements": ["Node"]
558 }],
559 "queries": [],
560 "mutations": [],
561 "interfaces": [{
562 "name": "Node",
563 "fields": [
564 {"name": "id", "type": "ID", "nullable": false}
565 ]
566 }]
567 }"#;
568
569 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
570 assert_eq!(schema.types.len(), 1);
571 assert_eq!(schema.types[0].name, "User");
572 assert_eq!(schema.types[0].implements, vec!["Node"]);
573
574 assert_eq!(schema.interfaces.len(), 1);
575 assert_eq!(schema.interfaces[0].name, "Node");
576 }
577
578 #[test]
579 fn test_parse_input_object_with_deprecated_field() {
580 let json = r#"{
581 "types": [],
582 "queries": [],
583 "mutations": [],
584 "input_types": [{
585 "name": "CreateUserInput",
586 "fields": [
587 {"name": "email", "type": "String!", "nullable": false},
588 {"name": "name", "type": "String!", "nullable": false},
589 {
590 "name": "username",
591 "type": "String",
592 "nullable": true,
593 "deprecated": {"reason": "Use email as unique identifier instead"}
594 }
595 ]
596 }]
597 }"#;
598
599 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
600 let input = &schema.input_types[0];
601
602 let username_field = &input.fields[2];
604 assert_eq!(username_field.name, "username");
605 assert!(username_field.deprecated.is_some());
606 assert_eq!(
607 username_field.deprecated.as_ref().unwrap().reason,
608 Some("Use email as unique identifier instead".to_string())
609 );
610 }
611
612 #[test]
613 fn test_parse_union() {
614 let json = r#"{
615 "types": [
616 {"name": "User", "fields": [{"name": "id", "type": "ID", "nullable": false}]},
617 {"name": "Post", "fields": [{"name": "id", "type": "ID", "nullable": false}]}
618 ],
619 "queries": [],
620 "mutations": [],
621 "unions": [{
622 "name": "SearchResult",
623 "member_types": ["User", "Post"],
624 "description": "Result from a search query"
625 }]
626 }"#;
627
628 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
629 assert_eq!(schema.unions.len(), 1);
630 let union_def = &schema.unions[0];
631 assert_eq!(union_def.name, "SearchResult");
632 assert_eq!(union_def.member_types, vec!["User", "Post"]);
633 assert_eq!(union_def.description, Some("Result from a search query".to_string()));
634 }
635
636 #[test]
637 fn test_parse_field_with_requires_scope() {
638 let json = r#"{
639 "types": [{
640 "name": "Employee",
641 "fields": [
642 {
643 "name": "id",
644 "type": "ID",
645 "nullable": false
646 },
647 {
648 "name": "name",
649 "type": "String",
650 "nullable": false
651 },
652 {
653 "name": "salary",
654 "type": "Float",
655 "nullable": false,
656 "description": "Employee salary - protected field",
657 "requires_scope": "read:Employee.salary"
658 },
659 {
660 "name": "ssn",
661 "type": "String",
662 "nullable": true,
663 "description": "Social Security Number",
664 "requires_scope": "admin"
665 }
666 ]
667 }],
668 "queries": [],
669 "mutations": []
670 }"#;
671
672 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
673 assert_eq!(schema.types.len(), 1);
674
675 let employee = &schema.types[0];
676 assert_eq!(employee.name, "Employee");
677 assert_eq!(employee.fields.len(), 4);
678
679 assert_eq!(employee.fields[0].name, "id");
681 assert!(employee.fields[0].requires_scope.is_none());
682
683 assert_eq!(employee.fields[1].name, "name");
685 assert!(employee.fields[1].requires_scope.is_none());
686
687 assert_eq!(employee.fields[2].name, "salary");
689 assert_eq!(employee.fields[2].requires_scope, Some("read:Employee.salary".to_string()));
690
691 assert_eq!(employee.fields[3].name, "ssn");
693 assert_eq!(employee.fields[3].requires_scope, Some("admin".to_string()));
694 }
695}