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}