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