fraiseql_server/lib.rs
1//! FraiseQL HTTP Server
2//!
3//! HTTP server for FraiseQL v2 compiled GraphQL execution engine.
4//!
5//! # Architecture
6//!
7//! The server exposes a GraphQL HTTP endpoint that:
8//! 1. Receives GraphQL queries via POST
9//! 2. Executes queries using the runtime Executor
10//! 3. Returns GraphQL-compliant JSON responses
11//!
12//! # Features
13//!
14//! - GraphQL endpoint (`/graphql`)
15//! - Health check endpoint (`/health`)
16//! - Schema introspection endpoint (`/introspection`)
17//! - CORS support
18//! - Compression (gzip, br, zstd)
19//! - Request tracing
20//! - APQ (Automatic Persisted Queries)
21//! - Query caching
22//! - Authentication middleware (optional)
23
24#![forbid(unsafe_code)]
25
26// CLI argument parsing (shared with fraiseql-cli) — requires `cli` feature
27#[cfg(feature = "cli")]
28pub mod cli;
29// API key authentication
30pub mod api_key;
31// Token revocation
32pub mod token_revocation;
33
34// Original fraiseql-server modules
35pub mod api;
36pub mod error;
37pub mod extractors;
38#[cfg(feature = "federation")]
39pub mod federation;
40pub mod logging;
41pub mod middleware;
42pub mod routes;
43pub mod schema;
44pub mod server;
45pub mod server_config;
46pub mod subscriptions;
47pub mod url_guard;
48pub mod validation;
49
50// Renamed to avoid conflicts with runtime modules
51pub mod metrics_server;
52
53// fraiseql-runtime modules (merged)
54
55/// Runtime configuration types loaded from `fraiseql.toml` or environment variables.
56pub mod config;
57/// Resilience primitives: backpressure and retry policies.
58pub mod resilience;
59/// Utilities for distributed tracing, span propagation, and trace context formatting.
60#[cfg(feature = "federation")]
61pub mod tracing_utils;
62#[cfg(not(feature = "federation"))]
63pub mod tracing_utils {
64 //! Stub tracing utilities when federation is disabled.
65 use axum::http::HeaderMap;
66
67 /// Stub trace context extraction when federation is disabled.
68 #[allow(clippy::missing_const_for_fn)] // Reason: signature must match federation-enabled version which is not const
69 #[must_use]
70 pub fn extract_trace_context(_headers: &HeaderMap) -> Option<()> {
71 None
72 }
73
74 /// Extract the W3C trace id from the inbound `traceparent` header.
75 ///
76 /// Feature-independent (used to stamp the change-log `trace_id`, #375), so it
77 /// is a real implementation even without federation — identical to the
78 /// federation-enabled [`tracing_utils::extract_trace_id`](super::tracing_utils).
79 #[must_use]
80 pub fn extract_trace_id(headers: &HeaderMap) -> Option<String> {
81 let value = headers.get("traceparent")?.to_str().ok()?;
82 let trace_id = value.split('-').nth(1)?;
83 let valid = trace_id.len() == 32
84 && trace_id.bytes().all(|b| b.is_ascii_hexdigit())
85 && trace_id.bytes().any(|b| b != b'0');
86 valid.then(|| trace_id.to_ascii_lowercase())
87 }
88
89 /// Extract the full W3C trace context as a JSON object for the change-log
90 /// `trace_context` column (#375).
91 ///
92 /// Feature-independent — identical to the federation-enabled
93 /// [`tracing_utils::extract_trace_context_json`](super::tracing_utils).
94 #[must_use]
95 pub fn extract_trace_context_json(headers: &HeaderMap) -> Option<serde_json::Value> {
96 let traceparent = headers.get("traceparent")?.to_str().ok()?;
97 let mut parts = traceparent.split('-');
98 let (version, trace_id, parent_id, trace_flags) =
99 (parts.next()?, parts.next()?, parts.next()?, parts.next()?);
100 let is_hex =
101 |s: &str, len: usize| s.len() == len && s.bytes().all(|b| b.is_ascii_hexdigit());
102 let valid = is_hex(version, 2)
103 && is_hex(trace_id, 32)
104 && trace_id.bytes().any(|b| b != b'0')
105 && is_hex(parent_id, 16)
106 && is_hex(trace_flags, 2);
107 if !valid {
108 return None;
109 }
110 let mut obj = serde_json::Map::with_capacity(5);
111 obj.insert("version".to_owned(), version.to_ascii_lowercase().into());
112 obj.insert("trace_id".to_owned(), trace_id.to_ascii_lowercase().into());
113 obj.insert("parent_id".to_owned(), parent_id.to_ascii_lowercase().into());
114 obj.insert("trace_flags".to_owned(), trace_flags.to_ascii_lowercase().into());
115 if let Some(tracestate) = headers
116 .get("tracestate")
117 .and_then(|h| h.to_str().ok())
118 .map(str::trim)
119 .filter(|s| !s.is_empty())
120 {
121 obj.insert("tracestate".to_owned(), tracestate.into());
122 }
123 Some(serde_json::Value::Object(obj))
124 }
125}
126
127// Webhooks (extracted to fraiseql-webhooks crate) — optional, enable with `features = ["webhooks"]`
128// Authentication (extracted to fraiseql-auth crate) — optional, enable with `features =
129// ["auth"]`
130#[cfg(feature = "auth")]
131pub use fraiseql_auth as auth;
132#[cfg(feature = "webhooks")]
133pub use fraiseql_webhooks as webhooks;
134
135/// Stub auth types compiled when the `auth` feature is disabled.
136///
137/// These zero-sized types allow internal code that references `crate::auth::*` to compile
138/// in no-auth builds without requiring every call-site to be cfg-gated. All stub methods
139/// are pure stubs that the compiler will dead-code-eliminate.
140#[cfg(not(feature = "auth"))]
141pub mod auth {
142 use std::sync::Arc;
143
144 /// Stub for `fraiseql_auth::state_encryption::StateEncryptionService`.
145 pub mod state_encryption {
146 /// Zero-sized stub; never instantiated when `auth` feature is off.
147 pub struct StateEncryptionService;
148 impl StateEncryptionService {
149 /// Stub: returns `None`.
150 ///
151 /// # Errors
152 ///
153 /// Currently infallible — always returns `Ok(None)`.
154 /// Errors may be returned when the `auth` feature is enabled.
155 pub fn from_compiled_schema(
156 _s: &serde_json::Value,
157 ) -> crate::Result<Option<std::sync::Arc<Self>>> {
158 Ok(None)
159 }
160 }
161 }
162
163 /// Stub for `fraiseql_auth::PkceStateStore`.
164 pub struct PkceStateStore;
165 impl PkceStateStore {
166 /// Stub: always returns `true` (in-memory).
167 pub fn is_in_memory(&self) -> bool {
168 true
169 }
170
171 /// Stub: no-op.
172 pub async fn cleanup_expired(&self) {}
173 }
174
175 /// Stub for `fraiseql_auth::OidcServerClient`.
176 pub struct OidcServerClient;
177 impl OidcServerClient {
178 /// Stub: always returns `None`.
179 pub fn from_compiled_schema(_schema_json: &serde_json::Value) -> Option<Arc<Self>> {
180 None
181 }
182 }
183}
184
185// Secrets management and encryption (extracted to fraiseql-secrets crate) — optional, enable with
186// `features = ["secrets"]`
187#[cfg(feature = "secrets")]
188pub use fraiseql_secrets::{encryption, secrets_manager};
189
190// TLS/SSL and encryption
191pub mod tls;
192
193// Observer management - optional
194#[cfg(feature = "observers")]
195pub mod observers;
196
197// Arrow Flight integration - optional
198#[cfg(feature = "arrow")]
199pub mod arrow;
200
201// MCP (Model Context Protocol) server - optional
202#[cfg(feature = "mcp")]
203pub mod mcp;
204
205// Connection pool management and auto-tuning
206pub mod pool;
207
208// Realtime WebSocket server — entity change streams (complementary to subscriptions/)
209pub mod realtime;
210
211// Object storage backends (local, S3, GCS, Azure Blob)
212pub mod storage;
213
214// Server subsystem assembly and lifecycle management
215pub mod subsystems;
216
217// Trusted documents (query allowlist)
218pub mod trusted_documents;
219
220// Multi-tenancy: pool factory, executor construction, health monitoring
221pub mod tenancy;
222
223// Usage aggregation: in-memory mutation counters fed by tracing events
224pub mod usage;
225
226// Testing utilities
227#[cfg(any(test, feature = "testing"))]
228pub mod testing;
229
230#[cfg(test)]
231mod tests;
232
233#[cfg(feature = "cli")]
234pub use cli::{Cli, ServerArgs};
235pub use logging::{
236 ErrorDetails, LogLevel, LogMetrics, RequestContext, RequestId, RequestLogger, SourceLocation,
237 StructuredLogEntry,
238};
239pub use metrics_server::{MetricsCollector, PrometheusMetrics};
240pub use schema::CompiledSchemaLoader;
241pub use server::Server;
242pub use server_config::ServerConfig;
243pub use tls::TlsSetup;
244pub use validation::{ComplexityValidationError, RequestValidator};
245
246/// Convenience re-exports for building a FraiseQL HTTP server.
247///
248/// Provides [`Server`], [`ServerConfig`], `CompiledSchema`, and [`RequestValidator`].
249///
250/// ```rust
251/// use fraiseql_server::prelude::*;
252/// ```
253pub mod prelude {
254 pub use fraiseql_core::schema::CompiledSchema;
255
256 pub use crate::{
257 ComplexityValidationError, RequestValidator, Server, ServerConfig, ServerError, TlsSetup,
258 };
259}
260
261/// Server error type.
262#[derive(Debug, thiserror::Error)]
263#[non_exhaustive]
264pub enum ServerError {
265 /// Server binding error.
266 #[error("Failed to bind server: {0}")]
267 BindError(String),
268
269 /// Configuration error.
270 #[error("Configuration error: {0}")]
271 ConfigError(String),
272
273 /// Error from the FraiseQL execution engine (parse, validate, execute, …).
274 ///
275 /// Wraps the canonical [`fraiseql_core::error::FraiseQLError`] so engine
276 /// failures bubble up through `ServerError` without losing the structured
277 /// payload. The original variant name (`RuntimeError`) collided with the
278 /// retired `fraiseql_error::RuntimeError` HTTP-shaped enum; `Engine`
279 /// reflects what the variant actually wraps.
280 #[error("Engine error: {0}")]
281 Engine(#[from] fraiseql_core::error::FraiseQLError),
282
283 /// IO error.
284 #[error("IO error: {0}")]
285 IoError(#[from] std::io::Error),
286
287 /// Database error.
288 #[error("Database error: {0}")]
289 Database(String),
290
291 /// Validation error.
292 #[error("Validation error: {0}")]
293 Validation(String),
294
295 /// Resource conflict error.
296 #[error("Conflict: {0}")]
297 Conflict(String),
298
299 /// Resource not found error.
300 #[error("Not found: {0}")]
301 NotFound(String),
302}
303
304/// Server result type.
305pub type Result<T> = std::result::Result<T, ServerError>;