fraiseql_core/schema/compiled/schema.rs
1//! Compiled schema types - pure Rust, no authoring-language references.
2//!
3//! These types represent GraphQL schemas after compilation from authoring languages.
4//! All data is owned by Rust - no 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 authoring-language callbacks or object references
11//! - Safe to use from any Tokio worker thread
12//!
13//! This enables the Axum server to handle requests without any
14//! interaction with the authoring-language runtime.
15
16use std::collections::HashMap;
17
18use serde::{Deserialize, Serialize};
19
20use super::{directive::DirectiveDefinition, mutation::MutationDefinition, query::QueryDefinition};
21use crate::{
22 compiler::fact_table::FactTableMetadata,
23 schema::{
24 config_types::{
25 DebugConfig, FederationConfig, GrpcConfig, McpConfig, NamingConvention,
26 ObserversConfig, RestConfig, SessionVariablesConfig, SubscriptionsConfig,
27 ValidationConfig,
28 },
29 graphql_type_defs::{
30 EnumDefinition, InputObjectDefinition, InterfaceDefinition, TypeDefinition,
31 UnionDefinition,
32 },
33 hierarchy::HierarchiesConfig,
34 observer_types::ObserverDefinition,
35 security_config::SecurityConfig,
36 subscription_types::SubscriptionDefinition,
37 },
38 validation::CustomTypeRegistry,
39};
40
41/// Current schema format version.
42///
43/// Increment this constant when the compiled schema JSON format changes in a
44/// backward-incompatible way so that startup rejects stale compiled schemas.
45pub const CURRENT_SCHEMA_FORMAT_VERSION: u32 = 1;
46
47/// Complete compiled schema - all type information for serving.
48///
49/// This is the central type that holds the entire GraphQL schema
50/// after compilation from any supported authoring language.
51///
52/// # Example
53///
54/// ```
55/// use fraiseql_core::schema::CompiledSchema;
56///
57/// let json = r#"{
58/// "types": [],
59/// "queries": [],
60/// "mutations": [],
61/// "subscriptions": []
62/// }"#;
63///
64/// let schema = CompiledSchema::from_json(json, false).unwrap();
65/// assert_eq!(schema.types.len(), 0);
66/// ```
67#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct CompiledSchema {
69 /// GraphQL object type definitions.
70 #[serde(default)]
71 pub types: Vec<TypeDefinition>,
72
73 /// GraphQL enum type definitions.
74 #[serde(default)]
75 pub enums: Vec<EnumDefinition>,
76
77 /// GraphQL input object type definitions.
78 #[serde(default)]
79 pub input_types: Vec<InputObjectDefinition>,
80
81 /// GraphQL interface type definitions.
82 #[serde(default)]
83 pub interfaces: Vec<InterfaceDefinition>,
84
85 /// GraphQL union type definitions.
86 #[serde(default)]
87 pub unions: Vec<UnionDefinition>,
88
89 /// GraphQL query definitions.
90 #[serde(default)]
91 pub queries: Vec<QueryDefinition>,
92
93 /// GraphQL mutation definitions.
94 #[serde(default)]
95 pub mutations: Vec<MutationDefinition>,
96
97 /// GraphQL subscription definitions.
98 #[serde(default)]
99 pub subscriptions: Vec<SubscriptionDefinition>,
100
101 /// Custom directive definitions.
102 /// These are user-defined directives beyond the built-in @skip, @include, @deprecated.
103 #[serde(default, skip_serializing_if = "Vec::is_empty")]
104 pub directives: Vec<DirectiveDefinition>,
105
106 /// Fact table metadata (for analytics queries).
107 /// Key: table name (e.g., `tf_sales`)
108 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
109 pub fact_tables: HashMap<String, FactTableMetadata>,
110
111 /// Observer definitions (database change event listeners).
112 #[serde(default, skip_serializing_if = "Vec::is_empty")]
113 pub observers: Vec<ObserverDefinition>,
114
115 /// Federation metadata for Apollo Federation v2 support.
116 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub federation: Option<FederationConfig>,
118
119 /// Security configuration (from fraiseql.toml).
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub security: Option<SecurityConfig>,
122
123 /// Observers/event system configuration (from fraiseql.toml).
124 ///
125 /// Contains backend connection settings (`redis_url`, `nats_url`, etc.) and
126 /// event handler definitions compiled from the `[observers]` TOML section.
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub observers_config: Option<ObserversConfig>,
129
130 /// `WebSocket` subscription configuration (hooks, limits).
131 /// Compiled from the `[subscriptions]` TOML section.
132 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub subscriptions_config: Option<SubscriptionsConfig>,
134
135 /// Query validation config (depth/complexity limits).
136 /// Compiled from the `[validation]` TOML section.
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub validation_config: Option<ValidationConfig>,
139
140 /// Debug/development configuration.
141 /// Compiled from the `[debug]` TOML section.
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub debug_config: Option<DebugConfig>,
144
145 /// MCP (Model Context Protocol) server configuration.
146 /// Compiled from the `[mcp]` TOML section.
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub mcp_config: Option<McpConfig>,
149
150 /// REST transport configuration.
151 /// Compiled from the `[rest]` TOML section.
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub rest_config: Option<RestConfig>,
154
155 /// gRPC transport configuration.
156 /// Compiled from the `[grpc]` TOML section.
157 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub grpc_config: Option<GrpcConfig>,
159
160 /// Session variable injection configuration.
161 ///
162 /// When populated, the executor calls PostgreSQL `set_config()` before each
163 /// mutation, injecting per-request values (JWT claims, HTTP headers, literals)
164 /// as transaction-scoped settings. SQL functions read these via
165 /// `current_setting('app.tenant_id', true)`.
166 ///
167 /// Compiled from the `[session_variables]` TOML section.
168 #[serde(default)]
169 pub session_variables: SessionVariablesConfig,
170
171 /// Hierarchy definitions for ID-based ltree operators.
172 ///
173 /// Maps hierarchy names to `table`/`path_column` pairs. Compiled from the
174 /// `[hierarchies]` TOML section. Used at runtime to resolve `HierarchyContext`
175 /// for `descendantOfId` / `ancestorOfId` WHERE clause generation.
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub hierarchies_config: Option<HierarchiesConfig>,
178
179 /// Naming convention for GraphQL operation names.
180 ///
181 /// When set to `CamelCase`, operation names are converted from `snake_case`
182 /// (e.g., `create_dns_server` → `createDnsServer`) in the introspection
183 /// schema and lookup indexes. Compiled from `[fraiseql]` in `fraiseql.toml`.
184 #[serde(default)]
185 pub naming_convention: NamingConvention,
186
187 /// Schema format version emitted by the compiler.
188 ///
189 /// Used to detect runtime/compiler skew. If present and ≠ `CURRENT_SCHEMA_FORMAT_VERSION`,
190 /// `validate_format_version()` returns an error.
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub schema_format_version: Option<u32>,
193
194 /// Raw GraphQL schema as string (for SDL generation).
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub schema_sdl: Option<String>,
197
198 /// Custom scalar type registry.
199 ///
200 /// Contains definitions for custom scalar types defined in the schema.
201 /// Built during code generation from `IRScalar` definitions.
202 /// Not serialized - populated at runtime from `ir.scalars`.
203 #[serde(skip)]
204 pub custom_scalars: CustomTypeRegistry,
205
206 /// O(1) lookup index: query name → index into `self.queries`.
207 /// Built at construction time by `build_indexes()`; not serialized.
208 /// Populated automatically by `from_json()`; call `build_indexes()` after
209 /// direct mutation of `self.queries`.
210 #[serde(skip)]
211 pub query_index: HashMap<String, usize>,
212
213 /// O(1) lookup index: mutation name → index into `self.mutations`.
214 /// Built at construction time by `build_indexes()`; not serialized.
215 /// Populated automatically by `from_json()`; call `build_indexes()` after
216 /// direct mutation of `self.mutations`.
217 #[serde(skip)]
218 pub mutation_index: HashMap<String, usize>,
219
220 /// O(1) lookup index: subscription name → index into `self.subscriptions`.
221 /// Built at construction time by `build_indexes()`; not serialized.
222 /// Populated automatically by `from_json()`; call `build_indexes()` after
223 /// direct mutation of `self.subscriptions`.
224 #[serde(skip)]
225 pub subscription_index: HashMap<String, usize>,
226}
227
228impl PartialEq for CompiledSchema {
229 fn eq(&self, other: &Self) -> bool {
230 // Compare all fields except custom_scalars (runtime state)
231 self.schema_format_version == other.schema_format_version
232 && self.types == other.types
233 && self.enums == other.enums
234 && self.input_types == other.input_types
235 && self.interfaces == other.interfaces
236 && self.unions == other.unions
237 && self.queries == other.queries
238 && self.mutations == other.mutations
239 && self.subscriptions == other.subscriptions
240 && self.directives == other.directives
241 && self.fact_tables == other.fact_tables
242 && self.observers == other.observers
243 && self.federation == other.federation
244 && self.security == other.security
245 && self.observers_config == other.observers_config
246 && self.subscriptions_config == other.subscriptions_config
247 && self.validation_config == other.validation_config
248 && self.debug_config == other.debug_config
249 && self.mcp_config == other.mcp_config
250 && self.naming_convention == other.naming_convention
251 && self.schema_sdl == other.schema_sdl
252 }
253}
254
255impl CompiledSchema {
256 /// Create empty schema.
257 #[must_use]
258 pub fn new() -> Self {
259 Self::default()
260 }
261}