Skip to main content

fraiseql_cli/schema/
intermediate.rs

1//! Intermediate Schema Format
2//!
3//! Language-agnostic schema representation that all language libraries output.
4//! See .`claude/INTERMEDIATE_SCHEMA_FORMAT.md` for full specification.
5
6use indexmap::IndexMap;
7use fraiseql_core::validation::ValidationRule;
8use serde::{Deserialize, Serialize};
9
10/// Intermediate schema - universal format from all language libraries
11#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
12pub struct IntermediateSchema {
13    /// Schema format version
14    #[serde(default = "default_version")]
15    pub version: String,
16
17    /// GraphQL object types
18    #[serde(default)]
19    pub types: Vec<IntermediateType>,
20
21    /// GraphQL enum types
22    #[serde(default)]
23    pub enums: Vec<IntermediateEnum>,
24
25    /// GraphQL input object types
26    #[serde(default)]
27    pub input_types: Vec<IntermediateInputObject>,
28
29    /// GraphQL interface types (per GraphQL spec §3.7)
30    #[serde(default)]
31    pub interfaces: Vec<IntermediateInterface>,
32
33    /// GraphQL union types (per GraphQL spec §3.10)
34    #[serde(default)]
35    pub unions: Vec<IntermediateUnion>,
36
37    /// GraphQL queries
38    #[serde(default)]
39    pub queries: Vec<IntermediateQuery>,
40
41    /// GraphQL mutations
42    #[serde(default)]
43    pub mutations: Vec<IntermediateMutation>,
44
45    /// GraphQL subscriptions
46    #[serde(default)]
47    pub subscriptions: Vec<IntermediateSubscription>,
48
49    /// GraphQL fragments (reusable field selections)
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub fragments: Option<Vec<IntermediateFragment>>,
52
53    /// GraphQL directive definitions (custom directives)
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub directives: Option<Vec<IntermediateDirective>>,
56
57    /// Analytics fact tables (optional)
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub fact_tables: Option<Vec<IntermediateFactTable>>,
60
61    /// Analytics aggregate queries (optional)
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub aggregate_queries: Option<Vec<IntermediateAggregateQuery>>,
64
65    /// Observer definitions (database change event listeners)
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub observers: Option<Vec<IntermediateObserver>>,
68
69    /// Custom scalar type definitions
70    ///
71    /// Defines custom GraphQL scalar types with validation rules.
72    /// Custom scalars can be defined in Python, TypeScript, Java, Go, and Rust SDKs,
73    /// and are compiled into the CompiledSchema's CustomTypeRegistry.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub custom_scalars: Option<Vec<IntermediateScalar>>,
76
77    /// Security configuration (from fraiseql.toml)
78    /// Compiled from the security section of fraiseql.toml at compile time.
79    /// Optional - if not provided, defaults are used.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub security: Option<serde_json::Value>,
82
83    /// Observers/event system configuration (from fraiseql.toml).
84    ///
85    /// Contains backend connection settings (redis_url, nats_url, etc.) compiled
86    /// from the `[observers]` TOML section. Embedded verbatim into the compiled schema.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub observers_config: Option<serde_json::Value>,
89
90    /// Federation configuration (from fraiseql.toml).
91    ///
92    /// Contains Apollo Federation settings and circuit breaker configuration compiled
93    /// from the `[federation]` TOML section. Embedded verbatim into the compiled schema.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub federation_config: Option<serde_json::Value>,
96
97    /// WebSocket subscription configuration (hooks, limits).
98    ///
99    /// Compiled from the `[subscriptions]` TOML section. Embedded verbatim into
100    /// the compiled schema for server-side consumption.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub subscriptions_config: Option<serde_json::Value>,
103
104    /// Query validation config (depth/complexity limits).
105    ///
106    /// Compiled from `[validation]` in `fraiseql.toml`. Embedded into the compiled
107    /// schema for server-side consumption.
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub validation_config: Option<serde_json::Value>,
110
111    /// Debug/development configuration.
112    ///
113    /// Compiled from `[debug]` in `fraiseql.toml`. Embedded into the compiled
114    /// schema for server-side consumption.
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub debug_config: Option<serde_json::Value>,
117
118    /// MCP (Model Context Protocol) server configuration.
119    ///
120    /// Compiled from `[mcp]` in `fraiseql.toml`. Embedded into the compiled
121    /// schema for server-side consumption.
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub mcp_config: Option<serde_json::Value>,
124
125    /// Global auto-param defaults for list queries (injected from TOML by the merger).
126    ///
127    /// Never present in `schema.json` — populated at compile time from `[query_defaults]`
128    /// in `fraiseql.toml`. Used by the converter to resolve per-query `auto_params`.
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub query_defaults: Option<IntermediateQueryDefaults>,
131}
132
133fn default_version() -> String {
134    "2.0.0".to_string()
135}
136
137/// Type definition in intermediate format
138#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
139pub struct IntermediateType {
140    /// Type name (e.g., "User")
141    pub name: String,
142
143    /// Type fields
144    pub fields: Vec<IntermediateField>,
145
146    /// Type description (from docstring)
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub description: Option<String>,
149
150    /// Interfaces this type implements (GraphQL spec §3.6)
151    #[serde(default, skip_serializing_if = "Vec::is_empty")]
152    pub implements: Vec<String>,
153
154    /// Role required to see this type in introspection and access queries returning it.
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub requires_role: Option<String>,
157
158    /// Whether this type is a mutation error type (tagged with `@fraiseql.error`).
159    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
160    pub is_error: bool,
161
162    /// Whether this type implements the Relay Node interface.
163    /// When true, the compiler generates global node IDs (`base64("TypeName:uuid")`)
164    /// and validates that `pk_{entity}` (BIGINT) is present in the view's data JSONB.
165    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
166    pub relay: bool,
167}
168
169/// Field definition in intermediate format
170///
171/// **NOTE**: Uses `type` field (not `field_type`)
172/// This is the language-agnostic format. Rust conversion happens in converter.
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174pub struct IntermediateField {
175    /// Field name (e.g., "id")
176    pub name: String,
177
178    /// Field type name (e.g., "Int", "String", "User")
179    ///
180    /// **Language-agnostic**: All languages use "type", not "`field_type`"
181    #[serde(rename = "type")]
182    pub field_type: String,
183
184    /// Is field nullable?
185    pub nullable: bool,
186
187    /// Field description (from docstring)
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub description: Option<String>,
190
191    /// Applied directives (e.g., @deprecated)
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub directives: Option<Vec<IntermediateAppliedDirective>>,
194
195    /// Scope required to access this field (field-level access control)
196    ///
197    /// When set, users must have this scope in their JWT to query this field.
198    /// Supports patterns like "read:Type.field" or custom scopes like "hr:view_pii".
199    ///
200    /// # Example
201    ///
202    /// ```json
203    /// {
204    ///   "name": "salary",
205    ///   "type": "Int",
206    ///   "nullable": false,
207    ///   "requires_scope": "read:Employee.salary"
208    /// }
209    /// ```
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub requires_scope: Option<String>,
212
213    /// Policy when the user lacks `requires_scope`: `"reject"` (default) or `"mask"`.
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    pub on_deny: Option<String>,
216}
217
218// =============================================================================
219// Enum Definitions
220// =============================================================================
221
222/// GraphQL enum type definition in intermediate format.
223///
224/// Enums represent a finite set of possible values.
225///
226/// # Example JSON
227///
228/// ```json
229/// {
230///   "name": "OrderStatus",
231///   "values": [
232///     {"name": "PENDING"},
233///     {"name": "PROCESSING"},
234///     {"name": "SHIPPED", "description": "Package has been shipped"},
235///     {"name": "DELIVERED"}
236///   ],
237///   "description": "Possible states of an order"
238/// }
239/// ```
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241pub struct IntermediateEnum {
242    /// Enum type name (e.g., "OrderStatus")
243    pub name: String,
244
245    /// Possible values for this enum
246    pub values: Vec<IntermediateEnumValue>,
247
248    /// Enum description (from docstring)
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub description: Option<String>,
251}
252
253/// A single value within an enum type.
254///
255/// # Example JSON
256///
257/// ```json
258/// {
259///   "name": "ACTIVE",
260///   "description": "The item is currently active",
261///   "deprecated": {"reason": "Use ENABLED instead"}
262/// }
263/// ```
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265pub struct IntermediateEnumValue {
266    /// Value name (e.g., "PENDING")
267    pub name: String,
268
269    /// Value description (from docstring)
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub description: Option<String>,
272
273    /// Deprecation info (if value is deprecated)
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub deprecated: Option<IntermediateDeprecation>,
276}
277
278/// Deprecation information for enum values or input fields.
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280pub struct IntermediateDeprecation {
281    /// Deprecation reason (what to use instead)
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub reason: Option<String>,
284}
285
286// =============================================================================
287// Custom Scalar Definitions
288// =============================================================================
289
290/// Custom scalar type definition in intermediate format.
291///
292/// Custom scalars allow applications to define domain-specific types with validation.
293/// Scalars are defined in language SDKs (Python, TypeScript, Java, Go, Rust)
294/// and compiled into the schema.
295///
296/// # Example JSON
297///
298/// ```json
299/// {
300///   "name": "Email",
301///   "description": "Valid email address",
302///   "specified_by_url": "https://tools.ietf.org/html/rfc5322",
303///   "base_type": "String",
304///   "validation_rules": [
305///     {
306///       "type": "pattern",
307///       "value": {
308///         "pattern": "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$"
309///       }
310///     }
311///   ]
312/// }
313/// ```
314#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
315pub struct IntermediateScalar {
316    /// Scalar name (e.g., "Email", "Phone", "ISBN")
317    pub name: String,
318
319    /// Scalar description
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub description: Option<String>,
322
323    /// URL to specification/RFC (GraphQL spec §3.5.1)
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub specified_by_url: Option<String>,
326
327    /// Built-in validation rules
328    #[serde(default)]
329    pub validation_rules: Vec<ValidationRule>,
330
331    /// Base type for type aliases (e.g., "String" for Email scalar)
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub base_type: Option<String>,
334}
335
336// =============================================================================
337// Input Object Definitions
338// =============================================================================
339
340/// GraphQL input object type definition in intermediate format.
341///
342/// Input objects are used for complex query arguments like filters,
343/// ordering, and mutation inputs.
344///
345/// # Example JSON
346///
347/// ```json
348/// {
349///   "name": "UserFilter",
350///   "fields": [
351///     {"name": "name", "type": "String", "nullable": true},
352///     {"name": "email", "type": "String", "nullable": true},
353///     {"name": "active", "type": "Boolean", "nullable": true, "default": true}
354///   ],
355///   "description": "Filter criteria for users"
356/// }
357/// ```
358#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
359pub struct IntermediateInputObject {
360    /// Input object type name (e.g., "UserFilter")
361    pub name: String,
362
363    /// Input fields
364    pub fields: Vec<IntermediateInputField>,
365
366    /// Input type description (from docstring)
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub description: Option<String>,
369}
370
371/// A field within an input object type.
372///
373/// # Example JSON
374///
375/// ```json
376/// {
377///   "name": "email",
378///   "type": "String!",
379///   "description": "User's email address",
380///   "default": "user@example.com"
381/// }
382/// ```
383#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
384pub struct IntermediateInputField {
385    /// Field name
386    pub name: String,
387
388    /// Field type name (e.g., `"String!"`, `"[Int]"`, `"UserFilter"`)
389    #[serde(rename = "type")]
390    pub field_type: String,
391
392    /// Is field nullable?
393    #[serde(default)]
394    pub nullable: bool,
395
396    /// Field description (from docstring)
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub description: Option<String>,
399
400    /// Default value (as JSON)
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub default: Option<serde_json::Value>,
403
404    /// Deprecation info (if field is deprecated)
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub deprecated: Option<IntermediateDeprecation>,
407}
408
409/// Query definition in intermediate format
410#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
411pub struct IntermediateQuery {
412    /// Query name (e.g., "users")
413    pub name: String,
414
415    /// Return type name (e.g., "User")
416    pub return_type: String,
417
418    /// Returns a list?
419    #[serde(default)]
420    pub returns_list: bool,
421
422    /// Result is nullable?
423    #[serde(default)]
424    pub nullable: bool,
425
426    /// Query arguments
427    #[serde(default)]
428    pub arguments: Vec<IntermediateArgument>,
429
430    /// Query description (from docstring)
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub description: Option<String>,
433
434    /// SQL source (table/view name)
435    #[serde(skip_serializing_if = "Option::is_none")]
436    pub sql_source: Option<String>,
437
438    /// Auto-generated parameters config
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub auto_params: Option<IntermediateAutoParams>,
441
442    /// Deprecation info (from @deprecated directive)
443    #[serde(skip_serializing_if = "Option::is_none")]
444    pub deprecated: Option<IntermediateDeprecation>,
445
446    /// JSONB column name for extracting data (e.g., "data")
447    /// Used for tv_* (denormalized JSONB tables) pattern
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub jsonb_column: Option<String>,
450
451    /// Whether this is a Relay connection query.
452    /// When true, the compiler wraps results in `{ edges: [{ node, cursor }], pageInfo }`
453    /// and generates `first`/`after`/`last`/`before` arguments instead of `limit`/`offset`.
454    /// Requires `returns_list = true` and `sql_source` to be set.
455    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
456    pub relay: bool,
457
458    /// Server-injected parameters: SQL column name → source expression (e.g. `"jwt:org_id"`).
459    /// Not exposed as GraphQL arguments.
460    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
461    pub inject: IndexMap<String, String>,
462
463    /// Per-query result cache TTL in seconds. Overrides the global cache TTL for this query.
464    #[serde(default, skip_serializing_if = "Option::is_none")]
465    pub cache_ttl_seconds: Option<u64>,
466
467    /// Additional database views this query reads beyond the primary `sql_source`.
468    ///
469    /// Used for correct cache invalidation when a query JOINs or reads multiple views.
470    /// Each entry is validated as a safe SQL identifier at schema compile time.
471    #[serde(default, skip_serializing_if = "Vec::is_empty")]
472    pub additional_views: Vec<String>,
473
474    /// Role required to execute this query and see it in introspection.
475    #[serde(default, skip_serializing_if = "Option::is_none")]
476    pub requires_role: Option<String>,
477
478    /// Relay cursor column type: `"uuid"` for UUID PKs, `"int64"` (or absent) for bigint PKs.
479    /// Only meaningful when `relay = true`.
480    #[serde(default, skip_serializing_if = "Option::is_none")]
481    pub relay_cursor_type: Option<String>,
482}
483
484/// Mutation definition in intermediate format
485#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
486pub struct IntermediateMutation {
487    /// Mutation name (e.g., "createUser")
488    pub name: String,
489
490    /// Return type name (e.g., "User")
491    pub return_type: String,
492
493    /// Returns a list?
494    #[serde(default)]
495    pub returns_list: bool,
496
497    /// Result is nullable?
498    #[serde(default)]
499    pub nullable: bool,
500
501    /// Mutation arguments
502    #[serde(default)]
503    pub arguments: Vec<IntermediateArgument>,
504
505    /// Mutation description (from docstring)
506    #[serde(skip_serializing_if = "Option::is_none")]
507    pub description: Option<String>,
508
509    /// SQL source (function name)
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub sql_source: Option<String>,
512
513    /// Operation type (CREATE, UPDATE, DELETE, CUSTOM)
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub operation: Option<String>,
516
517    /// Deprecation info (from @deprecated directive)
518    #[serde(skip_serializing_if = "Option::is_none")]
519    pub deprecated: Option<IntermediateDeprecation>,
520
521    /// Server-injected parameters: SQL parameter name → source expression (e.g. `"jwt:org_id"`).
522    /// Not exposed as GraphQL arguments.
523    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
524    pub inject: IndexMap<String, String>,
525
526    /// Fact tables whose version counter should be bumped after this mutation succeeds.
527    ///
528    /// Used for correct invalidation of analytic/aggregate cache entries.
529    #[serde(default, skip_serializing_if = "Vec::is_empty")]
530    pub invalidates_fact_tables: Vec<String>,
531
532    /// View names whose cached query results should be invalidated after this
533    /// mutation succeeds.
534    #[serde(default, skip_serializing_if = "Vec::is_empty")]
535    pub invalidates_views: Vec<String>,
536}
537
538// =============================================================================
539// Interface Definitions (GraphQL Spec §3.7)
540// =============================================================================
541
542/// GraphQL interface type definition in intermediate format.
543///
544/// Interfaces define a common set of fields that multiple object types can implement.
545/// Per GraphQL spec §3.7, interfaces enable polymorphic queries.
546///
547/// # Example JSON
548///
549/// ```json
550/// {
551///   "name": "Node",
552///   "fields": [
553///     {"name": "id", "type": "ID", "nullable": false}
554///   ],
555///   "description": "An object with a globally unique ID"
556/// }
557/// ```
558#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
559pub struct IntermediateInterface {
560    /// Interface name (e.g., "Node")
561    pub name: String,
562
563    /// Interface fields (all implementing types must have these fields)
564    pub fields: Vec<IntermediateField>,
565
566    /// Interface description (from docstring)
567    #[serde(skip_serializing_if = "Option::is_none")]
568    pub description: Option<String>,
569}
570
571/// Argument definition in intermediate format
572#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
573pub struct IntermediateArgument {
574    /// Argument name
575    pub name: String,
576
577    /// Argument type name
578    ///
579    /// **Language-agnostic**: Uses "type", not "`arg_type`"
580    #[serde(rename = "type")]
581    pub arg_type: String,
582
583    /// Is argument optional?
584    pub nullable: bool,
585
586    /// Default value (JSON)
587    #[serde(skip_serializing_if = "Option::is_none")]
588    pub default: Option<serde_json::Value>,
589
590    /// Deprecation info (from @deprecated directive)
591    #[serde(skip_serializing_if = "Option::is_none")]
592    pub deprecated: Option<IntermediateDeprecation>,
593}
594
595// =============================================================================
596// Union Definitions (GraphQL Spec §3.10)
597// =============================================================================
598
599/// GraphQL union type definition in intermediate format.
600///
601/// Unions represent a type that could be one of several object types.
602/// Per GraphQL spec §3.10, unions are abstract types with member types.
603/// Unlike interfaces, unions don't define common fields.
604///
605/// # Example JSON
606///
607/// ```json
608/// {
609///   "name": "SearchResult",
610///   "member_types": ["User", "Post", "Comment"],
611///   "description": "A result from a search query"
612/// }
613/// ```
614#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
615pub struct IntermediateUnion {
616    /// Union type name (e.g., "SearchResult")
617    pub name: String,
618
619    /// Member types (object type names that belong to this union)
620    pub member_types: Vec<String>,
621
622    /// Union description (from docstring)
623    #[serde(skip_serializing_if = "Option::is_none")]
624    pub description: Option<String>,
625}
626
627/// Auto-params configuration in intermediate format.
628///
629/// Each field is `Option<bool>`: `None` means "not specified — inherit from
630/// `[query_defaults]`"; `Some(v)` means explicitly set by the authoring-language decorator.
631#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
632pub struct IntermediateAutoParams {
633    /// Enable automatic limit parameter (None = inherit from query_defaults)
634    #[serde(default, skip_serializing_if = "Option::is_none")]
635    pub limit:        Option<bool>,
636    /// Enable automatic offset parameter (None = inherit from query_defaults)
637    #[serde(default, skip_serializing_if = "Option::is_none")]
638    pub offset:       Option<bool>,
639    /// Enable automatic where clause parameter (None = inherit from query_defaults)
640    #[serde(rename = "where", default, skip_serializing_if = "Option::is_none")]
641    pub where_clause: Option<bool>,
642    /// Enable automatic order_by parameter (None = inherit from query_defaults)
643    #[serde(default, skip_serializing_if = "Option::is_none")]
644    pub order_by:     Option<bool>,
645}
646
647/// Global auto-param defaults for list queries (injected from TOML by the merger).
648///
649/// Never present in `schema.json` — set only at compile time via `[query_defaults]`
650/// in `fraiseql.toml`.
651///
652/// The `Default` implementation returns all-`true`, matching the historical behaviour
653/// when no `[query_defaults]` section is present in TOML.
654#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
655pub struct IntermediateQueryDefaults {
656    /// Default for `where` parameter
657    pub where_clause: bool,
658    /// Default for `order_by` parameter
659    pub order_by:     bool,
660    /// Default for `limit` parameter
661    pub limit:        bool,
662    /// Default for `offset` parameter
663    pub offset:       bool,
664}
665
666impl Default for IntermediateQueryDefaults {
667    fn default() -> Self {
668        Self { where_clause: true, order_by: true, limit: true, offset: true }
669    }
670}
671
672// =============================================================================
673// Subscription Definitions
674// =============================================================================
675
676/// Subscription definition in intermediate format.
677///
678/// Subscriptions provide real-time event streams for GraphQL clients.
679///
680/// # Example JSON
681///
682/// ```json
683/// {
684///   "name": "orderUpdated",
685///   "return_type": "Order",
686///   "arguments": [
687///     {"name": "orderId", "type": "ID", "nullable": true}
688///   ],
689///   "topic": "order_events",
690///   "filter": {
691///     "conditions": [
692///       {"argument": "orderId", "path": "$.id"}
693///     ]
694///   },
695///   "description": "Stream of order update events"
696/// }
697/// ```
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
699pub struct IntermediateSubscription {
700    /// Subscription name (e.g., "orderUpdated")
701    pub name: String,
702
703    /// Return type name (e.g., "Order")
704    pub return_type: String,
705
706    /// Subscription arguments (for filtering events)
707    #[serde(default)]
708    pub arguments: Vec<IntermediateArgument>,
709
710    /// Subscription description (from docstring)
711    #[serde(skip_serializing_if = "Option::is_none")]
712    pub description: Option<String>,
713
714    /// Event topic to subscribe to (e.g., "order_events")
715    #[serde(skip_serializing_if = "Option::is_none")]
716    pub topic: Option<String>,
717
718    /// Filter configuration for event matching
719    #[serde(skip_serializing_if = "Option::is_none")]
720    pub filter: Option<IntermediateSubscriptionFilter>,
721
722    /// Fields to project from event data
723    #[serde(default, skip_serializing_if = "Vec::is_empty")]
724    pub fields: Vec<String>,
725
726    /// Deprecation info (from @deprecated directive)
727    #[serde(skip_serializing_if = "Option::is_none")]
728    pub deprecated: Option<IntermediateDeprecation>,
729}
730
731/// Subscription filter definition for event matching.
732///
733/// Maps subscription arguments to JSONB paths in event data.
734#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
735pub struct IntermediateSubscriptionFilter {
736    /// Filter conditions mapping arguments to event data paths
737    pub conditions: Vec<IntermediateFilterCondition>,
738}
739
740/// A single filter condition for subscription event matching.
741#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
742pub struct IntermediateFilterCondition {
743    /// Argument name from subscription arguments
744    pub argument: String,
745
746    /// JSON path to the value in event data (e.g., "$.id", "$.order_status")
747    pub path: String,
748}
749
750// =============================================================================
751// Fragment and Directive Definitions (GraphQL Spec §2.9-2.12)
752// =============================================================================
753
754/// Fragment definition in intermediate format.
755///
756/// Fragments are reusable field selections that can be spread into queries.
757/// Per GraphQL spec §2.9-2.10, fragments have a type condition and field list.
758///
759/// # Example JSON
760///
761/// ```json
762/// {
763///   "name": "UserFields",
764///   "on": "User",
765///   "fields": ["id", "name", "email"]
766/// }
767/// ```
768#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
769pub struct IntermediateFragment {
770    /// Fragment name (e.g., "UserFields")
771    pub name: String,
772
773    /// Type condition - the type this fragment applies to (e.g., "User")
774    #[serde(rename = "on")]
775    pub type_condition: String,
776
777    /// Fields to select (can be field names or nested fragment spreads)
778    pub fields: Vec<IntermediateFragmentField>,
779
780    /// Fragment description (from docstring)
781    #[serde(skip_serializing_if = "Option::is_none")]
782    pub description: Option<String>,
783}
784
785/// Fragment field selection - either a simple field or a nested object/fragment spread.
786#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
787#[serde(untagged)]
788pub enum IntermediateFragmentField {
789    /// Simple field name (e.g., "id", "name")
790    Simple(String),
791
792    /// Complex field with nested selections or directives
793    Complex(IntermediateFragmentFieldDef),
794}
795
796/// Complex fragment field definition with optional alias, directives, and nested fields.
797#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
798pub struct IntermediateFragmentFieldDef {
799    /// Field name (source field in the type)
800    pub name: String,
801
802    /// Output alias (optional, per GraphQL spec §2.13)
803    #[serde(skip_serializing_if = "Option::is_none")]
804    pub alias: Option<String>,
805
806    /// Nested field selections (for object fields)
807    #[serde(skip_serializing_if = "Option::is_none")]
808    pub fields: Option<Vec<IntermediateFragmentField>>,
809
810    /// Fragment spread (e.g., "...UserFields")
811    #[serde(skip_serializing_if = "Option::is_none")]
812    pub spread: Option<String>,
813
814    /// Applied directives (e.g., @skip, @include)
815    #[serde(skip_serializing_if = "Option::is_none")]
816    pub directives: Option<Vec<IntermediateAppliedDirective>>,
817}
818
819/// Directive definition in intermediate format.
820///
821/// Directives provide a way to describe alternate runtime execution and type validation.
822/// Per GraphQL spec §2.12, directives can be applied to various locations.
823///
824/// # Example JSON
825///
826/// ```json
827/// {
828///   "name": "auth",
829///   "locations": ["FIELD_DEFINITION", "OBJECT"],
830///   "arguments": [{"name": "role", "type": "String", "nullable": false}],
831///   "description": "Requires authentication with specified role"
832/// }
833/// ```
834#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
835pub struct IntermediateDirective {
836    /// Directive name (without @, e.g., "auth", "deprecated")
837    pub name: String,
838
839    /// Valid locations where this directive can be applied
840    pub locations: Vec<String>,
841
842    /// Directive arguments
843    #[serde(default)]
844    pub arguments: Vec<IntermediateArgument>,
845
846    /// Whether the directive can be applied multiple times
847    #[serde(default)]
848    pub repeatable: bool,
849
850    /// Directive description
851    #[serde(skip_serializing_if = "Option::is_none")]
852    pub description: Option<String>,
853}
854
855/// An applied directive instance (used on fields, types, etc.).
856///
857/// # Example JSON
858///
859/// ```json
860/// {
861///   "name": "skip",
862///   "arguments": {"if": true}
863/// }
864/// ```
865#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
866pub struct IntermediateAppliedDirective {
867    /// Directive name (without @)
868    pub name: String,
869
870    /// Directive arguments as key-value pairs
871    #[serde(default, skip_serializing_if = "Option::is_none")]
872    pub arguments: Option<serde_json::Value>,
873}
874
875// =============================================================================
876// Analytics Definitions
877// =============================================================================
878
879/// Fact table definition in intermediate format (Analytics)
880#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
881pub struct IntermediateFactTable {
882    /// Name of the fact table
883    pub table_name:           String,
884    /// Measure columns (numeric aggregates)
885    pub measures:             Vec<IntermediateMeasure>,
886    /// Dimension metadata
887    pub dimensions:           IntermediateDimensions,
888    /// Denormalized filter columns
889    pub denormalized_filters: Vec<IntermediateFilter>,
890}
891
892/// Measure column definition
893#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
894pub struct IntermediateMeasure {
895    /// Measure column name
896    pub name:     String,
897    /// SQL data type of the measure
898    pub sql_type: String,
899    /// Whether the column can be NULL
900    pub nullable: bool,
901}
902
903/// Dimensions metadata
904#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
905pub struct IntermediateDimensions {
906    /// Dimension name
907    pub name:  String,
908    /// Paths to dimension fields within JSONB
909    pub paths: Vec<IntermediateDimensionPath>,
910}
911
912/// Dimension path within JSONB
913#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
914pub struct IntermediateDimensionPath {
915    /// Path name identifier
916    pub name:      String,
917    /// JSON path (accepts both "`json_path`" and "path" for cross-language compat)
918    #[serde(alias = "path")]
919    pub json_path: String,
920    /// Data type (accepts both "`data_type`" and "type" for cross-language compat)
921    #[serde(alias = "type")]
922    pub data_type: String,
923}
924
925/// Denormalized filter column
926#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
927pub struct IntermediateFilter {
928    /// Filter column name
929    pub name:     String,
930    /// SQL data type of the filter
931    pub sql_type: String,
932    /// Whether this column should be indexed
933    pub indexed:  bool,
934}
935
936/// Aggregate query definition (Analytics)
937#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
938pub struct IntermediateAggregateQuery {
939    /// Aggregate query name
940    pub name:            String,
941    /// Fact table to aggregate from
942    pub fact_table:      String,
943    /// Automatically generate GROUP BY clauses
944    pub auto_group_by:   bool,
945    /// Automatically generate aggregate functions
946    pub auto_aggregates: bool,
947    /// Optional description
948    #[serde(skip_serializing_if = "Option::is_none")]
949    pub description:     Option<String>,
950}
951
952// =============================================================================
953// Observer Definitions
954// =============================================================================
955
956/// Observer definition in intermediate format.
957///
958/// Observers listen to database change events (INSERT/UPDATE/DELETE) and execute
959/// actions (webhooks, Slack, email) when conditions are met.
960///
961/// # Example JSON
962///
963/// ```json
964/// {
965///   "name": "onHighValueOrder",
966///   "entity": "Order",
967///   "event": "INSERT",
968///   "condition": "total > 1000",
969///   "actions": [
970///     {
971///       "type": "webhook",
972///       "url": "https://api.example.com/orders",
973///       "headers": {"Content-Type": "application/json"}
974///     },
975///     {
976///       "type": "slack",
977///       "channel": "#sales",
978///       "message": "New order: {id}",
979///       "webhook_url_env": "SLACK_WEBHOOK_URL"
980///     }
981///   ],
982///   "retry": {
983///     "max_attempts": 3,
984///     "backoff_strategy": "exponential",
985///     "initial_delay_ms": 100,
986///     "max_delay_ms": 60000
987///   }
988/// }
989/// ```
990#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
991pub struct IntermediateObserver {
992    /// Observer name (unique identifier)
993    pub name: String,
994
995    /// Entity type to observe (e.g., "Order", "User")
996    pub entity: String,
997
998    /// Event type: INSERT, UPDATE, or DELETE
999    pub event: String,
1000
1001    /// Actions to execute when observer triggers
1002    pub actions: Vec<IntermediateObserverAction>,
1003
1004    /// Optional condition expression in FraiseQL DSL
1005    #[serde(skip_serializing_if = "Option::is_none")]
1006    pub condition: Option<String>,
1007
1008    /// Retry configuration for action execution
1009    pub retry: IntermediateRetryConfig,
1010}
1011
1012/// Observer action (webhook, Slack, email, etc.).
1013///
1014/// Actions are stored as flexible JSON objects since they have different
1015/// structures based on action type.
1016pub type IntermediateObserverAction = serde_json::Value;
1017
1018/// Retry configuration for observer actions.
1019///
1020/// # Example JSON
1021///
1022/// ```json
1023/// {
1024///   "max_attempts": 5,
1025///   "backoff_strategy": "exponential",
1026///   "initial_delay_ms": 100,
1027///   "max_delay_ms": 60000
1028/// }
1029/// ```
1030#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1031pub struct IntermediateRetryConfig {
1032    /// Maximum number of retry attempts
1033    pub max_attempts: u32,
1034
1035    /// Backoff strategy: exponential, linear, or fixed
1036    pub backoff_strategy: String,
1037
1038    /// Initial delay in milliseconds
1039    pub initial_delay_ms: u32,
1040
1041    /// Maximum delay in milliseconds
1042    pub max_delay_ms: u32,
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047    use super::*;
1048
1049    #[test]
1050    fn test_parse_minimal_schema() {
1051        let json = r#"{
1052            "types": [],
1053            "queries": [],
1054            "mutations": []
1055        }"#;
1056
1057        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1058        assert_eq!(schema.version, "2.0.0");
1059        assert_eq!(schema.types.len(), 0);
1060        assert_eq!(schema.queries.len(), 0);
1061        assert_eq!(schema.mutations.len(), 0);
1062    }
1063
1064    #[test]
1065    fn test_parse_type_with_type_field() {
1066        let json = r#"{
1067            "types": [{
1068                "name": "User",
1069                "fields": [
1070                    {
1071                        "name": "id",
1072                        "type": "Int",
1073                        "nullable": false
1074                    },
1075                    {
1076                        "name": "name",
1077                        "type": "String",
1078                        "nullable": false
1079                    }
1080                ]
1081            }],
1082            "queries": [],
1083            "mutations": []
1084        }"#;
1085
1086        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1087        assert_eq!(schema.types.len(), 1);
1088        assert_eq!(schema.types[0].name, "User");
1089        assert_eq!(schema.types[0].fields.len(), 2);
1090        assert_eq!(schema.types[0].fields[0].name, "id");
1091        assert_eq!(schema.types[0].fields[0].field_type, "Int");
1092        assert!(!schema.types[0].fields[0].nullable);
1093    }
1094
1095    #[test]
1096    fn test_parse_query_with_arguments() {
1097        let json = r#"{
1098            "types": [],
1099            "queries": [{
1100                "name": "users",
1101                "return_type": "User",
1102                "returns_list": true,
1103                "nullable": false,
1104                "arguments": [
1105                    {
1106                        "name": "limit",
1107                        "type": "Int",
1108                        "nullable": false,
1109                        "default": 10
1110                    }
1111                ],
1112                "sql_source": "v_user"
1113            }],
1114            "mutations": []
1115        }"#;
1116
1117        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1118        assert_eq!(schema.queries.len(), 1);
1119        assert_eq!(schema.queries[0].arguments.len(), 1);
1120        assert_eq!(schema.queries[0].arguments[0].arg_type, "Int");
1121        assert_eq!(schema.queries[0].arguments[0].default, Some(serde_json::json!(10)));
1122    }
1123
1124    #[test]
1125    fn test_parse_fragment_simple() {
1126        let json = r#"{
1127            "types": [],
1128            "queries": [],
1129            "mutations": [],
1130            "fragments": [{
1131                "name": "UserFields",
1132                "on": "User",
1133                "fields": ["id", "name", "email"]
1134            }]
1135        }"#;
1136
1137        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1138        assert!(schema.fragments.is_some());
1139        let fragments = schema.fragments.unwrap();
1140        assert_eq!(fragments.len(), 1);
1141        assert_eq!(fragments[0].name, "UserFields");
1142        assert_eq!(fragments[0].type_condition, "User");
1143        assert_eq!(fragments[0].fields.len(), 3);
1144
1145        // Check simple fields
1146        match &fragments[0].fields[0] {
1147            IntermediateFragmentField::Simple(name) => assert_eq!(name, "id"),
1148            IntermediateFragmentField::Complex(_) => panic!("Expected simple field"),
1149        }
1150    }
1151
1152    #[test]
1153    fn test_parse_fragment_with_nested_fields() {
1154        let json = r#"{
1155            "types": [],
1156            "queries": [],
1157            "mutations": [],
1158            "fragments": [{
1159                "name": "PostFields",
1160                "on": "Post",
1161                "fields": [
1162                    "id",
1163                    "title",
1164                    {
1165                        "name": "author",
1166                        "alias": "writer",
1167                        "fields": ["id", "name"]
1168                    }
1169                ]
1170            }]
1171        }"#;
1172
1173        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1174        let fragments = schema.fragments.unwrap();
1175        assert_eq!(fragments[0].fields.len(), 3);
1176
1177        // Check nested field
1178        match &fragments[0].fields[2] {
1179            IntermediateFragmentField::Complex(def) => {
1180                assert_eq!(def.name, "author");
1181                assert_eq!(def.alias, Some("writer".to_string()));
1182                assert!(def.fields.is_some());
1183                assert_eq!(def.fields.as_ref().unwrap().len(), 2);
1184            },
1185            IntermediateFragmentField::Simple(_) => panic!("Expected complex field"),
1186        }
1187    }
1188
1189    #[test]
1190    fn test_parse_directive_definition() {
1191        let json = r#"{
1192            "types": [],
1193            "queries": [],
1194            "mutations": [],
1195            "directives": [{
1196                "name": "auth",
1197                "locations": ["FIELD_DEFINITION", "OBJECT"],
1198                "arguments": [
1199                    {"name": "role", "type": "String", "nullable": false}
1200                ],
1201                "description": "Requires authentication"
1202            }]
1203        }"#;
1204
1205        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1206        assert!(schema.directives.is_some());
1207        let directives = schema.directives.unwrap();
1208        assert_eq!(directives.len(), 1);
1209        assert_eq!(directives[0].name, "auth");
1210        assert_eq!(directives[0].locations, vec!["FIELD_DEFINITION", "OBJECT"]);
1211        assert_eq!(directives[0].arguments.len(), 1);
1212        assert_eq!(directives[0].description, Some("Requires authentication".to_string()));
1213    }
1214
1215    #[test]
1216    fn test_parse_field_with_directive() {
1217        let json = r#"{
1218            "types": [{
1219                "name": "User",
1220                "fields": [
1221                    {
1222                        "name": "oldId",
1223                        "type": "Int",
1224                        "nullable": false,
1225                        "directives": [
1226                            {"name": "deprecated", "arguments": {"reason": "Use 'id' instead"}}
1227                        ]
1228                    }
1229                ]
1230            }],
1231            "queries": [],
1232            "mutations": []
1233        }"#;
1234
1235        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1236        let field = &schema.types[0].fields[0];
1237        assert_eq!(field.name, "oldId");
1238        assert!(field.directives.is_some());
1239        let directives = field.directives.as_ref().unwrap();
1240        assert_eq!(directives.len(), 1);
1241        assert_eq!(directives[0].name, "deprecated");
1242        assert_eq!(
1243            directives[0].arguments,
1244            Some(serde_json::json!({"reason": "Use 'id' instead"}))
1245        );
1246    }
1247
1248    #[test]
1249    fn test_parse_fragment_with_spread() {
1250        let json = r#"{
1251            "types": [],
1252            "queries": [],
1253            "mutations": [],
1254            "fragments": [
1255                {
1256                    "name": "UserFields",
1257                    "on": "User",
1258                    "fields": ["id", "name"]
1259                },
1260                {
1261                    "name": "PostWithAuthor",
1262                    "on": "Post",
1263                    "fields": [
1264                        "id",
1265                        "title",
1266                        {
1267                            "name": "author",
1268                            "spread": "UserFields"
1269                        }
1270                    ]
1271                }
1272            ]
1273        }"#;
1274
1275        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1276        let fragments = schema.fragments.unwrap();
1277        assert_eq!(fragments.len(), 2);
1278
1279        // Check the spread reference
1280        match &fragments[1].fields[2] {
1281            IntermediateFragmentField::Complex(def) => {
1282                assert_eq!(def.name, "author");
1283                assert_eq!(def.spread, Some("UserFields".to_string()));
1284            },
1285            IntermediateFragmentField::Simple(_) => panic!("Expected complex field"),
1286        }
1287    }
1288
1289    #[test]
1290    fn test_parse_enum() {
1291        let json = r#"{
1292            "types": [],
1293            "queries": [],
1294            "mutations": [],
1295            "enums": [{
1296                "name": "OrderStatus",
1297                "values": [
1298                    {"name": "PENDING"},
1299                    {"name": "PROCESSING", "description": "Currently being processed"},
1300                    {"name": "SHIPPED"},
1301                    {"name": "DELIVERED"}
1302                ],
1303                "description": "Possible states of an order"
1304            }]
1305        }"#;
1306
1307        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1308        assert_eq!(schema.enums.len(), 1);
1309        let enum_def = &schema.enums[0];
1310        assert_eq!(enum_def.name, "OrderStatus");
1311        assert_eq!(enum_def.description, Some("Possible states of an order".to_string()));
1312        assert_eq!(enum_def.values.len(), 4);
1313        assert_eq!(enum_def.values[0].name, "PENDING");
1314        assert_eq!(enum_def.values[1].description, Some("Currently being processed".to_string()));
1315    }
1316
1317    #[test]
1318    fn test_parse_enum_with_deprecated_value() {
1319        let json = r#"{
1320            "types": [],
1321            "queries": [],
1322            "mutations": [],
1323            "enums": [{
1324                "name": "UserRole",
1325                "values": [
1326                    {"name": "ADMIN"},
1327                    {"name": "USER"},
1328                    {"name": "GUEST", "deprecated": {"reason": "Use USER with limited permissions instead"}}
1329                ]
1330            }]
1331        }"#;
1332
1333        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1334        let enum_def = &schema.enums[0];
1335        assert_eq!(enum_def.values.len(), 3);
1336
1337        // Check deprecated value
1338        let guest = &enum_def.values[2];
1339        assert_eq!(guest.name, "GUEST");
1340        assert!(guest.deprecated.is_some());
1341        assert_eq!(
1342            guest.deprecated.as_ref().unwrap().reason,
1343            Some("Use USER with limited permissions instead".to_string())
1344        );
1345    }
1346
1347    #[test]
1348    fn test_parse_input_object() {
1349        let json = r#"{
1350            "types": [],
1351            "queries": [],
1352            "mutations": [],
1353            "input_types": [{
1354                "name": "UserFilter",
1355                "fields": [
1356                    {"name": "name", "type": "String", "nullable": true},
1357                    {"name": "email", "type": "String", "nullable": true},
1358                    {"name": "active", "type": "Boolean", "nullable": true, "default": true}
1359                ],
1360                "description": "Filter criteria for users"
1361            }]
1362        }"#;
1363
1364        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1365        assert_eq!(schema.input_types.len(), 1);
1366        let input = &schema.input_types[0];
1367        assert_eq!(input.name, "UserFilter");
1368        assert_eq!(input.description, Some("Filter criteria for users".to_string()));
1369        assert_eq!(input.fields.len(), 3);
1370
1371        // Check fields
1372        assert_eq!(input.fields[0].name, "name");
1373        assert_eq!(input.fields[0].field_type, "String");
1374        assert!(input.fields[0].nullable);
1375
1376        // Check default value
1377        assert_eq!(input.fields[2].name, "active");
1378        assert_eq!(input.fields[2].default, Some(serde_json::json!(true)));
1379    }
1380
1381    #[test]
1382    fn test_parse_interface() {
1383        let json = r#"{
1384            "types": [],
1385            "queries": [],
1386            "mutations": [],
1387            "interfaces": [{
1388                "name": "Node",
1389                "fields": [
1390                    {"name": "id", "type": "ID", "nullable": false}
1391                ],
1392                "description": "An object with a globally unique ID"
1393            }]
1394        }"#;
1395
1396        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1397        assert_eq!(schema.interfaces.len(), 1);
1398        let interface = &schema.interfaces[0];
1399        assert_eq!(interface.name, "Node");
1400        assert_eq!(interface.description, Some("An object with a globally unique ID".to_string()));
1401        assert_eq!(interface.fields.len(), 1);
1402        assert_eq!(interface.fields[0].name, "id");
1403        assert_eq!(interface.fields[0].field_type, "ID");
1404        assert!(!interface.fields[0].nullable);
1405    }
1406
1407    #[test]
1408    fn test_parse_type_implements_interface() {
1409        let json = r#"{
1410            "types": [{
1411                "name": "User",
1412                "fields": [
1413                    {"name": "id", "type": "ID", "nullable": false},
1414                    {"name": "name", "type": "String", "nullable": false}
1415                ],
1416                "implements": ["Node"]
1417            }],
1418            "queries": [],
1419            "mutations": [],
1420            "interfaces": [{
1421                "name": "Node",
1422                "fields": [
1423                    {"name": "id", "type": "ID", "nullable": false}
1424                ]
1425            }]
1426        }"#;
1427
1428        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1429        assert_eq!(schema.types.len(), 1);
1430        assert_eq!(schema.types[0].name, "User");
1431        assert_eq!(schema.types[0].implements, vec!["Node"]);
1432
1433        assert_eq!(schema.interfaces.len(), 1);
1434        assert_eq!(schema.interfaces[0].name, "Node");
1435    }
1436
1437    #[test]
1438    fn test_parse_input_object_with_deprecated_field() {
1439        let json = r#"{
1440            "types": [],
1441            "queries": [],
1442            "mutations": [],
1443            "input_types": [{
1444                "name": "CreateUserInput",
1445                "fields": [
1446                    {"name": "email", "type": "String!", "nullable": false},
1447                    {"name": "name", "type": "String!", "nullable": false},
1448                    {
1449                        "name": "username",
1450                        "type": "String",
1451                        "nullable": true,
1452                        "deprecated": {"reason": "Use email as unique identifier instead"}
1453                    }
1454                ]
1455            }]
1456        }"#;
1457
1458        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1459        let input = &schema.input_types[0];
1460
1461        // Check deprecated field
1462        let username_field = &input.fields[2];
1463        assert_eq!(username_field.name, "username");
1464        assert!(username_field.deprecated.is_some());
1465        assert_eq!(
1466            username_field.deprecated.as_ref().unwrap().reason,
1467            Some("Use email as unique identifier instead".to_string())
1468        );
1469    }
1470
1471    #[test]
1472    fn test_parse_union() {
1473        let json = r#"{
1474            "types": [
1475                {"name": "User", "fields": [{"name": "id", "type": "ID", "nullable": false}]},
1476                {"name": "Post", "fields": [{"name": "id", "type": "ID", "nullable": false}]}
1477            ],
1478            "queries": [],
1479            "mutations": [],
1480            "unions": [{
1481                "name": "SearchResult",
1482                "member_types": ["User", "Post"],
1483                "description": "Result from a search query"
1484            }]
1485        }"#;
1486
1487        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1488        assert_eq!(schema.unions.len(), 1);
1489        let union_def = &schema.unions[0];
1490        assert_eq!(union_def.name, "SearchResult");
1491        assert_eq!(union_def.member_types, vec!["User", "Post"]);
1492        assert_eq!(union_def.description, Some("Result from a search query".to_string()));
1493    }
1494
1495    #[test]
1496    fn test_parse_field_with_requires_scope() {
1497        let json = r#"{
1498            "types": [{
1499                "name": "Employee",
1500                "fields": [
1501                    {
1502                        "name": "id",
1503                        "type": "ID",
1504                        "nullable": false
1505                    },
1506                    {
1507                        "name": "name",
1508                        "type": "String",
1509                        "nullable": false
1510                    },
1511                    {
1512                        "name": "salary",
1513                        "type": "Float",
1514                        "nullable": false,
1515                        "description": "Employee salary - protected field",
1516                        "requires_scope": "read:Employee.salary"
1517                    },
1518                    {
1519                        "name": "ssn",
1520                        "type": "String",
1521                        "nullable": true,
1522                        "description": "Social Security Number",
1523                        "requires_scope": "admin"
1524                    }
1525                ]
1526            }],
1527            "queries": [],
1528            "mutations": []
1529        }"#;
1530
1531        let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
1532        assert_eq!(schema.types.len(), 1);
1533
1534        let employee = &schema.types[0];
1535        assert_eq!(employee.name, "Employee");
1536        assert_eq!(employee.fields.len(), 4);
1537
1538        // id - no scope required
1539        assert_eq!(employee.fields[0].name, "id");
1540        assert!(employee.fields[0].requires_scope.is_none());
1541
1542        // name - no scope required
1543        assert_eq!(employee.fields[1].name, "name");
1544        assert!(employee.fields[1].requires_scope.is_none());
1545
1546        // salary - requires specific scope
1547        assert_eq!(employee.fields[2].name, "salary");
1548        assert_eq!(employee.fields[2].requires_scope, Some("read:Employee.salary".to_string()));
1549
1550        // ssn - requires admin scope
1551        assert_eq!(employee.fields[3].name, "ssn");
1552        assert_eq!(employee.fields[3].requires_scope, Some("admin".to_string()));
1553    }
1554}