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