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