Skip to main content

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            ChangelogConfig, DebugConfig, FederationConfig, GrpcConfig, McpConfig,
26            NamingConvention, ObserversConfig, RestConfig, SessionVariablesConfig,
27            SubscriptionsConfig, 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    /// Changelog GraphQL-exposure configuration.
161    ///
162    /// Compiled from the `[changelog]` TOML section. When present with
163    /// `expose = true`, the compiler injects the `EntityChangeLog` /
164    /// `TransportCheckpoint` types plus their cursor query, point-lookup query, and
165    /// checkpoint upsert mutation. `None` when the block is absent (the default).
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub changelog: Option<ChangelogConfig>,
168
169    /// Session variable injection configuration.
170    ///
171    /// When populated, the executor calls PostgreSQL `set_config()` before each
172    /// mutation, injecting per-request values (JWT claims, HTTP headers, literals)
173    /// as transaction-scoped settings.  SQL functions read these via
174    /// `current_setting('app.tenant_id', true)`.
175    ///
176    /// Compiled from the `[session_variables]` TOML section.
177    #[serde(default)]
178    pub session_variables: SessionVariablesConfig,
179
180    /// Hierarchy definitions for ID-based ltree operators.
181    ///
182    /// Maps hierarchy names to `table`/`path_column` pairs. Compiled from the
183    /// `[hierarchies]` TOML section. Used at runtime to resolve `HierarchyContext`
184    /// for `descendantOfId` / `ancestorOfId` WHERE clause generation.
185    #[serde(default, skip_serializing_if = "Option::is_none")]
186    pub hierarchies_config: Option<HierarchiesConfig>,
187
188    /// Naming convention for GraphQL operation names.
189    ///
190    /// When set to `CamelCase`, operation names are converted from `snake_case`
191    /// (e.g., `create_dns_server` → `createDnsServer`) in the introspection
192    /// schema and lookup indexes. Compiled from `[fraiseql]` in `fraiseql.toml`.
193    #[serde(default)]
194    pub naming_convention: NamingConvention,
195
196    /// Schema format version emitted by the compiler.
197    ///
198    /// Used to detect runtime/compiler skew. If present and ≠ `CURRENT_SCHEMA_FORMAT_VERSION`,
199    /// `validate_format_version()` returns an error.
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub schema_format_version: Option<u32>,
202
203    /// Raw GraphQL schema as string (for SDL generation).
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub schema_sdl: Option<String>,
206
207    /// Custom scalar type registry.
208    ///
209    /// Contains definitions for custom scalar types defined in the schema.
210    /// Built during code generation from `IRScalar` definitions.
211    /// Not serialized - populated at runtime from `ir.scalars`.
212    #[serde(skip)]
213    pub custom_scalars: CustomTypeRegistry,
214
215    /// O(1) lookup index: query name → index into `self.queries`.
216    /// Built at construction time by `build_indexes()`; not serialized.
217    /// Populated automatically by `from_json()`; call `build_indexes()` after
218    /// direct mutation of `self.queries`.
219    #[serde(skip)]
220    pub query_index: HashMap<String, usize>,
221
222    /// O(1) lookup index: mutation name → index into `self.mutations`.
223    /// Built at construction time by `build_indexes()`; not serialized.
224    /// Populated automatically by `from_json()`; call `build_indexes()` after
225    /// direct mutation of `self.mutations`.
226    #[serde(skip)]
227    pub mutation_index: HashMap<String, usize>,
228
229    /// O(1) lookup index: subscription name → index into `self.subscriptions`.
230    /// Built at construction time by `build_indexes()`; not serialized.
231    /// Populated automatically by `from_json()`; call `build_indexes()` after
232    /// direct mutation of `self.subscriptions`.
233    #[serde(skip)]
234    pub subscription_index: HashMap<String, usize>,
235}
236
237impl PartialEq for CompiledSchema {
238    fn eq(&self, other: &Self) -> bool {
239        // Compare all fields except custom_scalars (runtime state)
240        self.schema_format_version == other.schema_format_version
241            && self.types == other.types
242            && self.enums == other.enums
243            && self.input_types == other.input_types
244            && self.interfaces == other.interfaces
245            && self.unions == other.unions
246            && self.queries == other.queries
247            && self.mutations == other.mutations
248            && self.subscriptions == other.subscriptions
249            && self.directives == other.directives
250            && self.fact_tables == other.fact_tables
251            && self.observers == other.observers
252            && self.federation == other.federation
253            && self.security == other.security
254            && self.observers_config == other.observers_config
255            && self.subscriptions_config == other.subscriptions_config
256            && self.validation_config == other.validation_config
257            && self.debug_config == other.debug_config
258            && self.mcp_config == other.mcp_config
259            && self.changelog == other.changelog
260            && self.naming_convention == other.naming_convention
261            && self.schema_sdl == other.schema_sdl
262    }
263}
264
265impl CompiledSchema {
266    /// Create empty schema.
267    #[must_use]
268    pub fn new() -> Self {
269        Self::default()
270    }
271}