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