Skip to main content

fraiseql_core/schema/
compiled.rs

1//! Compiled schema types - pure Rust, no Python/TypeScript references.
2//!
3//! These types represent GraphQL schemas after compilation from authoring languages.
4//! All data is owned by Rust - no `Py<T>` or foreign object references.
5//!
6//! # Schema Freeze Invariant
7//!
8//! After `CompiledSchema::from_json()`, the schema is frozen:
9//! - All data is Rust-owned
10//! - No Python/TypeScript callbacks
11//! - No foreign object references
12//! - Safe to use from any Tokio worker thread
13//!
14//! This enables the Axum server to handle requests without any
15//! interaction with Python/TypeScript runtimes.
16
17use std::collections::HashMap;
18
19use serde::{Deserialize, Serialize};
20
21use super::field_type::{FieldDefinition, FieldType};
22use crate::validation::{CustomTypeRegistry, ValidationRule};
23
24/// Role definition for field-level RBAC.
25///
26/// Defines which GraphQL scopes a role grants access to.
27/// Used by the runtime to determine which fields a user can access
28/// based on their assigned roles.
29///
30/// # Example
31///
32/// ```json
33/// {
34///   "name": "admin",
35///   "description": "Administrator with all scopes",
36///   "scopes": ["admin:*"]
37/// }
38/// ```
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct RoleDefinition {
41    /// Role name (e.g., "admin", "user", "viewer").
42    pub name: String,
43
44    /// Optional role description for documentation.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub description: Option<String>,
47
48    /// List of scopes this role grants access to.
49    /// Scopes follow the format: `action:resource` (e.g., "read:User.email", "admin:*")
50    pub scopes: Vec<String>,
51}
52
53impl RoleDefinition {
54    /// Create a new role definition.
55    #[must_use]
56    pub fn new(name: String, scopes: Vec<String>) -> Self {
57        Self {
58            name,
59            description: None,
60            scopes,
61        }
62    }
63
64    /// Add a description to the role.
65    pub fn with_description(mut self, description: String) -> Self {
66        self.description = Some(description);
67        self
68    }
69
70    /// Check if this role has a specific scope.
71    ///
72    /// Supports exact matching and wildcard patterns:
73    /// - `read:User.email` matches exactly
74    /// - `read:*` matches any scope starting with "read:"
75    /// - `read:User.*` matches "read:User.email", "read:User.name", etc.
76    /// - `admin:*` matches any admin scope
77    #[must_use]
78    pub fn has_scope(&self, required_scope: &str) -> bool {
79        self.scopes.iter().any(|scope| {
80            if scope == "*" {
81                return true; // Wildcard matches everything
82            }
83
84            if scope == required_scope {
85                return true; // Exact match
86            }
87
88            // Handle wildcard patterns like "read:*" or "admin:*"
89            if scope.ends_with(":*") {
90                let prefix = &scope[..scope.len() - 2]; // Remove ":*"
91                return required_scope.starts_with(prefix) && required_scope.contains(':');
92            }
93
94            // Handle Type.* wildcard patterns like "read:User.*"
95            if scope.ends_with(".*") {
96                let prefix = &scope[..scope.len() - 1]; // Remove "*", keep the dot
97                return required_scope.starts_with(prefix);
98            }
99
100            false
101        })
102    }
103}
104
105/// Security configuration from fraiseql.toml.
106///
107/// Contains role definitions and other security-related settings
108/// that are compiled into schema.compiled.json.
109#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
110pub struct SecurityConfig {
111    /// Role definitions mapping role names to their granted scopes.
112    #[serde(default, skip_serializing_if = "Vec::is_empty")]
113    pub role_definitions: Vec<RoleDefinition>,
114
115    /// Default role when none is specified.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub default_role: Option<String>,
118
119    /// Additional security settings (rate limiting, audit logging, etc.)
120    #[serde(flatten)]
121    pub additional: HashMap<String, serde_json::Value>,
122}
123
124impl SecurityConfig {
125    /// Create a new empty security configuration.
126    #[must_use]
127    pub fn new() -> Self {
128        Self::default()
129    }
130
131    /// Add a role definition.
132    pub fn add_role(&mut self, role: RoleDefinition) {
133        self.role_definitions.push(role);
134    }
135
136    /// Find a role definition by name.
137    #[must_use]
138    pub fn find_role(&self, name: &str) -> Option<&RoleDefinition> {
139        self.role_definitions.iter().find(|r| r.name == name)
140    }
141
142    /// Get all scopes granted to a role.
143    #[must_use]
144    pub fn get_role_scopes(&self, role_name: &str) -> Vec<String> {
145        self.find_role(role_name).map(|role| role.scopes.clone()).unwrap_or_default()
146    }
147
148    /// Check if a role has a specific scope.
149    #[must_use]
150    pub fn role_has_scope(&self, role_name: &str, scope: &str) -> bool {
151        self.find_role(role_name).map(|role| role.has_scope(scope)).unwrap_or(false)
152    }
153}
154
155/// Complete compiled schema - all type information for serving.
156///
157/// This is the central type that holds the entire GraphQL schema
158/// after compilation from Python/TypeScript decorators.
159///
160/// # Example
161///
162/// ```
163/// use fraiseql_core::schema::CompiledSchema;
164///
165/// let json = r#"{
166///     "types": [],
167///     "queries": [],
168///     "mutations": [],
169///     "subscriptions": []
170/// }"#;
171///
172/// let schema = CompiledSchema::from_json(json).unwrap();
173/// assert_eq!(schema.types.len(), 0);
174/// ```
175#[derive(Debug, Clone, Default, Serialize, Deserialize)]
176pub struct CompiledSchema {
177    /// GraphQL object type definitions.
178    #[serde(default)]
179    pub types: Vec<TypeDefinition>,
180
181    /// GraphQL enum type definitions.
182    #[serde(default)]
183    pub enums: Vec<EnumDefinition>,
184
185    /// GraphQL input object type definitions.
186    #[serde(default)]
187    pub input_types: Vec<InputObjectDefinition>,
188
189    /// GraphQL interface type definitions.
190    #[serde(default)]
191    pub interfaces: Vec<InterfaceDefinition>,
192
193    /// GraphQL union type definitions.
194    #[serde(default)]
195    pub unions: Vec<UnionDefinition>,
196
197    /// GraphQL query definitions.
198    #[serde(default)]
199    pub queries: Vec<QueryDefinition>,
200
201    /// GraphQL mutation definitions.
202    #[serde(default)]
203    pub mutations: Vec<MutationDefinition>,
204
205    /// GraphQL subscription definitions.
206    #[serde(default)]
207    pub subscriptions: Vec<SubscriptionDefinition>,
208
209    /// Custom directive definitions.
210    /// These are user-defined directives beyond the built-in @skip, @include, @deprecated.
211    #[serde(default, skip_serializing_if = "Vec::is_empty")]
212    pub directives: Vec<DirectiveDefinition>,
213
214    /// Fact table metadata (for analytics queries).
215    /// Key: table name (e.g., `tf_sales`)
216    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
217    pub fact_tables: HashMap<String, serde_json::Value>,
218
219    /// Observer definitions (database change event listeners).
220    #[serde(default, skip_serializing_if = "Vec::is_empty")]
221    pub observers: Vec<ObserverDefinition>,
222
223    /// Federation metadata for Apollo Federation v2 support.
224    #[serde(default, skip_serializing_if = "Option::is_none")]
225    pub federation: Option<serde_json::Value>,
226
227    /// Security configuration (from fraiseql.toml).
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    pub security: Option<serde_json::Value>,
230
231    /// Observers/event system configuration (from fraiseql.toml).
232    ///
233    /// Contains backend connection settings (redis_url, nats_url, etc.) and
234    /// event handler definitions compiled from the `[observers]` TOML section.
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub observers_config: Option<serde_json::Value>,
237
238    /// Raw GraphQL schema as string (for SDL generation).
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub schema_sdl: Option<String>,
241
242    /// Custom scalar type registry.
243    ///
244    /// Contains definitions for custom scalar types defined in the schema.
245    /// Built during code generation from IRScalar definitions.
246    /// Not serialized - populated at runtime from `ir.scalars`.
247    #[serde(skip)]
248    pub custom_scalars: CustomTypeRegistry,
249}
250
251impl PartialEq for CompiledSchema {
252    fn eq(&self, other: &Self) -> bool {
253        // Compare all fields except custom_scalars (runtime state)
254        self.types == other.types
255            && self.enums == other.enums
256            && self.input_types == other.input_types
257            && self.interfaces == other.interfaces
258            && self.unions == other.unions
259            && self.queries == other.queries
260            && self.mutations == other.mutations
261            && self.subscriptions == other.subscriptions
262            && self.directives == other.directives
263            && self.fact_tables == other.fact_tables
264            && self.observers == other.observers
265            && self.federation == other.federation
266            && self.security == other.security
267            && self.observers_config == other.observers_config
268            && self.schema_sdl == other.schema_sdl
269    }
270}
271
272impl CompiledSchema {
273    /// Create empty schema.
274    #[must_use]
275    pub fn new() -> Self {
276        Self::default()
277    }
278
279    /// Deserialize from JSON string.
280    ///
281    /// This is the primary way to create a schema from Python/TypeScript.
282    /// The authoring language compiles to JSON, Rust deserializes and owns it.
283    ///
284    /// # Errors
285    ///
286    /// Returns error if JSON is malformed or doesn't match schema structure.
287    ///
288    /// # Example
289    ///
290    /// ```
291    /// use fraiseql_core::schema::CompiledSchema;
292    ///
293    /// let json = r#"{"types": [], "queries": [], "mutations": [], "subscriptions": []}"#;
294    /// let schema = CompiledSchema::from_json(json).unwrap();
295    /// ```
296    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
297        serde_json::from_str(json)
298    }
299
300    /// Serialize to JSON string.
301    ///
302    /// # Errors
303    ///
304    /// Returns error if serialization fails (should not happen for valid schema).
305    pub fn to_json(&self) -> Result<String, serde_json::Error> {
306        serde_json::to_string(self)
307    }
308
309    /// Serialize to pretty JSON string (for debugging/config files).
310    ///
311    /// # Errors
312    ///
313    /// Returns error if serialization fails.
314    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
315        serde_json::to_string_pretty(self)
316    }
317
318    /// Find a type definition by name.
319    #[must_use]
320    pub fn find_type(&self, name: &str) -> Option<&TypeDefinition> {
321        self.types.iter().find(|t| t.name == name)
322    }
323
324    /// Find an enum definition by name.
325    #[must_use]
326    pub fn find_enum(&self, name: &str) -> Option<&EnumDefinition> {
327        self.enums.iter().find(|e| e.name == name)
328    }
329
330    /// Find an input object definition by name.
331    #[must_use]
332    pub fn find_input_type(&self, name: &str) -> Option<&InputObjectDefinition> {
333        self.input_types.iter().find(|i| i.name == name)
334    }
335
336    /// Find an interface definition by name.
337    #[must_use]
338    pub fn find_interface(&self, name: &str) -> Option<&InterfaceDefinition> {
339        self.interfaces.iter().find(|i| i.name == name)
340    }
341
342    /// Find all types that implement a given interface.
343    #[must_use]
344    pub fn find_implementors(&self, interface_name: &str) -> Vec<&TypeDefinition> {
345        self.types
346            .iter()
347            .filter(|t| t.implements.contains(&interface_name.to_string()))
348            .collect()
349    }
350
351    /// Find a union definition by name.
352    #[must_use]
353    pub fn find_union(&self, name: &str) -> Option<&UnionDefinition> {
354        self.unions.iter().find(|u| u.name == name)
355    }
356
357    /// Find a query definition by name.
358    #[must_use]
359    pub fn find_query(&self, name: &str) -> Option<&QueryDefinition> {
360        self.queries.iter().find(|q| q.name == name)
361    }
362
363    /// Find a mutation definition by name.
364    #[must_use]
365    pub fn find_mutation(&self, name: &str) -> Option<&MutationDefinition> {
366        self.mutations.iter().find(|m| m.name == name)
367    }
368
369    /// Find a subscription definition by name.
370    #[must_use]
371    pub fn find_subscription(&self, name: &str) -> Option<&SubscriptionDefinition> {
372        self.subscriptions.iter().find(|s| s.name == name)
373    }
374
375    /// Find a custom directive definition by name.
376    #[must_use]
377    pub fn find_directive(&self, name: &str) -> Option<&DirectiveDefinition> {
378        self.directives.iter().find(|d| d.name == name)
379    }
380
381    /// Get total number of operations (queries + mutations + subscriptions).
382    #[must_use]
383    pub fn operation_count(&self) -> usize {
384        self.queries.len() + self.mutations.len() + self.subscriptions.len()
385    }
386
387    /// Register fact table metadata.
388    ///
389    /// # Arguments
390    ///
391    /// * `table_name` - Fact table name (e.g., `tf_sales`)
392    /// * `metadata` - Serialized `FactTableMetadata`
393    pub fn add_fact_table(&mut self, table_name: String, metadata: serde_json::Value) {
394        self.fact_tables.insert(table_name, metadata);
395    }
396
397    /// Get fact table metadata by name.
398    ///
399    /// # Arguments
400    ///
401    /// * `name` - Fact table name
402    ///
403    /// # Returns
404    ///
405    /// Fact table metadata if found
406    #[must_use]
407    pub fn get_fact_table(&self, name: &str) -> Option<&serde_json::Value> {
408        self.fact_tables.get(name)
409    }
410
411    /// List all fact table names.
412    ///
413    /// # Returns
414    ///
415    /// Vector of fact table names
416    #[must_use]
417    pub fn list_fact_tables(&self) -> Vec<&str> {
418        self.fact_tables.keys().map(String::as_str).collect()
419    }
420
421    /// Check if schema contains any fact tables.
422    #[must_use]
423    pub fn has_fact_tables(&self) -> bool {
424        !self.fact_tables.is_empty()
425    }
426
427    /// Find an observer definition by name.
428    #[must_use]
429    pub fn find_observer(&self, name: &str) -> Option<&ObserverDefinition> {
430        self.observers.iter().find(|o| o.name == name)
431    }
432
433    /// Get all observers for a specific entity type.
434    #[must_use]
435    pub fn find_observers_for_entity(&self, entity: &str) -> Vec<&ObserverDefinition> {
436        self.observers.iter().filter(|o| o.entity == entity).collect()
437    }
438
439    /// Get all observers for a specific event type (INSERT, UPDATE, DELETE).
440    #[must_use]
441    pub fn find_observers_for_event(&self, event: &str) -> Vec<&ObserverDefinition> {
442        self.observers.iter().filter(|o| o.event == event).collect()
443    }
444
445    /// Check if schema contains any observers.
446    #[must_use]
447    pub fn has_observers(&self) -> bool {
448        !self.observers.is_empty()
449    }
450
451    /// Get total number of observers.
452    #[must_use]
453    pub fn observer_count(&self) -> usize {
454        self.observers.len()
455    }
456
457    /// Get federation metadata from schema.
458    ///
459    /// # Returns
460    ///
461    /// Federation metadata if configured in schema
462    #[must_use]
463    pub fn federation_metadata(&self) -> Option<crate::federation::FederationMetadata> {
464        self.federation
465            .as_ref()
466            .and_then(|fed_json| serde_json::from_value(fed_json.clone()).ok())
467    }
468
469    /// Get security configuration from schema.
470    ///
471    /// # Returns
472    ///
473    /// Security configuration if present (includes role definitions)
474    #[must_use]
475    pub fn security_config(&self) -> Option<SecurityConfig> {
476        self.security
477            .as_ref()
478            .and_then(|sec_json| serde_json::from_value(sec_json.clone()).ok())
479    }
480
481    /// Find a role definition by name.
482    ///
483    /// # Arguments
484    ///
485    /// * `role_name` - Name of the role to find
486    ///
487    /// # Returns
488    ///
489    /// Role definition if found
490    #[must_use]
491    pub fn find_role(&self, role_name: &str) -> Option<RoleDefinition> {
492        self.security_config().and_then(|config| config.find_role(role_name).cloned())
493    }
494
495    /// Get scopes for a role.
496    ///
497    /// # Arguments
498    ///
499    /// * `role_name` - Name of the role
500    ///
501    /// # Returns
502    ///
503    /// Vector of scopes granted to the role
504    #[must_use]
505    pub fn get_role_scopes(&self, role_name: &str) -> Vec<String> {
506        self.security_config()
507            .map(|config| config.get_role_scopes(role_name))
508            .unwrap_or_default()
509    }
510
511    /// Check if a role has a specific scope.
512    ///
513    /// # Arguments
514    ///
515    /// * `role_name` - Name of the role
516    /// * `scope` - Scope to check for
517    ///
518    /// # Returns
519    ///
520    /// true if role has the scope, false otherwise
521    #[must_use]
522    pub fn role_has_scope(&self, role_name: &str, scope: &str) -> bool {
523        self.security_config()
524            .map(|config| config.role_has_scope(role_name, scope))
525            .unwrap_or(false)
526    }
527
528    /// Get raw GraphQL schema SDL.
529    ///
530    /// # Returns
531    ///
532    /// Raw schema string if available, otherwise generates from type definitions
533    #[must_use]
534    pub fn raw_schema(&self) -> String {
535        self.schema_sdl.clone().unwrap_or_else(|| {
536            // Generate basic SDL from type definitions if not provided
537            let mut sdl = String::new();
538
539            // Add types
540            for type_def in &self.types {
541                sdl.push_str(&format!("type {} {{\n", type_def.name));
542                for field in &type_def.fields {
543                    sdl.push_str(&format!("  {}: {}\n", field.name, field.field_type));
544                }
545                sdl.push_str("}\n\n");
546            }
547
548            sdl
549        })
550    }
551
552    /// Validate the schema for internal consistency.
553    ///
554    /// Checks:
555    /// - All type references resolve to defined types
556    /// - No duplicate type/operation names
557    /// - Required fields have valid types
558    ///
559    /// # Errors
560    ///
561    /// Returns list of validation errors if schema is invalid.
562    pub fn validate(&self) -> Result<(), Vec<String>> {
563        let mut errors = Vec::new();
564
565        // Check for duplicate type names
566        let mut type_names: std::collections::HashSet<&str> = std::collections::HashSet::new();
567        for type_def in &self.types {
568            if !type_names.insert(&type_def.name) {
569                errors.push(format!("Duplicate type name: {}", type_def.name));
570            }
571        }
572
573        // Check for duplicate query names
574        let mut query_names: std::collections::HashSet<&str> = std::collections::HashSet::new();
575        for query in &self.queries {
576            if !query_names.insert(&query.name) {
577                errors.push(format!("Duplicate query name: {}", query.name));
578            }
579        }
580
581        // Check for duplicate mutation names
582        let mut mutation_names: std::collections::HashSet<&str> = std::collections::HashSet::new();
583        for mutation in &self.mutations {
584            if !mutation_names.insert(&mutation.name) {
585                errors.push(format!("Duplicate mutation name: {}", mutation.name));
586            }
587        }
588
589        // Check type references in queries
590        for query in &self.queries {
591            if !type_names.contains(query.return_type.as_str())
592                && !is_builtin_type(&query.return_type)
593            {
594                errors.push(format!(
595                    "Query '{}' references undefined type '{}'",
596                    query.name, query.return_type
597                ));
598            }
599        }
600
601        // Check type references in mutations
602        for mutation in &self.mutations {
603            if !type_names.contains(mutation.return_type.as_str())
604                && !is_builtin_type(&mutation.return_type)
605            {
606                errors.push(format!(
607                    "Mutation '{}' references undefined type '{}'",
608                    mutation.name, mutation.return_type
609                ));
610            }
611        }
612
613        if errors.is_empty() {
614            Ok(())
615        } else {
616            Err(errors)
617        }
618    }
619}
620
621/// Check if a type name is a built-in scalar type.
622fn is_builtin_type(name: &str) -> bool {
623    matches!(
624        name,
625        "String"
626            | "Int"
627            | "Float"
628            | "Boolean"
629            | "ID"
630            | "DateTime"
631            | "Date"
632            | "Time"
633            | "JSON"
634            | "UUID"
635            | "Decimal"
636    )
637}
638
639/// A GraphQL type definition compiled from `@fraiseql.type`.
640///
641/// This represents a complete object type with its fields and database binding.
642///
643/// # Example
644///
645/// ```
646/// use fraiseql_core::schema::{TypeDefinition, FieldDefinition, FieldType};
647///
648/// let user_type = TypeDefinition {
649///     name: "User".to_string(),
650///     sql_source: "v_user".to_string(),
651///     jsonb_column: "data".to_string(),
652///     fields: vec![
653///         FieldDefinition::new("id", FieldType::Id),
654///         FieldDefinition::new("email", FieldType::String),
655///     ],
656///     description: Some("A user in the system".to_string()),
657///     sql_projection_hint: None,
658///     implements: vec![],
659/// };
660/// ```
661#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
662pub struct TypeDefinition {
663    /// GraphQL type name (e.g., "User").
664    pub name: String,
665
666    /// SQL source table/view (e.g., `v_user`).
667    pub sql_source: String,
668
669    /// JSONB column name (e.g., "data").
670    #[serde(default = "default_jsonb_column")]
671    pub jsonb_column: String,
672
673    /// Field definitions.
674    #[serde(default)]
675    pub fields: Vec<FieldDefinition>,
676
677    /// Optional description (from docstring).
678    #[serde(default, skip_serializing_if = "Option::is_none")]
679    pub description: Option<String>,
680
681    /// SQL projection hint for PostgreSQL optimization.
682    /// Generated at compile time to reduce payload size for large JSONB objects.
683    /// Example: `jsonb_build_object('id', data->>'id', 'email', data->>'email')`
684    #[serde(default, skip_serializing_if = "Option::is_none")]
685    pub sql_projection_hint: Option<SqlProjectionHint>,
686
687    /// Interfaces this type implements.
688    #[serde(default, skip_serializing_if = "Vec::is_empty")]
689    pub implements: Vec<String>,
690
691    /// Whether this type is a mutation error type (tagged with `@fraiseql.error`).
692    ///
693    /// Error types are populated from `mutation_response.metadata` JSONB rather than
694    /// the `entity` field.  Both scalar primitives and nested objects are supported.
695    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
696    pub is_error: bool,
697}
698
699/// SQL projection hint for database-specific field projection optimization.
700///
701/// When a type has a large JSONB payload, the compiler can generate
702/// SQL that projects only the requested fields, reducing network payload
703/// and JSON deserialization overhead.
704#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
705pub struct SqlProjectionHint {
706    /// Database type (e.g., "postgresql", "mysql", "sqlite").
707    pub database: String,
708
709    /// The projection SQL template.
710    /// Example for PostgreSQL:
711    /// `jsonb_build_object('id', data->>'id', 'email', data->>'email')`
712    pub projection_template: String,
713
714    /// Estimated reduction in payload size (percentage 0-100).
715    pub estimated_reduction_percent: u32,
716}
717
718fn default_jsonb_column() -> String {
719    "data".to_string()
720}
721
722impl TypeDefinition {
723    /// Create a new type definition.
724    #[must_use]
725    pub fn new(name: impl Into<String>, sql_source: impl Into<String>) -> Self {
726        Self {
727            name:                name.into(),
728            sql_source:          sql_source.into(),
729            jsonb_column:        "data".to_string(),
730            fields:              Vec::new(),
731            description:         None,
732            sql_projection_hint: None,
733            implements:          Vec::new(),
734            is_error:            false,
735        }
736    }
737
738    /// Add a field to this type.
739    #[must_use]
740    pub fn with_field(mut self, field: FieldDefinition) -> Self {
741        self.fields.push(field);
742        self
743    }
744
745    /// Set the JSONB column name.
746    #[must_use]
747    pub fn with_jsonb_column(mut self, column: impl Into<String>) -> Self {
748        self.jsonb_column = column.into();
749        self
750    }
751
752    /// Set the description.
753    #[must_use]
754    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
755        self.description = Some(desc.into());
756        self
757    }
758
759    /// Find a field by name (JSONB key).
760    #[must_use]
761    pub fn find_field(&self, name: &str) -> Option<&FieldDefinition> {
762        self.fields.iter().find(|f| f.name == name)
763    }
764
765    /// Find a field by its output name (alias if set, otherwise name).
766    ///
767    /// Useful for resolving field references in GraphQL queries where
768    /// aliases may be used.
769    #[must_use]
770    pub fn find_field_by_output_name(&self, output_name: &str) -> Option<&FieldDefinition> {
771        self.fields.iter().find(|f| f.output_name() == output_name)
772    }
773
774    /// Set SQL projection hint for optimization.
775    #[must_use]
776    pub fn with_sql_projection(mut self, hint: SqlProjectionHint) -> Self {
777        self.sql_projection_hint = Some(hint);
778        self
779    }
780
781    /// Check if type has SQL projection hint.
782    #[must_use]
783    pub fn has_sql_projection(&self) -> bool {
784        self.sql_projection_hint.is_some()
785    }
786
787    /// Get the `__typename` value for this type.
788    ///
789    /// Returns the GraphQL type name, used for type introspection in responses.
790    /// Per GraphQL spec ยง2.7, `__typename` returns the name of the object type.
791    #[must_use]
792    pub fn typename(&self) -> &str {
793        &self.name
794    }
795}
796
797// =============================================================================
798// Enum Definitions
799// =============================================================================
800
801/// A GraphQL enum type definition.
802///
803/// Enums represent a finite set of possible values, useful for
804/// categorization fields like status, role, or priority.
805///
806/// # Example
807///
808/// ```
809/// use fraiseql_core::schema::{EnumDefinition, EnumValueDefinition};
810///
811/// let status_enum = EnumDefinition {
812///     name: "OrderStatus".to_string(),
813///     values: vec![
814///         EnumValueDefinition::new("PENDING"),
815///         EnumValueDefinition::new("PROCESSING"),
816///         EnumValueDefinition::new("SHIPPED"),
817///         EnumValueDefinition::new("DELIVERED"),
818///     ],
819///     description: Some("Possible states of an order".to_string()),
820/// };
821/// ```
822#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
823pub struct EnumDefinition {
824    /// Enum type name (e.g., "OrderStatus").
825    pub name: String,
826
827    /// Possible values for this enum.
828    #[serde(default)]
829    pub values: Vec<EnumValueDefinition>,
830
831    /// Description of the enum type.
832    #[serde(default, skip_serializing_if = "Option::is_none")]
833    pub description: Option<String>,
834}
835
836impl EnumDefinition {
837    /// Create a new enum definition.
838    #[must_use]
839    pub fn new(name: impl Into<String>) -> Self {
840        Self {
841            name:        name.into(),
842            values:      Vec::new(),
843            description: None,
844        }
845    }
846
847    /// Add a value to this enum.
848    #[must_use]
849    pub fn with_value(mut self, value: EnumValueDefinition) -> Self {
850        self.values.push(value);
851        self
852    }
853
854    /// Add multiple values to this enum.
855    #[must_use]
856    pub fn with_values(mut self, values: Vec<EnumValueDefinition>) -> Self {
857        self.values = values;
858        self
859    }
860
861    /// Set description.
862    #[must_use]
863    pub fn with_description(mut self, description: impl Into<String>) -> Self {
864        self.description = Some(description.into());
865        self
866    }
867
868    /// Check if a value exists in this enum.
869    #[must_use]
870    pub fn has_value(&self, name: &str) -> bool {
871        self.values.iter().any(|v| v.name == name)
872    }
873
874    /// Find a value by name.
875    #[must_use]
876    pub fn find_value(&self, name: &str) -> Option<&EnumValueDefinition> {
877        self.values.iter().find(|v| v.name == name)
878    }
879}
880
881/// A single value within a GraphQL enum type.
882///
883/// # Example
884///
885/// ```
886/// use fraiseql_core::schema::EnumValueDefinition;
887///
888/// let value = EnumValueDefinition::new("ACTIVE")
889///     .with_description("The item is currently active");
890/// ```
891#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
892pub struct EnumValueDefinition {
893    /// Value name (e.g., "PENDING").
894    pub name: String,
895
896    /// Description of this value.
897    #[serde(default, skip_serializing_if = "Option::is_none")]
898    pub description: Option<String>,
899
900    /// Deprecation information (if this value is deprecated).
901    #[serde(default, skip_serializing_if = "Option::is_none")]
902    pub deprecation: Option<super::field_type::DeprecationInfo>,
903}
904
905impl EnumValueDefinition {
906    /// Create a new enum value.
907    #[must_use]
908    pub fn new(name: impl Into<String>) -> Self {
909        Self {
910            name:        name.into(),
911            description: None,
912            deprecation: None,
913        }
914    }
915
916    /// Set description.
917    #[must_use]
918    pub fn with_description(mut self, description: impl Into<String>) -> Self {
919        self.description = Some(description.into());
920        self
921    }
922
923    /// Mark this value as deprecated.
924    #[must_use]
925    pub fn deprecated(mut self, reason: Option<String>) -> Self {
926        self.deprecation = Some(super::field_type::DeprecationInfo { reason });
927        self
928    }
929
930    /// Check if this value is deprecated.
931    #[must_use]
932    pub fn is_deprecated(&self) -> bool {
933        self.deprecation.is_some()
934    }
935}
936
937// =============================================================================
938// Input Object Definitions
939// =============================================================================
940
941/// A GraphQL input object type definition.
942///
943/// Input objects are used for complex query arguments like filters,
944/// ordering, and mutation inputs.
945///
946/// # Example
947///
948/// ```
949/// use fraiseql_core::schema::{InputObjectDefinition, InputFieldDefinition};
950///
951/// let user_filter = InputObjectDefinition {
952///     name: "UserFilter".to_string(),
953///     fields: vec![
954///         InputFieldDefinition::new("name", "String"),
955///         InputFieldDefinition::new("email", "String"),
956///         InputFieldDefinition::new("active", "Boolean"),
957///     ],
958///     description: Some("Filter criteria for users".to_string()),
959///     metadata: None,
960/// };
961/// ```
962#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
963pub struct InputObjectDefinition {
964    /// Input object type name (e.g., "UserFilter").
965    pub name: String,
966
967    /// Input fields.
968    #[serde(default)]
969    pub fields: Vec<InputFieldDefinition>,
970
971    /// Description of the input type.
972    #[serde(default, skip_serializing_if = "Option::is_none")]
973    pub description: Option<String>,
974
975    /// Optional metadata for specialized input types (e.g., SQL templates for rich filters).
976    /// Used by the compiler and runtime for code generation and query execution.
977    #[serde(default, skip_serializing_if = "Option::is_none")]
978    pub metadata: Option<serde_json::Value>,
979}
980
981impl InputObjectDefinition {
982    /// Create a new input object definition.
983    #[must_use]
984    pub fn new(name: impl Into<String>) -> Self {
985        Self {
986            name:        name.into(),
987            fields:      Vec::new(),
988            description: None,
989            metadata:    None,
990        }
991    }
992
993    /// Add a field to this input object.
994    #[must_use]
995    pub fn with_field(mut self, field: InputFieldDefinition) -> Self {
996        self.fields.push(field);
997        self
998    }
999
1000    /// Add multiple fields to this input object.
1001    #[must_use]
1002    pub fn with_fields(mut self, fields: Vec<InputFieldDefinition>) -> Self {
1003        self.fields = fields;
1004        self
1005    }
1006
1007    /// Set description.
1008    #[must_use]
1009    pub fn with_description(mut self, description: impl Into<String>) -> Self {
1010        self.description = Some(description.into());
1011        self
1012    }
1013
1014    /// Set metadata (for specialized input types like rich filters).
1015    #[must_use]
1016    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
1017        self.metadata = Some(metadata);
1018        self
1019    }
1020
1021    /// Find a field by name.
1022    #[must_use]
1023    pub fn find_field(&self, name: &str) -> Option<&InputFieldDefinition> {
1024        self.fields.iter().find(|f| f.name == name)
1025    }
1026}
1027
1028/// A field within a GraphQL input object type.
1029///
1030/// # Example
1031///
1032/// ```
1033/// use fraiseql_core::schema::InputFieldDefinition;
1034///
1035/// let field = InputFieldDefinition::new("email", "String!")
1036///     .with_description("User's email address")
1037///     .with_default_value("\"user@example.com\"");
1038/// ```
1039#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1040pub struct InputFieldDefinition {
1041    /// Field name.
1042    pub name: String,
1043
1044    /// Field type (e.g., `"String!"`, `"[Int]"`, `"UserFilter"`).
1045    pub field_type: String,
1046
1047    /// Description.
1048    #[serde(default, skip_serializing_if = "Option::is_none")]
1049    pub description: Option<String>,
1050
1051    /// Default value (as JSON string).
1052    #[serde(default, skip_serializing_if = "Option::is_none")]
1053    pub default_value: Option<String>,
1054
1055    /// Deprecation information (if this field is deprecated).
1056    #[serde(default, skip_serializing_if = "Option::is_none")]
1057    pub deprecation: Option<super::field_type::DeprecationInfo>,
1058
1059    /// Validation rules applied to this field (from @validate directives).
1060    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1061    pub validation_rules: Vec<ValidationRule>,
1062}
1063
1064impl InputFieldDefinition {
1065    /// Create a new input field.
1066    #[must_use]
1067    pub fn new(name: impl Into<String>, field_type: impl Into<String>) -> Self {
1068        Self {
1069            name:             name.into(),
1070            field_type:       field_type.into(),
1071            description:      None,
1072            default_value:    None,
1073            deprecation:      None,
1074            validation_rules: Vec::new(),
1075        }
1076    }
1077
1078    /// Set description.
1079    #[must_use]
1080    pub fn with_description(mut self, description: impl Into<String>) -> Self {
1081        self.description = Some(description.into());
1082        self
1083    }
1084
1085    /// Set default value.
1086    #[must_use]
1087    pub fn with_default_value(mut self, value: impl Into<String>) -> Self {
1088        self.default_value = Some(value.into());
1089        self
1090    }
1091
1092    /// Mark this field as deprecated.
1093    #[must_use]
1094    pub fn deprecated(mut self, reason: Option<String>) -> Self {
1095        self.deprecation = Some(super::field_type::DeprecationInfo { reason });
1096        self
1097    }
1098
1099    /// Check if this field is deprecated.
1100    #[must_use]
1101    pub fn is_deprecated(&self) -> bool {
1102        self.deprecation.is_some()
1103    }
1104
1105    /// Check if this field is required (non-nullable without default).
1106    #[must_use]
1107    pub fn is_required(&self) -> bool {
1108        self.field_type.ends_with('!') && self.default_value.is_none()
1109    }
1110
1111    /// Add a validation rule to this field.
1112    #[must_use]
1113    pub fn with_validation_rule(mut self, rule: ValidationRule) -> Self {
1114        self.validation_rules.push(rule);
1115        self
1116    }
1117
1118    /// Add multiple validation rules to this field.
1119    #[must_use]
1120    pub fn with_validation_rules(mut self, rules: Vec<ValidationRule>) -> Self {
1121        self.validation_rules.extend(rules);
1122        self
1123    }
1124
1125    /// Check if this field has validation rules.
1126    #[must_use]
1127    pub fn has_validation_rules(&self) -> bool {
1128        !self.validation_rules.is_empty()
1129    }
1130}
1131
1132// =============================================================================
1133// Interface Definitions
1134// =============================================================================
1135
1136/// A GraphQL interface type definition.
1137///
1138/// Interfaces define a common set of fields that multiple types can implement.
1139/// They enable polymorphic queries where a field can return any type that
1140/// implements the interface.
1141///
1142/// # Example
1143///
1144/// ```
1145/// use fraiseql_core::schema::{InterfaceDefinition, FieldDefinition, FieldType};
1146///
1147/// let node_interface = InterfaceDefinition {
1148///     name: "Node".to_string(),
1149///     fields: vec![
1150///         FieldDefinition::new("id", FieldType::Id),
1151///     ],
1152///     description: Some("An object with an ID".to_string()),
1153/// };
1154/// ```
1155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1156pub struct InterfaceDefinition {
1157    /// Interface name (e.g., "Node").
1158    pub name: String,
1159
1160    /// Fields that implementing types must define.
1161    #[serde(default)]
1162    pub fields: Vec<FieldDefinition>,
1163
1164    /// Description of the interface.
1165    #[serde(default, skip_serializing_if = "Option::is_none")]
1166    pub description: Option<String>,
1167}
1168
1169impl InterfaceDefinition {
1170    /// Create a new interface definition.
1171    #[must_use]
1172    pub fn new(name: impl Into<String>) -> Self {
1173        Self {
1174            name:        name.into(),
1175            fields:      Vec::new(),
1176            description: None,
1177        }
1178    }
1179
1180    /// Add a field to this interface.
1181    #[must_use]
1182    pub fn with_field(mut self, field: FieldDefinition) -> Self {
1183        self.fields.push(field);
1184        self
1185    }
1186
1187    /// Add multiple fields to this interface.
1188    #[must_use]
1189    pub fn with_fields(mut self, fields: Vec<FieldDefinition>) -> Self {
1190        self.fields = fields;
1191        self
1192    }
1193
1194    /// Set description.
1195    #[must_use]
1196    pub fn with_description(mut self, description: impl Into<String>) -> Self {
1197        self.description = Some(description.into());
1198        self
1199    }
1200
1201    /// Find a field by name.
1202    #[must_use]
1203    pub fn find_field(&self, name: &str) -> Option<&FieldDefinition> {
1204        self.fields.iter().find(|f| f.name == name)
1205    }
1206}
1207
1208// =============================================================================
1209// Union Definitions
1210// =============================================================================
1211
1212/// A GraphQL union type definition.
1213///
1214/// Unions represent a type that can be one of several possible object types.
1215/// Unlike interfaces, union member types don't need to share any fields.
1216/// Per GraphQL spec ยง3.8, unions are useful for polymorphic returns.
1217///
1218/// # Example
1219///
1220/// ```
1221/// use fraiseql_core::schema::UnionDefinition;
1222///
1223/// let search_result = UnionDefinition {
1224///     name: "SearchResult".to_string(),
1225///     member_types: vec!["User".to_string(), "Post".to_string(), "Comment".to_string()],
1226///     description: Some("Possible search result types".to_string()),
1227/// };
1228/// ```
1229#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1230pub struct UnionDefinition {
1231    /// Union name (e.g., "SearchResult").
1232    pub name: String,
1233
1234    /// Member types that this union can represent.
1235    /// Order is significant for resolution.
1236    pub member_types: Vec<String>,
1237
1238    /// Description of the union.
1239    #[serde(default, skip_serializing_if = "Option::is_none")]
1240    pub description: Option<String>,
1241}
1242
1243impl UnionDefinition {
1244    /// Create a new union definition.
1245    #[must_use]
1246    pub fn new(name: impl Into<String>) -> Self {
1247        Self {
1248            name:         name.into(),
1249            member_types: Vec::new(),
1250            description:  None,
1251        }
1252    }
1253
1254    /// Add a member type to this union.
1255    #[must_use]
1256    pub fn with_member(mut self, type_name: impl Into<String>) -> Self {
1257        self.member_types.push(type_name.into());
1258        self
1259    }
1260
1261    /// Add multiple member types to this union.
1262    #[must_use]
1263    pub fn with_members(mut self, members: Vec<String>) -> Self {
1264        self.member_types = members;
1265        self
1266    }
1267
1268    /// Set description.
1269    #[must_use]
1270    pub fn with_description(mut self, description: impl Into<String>) -> Self {
1271        self.description = Some(description.into());
1272        self
1273    }
1274
1275    /// Check if a type is a member of this union.
1276    #[must_use]
1277    pub fn contains_type(&self, type_name: &str) -> bool {
1278        self.member_types.iter().any(|t| t == type_name)
1279    }
1280}
1281
1282/// A query definition compiled from `@fraiseql.query`.
1283///
1284/// Queries are declarative bindings to database views/tables.
1285/// They describe *what* to fetch, not *how* to fetch it.
1286///
1287/// # Example
1288///
1289/// ```
1290/// use fraiseql_core::schema::{QueryDefinition, AutoParams};
1291///
1292/// let query = QueryDefinition {
1293///     name: "users".to_string(),
1294///     return_type: "User".to_string(),
1295///     returns_list: true,
1296///     nullable: false,
1297///     arguments: vec![],
1298///     sql_source: Some("v_user".to_string()),
1299///     description: Some("Get all users".to_string()),
1300///     auto_params: AutoParams::default(),
1301///     deprecation: None,
1302///     jsonb_column: "data".to_string(),
1303/// };
1304/// ```
1305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1306pub struct QueryDefinition {
1307    /// Query name (e.g., "users").
1308    pub name: String,
1309
1310    /// Return type name (e.g., "User").
1311    pub return_type: String,
1312
1313    /// Does this query return a list?
1314    #[serde(default)]
1315    pub returns_list: bool,
1316
1317    /// Is the return value nullable?
1318    #[serde(default)]
1319    pub nullable: bool,
1320
1321    /// Query arguments.
1322    #[serde(default)]
1323    pub arguments: Vec<ArgumentDefinition>,
1324
1325    /// SQL source table/view (for direct table queries).
1326    #[serde(default, skip_serializing_if = "Option::is_none")]
1327    pub sql_source: Option<String>,
1328
1329    /// Description.
1330    #[serde(default, skip_serializing_if = "Option::is_none")]
1331    pub description: Option<String>,
1332
1333    /// Auto-wired parameters (where, orderBy, limit, offset).
1334    #[serde(default)]
1335    pub auto_params: AutoParams,
1336
1337    /// Deprecation information (from @deprecated directive).
1338    /// When set, this query is marked as deprecated in the schema.
1339    #[serde(default, skip_serializing_if = "Option::is_none")]
1340    pub deprecation: Option<super::field_type::DeprecationInfo>,
1341
1342    /// JSONB column name (e.g., "data").
1343    /// Used to extract data from JSONB columns in query results.
1344    #[serde(default = "default_jsonb_column")]
1345    pub jsonb_column: String,
1346}
1347
1348impl QueryDefinition {
1349    /// Create a new query definition.
1350    #[must_use]
1351    pub fn new(name: impl Into<String>, return_type: impl Into<String>) -> Self {
1352        Self {
1353            name:         name.into(),
1354            return_type:  return_type.into(),
1355            returns_list: false,
1356            nullable:     false,
1357            arguments:    Vec::new(),
1358            sql_source:   None,
1359            description:  None,
1360            auto_params:  AutoParams::default(),
1361            deprecation:  None,
1362            jsonb_column: "data".to_string(),
1363        }
1364    }
1365
1366    /// Set this query to return a list.
1367    #[must_use]
1368    pub fn returning_list(mut self) -> Self {
1369        self.returns_list = true;
1370        self
1371    }
1372
1373    /// Set the SQL source.
1374    #[must_use]
1375    pub fn with_sql_source(mut self, source: impl Into<String>) -> Self {
1376        self.sql_source = Some(source.into());
1377        self
1378    }
1379
1380    /// Mark this query as deprecated.
1381    ///
1382    /// # Example
1383    ///
1384    /// ```
1385    /// use fraiseql_core::schema::QueryDefinition;
1386    ///
1387    /// let query = QueryDefinition::new("oldUsers", "User")
1388    ///     .deprecated(Some("Use 'users' instead".to_string()));
1389    /// assert!(query.is_deprecated());
1390    /// ```
1391    #[must_use]
1392    pub fn deprecated(mut self, reason: Option<String>) -> Self {
1393        self.deprecation = Some(super::field_type::DeprecationInfo { reason });
1394        self
1395    }
1396
1397    /// Check if this query is deprecated.
1398    #[must_use]
1399    pub fn is_deprecated(&self) -> bool {
1400        self.deprecation.is_some()
1401    }
1402
1403    /// Get the deprecation reason if deprecated.
1404    #[must_use]
1405    pub fn deprecation_reason(&self) -> Option<&str> {
1406        self.deprecation.as_ref().and_then(|d| d.reason.as_deref())
1407    }
1408}
1409
1410/// A mutation definition compiled from `@fraiseql.mutation`.
1411///
1412/// Mutations are declarative bindings to database functions.
1413/// They describe *which function* to call, not arbitrary logic.
1414///
1415/// # Example
1416///
1417/// ```
1418/// use fraiseql_core::schema::{MutationDefinition, MutationOperation};
1419///
1420/// let mutation = MutationDefinition {
1421///     name: "createUser".to_string(),
1422///     return_type: "User".to_string(),
1423///     arguments: vec![],
1424///     description: Some("Create a new user".to_string()),
1425///     operation: MutationOperation::Insert { table: "users".to_string() },
1426///     deprecation: None,
1427/// };
1428/// ```
1429#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1430pub struct MutationDefinition {
1431    /// Mutation name (e.g., "createUser").
1432    pub name: String,
1433
1434    /// Return type name.
1435    pub return_type: String,
1436
1437    /// Input arguments.
1438    #[serde(default)]
1439    pub arguments: Vec<ArgumentDefinition>,
1440
1441    /// Description.
1442    #[serde(default, skip_serializing_if = "Option::is_none")]
1443    pub description: Option<String>,
1444
1445    /// SQL operation type.
1446    #[serde(default)]
1447    pub operation: MutationOperation,
1448
1449    /// Deprecation information (from @deprecated directive).
1450    /// When set, this mutation is marked as deprecated in the schema.
1451    #[serde(default, skip_serializing_if = "Option::is_none")]
1452    pub deprecation: Option<super::field_type::DeprecationInfo>,
1453
1454    /// PostgreSQL function name to call for this mutation.
1455    ///
1456    /// When set, the runtime calls `SELECT * FROM {sql_source}($1, $2, ...)` with the
1457    /// mutation arguments in `ArgumentDefinition` order, and parses the result as an
1458    /// `app.mutation_response` composite row.
1459    #[serde(default, skip_serializing_if = "Option::is_none")]
1460    pub sql_source: Option<String>,
1461}
1462
1463impl MutationDefinition {
1464    /// Create a new mutation definition.
1465    #[must_use]
1466    pub fn new(name: impl Into<String>, return_type: impl Into<String>) -> Self {
1467        Self {
1468            name:        name.into(),
1469            return_type: return_type.into(),
1470            arguments:   Vec::new(),
1471            description: None,
1472            operation:   MutationOperation::default(),
1473            deprecation: None,
1474            sql_source:  None,
1475        }
1476    }
1477
1478    /// Mark this mutation as deprecated.
1479    ///
1480    /// # Example
1481    ///
1482    /// ```
1483    /// use fraiseql_core::schema::MutationDefinition;
1484    ///
1485    /// let mutation = MutationDefinition::new("oldCreateUser", "User")
1486    ///     .deprecated(Some("Use 'createUser' instead".to_string()));
1487    /// assert!(mutation.is_deprecated());
1488    /// ```
1489    #[must_use]
1490    pub fn deprecated(mut self, reason: Option<String>) -> Self {
1491        self.deprecation = Some(super::field_type::DeprecationInfo { reason });
1492        self
1493    }
1494
1495    /// Check if this mutation is deprecated.
1496    #[must_use]
1497    pub fn is_deprecated(&self) -> bool {
1498        self.deprecation.is_some()
1499    }
1500
1501    /// Get the deprecation reason if deprecated.
1502    #[must_use]
1503    pub fn deprecation_reason(&self) -> Option<&str> {
1504        self.deprecation.as_ref().and_then(|d| d.reason.as_deref())
1505    }
1506}
1507
1508/// Mutation operation types.
1509///
1510/// This enum describes what kind of database operation a mutation performs.
1511#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1512#[serde(rename_all = "PascalCase")]
1513pub enum MutationOperation {
1514    /// INSERT into a table.
1515    Insert {
1516        /// Target table name.
1517        table: String,
1518    },
1519
1520    /// UPDATE a table.
1521    Update {
1522        /// Target table name.
1523        table: String,
1524    },
1525
1526    /// DELETE from a table.
1527    Delete {
1528        /// Target table name.
1529        table: String,
1530    },
1531
1532    /// Custom mutation (for complex operations).
1533    #[default]
1534    Custom,
1535}
1536
1537/// A subscription definition.
1538///
1539/// Subscriptions are declarative bindings to event topics.
1540#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1541pub struct SubscriptionDefinition {
1542    /// Subscription name.
1543    pub name: String,
1544
1545    /// Return type name.
1546    pub return_type: String,
1547
1548    /// Arguments.
1549    #[serde(default)]
1550    pub arguments: Vec<ArgumentDefinition>,
1551
1552    /// Description.
1553    #[serde(default, skip_serializing_if = "Option::is_none")]
1554    pub description: Option<String>,
1555
1556    /// Event topic to subscribe to.
1557    #[serde(default, skip_serializing_if = "Option::is_none")]
1558    pub topic: Option<String>,
1559
1560    /// Compiled filter expression for event matching.
1561    /// Maps argument names to JSONB paths in event data.
1562    /// Example: `{"orderId": "$.id", "status": "$.order_status"}`
1563    #[serde(default, skip_serializing_if = "Option::is_none")]
1564    pub filter: Option<SubscriptionFilter>,
1565
1566    /// Fields to project from event data.
1567    /// If empty, all fields are returned.
1568    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1569    pub fields: Vec<String>,
1570
1571    /// Deprecation information (from @deprecated directive).
1572    /// When set, this subscription is marked as deprecated in the schema.
1573    #[serde(default, skip_serializing_if = "Option::is_none")]
1574    pub deprecation: Option<super::field_type::DeprecationInfo>,
1575}
1576
1577/// Filter configuration for subscription event matching.
1578#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1579pub struct SubscriptionFilter {
1580    /// Mapping of argument names to JSONB paths in event data.
1581    /// The path uses JSON pointer syntax (e.g., "/id", "/user/name").
1582    #[serde(default)]
1583    pub argument_paths: std::collections::HashMap<String, String>,
1584
1585    /// Static filter conditions that must always match.
1586    /// Each entry is a path and expected value.
1587    #[serde(default)]
1588    pub static_filters: Vec<StaticFilterCondition>,
1589}
1590
1591/// A static filter condition for subscription matching.
1592#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1593pub struct StaticFilterCondition {
1594    /// JSONB path in event data.
1595    pub path:     String,
1596    /// Comparison operator.
1597    pub operator: FilterOperator,
1598    /// Value to compare against.
1599    pub value:    serde_json::Value,
1600}
1601
1602/// Filter comparison operators.
1603#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1604#[serde(rename_all = "snake_case")]
1605pub enum FilterOperator {
1606    /// Equals (==).
1607    Eq,
1608    /// Not equals (!=).
1609    Ne,
1610    /// Greater than (>).
1611    Gt,
1612    /// Greater than or equal (>=).
1613    Gte,
1614    /// Less than (<).
1615    Lt,
1616    /// Less than or equal (<=).
1617    Lte,
1618    /// Contains (for arrays/strings).
1619    Contains,
1620    /// Starts with (for strings).
1621    StartsWith,
1622    /// Ends with (for strings).
1623    EndsWith,
1624}
1625
1626impl SubscriptionDefinition {
1627    /// Create a new subscription definition.
1628    #[must_use]
1629    pub fn new(name: impl Into<String>, return_type: impl Into<String>) -> Self {
1630        Self {
1631            name:        name.into(),
1632            return_type: return_type.into(),
1633            arguments:   Vec::new(),
1634            description: None,
1635            topic:       None,
1636            filter:      None,
1637            fields:      Vec::new(),
1638            deprecation: None,
1639        }
1640    }
1641
1642    /// Set the event topic for this subscription.
1643    ///
1644    /// # Example
1645    ///
1646    /// ```
1647    /// use fraiseql_core::schema::SubscriptionDefinition;
1648    ///
1649    /// let subscription = SubscriptionDefinition::new("orderCreated", "Order")
1650    ///     .with_topic("order_created");
1651    /// assert_eq!(subscription.topic, Some("order_created".to_string()));
1652    /// ```
1653    #[must_use]
1654    pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
1655        self.topic = Some(topic.into());
1656        self
1657    }
1658
1659    /// Set the description for this subscription.
1660    #[must_use]
1661    pub fn with_description(mut self, description: impl Into<String>) -> Self {
1662        self.description = Some(description.into());
1663        self
1664    }
1665
1666    /// Add an argument to this subscription.
1667    #[must_use]
1668    pub fn with_argument(mut self, arg: ArgumentDefinition) -> Self {
1669        self.arguments.push(arg);
1670        self
1671    }
1672
1673    /// Set the filter configuration for event matching.
1674    #[must_use]
1675    pub fn with_filter(mut self, filter: SubscriptionFilter) -> Self {
1676        self.filter = Some(filter);
1677        self
1678    }
1679
1680    /// Set the fields to project from event data.
1681    #[must_use]
1682    pub fn with_fields(mut self, fields: Vec<String>) -> Self {
1683        self.fields = fields;
1684        self
1685    }
1686
1687    /// Add a field to project from event data.
1688    #[must_use]
1689    pub fn with_field(mut self, field: impl Into<String>) -> Self {
1690        self.fields.push(field.into());
1691        self
1692    }
1693
1694    /// Mark this subscription as deprecated.
1695    ///
1696    /// # Example
1697    ///
1698    /// ```
1699    /// use fraiseql_core::schema::SubscriptionDefinition;
1700    ///
1701    /// let subscription = SubscriptionDefinition::new("oldUserEvents", "User")
1702    ///     .deprecated(Some("Use 'userEvents' instead".to_string()));
1703    /// assert!(subscription.is_deprecated());
1704    /// ```
1705    #[must_use]
1706    pub fn deprecated(mut self, reason: Option<String>) -> Self {
1707        self.deprecation = Some(super::field_type::DeprecationInfo { reason });
1708        self
1709    }
1710
1711    /// Check if this subscription is deprecated.
1712    #[must_use]
1713    pub fn is_deprecated(&self) -> bool {
1714        self.deprecation.is_some()
1715    }
1716
1717    /// Get the deprecation reason if deprecated.
1718    #[must_use]
1719    pub fn deprecation_reason(&self) -> Option<&str> {
1720        self.deprecation.as_ref().and_then(|d| d.reason.as_deref())
1721    }
1722}
1723
1724/// Query/mutation/subscription argument definition.
1725#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1726pub struct ArgumentDefinition {
1727    /// Argument name.
1728    pub name: String,
1729
1730    /// Argument type.
1731    pub arg_type: FieldType,
1732
1733    /// Is this argument optional?
1734    #[serde(default)]
1735    pub nullable: bool,
1736
1737    /// Default value (JSON representation).
1738    #[serde(default, skip_serializing_if = "Option::is_none")]
1739    pub default_value: Option<serde_json::Value>,
1740
1741    /// Description.
1742    #[serde(default, skip_serializing_if = "Option::is_none")]
1743    pub description: Option<String>,
1744
1745    /// Deprecation information (from @deprecated directive).
1746    /// When set, this argument is marked as deprecated in the schema.
1747    /// Per GraphQL spec, deprecated arguments should still be accepted but
1748    /// clients are encouraged to migrate to alternatives.
1749    #[serde(default, skip_serializing_if = "Option::is_none")]
1750    pub deprecation: Option<super::field_type::DeprecationInfo>,
1751}
1752
1753impl ArgumentDefinition {
1754    /// Create a new required argument.
1755    #[must_use]
1756    pub fn new(name: impl Into<String>, arg_type: FieldType) -> Self {
1757        Self {
1758            name: name.into(),
1759            arg_type,
1760            nullable: false,
1761            default_value: None,
1762            description: None,
1763            deprecation: None,
1764        }
1765    }
1766
1767    /// Create a new optional argument.
1768    #[must_use]
1769    pub fn optional(name: impl Into<String>, arg_type: FieldType) -> Self {
1770        Self {
1771            name: name.into(),
1772            arg_type,
1773            nullable: true,
1774            default_value: None,
1775            description: None,
1776            deprecation: None,
1777        }
1778    }
1779
1780    /// Mark this argument as deprecated.
1781    ///
1782    /// # Example
1783    ///
1784    /// ```
1785    /// use fraiseql_core::schema::{ArgumentDefinition, FieldType};
1786    ///
1787    /// let arg = ArgumentDefinition::optional("oldLimit", FieldType::Int)
1788    ///     .deprecated(Some("Use 'first' instead".to_string()));
1789    /// assert!(arg.is_deprecated());
1790    /// ```
1791    #[must_use]
1792    pub fn deprecated(mut self, reason: Option<String>) -> Self {
1793        self.deprecation = Some(super::field_type::DeprecationInfo { reason });
1794        self
1795    }
1796
1797    /// Check if this argument is deprecated.
1798    #[must_use]
1799    pub fn is_deprecated(&self) -> bool {
1800        self.deprecation.is_some()
1801    }
1802
1803    /// Get the deprecation reason if deprecated.
1804    #[must_use]
1805    pub fn deprecation_reason(&self) -> Option<&str> {
1806        self.deprecation.as_ref().and_then(|d| d.reason.as_deref())
1807    }
1808}
1809
1810/// Auto-wired query parameters.
1811///
1812/// These are standard parameters automatically added to list queries.
1813#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1814#[allow(clippy::struct_excessive_bools)] // Reason: these are intentional feature flags
1815pub struct AutoParams {
1816    /// Enable `where` filtering.
1817    #[serde(default)]
1818    pub has_where: bool,
1819
1820    /// Enable `orderBy` sorting.
1821    #[serde(default)]
1822    pub has_order_by: bool,
1823
1824    /// Enable `limit` pagination.
1825    #[serde(default)]
1826    pub has_limit: bool,
1827
1828    /// Enable `offset` pagination.
1829    #[serde(default)]
1830    pub has_offset: bool,
1831}
1832
1833impl AutoParams {
1834    /// Create with all auto-params enabled (common for list queries).
1835    #[must_use]
1836    pub fn all() -> Self {
1837        Self {
1838            has_where:    true,
1839            has_order_by: true,
1840            has_limit:    true,
1841            has_offset:   true,
1842        }
1843    }
1844
1845    /// Create with no auto-params (common for single-item queries).
1846    #[must_use]
1847    pub fn none() -> Self {
1848        Self::default()
1849    }
1850}
1851
1852// =============================================================================
1853// Custom Directive Definitions
1854// =============================================================================
1855
1856/// A custom directive definition for schema extension.
1857///
1858/// Allows defining custom directives beyond the built-in `@skip`, `@include`,
1859/// and `@deprecated` directives. Custom directives are exposed via introspection
1860/// and can be evaluated at runtime via registered handlers.
1861///
1862/// # Example
1863///
1864/// ```
1865/// use fraiseql_core::schema::{DirectiveDefinition, DirectiveLocationKind, ArgumentDefinition, FieldType};
1866///
1867/// let rate_limit = DirectiveDefinition {
1868///     name: "rateLimit".to_string(),
1869///     description: Some("Apply rate limiting to this field".to_string()),
1870///     locations: vec![DirectiveLocationKind::FieldDefinition],
1871///     arguments: vec![
1872///         ArgumentDefinition::new("limit", FieldType::Int),
1873///         ArgumentDefinition::optional("window", FieldType::String),
1874///     ],
1875///     is_repeatable: false,
1876/// };
1877/// ```
1878#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1879pub struct DirectiveDefinition {
1880    /// Directive name (e.g., "rateLimit", "auth").
1881    pub name: String,
1882
1883    /// Description of what this directive does.
1884    #[serde(default, skip_serializing_if = "Option::is_none")]
1885    pub description: Option<String>,
1886
1887    /// Valid locations where this directive can be applied.
1888    pub locations: Vec<DirectiveLocationKind>,
1889
1890    /// Arguments this directive accepts.
1891    #[serde(default)]
1892    pub arguments: Vec<ArgumentDefinition>,
1893
1894    /// Whether this directive can be applied multiple times to the same location.
1895    #[serde(default)]
1896    pub is_repeatable: bool,
1897}
1898
1899impl DirectiveDefinition {
1900    /// Create a new directive definition.
1901    #[must_use]
1902    pub fn new(name: impl Into<String>, locations: Vec<DirectiveLocationKind>) -> Self {
1903        Self {
1904            name: name.into(),
1905            description: None,
1906            locations,
1907            arguments: Vec::new(),
1908            is_repeatable: false,
1909        }
1910    }
1911
1912    /// Set the description.
1913    #[must_use]
1914    pub fn with_description(mut self, description: impl Into<String>) -> Self {
1915        self.description = Some(description.into());
1916        self
1917    }
1918
1919    /// Add an argument to this directive.
1920    #[must_use]
1921    pub fn with_argument(mut self, arg: ArgumentDefinition) -> Self {
1922        self.arguments.push(arg);
1923        self
1924    }
1925
1926    /// Add multiple arguments to this directive.
1927    #[must_use]
1928    pub fn with_arguments(mut self, args: Vec<ArgumentDefinition>) -> Self {
1929        self.arguments = args;
1930        self
1931    }
1932
1933    /// Mark this directive as repeatable.
1934    #[must_use]
1935    pub fn repeatable(mut self) -> Self {
1936        self.is_repeatable = true;
1937        self
1938    }
1939
1940    /// Check if this directive can be applied at the given location.
1941    #[must_use]
1942    pub fn valid_at(&self, location: DirectiveLocationKind) -> bool {
1943        self.locations.contains(&location)
1944    }
1945
1946    /// Find an argument by name.
1947    #[must_use]
1948    pub fn find_argument(&self, name: &str) -> Option<&ArgumentDefinition> {
1949        self.arguments.iter().find(|a| a.name == name)
1950    }
1951}
1952
1953/// Directive location kinds for custom directive definitions.
1954///
1955/// This mirrors `DirectiveLocation` in introspection but is used for
1956/// compiled schema definitions. The two types can be converted between
1957/// each other for introspection purposes.
1958///
1959/// Per GraphQL spec ยง3.13, directive locations fall into two categories:
1960/// - Executable locations (operations, fields, fragments)
1961/// - Type system locations (schema definitions)
1962#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1963#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1964pub enum DirectiveLocationKind {
1965    // Executable directive locations
1966    /// Directive on query operation.
1967    Query,
1968    /// Directive on mutation operation.
1969    Mutation,
1970    /// Directive on subscription operation.
1971    Subscription,
1972    /// Directive on field selection.
1973    Field,
1974    /// Directive on fragment definition.
1975    FragmentDefinition,
1976    /// Directive on fragment spread.
1977    FragmentSpread,
1978    /// Directive on inline fragment.
1979    InlineFragment,
1980    /// Directive on variable definition.
1981    VariableDefinition,
1982
1983    // Type system directive locations
1984    /// Directive on schema definition.
1985    Schema,
1986    /// Directive on scalar type definition.
1987    Scalar,
1988    /// Directive on object type definition.
1989    Object,
1990    /// Directive on field definition.
1991    FieldDefinition,
1992    /// Directive on argument definition.
1993    ArgumentDefinition,
1994    /// Directive on interface definition.
1995    Interface,
1996    /// Directive on union definition.
1997    Union,
1998    /// Directive on enum definition.
1999    Enum,
2000    /// Directive on enum value definition.
2001    EnumValue,
2002    /// Directive on input object definition.
2003    InputObject,
2004    /// Directive on input field definition.
2005    InputFieldDefinition,
2006}
2007
2008impl DirectiveLocationKind {
2009    /// Check if this is an executable directive location.
2010    #[must_use]
2011    pub fn is_executable(&self) -> bool {
2012        matches!(
2013            self,
2014            Self::Query
2015                | Self::Mutation
2016                | Self::Subscription
2017                | Self::Field
2018                | Self::FragmentDefinition
2019                | Self::FragmentSpread
2020                | Self::InlineFragment
2021                | Self::VariableDefinition
2022        )
2023    }
2024
2025    /// Check if this is a type system directive location.
2026    #[must_use]
2027    pub fn is_type_system(&self) -> bool {
2028        !self.is_executable()
2029    }
2030}
2031
2032// =============================================================================
2033// Observer Definitions
2034// =============================================================================
2035
2036/// Observer definition - database change event listener.
2037///
2038/// Observers trigger actions (webhooks, notifications) when database
2039/// changes occur, enabling event-driven architectures.
2040///
2041/// # Example
2042///
2043/// ```
2044/// use fraiseql_core::schema::{ObserverDefinition, RetryConfig};
2045///
2046/// let observer = ObserverDefinition {
2047///     name: "onHighValueOrder".to_string(),
2048///     entity: "Order".to_string(),
2049///     event: "INSERT".to_string(),
2050///     condition: Some("total > 1000".to_string()),
2051///     actions: vec![
2052///         serde_json::json!({
2053///             "type": "webhook",
2054///             "url": "https://api.example.com/high-value-orders"
2055///         }),
2056///     ],
2057///     retry: RetryConfig {
2058///         max_attempts: 3,
2059///         backoff_strategy: "exponential".to_string(),
2060///         initial_delay_ms: 1000,
2061///         max_delay_ms: 60000,
2062///     },
2063/// };
2064/// ```
2065#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2066pub struct ObserverDefinition {
2067    /// Observer name (unique identifier).
2068    pub name: String,
2069
2070    /// Entity type to observe (e.g., "Order", "User").
2071    pub entity: String,
2072
2073    /// Event type: INSERT, UPDATE, or DELETE.
2074    pub event: String,
2075
2076    /// Optional condition expression in FraiseQL DSL.
2077    /// Example: "total > 1000" or "status.changed() and status == 'shipped'"
2078    #[serde(skip_serializing_if = "Option::is_none")]
2079    pub condition: Option<String>,
2080
2081    /// Actions to execute when observer triggers.
2082    /// Each action is a JSON object with a "type" field (webhook, slack, email).
2083    pub actions: Vec<serde_json::Value>,
2084
2085    /// Retry configuration for action execution.
2086    pub retry: RetryConfig,
2087}
2088
2089impl ObserverDefinition {
2090    /// Create a new observer definition.
2091    #[must_use]
2092    pub fn new(
2093        name: impl Into<String>,
2094        entity: impl Into<String>,
2095        event: impl Into<String>,
2096    ) -> Self {
2097        Self {
2098            name:      name.into(),
2099            entity:    entity.into(),
2100            event:     event.into(),
2101            condition: None,
2102            actions:   Vec::new(),
2103            retry:     RetryConfig::default(),
2104        }
2105    }
2106
2107    /// Set the condition expression.
2108    #[must_use]
2109    pub fn with_condition(mut self, condition: impl Into<String>) -> Self {
2110        self.condition = Some(condition.into());
2111        self
2112    }
2113
2114    /// Add an action to this observer.
2115    #[must_use]
2116    pub fn with_action(mut self, action: serde_json::Value) -> Self {
2117        self.actions.push(action);
2118        self
2119    }
2120
2121    /// Add multiple actions to this observer.
2122    #[must_use]
2123    pub fn with_actions(mut self, actions: Vec<serde_json::Value>) -> Self {
2124        self.actions = actions;
2125        self
2126    }
2127
2128    /// Set the retry configuration.
2129    #[must_use]
2130    pub fn with_retry(mut self, retry: RetryConfig) -> Self {
2131        self.retry = retry;
2132        self
2133    }
2134
2135    /// Check if this observer has a condition.
2136    #[must_use]
2137    pub fn has_condition(&self) -> bool {
2138        self.condition.is_some()
2139    }
2140
2141    /// Get the number of actions.
2142    #[must_use]
2143    pub fn action_count(&self) -> usize {
2144        self.actions.len()
2145    }
2146}
2147
2148/// Retry configuration for observer actions.
2149///
2150/// Controls how failed actions are retried with configurable
2151/// backoff strategies.
2152///
2153/// # Example
2154///
2155/// ```
2156/// use fraiseql_core::schema::RetryConfig;
2157///
2158/// let retry = RetryConfig {
2159///     max_attempts: 5,
2160///     backoff_strategy: "exponential".to_string(),
2161///     initial_delay_ms: 1000,
2162///     max_delay_ms: 60000,
2163/// };
2164/// ```
2165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2166pub struct RetryConfig {
2167    /// Maximum number of retry attempts.
2168    pub max_attempts: u32,
2169
2170    /// Backoff strategy: exponential, linear, or fixed.
2171    pub backoff_strategy: String,
2172
2173    /// Initial delay in milliseconds.
2174    pub initial_delay_ms: u32,
2175
2176    /// Maximum delay in milliseconds (cap for exponential backoff).
2177    pub max_delay_ms: u32,
2178}
2179
2180impl Default for RetryConfig {
2181    fn default() -> Self {
2182        Self {
2183            max_attempts:     3,
2184            backoff_strategy: "exponential".to_string(),
2185            initial_delay_ms: 1000,
2186            max_delay_ms:     60000,
2187        }
2188    }
2189}
2190
2191impl RetryConfig {
2192    /// Create a new retry configuration.
2193    #[must_use]
2194    pub fn new(
2195        max_attempts: u32,
2196        backoff_strategy: impl Into<String>,
2197        initial_delay_ms: u32,
2198        max_delay_ms: u32,
2199    ) -> Self {
2200        Self {
2201            max_attempts,
2202            backoff_strategy: backoff_strategy.into(),
2203            initial_delay_ms,
2204            max_delay_ms,
2205        }
2206    }
2207
2208    /// Create exponential backoff configuration.
2209    #[must_use]
2210    pub fn exponential(max_attempts: u32, initial_delay_ms: u32, max_delay_ms: u32) -> Self {
2211        Self::new(max_attempts, "exponential", initial_delay_ms, max_delay_ms)
2212    }
2213
2214    /// Create linear backoff configuration.
2215    #[must_use]
2216    pub fn linear(max_attempts: u32, initial_delay_ms: u32, max_delay_ms: u32) -> Self {
2217        Self::new(max_attempts, "linear", initial_delay_ms, max_delay_ms)
2218    }
2219
2220    /// Create fixed delay configuration.
2221    #[must_use]
2222    pub fn fixed(max_attempts: u32, delay_ms: u32) -> Self {
2223        Self::new(max_attempts, "fixed", delay_ms, delay_ms)
2224    }
2225
2226    /// Check if backoff strategy is exponential.
2227    #[must_use]
2228    pub fn is_exponential(&self) -> bool {
2229        self.backoff_strategy == "exponential"
2230    }
2231
2232    /// Check if backoff strategy is linear.
2233    #[must_use]
2234    pub fn is_linear(&self) -> bool {
2235        self.backoff_strategy == "linear"
2236    }
2237
2238    /// Check if backoff strategy is fixed.
2239    #[must_use]
2240    pub fn is_fixed(&self) -> bool {
2241        self.backoff_strategy == "fixed"
2242    }
2243}
2244
2245// =============================================================================
2246// Tests
2247// =============================================================================
2248
2249#[cfg(test)]
2250mod tests {
2251    use super::*;
2252
2253    #[test]
2254    fn test_compiled_schema_with_observers() {
2255        let json = r#"{
2256            "types": [],
2257            "enums": [],
2258            "input_types": [],
2259            "interfaces": [],
2260            "unions": [],
2261            "queries": [],
2262            "mutations": [],
2263            "subscriptions": [],
2264            "observers": [
2265                {
2266                    "name": "onHighValueOrder",
2267                    "entity": "Order",
2268                    "event": "INSERT",
2269                    "condition": "total > 1000",
2270                    "actions": [
2271                        {
2272                            "type": "webhook",
2273                            "url": "https://api.example.com/webhook"
2274                        }
2275                    ],
2276                    "retry": {
2277                        "max_attempts": 3,
2278                        "backoff_strategy": "exponential",
2279                        "initial_delay_ms": 1000,
2280                        "max_delay_ms": 60000
2281                    }
2282                }
2283            ]
2284        }"#;
2285
2286        let schema = CompiledSchema::from_json(json).unwrap();
2287
2288        assert!(schema.has_observers());
2289        assert_eq!(schema.observer_count(), 1);
2290
2291        let observer = schema.find_observer("onHighValueOrder").unwrap();
2292        assert_eq!(observer.entity, "Order");
2293        assert_eq!(observer.event, "INSERT");
2294        assert_eq!(observer.condition, Some("total > 1000".to_string()));
2295        assert_eq!(observer.actions.len(), 1);
2296        assert_eq!(observer.retry.max_attempts, 3);
2297        assert!(observer.retry.is_exponential());
2298    }
2299
2300    #[test]
2301    fn test_compiled_schema_backward_compatible() {
2302        // Schema without observers field should still load
2303        let json = r#"{
2304            "types": [],
2305            "enums": [],
2306            "input_types": [],
2307            "interfaces": [],
2308            "unions": [],
2309            "queries": [],
2310            "mutations": [],
2311            "subscriptions": []
2312        }"#;
2313
2314        let schema = CompiledSchema::from_json(json).unwrap();
2315        assert!(!schema.has_observers());
2316        assert_eq!(schema.observer_count(), 0);
2317    }
2318
2319    #[test]
2320    fn test_find_observers_for_entity() {
2321        let schema = CompiledSchema {
2322            observers: vec![
2323                ObserverDefinition::new("onOrderInsert", "Order", "INSERT"),
2324                ObserverDefinition::new("onOrderUpdate", "Order", "UPDATE"),
2325                ObserverDefinition::new("onUserInsert", "User", "INSERT"),
2326            ],
2327            ..Default::default()
2328        };
2329
2330        let order_observers = schema.find_observers_for_entity("Order");
2331        assert_eq!(order_observers.len(), 2);
2332
2333        let user_observers = schema.find_observers_for_entity("User");
2334        assert_eq!(user_observers.len(), 1);
2335    }
2336
2337    #[test]
2338    fn test_find_observers_for_event() {
2339        let schema = CompiledSchema {
2340            observers: vec![
2341                ObserverDefinition::new("onOrderInsert", "Order", "INSERT"),
2342                ObserverDefinition::new("onOrderUpdate", "Order", "UPDATE"),
2343                ObserverDefinition::new("onUserInsert", "User", "INSERT"),
2344            ],
2345            ..Default::default()
2346        };
2347
2348        let insert_observers = schema.find_observers_for_event("INSERT");
2349        assert_eq!(insert_observers.len(), 2);
2350
2351        let update_observers = schema.find_observers_for_event("UPDATE");
2352        assert_eq!(update_observers.len(), 1);
2353    }
2354
2355    #[test]
2356    fn test_observer_definition_builder() {
2357        let observer = ObserverDefinition::new("test", "Order", "INSERT")
2358            .with_condition("total > 1000")
2359            .with_action(serde_json::json!({"type": "webhook", "url": "https://example.com"}))
2360            .with_retry(RetryConfig::exponential(5, 1000, 60000));
2361
2362        assert_eq!(observer.name, "test");
2363        assert_eq!(observer.entity, "Order");
2364        assert_eq!(observer.event, "INSERT");
2365        assert!(observer.has_condition());
2366        assert_eq!(observer.action_count(), 1);
2367        assert_eq!(observer.retry.max_attempts, 5);
2368    }
2369
2370    #[test]
2371    fn test_retry_config_types() {
2372        let exponential = RetryConfig::exponential(3, 1000, 60000);
2373        assert!(exponential.is_exponential());
2374        assert!(!exponential.is_linear());
2375        assert!(!exponential.is_fixed());
2376
2377        let linear = RetryConfig::linear(3, 1000, 60000);
2378        assert!(!linear.is_exponential());
2379        assert!(linear.is_linear());
2380        assert!(!linear.is_fixed());
2381
2382        let fixed = RetryConfig::fixed(3, 5000);
2383        assert!(!fixed.is_exponential());
2384        assert!(!fixed.is_linear());
2385        assert!(fixed.is_fixed());
2386        assert_eq!(fixed.initial_delay_ms, 5000);
2387        assert_eq!(fixed.max_delay_ms, 5000);
2388    }
2389}