Something went wrong. Try again.
Highly ambitious ATProtocol AppView service and sdks forked from slices.network/slices
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336//! GraphQL HTTP handler for Axum
use async_graphql::dynamic::Schema;use async_graphql::http::{WebSocket as GraphQLWebSocket, WebSocketProtocols, WsMessage};use async_graphql_axum::{GraphQLRequest, GraphQLResponse};use axum::{ extract::{ Query, State, WebSocketUpgrade, ws::{Message, WebSocket}, }, http::{HeaderMap, StatusCode}, response::{Html, Response},};use futures_util::{SinkExt, StreamExt};use serde::Deserialize;use std::sync::Arc;use tokio::sync::RwLock;
use crate::AppState;use crate::errors::AppError;use crate::graphql::GraphQLContext;
/// Global schema cache (one schema per slice)/// This prevents rebuilding the schema on every requesttype SchemaCache = Arc<RwLock<std::collections::HashMap<String, Schema>>>;
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<String>,}
/// 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<AppState>, Query(params): Query<GraphQLParams>, headers: HeaderMap, req: GraphQLRequest,) -> Result<GraphQLResponse, (StatusCode, String)> { // 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()); } };
// Extract optional bearer token for mutations let auth_token = headers .get("authorization") .and_then(|h| h.to_str().ok()) .and_then(|s| s.strip_prefix("Bearer ")) .map(|s| s.to_string());
// Create GraphQL context with DataLoader and auth let gql_context = GraphQLContext::with_auth( state.database.clone(), auth_token.clone(), state.config.auth_base_url.clone(), Some(state.auth_cache.clone()), );
// Verify auth token and get user DID for mutations let mut request = req.into_inner().data(gql_context).data(state.database_pool.clone());
if let Some(token) = auth_token { // Verify token and add user DID to context match crate::auth::verify_oauth_token_cached( &token, &state.config.auth_base_url, Some(state.auth_cache.clone()), ) .await { Ok(user_info) => { request = request.data(user_info.sub); } Err(_) => { // Invalid token - let the mutation handle the error } } }
// Execute query with context Ok(schema.execute(request).await.into())}
/// GraphiQL UI handler/// Configures GraphiQL with the slice URI in headerspub async fn graphql_playground( Query(params): Query<GraphQLParams>,) -> Result<Html<String>, (StatusCode, String)> { let slice_uri = params.slice.ok_or_else(|| { ( StatusCode::BAD_REQUEST, "Missing slice parameter. Provide ?slice=... query parameter".to_string(), ) })?;
// Create GraphiQL with pre-configured headers using React 19 and modern ESM let graphiql_html = format!( r#"<!doctype html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Slices GraphiQL</title> <style> body {{ margin: 0; }}
#graphiql {{ height: 100dvh; }}
.loading {{ height: 100%; display: flex; align-items: center; justify-content: center; font-size: 4rem; }} </style> <link rel="stylesheet" href="https://esm.sh/graphiql/dist/style.css" /> <link rel="stylesheet" href="https://esm.sh/@graphiql/plugin-explorer/dist/style.css" /> <script type="importmap"> {{ "imports": {{ "react": "https://esm.sh/react@19.1.0", "react/": "https://esm.sh/react@19.1.0/",
"react-dom": "https://esm.sh/react-dom@19.1.0", "react-dom/": "https://esm.sh/react-dom@19.1.0/",
"graphiql": "https://esm.sh/graphiql?standalone&external=react,react-dom,@graphiql/react,graphql", "graphiql/": "https://esm.sh/graphiql/", "@graphiql/plugin-explorer": "https://esm.sh/@graphiql/plugin-explorer?standalone&external=react,@graphiql/react,graphql", "@graphiql/react": "https://esm.sh/@graphiql/react?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid",
"@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit?standalone&external=graphql", "graphql": "https://esm.sh/graphql@16.11.0", "@emotion/is-prop-valid": "data:text/javascript," }} }} </script> <script type="module"> import React from 'react'; import ReactDOM from 'react-dom/client'; import {{ GraphiQL, HISTORY_PLUGIN }} from 'graphiql'; import {{ createGraphiQLFetcher }} from '@graphiql/toolkit'; import {{ explorerPlugin }} from '@graphiql/plugin-explorer'; import 'graphiql/setup-workers/esm.sh';
const fetcher = createGraphiQLFetcher({{ url: '/graphql', subscriptionUrl: '/graphql/ws?slice={}', headers: {{ 'X-Slice-Uri': '{}' }} }}); const plugins = [HISTORY_PLUGIN, explorerPlugin()];
function App() {{ return React.createElement(GraphiQL, {{ fetcher, plugins, defaultEditorToolsVisibility: true, }}); }}
const container = document.getElementById('graphiql'); const root = ReactDOM.createRoot(container); root.render(React.createElement(App)); </script> </head> <body> <div id="graphiql"> <div class="loading">Loading…</div> </div> </body></html>"#, slice_uri.replace("'", "\\'").replace("\"", "\\\""), slice_uri.replace("'", "\\'").replace("\"", "\\\"") );
Ok(Html(graphiql_html))}
/// GraphQL WebSocket handler for subscriptions/// Accepts slice URI from query parameter (?slice=...)pub async fn graphql_subscription_handler( State(state): State<AppState>, Query(params): Query<GraphQLParams>, ws: WebSocketUpgrade,) -> Result<Response, (StatusCode, String)> { let slice_uri = params.slice.ok_or_else(|| { ( StatusCode::BAD_REQUEST, "Missing slice parameter. Provide ?slice=... query parameter".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 Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Schema error: {:?}", e), )); } };
// Create GraphQL context with DataLoader (subscriptions don't need auth typically) let gql_context = GraphQLContext::with_auth( state.database.clone(), None, state.config.auth_base_url.clone(), Some(state.auth_cache.clone()), );
// Upgrade to WebSocket and handle GraphQL subscriptions manually let db_pool = state.database_pool.clone(); Ok(ws .protocols(["graphql-transport-ws", "graphql-ws"]) .on_upgrade(move |socket| handle_graphql_ws(socket, schema, gql_context, db_pool)))}
/// Handle GraphQL WebSocket connectionasync fn handle_graphql_ws(socket: WebSocket, schema: Schema, gql_context: GraphQLContext, state_pool: sqlx::PgPool) { let (ws_sender, ws_receiver) = socket.split();
// Convert axum WebSocket messages to strings for async-graphql let input = ws_receiver.filter_map(|msg| { futures_util::future::ready(match msg { Ok(Message::Text(text)) => Some(text.to_string()), _ => None, // Ignore other message types }) });
// Create GraphQL WebSocket handler with context and database pool let mut stream = GraphQLWebSocket::new(schema.clone(), input, WebSocketProtocols::GraphQLWS) .on_connection_init(move |_| { let gql_ctx = gql_context.clone(); let pool = state_pool.clone(); async move { let mut data = async_graphql::Data::default(); data.insert(gql_ctx); data.insert(pool); Ok(data) } });
// Send GraphQL messages back through WebSocket let mut ws_sender = ws_sender; while let Some(msg) = stream.next().await { let axum_msg = match msg { WsMessage::Text(text) => Message::Text(text.into()), WsMessage::Close(code, reason) => Message::Close(Some(axum::extract::ws::CloseFrame { code, reason: reason.into(), })), };
if ws_sender.send(axum_msg).await.is_err() { break; } }}
/// Gets schema from cache or builds it if not cachedasync fn get_or_build_schema(state: &AppState, slice_uri: &str) -> Result<Schema, AppError> { // 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(), state.config.auth_base_url.clone(), ) .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)}
/// Invalidates the cached GraphQL schema for a given slice////// This should be called when lexicon records are created, updated, or deleted/// to ensure the schema is rebuilt with the new lexicon definitions.pub async fn invalidate_schema_cache(slice_uri: &str) { let mut cache = SCHEMA_CACHE.write().await; cache.remove(slice_uri); tracing::debug!("Invalidated GraphQL schema cache for slice: {}", slice_uri);}