forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
1//! GraphQL HTTP handler for Axum
2
3use async_graphql::dynamic::Schema;
4use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
5use axum::{
6 extract::{Query, State},
7 http::{HeaderMap, StatusCode},
8 response::Html,
9};
10use serde::Deserialize;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13
14use crate::errors::AppError;
15use crate::AppState;
16
17/// Global schema cache (one schema per slice)
18/// This prevents rebuilding the schema on every request
19type SchemaCache = Arc<RwLock<std::collections::HashMap<String, Schema>>>;
20
21lazy_static::lazy_static! {
22 static ref SCHEMA_CACHE: SchemaCache = Arc::new(RwLock::new(std::collections::HashMap::new()));
23}
24
25#[derive(Deserialize, Default)]
26pub struct GraphQLParams {
27 pub slice: Option<String>,
28}
29
30/// GraphQL query handler
31/// Accepts slice URI from either query parameter (?slice=...) or HTTP header (X-Slice-Uri)
32pub async fn graphql_handler(
33 State(state): State<AppState>,
34 Query(params): Query<GraphQLParams>,
35 headers: HeaderMap,
36 req: GraphQLRequest,
37) -> Result<GraphQLResponse, (StatusCode, String)> {
38 // Get slice URI from query param or header
39 let slice_uri = params
40 .slice
41 .or_else(|| {
42 headers
43 .get("x-slice-uri")
44 .and_then(|h| h.to_str().ok())
45 .map(|s| s.to_string())
46 })
47 .ok_or_else(|| {
48 (
49 StatusCode::BAD_REQUEST,
50 "Missing slice parameter. Provide either ?slice=... query parameter or X-Slice-Uri header".to_string(),
51 )
52 })?;
53
54 let schema = match get_or_build_schema(&state, &slice_uri).await {
55 Ok(s) => s,
56 Err(e) => {
57 tracing::error!("Failed to get GraphQL schema: {:?}", e);
58 return Ok(async_graphql::Response::from_errors(vec![async_graphql::ServerError::new(
59 format!("Schema error: {:?}", e),
60 None,
61 )])
62 .into());
63 }
64 };
65
66 Ok(schema.execute(req.into_inner()).await.into())
67}
68
69/// GraphQL Playground UI handler
70/// Configures the playground with the slice URI in headers
71pub async fn graphql_playground(
72 Query(params): Query<GraphQLParams>,
73) -> Result<Html<String>, (StatusCode, String)> {
74 let slice_uri = params.slice.ok_or_else(|| {
75 (
76 StatusCode::BAD_REQUEST,
77 "Missing slice parameter. Provide ?slice=... query parameter".to_string(),
78 )
79 })?;
80
81 // Create playground with pre-configured headers
82 let playground_html = format!(
83 r#"<!DOCTYPE html>
84<html>
85<head>
86 <meta charset=utf-8 />
87 <meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
88 <title>Slices GraphQL Playground</title>
89 <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css" />
90 <link rel="shortcut icon" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/favicon.png" />
91 <script src="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js"></script>
92</head>
93<body>
94 <div id="root"></div>
95 <script>
96 window.addEventListener('load', function (event) {{
97 GraphQLPlayground.init(document.getElementById('root'), {{
98 endpoint: '/graphql',
99 settings: {{
100 'request.credentials': 'omit',
101 }},
102 tabs: [{{
103 endpoint: '/graphql',
104 headers: {{
105 'X-Slice-Uri': '{}'
106 }}
107 }}]
108 }})
109 }})
110 </script>
111</body>
112</html>"#,
113 slice_uri.replace("'", "\\'").replace("\"", "\\\"")
114 );
115
116 Ok(Html(playground_html))
117}
118
119/// Gets schema from cache or builds it if not cached
120async fn get_or_build_schema(
121 state: &AppState,
122 slice_uri: &str,
123) -> Result<Schema, AppError> {
124 // Check cache first
125 {
126 let cache = SCHEMA_CACHE.read().await;
127 if let Some(schema) = cache.get(slice_uri) {
128 return Ok(schema.clone());
129 }
130 }
131
132 // Build schema
133 let schema = crate::graphql::build_graphql_schema(
134 state.database.clone(),
135 slice_uri.to_string(),
136 )
137 .await
138 .map_err(|e| AppError::Internal(format!("Failed to build GraphQL schema: {}", e)))?;
139
140 // Cache it
141 {
142 let mut cache = SCHEMA_CACHE.write().await;
143 cache.insert(slice_uri.to_string(), schema.clone());
144 }
145
146 Ok(schema)
147}