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