1use std::{
8 sync::{Arc, atomic::Ordering},
9 time::Instant,
10};
11
12use axum::{
13 Json,
14 extract::{Query, State},
15 http::HeaderMap,
16 response::{IntoResponse, Response},
17};
18use fraiseql_core::{db::traits::DatabaseAdapter, runtime::Executor, security::SecurityContext};
19use serde::{Deserialize, Serialize};
20use tracing::{debug, error, info, warn};
21
22use crate::{
23 error::{ErrorResponse, GraphQLError},
24 extractors::OptionalSecurityContext,
25 metrics_server::MetricsCollector,
26 tracing_utils,
27 validation::RequestValidator,
28};
29
30#[derive(Debug, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct GraphQLRequest {
34 pub query: String,
36
37 #[serde(default)]
39 pub variables: Option<serde_json::Value>,
40
41 #[serde(default)]
43 pub operation_name: Option<String>,
44}
45
46#[derive(Debug, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct GraphQLGetParams {
55 pub query: String,
57
58 #[serde(default)]
60 pub variables: Option<String>,
61
62 #[serde(default)]
64 pub operation_name: Option<String>,
65}
66
67#[derive(Debug, Serialize)]
69pub struct GraphQLResponse {
70 #[serde(flatten)]
72 pub body: serde_json::Value,
73}
74
75impl IntoResponse for GraphQLResponse {
76 fn into_response(self) -> Response {
77 Json(self.body).into_response()
78 }
79}
80
81#[derive(Clone)]
85pub struct AppState<A: DatabaseAdapter> {
86 pub executor: Arc<Executor<A>>,
88 pub metrics: Arc<MetricsCollector>,
90 pub cache: Option<Arc<fraiseql_arrow::cache::QueryCache>>,
92 pub config: Option<Arc<crate::config::ServerConfig>>,
94}
95
96impl<A: DatabaseAdapter> AppState<A> {
97 #[must_use]
99 pub fn new(executor: Arc<Executor<A>>) -> Self {
100 Self {
101 executor,
102 metrics: Arc::new(MetricsCollector::new()),
103 cache: None,
104 config: None,
105 }
106 }
107
108 #[must_use]
110 pub fn with_metrics(executor: Arc<Executor<A>>, metrics: Arc<MetricsCollector>) -> Self {
111 Self {
112 executor,
113 metrics,
114 cache: None,
115 config: None,
116 }
117 }
118
119 #[must_use]
123 pub fn with_cache(
124 executor: Arc<Executor<A>>,
125 cache: Arc<fraiseql_arrow::cache::QueryCache>,
126 ) -> Self {
127 Self {
128 executor,
129 metrics: Arc::new(MetricsCollector::new()),
130 cache: Some(cache),
131 config: None,
132 }
133 }
134
135 #[must_use]
139 pub fn with_cache_and_config(
140 executor: Arc<Executor<A>>,
141 cache: Arc<fraiseql_arrow::cache::QueryCache>,
142 config: Arc<crate::config::ServerConfig>,
143 ) -> Self {
144 Self {
145 executor,
146 metrics: Arc::new(MetricsCollector::new()),
147 cache: Some(cache),
148 config: Some(config),
149 }
150 }
151
152 pub fn cache(&self) -> Option<&Arc<fraiseql_arrow::cache::QueryCache>> {
154 self.cache.as_ref()
155 }
156
157 pub fn server_config(&self) -> Option<&Arc<crate::config::ServerConfig>> {
159 self.config.as_ref()
160 }
161
162 pub fn sanitized_config(&self) -> Option<crate::routes::api::types::SanitizedConfig> {
166 self.config
167 .as_ref()
168 .map(|cfg| crate::routes::api::types::SanitizedConfig::from_config(cfg))
169 }
170}
171
172pub async fn graphql_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
190 State(state): State<AppState<A>>,
191 headers: HeaderMap,
192 OptionalSecurityContext(security_context): OptionalSecurityContext,
193 Json(request): Json<GraphQLRequest>,
194) -> Result<GraphQLResponse, ErrorResponse> {
195 let trace_context = tracing_utils::extract_trace_context(&headers);
197 if trace_context.is_some() {
198 debug!("Extracted W3C trace context from incoming request");
199 }
200
201 if security_context.is_some() {
202 debug!("Authenticated request with security context");
203 }
204
205 execute_graphql_request(state, request, trace_context, security_context).await
206}
207
208pub async fn graphql_get_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
233 State(state): State<AppState<A>>,
234 headers: HeaderMap,
235 Query(params): Query<GraphQLGetParams>,
236) -> Result<GraphQLResponse, ErrorResponse> {
237 let variables = if let Some(vars_str) = params.variables {
239 match serde_json::from_str::<serde_json::Value>(&vars_str) {
240 Ok(v) => Some(v),
241 Err(e) => {
242 warn!(
243 error = %e,
244 variables = %vars_str,
245 "Failed to parse variables JSON in GET request"
246 );
247 return Err(ErrorResponse::from_error(GraphQLError::request(format!(
248 "Invalid variables JSON: {e}"
249 ))));
250 },
251 }
252 } else {
253 None
254 };
255
256 if params.query.trim_start().starts_with("mutation") {
258 warn!(
259 operation_name = ?params.operation_name,
260 "Mutation sent via GET request - should use POST"
261 );
262 }
263
264 let trace_context = tracing_utils::extract_trace_context(&headers);
265 if trace_context.is_some() {
266 debug!("Extracted W3C trace context from incoming request");
267 }
268
269 let request = GraphQLRequest {
270 query: params.query,
271 variables,
272 operation_name: params.operation_name,
273 };
274
275 execute_graphql_request(state, request, trace_context, None).await
278}
279
280async fn execute_graphql_request<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
282 state: AppState<A>,
283 request: GraphQLRequest,
284 _trace_context: Option<fraiseql_core::federation::FederationTraceContext>,
285 security_context: Option<SecurityContext>,
286) -> Result<GraphQLResponse, ErrorResponse> {
287 let start_time = Instant::now();
288 let metrics = &state.metrics;
289
290 metrics.queries_total.fetch_add(1, Ordering::Relaxed);
292
293 info!(
294 query_length = request.query.len(),
295 has_variables = request.variables.is_some(),
296 operation_name = ?request.operation_name,
297 "Executing GraphQL query"
298 );
299
300 let validator = RequestValidator::new();
302
303 if let Err(e) = validator.validate_query(&request.query) {
305 error!(
306 error = %e,
307 operation_name = ?request.operation_name,
308 "Query validation failed"
309 );
310 metrics.queries_error.fetch_add(1, Ordering::Relaxed);
311 metrics.validation_errors_total.fetch_add(1, Ordering::Relaxed);
312 let graphql_error = match e {
313 crate::validation::ValidationError::QueryTooDeep {
314 max_depth,
315 actual_depth,
316 } => GraphQLError::validation(format!(
317 "Query exceeds maximum depth: {actual_depth} > {max_depth}"
318 )),
319 crate::validation::ValidationError::QueryTooComplex {
320 max_complexity,
321 actual_complexity,
322 } => GraphQLError::validation(format!(
323 "Query exceeds maximum complexity: {actual_complexity} > {max_complexity}"
324 )),
325 crate::validation::ValidationError::MalformedQuery(msg) => {
326 metrics.parse_errors_total.fetch_add(1, Ordering::Relaxed);
327 GraphQLError::parse(msg)
328 },
329 crate::validation::ValidationError::InvalidVariables(msg) => GraphQLError::request(msg),
330 };
331 return Err(ErrorResponse::from_error(graphql_error));
332 }
333
334 if let Err(e) = validator.validate_variables(request.variables.as_ref()) {
336 error!(
337 error = %e,
338 operation_name = ?request.operation_name,
339 "Variables validation failed"
340 );
341 metrics.queries_error.fetch_add(1, Ordering::Relaxed);
342 metrics.validation_errors_total.fetch_add(1, Ordering::Relaxed);
343 return Err(ErrorResponse::from_error(GraphQLError::request(e.to_string())));
344 }
345
346 let result = if let Some(sec_ctx) = security_context {
348 state
349 .executor
350 .execute_with_security(&request.query, request.variables.as_ref(), &sec_ctx)
351 .await
352 .map_err(|e| {
353 let elapsed = start_time.elapsed();
354 error!(
355 error = %e,
356 elapsed_ms = elapsed.as_millis(),
357 operation_name = ?request.operation_name,
358 "Query execution failed"
359 );
360 metrics.queries_error.fetch_add(1, Ordering::Relaxed);
361 metrics.execution_errors_total.fetch_add(1, Ordering::Relaxed);
362 metrics
364 .queries_duration_us
365 .fetch_add(elapsed.as_micros() as u64, Ordering::Relaxed);
366 ErrorResponse::from_error(GraphQLError::execution(&e.to_string()))
367 })?
368 } else {
369 state
370 .executor
371 .execute(&request.query, request.variables.as_ref())
372 .await
373 .map_err(|e| {
374 let elapsed = start_time.elapsed();
375 error!(
376 error = %e,
377 elapsed_ms = elapsed.as_millis(),
378 operation_name = ?request.operation_name,
379 "Query execution failed"
380 );
381 metrics.queries_error.fetch_add(1, Ordering::Relaxed);
382 metrics.execution_errors_total.fetch_add(1, Ordering::Relaxed);
383 metrics
385 .queries_duration_us
386 .fetch_add(elapsed.as_micros() as u64, Ordering::Relaxed);
387 ErrorResponse::from_error(GraphQLError::execution(&e.to_string()))
388 })?
389 };
390
391 let elapsed = start_time.elapsed();
392 let elapsed_us = elapsed.as_micros() as u64;
393
394 metrics.queries_success.fetch_add(1, Ordering::Relaxed);
396 metrics.queries_duration_us.fetch_add(elapsed_us, Ordering::Relaxed);
397 metrics.db_queries_total.fetch_add(1, Ordering::Relaxed);
398 metrics.db_queries_duration_us.fetch_add(elapsed_us, Ordering::Relaxed);
399
400 if fraiseql_core::federation::is_federation_query(&request.query) {
402 metrics.record_entity_resolution(elapsed_us, true);
403 }
404
405 debug!(
406 response_length = result.len(),
407 elapsed_ms = elapsed.as_millis(),
408 operation_name = ?request.operation_name,
409 "Query executed successfully"
410 );
411
412 let response_json: serde_json::Value = serde_json::from_str(&result).map_err(|e| {
414 error!(
415 error = %e,
416 response_length = result.len(),
417 "Failed to deserialize executor response"
418 );
419 ErrorResponse::from_error(GraphQLError::internal(format!(
420 "Failed to process response: {e}"
421 )))
422 })?;
423
424 Ok(GraphQLResponse {
425 body: response_json,
426 })
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_graphql_request_deserialize() {
435 let json = r#"{"query": "{ users { id } }"}"#;
436 let request: GraphQLRequest = serde_json::from_str(json).unwrap();
437 assert_eq!(request.query, "{ users { id } }");
438 assert!(request.variables.is_none());
439 }
440
441 #[test]
442 fn test_graphql_request_with_variables() {
443 let json = r#"{"query": "query($id: ID!) { user(id: $id) { name } }", "variables": {"id": "123"}}"#;
444 let request: GraphQLRequest = serde_json::from_str(json).unwrap();
445 assert!(request.variables.is_some());
446 }
447
448 #[test]
449 fn test_graphql_get_params_deserialize() {
450 let params: GraphQLGetParams = serde_json::from_value(serde_json::json!({
452 "query": "{ users { id } }",
453 "operationName": "GetUsers"
454 }))
455 .unwrap();
456
457 assert_eq!(params.query, "{ users { id } }");
458 assert_eq!(params.operation_name, Some("GetUsers".to_string()));
459 assert!(params.variables.is_none());
460 }
461
462 #[test]
463 fn test_graphql_get_params_with_variables() {
464 let params: GraphQLGetParams = serde_json::from_value(serde_json::json!({
466 "query": "query($id: ID!) { user(id: $id) { name } }",
467 "variables": r#"{"id": "123"}"#
468 }))
469 .unwrap();
470
471 assert!(params.variables.is_some());
472 let vars_str = params.variables.unwrap();
473 let vars: serde_json::Value = serde_json::from_str(&vars_str).unwrap();
474 assert_eq!(vars["id"], "123");
475 }
476
477 #[test]
478 fn test_graphql_get_params_camel_case() {
479 let params: GraphQLGetParams = serde_json::from_value(serde_json::json!({
481 "query": "{ users { id } }",
482 "operationName": "TestOp"
483 }))
484 .unwrap();
485
486 assert_eq!(params.operation_name, Some("TestOp".to_string()));
487 }
488
489 #[test]
494 fn test_appstate_has_cache_field() {
495 let _note = "AppState<A> includes: executor, metrics, cache, config";
497 assert!(_note.len() > 0);
498 }
499
500 #[test]
501 fn test_appstate_has_config_field() {
502 let _note = "AppState<A>::cache: Option<Arc<QueryCache>>";
504 assert!(_note.len() > 0);
505 }
506
507 #[test]
508 fn test_appstate_with_cache_constructor() {
509 let _note = "AppState::with_cache(executor, cache) -> Self";
511 assert!(_note.len() > 0);
512 }
513
514 #[test]
515 fn test_appstate_with_cache_and_config_constructor() {
516 let _note = "AppState::with_cache_and_config(executor, cache, config) -> Self";
518 assert!(_note.len() > 0);
519 }
520
521 #[test]
522 fn test_appstate_cache_accessor() {
523 let _note = "AppState::cache() -> Option<&Arc<QueryCache>>";
525 assert!(_note.len() > 0);
526 }
527
528 #[test]
529 fn test_appstate_server_config_accessor() {
530 let _note = "AppState::server_config() -> Option<&Arc<ServerConfig>>";
532 assert!(_note.len() > 0);
533 }
534
535 #[test]
537 fn test_sanitized_config_from_server_config() {
538 use crate::routes::api::types::SanitizedConfig;
540
541 let config = crate::config::ServerConfig {
542 port: 8080,
543 host: "0.0.0.0".to_string(),
544 workers: Some(4),
545 tls: None,
546 limits: None,
547 };
548
549 let sanitized = SanitizedConfig::from_config(&config);
550
551 assert_eq!(sanitized.port, 8080, "Port should be preserved");
552 assert_eq!(sanitized.host, "0.0.0.0", "Host should be preserved");
553 assert_eq!(sanitized.workers, Some(4), "Workers count should be preserved");
554 assert!(!sanitized.tls_enabled, "TLS should be false when not configured");
555 assert!(sanitized.is_sanitized(), "Should be marked as sanitized");
556 }
557
558 #[test]
559 fn test_sanitized_config_indicates_tls_without_exposing_keys() {
560 use std::path::PathBuf;
562
563 use crate::routes::api::types::SanitizedConfig;
564
565 let config = crate::config::ServerConfig {
566 port: 8080,
567 host: "localhost".to_string(),
568 workers: None,
569 tls: Some(crate::config::TlsConfig {
570 cert_file: PathBuf::from("/path/to/cert.pem"),
571 key_file: PathBuf::from("/path/to/key.pem"),
572 }),
573 limits: None,
574 };
575
576 let sanitized = SanitizedConfig::from_config(&config);
577
578 assert!(sanitized.tls_enabled, "TLS should be true when configured");
579 let json = serde_json::to_string(&sanitized).unwrap();
581 assert!(!json.contains("cert"), "Certificate file path should not be exposed");
582 assert!(!json.contains("key"), "Key file path should not be exposed");
583 }
584
585 #[test]
586 fn test_sanitized_config_redaction() {
587 use crate::routes::api::types::SanitizedConfig;
589
590 let config1 = crate::config::ServerConfig {
591 port: 8000,
592 host: "127.0.0.1".to_string(),
593 workers: None,
594 tls: None,
595 limits: None,
596 };
597
598 let config2 = crate::config::ServerConfig {
599 port: 8000,
600 host: "127.0.0.1".to_string(),
601 workers: None,
602 tls: Some(crate::config::TlsConfig {
603 cert_file: std::path::PathBuf::from("secret.cert"),
604 key_file: std::path::PathBuf::from("secret.key"),
605 }),
606 limits: None,
607 };
608
609 let san1 = SanitizedConfig::from_config(&config1);
610 let san2 = SanitizedConfig::from_config(&config2);
611
612 assert_eq!(san1.port, san2.port);
614 assert_eq!(san1.host, san2.host);
615
616 assert!(!san1.tls_enabled);
618 assert!(san2.tls_enabled);
619 }
620
621 #[test]
623 fn test_appstate_executor_provides_access_to_schema() {
624 let _note = "AppState<A>::executor can be queried for schema information";
626 assert!(_note.len() > 0);
627 }
628
629 #[test]
630 fn test_schema_access_for_api_endpoints() {
631 let _note = "API routes can access schema via state.executor for introspection";
633 assert!(_note.len() > 0);
634 }
635}