//! GraphQL HTTP handler for Axum use async_graphql::dynamic::Schema; use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; use axum::{ extract::{Query, State}, http::{HeaderMap, StatusCode}, response::Html, }; use serde::Deserialize; use std::sync::Arc; use tokio::sync::RwLock; use crate::errors::AppError; use crate::AppState; /// Global schema cache (one schema per slice) /// This prevents rebuilding the schema on every request type SchemaCache = Arc>>; lazy_static::lazy_static! { static ref SCHEMA_CACHE: SchemaCache = Arc::new(RwLock::new(std::collections::HashMap::new())); } #[derive(Deserialize, Default)] pub struct GraphQLParams { pub slice: Option, } /// GraphQL query handler /// Accepts slice URI from either query parameter (?slice=...) or HTTP header (X-Slice-Uri) pub async fn graphql_handler( State(state): State, Query(params): Query, headers: HeaderMap, req: GraphQLRequest, ) -> Result { // Get slice URI from query param or header let slice_uri = params .slice .or_else(|| { headers .get("x-slice-uri") .and_then(|h| h.to_str().ok()) .map(|s| s.to_string()) }) .ok_or_else(|| { ( StatusCode::BAD_REQUEST, "Missing slice parameter. Provide either ?slice=... query parameter or X-Slice-Uri header".to_string(), ) })?; let schema = match get_or_build_schema(&state, &slice_uri).await { Ok(s) => s, Err(e) => { tracing::error!("Failed to get GraphQL schema: {:?}", e); return Ok(async_graphql::Response::from_errors(vec![async_graphql::ServerError::new( format!("Schema error: {:?}", e), None, )]) .into()); } }; Ok(schema.execute(req.into_inner()).await.into()) } /// GraphQL Playground UI handler /// Configures the playground with the slice URI in headers pub async fn graphql_playground( Query(params): Query, ) -> Result, (StatusCode, String)> { let slice_uri = params.slice.ok_or_else(|| { ( StatusCode::BAD_REQUEST, "Missing slice parameter. Provide ?slice=... query parameter".to_string(), ) })?; // Create playground with pre-configured headers let playground_html = format!( r#" Slices GraphQL Playground
"#, slice_uri.replace("'", "\\'").replace("\"", "\\\"") ); Ok(Html(playground_html)) } /// Gets schema from cache or builds it if not cached async fn get_or_build_schema( state: &AppState, slice_uri: &str, ) -> Result { // Check cache first { let cache = SCHEMA_CACHE.read().await; if let Some(schema) = cache.get(slice_uri) { return Ok(schema.clone()); } } // Build schema let schema = crate::graphql::build_graphql_schema( state.database.clone(), slice_uri.to_string(), ) .await .map_err(|e| AppError::Internal(format!("Failed to build GraphQL schema: {}", e)))?; // Cache it { let mut cache = SCHEMA_CACHE.write().await; cache.insert(slice_uri.to_string(), schema.clone()); } Ok(schema) }