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