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