1use std::borrow::Cow;
4use std::sync::{Arc, OnceLock};
5use std::time::Duration;
6
7use async_graphql::{Executor, ObjectType, Schema, SchemaBuilder, SubscriptionType};
8use autumn_web::AppState;
9use autumn_web::app::AppBuilder;
10use autumn_web::plugin::Plugin;
11use autumn_web::plugin_contract::PluginContract;
12use autumn_web::route_listing::{RouteClassification, RouteInfo};
13use axum::Router;
14use axum::extract::Extension;
15use axum::response::IntoResponse;
16use axum::routing::{get, post};
17use tokio::sync::{Semaphore, watch};
18
19use crate::config::{
20 ConfigError, DEFAULT_SECTION, GraphqlConfig, PersistedQueryMode, Resolved, Toggle,
21};
22use crate::context::{ContextHook, WsInitHook};
23use crate::metrics::Metrics;
24use crate::persisted::{InMemoryPersistedQueryStore, PersistedQueryStore, TrustedDocuments};
25use crate::pipeline::{Pipeline, PipelineSettings};
26use crate::transport::{Endpoint, http, ws};
27
28pub const PLUGIN_NAME: &str = "autumn-plugin-graphql";
31
32pub const SUPPORTED_AUTUMN_WEB: &str = "0.10";
36
37type Materialize<E> = Box<dyn FnOnce(&GraphqlConfig) -> (E, Option<String>) + Send>;
38type RouterTransform = Box<dyn FnOnce(Router<AppState>) -> Router<AppState> + Send>;
39type Override = Arc<dyn Fn(&mut GraphqlConfig) + Send + Sync>;
40
41enum Source<E> {
42 Ready { executor: E, sdl: Option<String> },
43 Deferred(Materialize<E>),
44}
45
46enum ConfigSource {
47 Resolve(String),
49 Explicit(Box<GraphqlConfig>),
51}
52
53struct Guard {
54 apply: RouterTransform,
55 label: String,
56}
57
58pub struct GraphqlPlugin<E> {
76 source: Option<Source<E>>,
78 subscriptions_supported: bool,
79 has_sdl: bool,
80 config_source: ConfigSource,
81 overrides: Vec<Override>,
82 resolved: OnceLock<Result<Resolved, ConfigError>>,
83 development: Option<bool>,
84 guard: Option<Guard>,
85 hooks: Vec<Arc<dyn ContextHook>>,
86 ws_init: Option<Arc<dyn WsInitHook>>,
87 store: Option<Arc<dyn PersistedQueryStore>>,
88 trusted: TrustedDocuments,
89 drain: DrainHandle,
90}
91
92#[derive(Clone, Debug)]
101pub struct DrainHandle {
102 tx: watch::Sender<u64>,
106}
107
108impl DrainHandle {
109 pub(crate) fn new() -> Self {
110 Self {
111 tx: watch::channel(0).0,
112 }
113 }
114
115 pub fn drain(&self) {
117 self.tx
118 .send_modify(|generation| *generation = generation.wrapping_add(1));
119 }
120
121 pub(crate) fn subscribe(&self) -> watch::Receiver<u64> {
123 self.tx.subscribe()
124 }
125}
126
127impl<Q, M, S> GraphqlPlugin<Schema<Q, M, S>>
128where
129 Q: ObjectType + 'static,
130 M: ObjectType + 'static,
131 S: SubscriptionType + 'static,
132{
133 #[must_use]
137 pub fn new(schema: Schema<Q, M, S>) -> Self {
138 let sdl = schema.sdl();
139 Self::with_source(
140 Source::Ready {
141 executor: schema,
142 sdl: Some(sdl),
143 },
144 !S::is_empty(),
145 true,
146 )
147 }
148
149 #[must_use]
155 pub fn from_builder(builder: SchemaBuilder<Q, M, S>) -> Self
156 where
157 SchemaBuilder<Q, M, S>: Send,
158 {
159 Self::with_source(
160 Source::Deferred(Box::new(move |config: &GraphqlConfig| {
161 let limits = &config.limits;
162 let mut builder = builder;
163 if limits.max_depth != 0 {
164 builder = builder.limit_depth(limits.max_depth);
165 }
166 if limits.max_complexity != 0 {
167 builder = builder.limit_complexity(limits.max_complexity);
168 }
169 if limits.max_directives != 0 {
170 builder = builder.limit_directives(limits.max_directives);
171 }
172 let schema = builder.finish();
173 let sdl = schema.sdl();
174 (schema, Some(sdl))
175 })),
176 !S::is_empty(),
177 true,
178 )
179 }
180}
181
182impl<E: Executor> GraphqlPlugin<E> {
183 #[must_use]
189 pub fn from_executor(executor: E) -> Self {
190 Self::with_source(
191 Source::Ready {
192 executor,
193 sdl: None,
194 },
195 true,
196 false,
197 )
198 }
199
200 fn with_source(source: Source<E>, subscriptions_supported: bool, has_sdl: bool) -> Self {
201 Self {
202 source: Some(source),
203 subscriptions_supported,
204 has_sdl,
205 config_source: ConfigSource::Resolve(DEFAULT_SECTION.to_owned()),
206 overrides: Vec::new(),
207 resolved: OnceLock::new(),
208 development: None,
209 guard: None,
210 hooks: Vec::new(),
211 ws_init: None,
212 store: None,
213 trusted: TrustedDocuments::new(),
214 drain: DrainHandle::new(),
215 }
216 }
217
218 #[must_use]
221 pub fn drain_handle(&self) -> DrainHandle {
222 self.drain.clone()
223 }
224
225 #[must_use]
228 pub fn sdl_text(mut self, sdl: impl Into<String>) -> Self {
229 let sdl = sdl.into();
230 self.has_sdl = true;
231 self.source = match self.source {
232 Some(Source::Ready { executor, .. }) => Some(Source::Ready {
233 executor,
234 sdl: Some(sdl),
235 }),
236 other => other,
237 };
238 self
239 }
240
241 fn reset(&mut self) {
242 self.resolved = OnceLock::new();
243 }
244
245 #[must_use]
254 pub fn configure(mut self, apply: impl Fn(&mut GraphqlConfig) + Send + Sync + 'static) -> Self {
255 self.overrides.push(Arc::new(apply));
256 self.reset();
257 self
258 }
259
260 #[must_use]
263 pub fn config(mut self, config: GraphqlConfig) -> Self {
264 self.config_source = ConfigSource::Explicit(Box::new(config));
265 self.reset();
266 self
267 }
268
269 #[must_use]
272 pub fn config_section(mut self, section: impl Into<String>) -> Self {
273 self.config_source = ConfigSource::Resolve(section.into());
274 self.reset();
275 self
276 }
277
278 #[must_use]
281 pub fn development(mut self, development: bool) -> Self {
282 self.development = Some(development);
283 self.reset();
284 self
285 }
286
287 #[must_use]
289 pub fn path(self, path: impl Into<String>) -> Self {
290 let path = path.into();
291 self.configure(move |c| c.path.clone_from(&path))
292 }
293
294 #[must_use]
296 pub fn introspection(self, enabled: bool) -> Self {
297 self.configure(move |c| c.introspection = enabled.into())
298 }
299
300 #[must_use]
302 pub fn without_sdl(self) -> Self {
303 self.configure(|c| c.sdl = Toggle::Off)
304 }
305
306 #[must_use]
308 pub fn mask_unexpected_errors(self, mask: bool) -> Self {
309 self.configure(move |c| c.mask_unexpected_errors = mask.into())
310 }
311
312 #[must_use]
314 pub fn timeout(self, timeout: Duration) -> Self {
315 let millis = u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX);
316 self.configure(move |c| c.timeout_ms = millis)
317 }
318
319 #[must_use]
321 pub fn batching(self, max_operations: usize) -> Self {
322 self.configure(move |c| {
323 c.batching.enabled = true;
324 c.batching.max_operations = max_operations;
325 })
326 }
327
328 #[must_use]
330 pub fn persisted_queries(self, mode: PersistedQueryMode) -> Self {
331 self.configure(move |c| c.persisted_queries.mode = mode)
332 }
333
334 #[must_use]
336 pub fn trusted_documents(mut self, documents: TrustedDocuments) -> Self {
337 self.trusted.extend(documents);
338 self
339 }
340
341 #[must_use]
344 pub fn persisted_query_store(mut self, store: impl PersistedQueryStore) -> Self {
345 self.store = Some(Arc::new(store));
346 self
347 }
348
349 #[must_use]
362 pub fn context<F>(mut self, hook: F) -> Self
363 where
364 F: for<'a> Fn(
365 &'a mut ::http::request::Parts,
366 &'a AppState,
367 &'a mut async_graphql::Data,
368 ) -> crate::persisted::BoxFuture<'a, autumn_web::AutumnResult<()>>
369 + Send
370 + Sync
371 + 'static,
372 {
373 self.hooks.push(Arc::new(hook));
374 self
375 }
376
377 #[must_use]
379 pub fn context_hook(mut self, hook: impl ContextHook) -> Self {
380 self.hooks.push(Arc::new(hook));
381 self
382 }
383
384 #[must_use]
386 pub fn on_ws_init(mut self, hook: impl WsInitHook) -> Self {
387 self.ws_init = Some(Arc::new(hook));
388 self
389 }
390
391 #[must_use]
401 pub fn guard<L>(mut self, layer: L, label: impl Into<String>) -> Self
402 where
403 L: tower::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
404 L::Service: tower::Service<axum::extract::Request> + Clone + Send + Sync + 'static,
405 <L::Service as tower::Service<axum::extract::Request>>::Response: IntoResponse + 'static,
406 <L::Service as tower::Service<axum::extract::Request>>::Error:
407 Into<std::convert::Infallible> + 'static,
408 <L::Service as tower::Service<axum::extract::Request>>::Future: Send + 'static,
409 {
410 self.guard = Some(Guard {
411 apply: Box::new(move |router| router.layer(layer)),
412 label: label.into(),
413 });
414 self
415 }
416
417 pub fn effective_config(&self) -> Result<&GraphqlConfig, &ConfigError> {
424 self.resolved().as_ref().map(|r| &r.config)
425 }
426
427 fn resolved(&self) -> &Result<Resolved, ConfigError> {
428 self.resolved.get_or_init(|| {
429 let mut resolved = match &self.config_source {
430 ConfigSource::Resolve(section) => GraphqlConfig::resolve(section)?,
431 ConfigSource::Explicit(config) => Resolved::explicit((**config).clone()),
432 };
433 for apply in &self.overrides {
434 apply(&mut resolved.config);
435 }
436 if let Some(development) = self.development {
437 if development { "dev" } else { "prod" }.clone_into(&mut resolved.profile);
438 }
439 resolved.config.validate()?;
440 Ok(resolved)
441 })
442 }
443
444 fn config_or_default(&self) -> (GraphqlConfig, bool) {
448 self.resolved().as_ref().map_or_else(
449 |_| {
450 let mut config = GraphqlConfig::default();
451 for apply in &self.overrides {
452 apply(&mut config);
453 }
454 (config, self.development.unwrap_or(false))
455 },
456 |resolved| (resolved.config.clone(), resolved.is_development()),
457 )
458 }
459
460 fn mount_plan(&self) -> MountPlan {
461 let (config, development) = self.config_or_default();
462 let introspection = config.introspection.resolve(development);
463 MountPlan {
464 sdl: self.has_sdl && config.sdl.resolve(introspection),
465 websocket: config
466 .subscriptions
467 .websocket
468 .resolve(self.subscriptions_supported),
469 sse: config
470 .subscriptions
471 .sse
472 .resolve(self.subscriptions_supported),
473 introspection,
474 development,
475 config,
476 }
477 }
478
479 #[must_use]
485 pub fn route_infos(&self) -> Vec<RouteInfo> {
486 let plan = self.mount_plan();
487 if !plan.config.enabled {
488 return Vec::new();
489 }
490 let path = plan.config.path.trim().to_owned();
491 let guard = self.guard.as_ref().map(|g| g.label.as_str());
492 let mut routes = vec![route("POST", path.clone(), "post_graphql", guard)];
493 if plan.config.allow_get || plan.sse {
494 routes.push(route("GET", path.clone(), "get_graphql", guard));
495 }
496 if plan.sdl {
497 routes.push(route("GET", format!("{path}/sdl"), "sdl", guard));
498 }
499 if plan.websocket {
500 routes.push(route("GET", format!("{path}/ws"), "websocket", guard));
501 }
502 routes
503 }
504
505 fn trusted_documents_for(&self, plan: &MountPlan) -> Result<TrustedDocuments, String> {
506 let mut trusted = self.trusted.clone();
507 let manifest = plan.config.persisted_queries.manifest.trim();
508 if !manifest.is_empty() {
509 let base = self
510 .resolved()
511 .as_ref()
512 .map_or_else(|_| std::path::PathBuf::from("."), |r| r.base_dir.clone());
513 let path = base.join(manifest);
514 trusted.extend(TrustedDocuments::from_file(&path).map_err(|e| e.to_string())?);
515 }
516 if plan.config.persisted_queries.mode == PersistedQueryMode::Trusted && trusted.is_empty() {
517 return Err(
518 "graphql.persisted_queries.mode = \"trusted\" needs a manifest (or documents \
519 added with `trusted_documents`); with none, every operation would be refused"
520 .to_owned(),
521 );
522 }
523 Ok(trusted)
524 }
525
526 fn endpoint(
528 &mut self,
529 plan: &MountPlan,
530 path: &str,
531 executor: E,
532 sdl: Option<String>,
533 trusted: TrustedDocuments,
534 ) -> Endpoint<E> {
535 let config = &plan.config;
536 let store = self.store.take().unwrap_or_else(|| {
537 Arc::new(InMemoryPersistedQueryStore::new(
538 config.persisted_queries.cache_capacity,
539 ))
540 });
541 let metrics = Metrics::new(path);
542 let millis = |ms: u64| (ms > 0).then(|| Duration::from_millis(ms));
543 let pipeline = Arc::new(Pipeline {
544 settings: PipelineSettings {
545 introspection: plan.introspection,
546 mask_unexpected_errors: config.mask_unexpected_errors.resolve(!plan.development),
547 timeout: millis(config.timeout_ms),
548 slow_operation: millis(config.slow_operation_ms),
549 limits: config.limits.clone(),
550 persisted_mode: config.persisted_queries.mode,
551 },
552 store,
553 trusted: Arc::new(trusted),
554 metrics: metrics.clone(),
555 });
556 let subs = &config.subscriptions;
557 Endpoint {
558 executor,
559 pipeline,
560 hooks: std::mem::take(&mut self.hooks),
561 ws_init: self.ws_init.take(),
562 path: Arc::from(path),
563 sdl: if plan.sdl { sdl.map(Arc::from) } else { None },
564 allow_get: config.allow_get,
565 max_body_bytes: config.max_body_bytes,
566 batching: config.batching.clone(),
567 uploads: config.uploads.clone(),
568 csrf_prevention: config.csrf_prevention,
569 sse: plan.sse,
570 keepalive: Duration::from_secs(subs.keepalive_secs.max(1)),
571 ws_init_timeout: Duration::from_secs(subs.init_timeout_secs.max(1)),
572 streams: (subs.max_connections > 0)
573 .then(|| Arc::new(Semaphore::new(subs.max_connections))),
574 shutdown: self.drain.clone(),
575 metrics,
576 }
577 }
578}
579
580#[allow(clippy::struct_excessive_bools)] struct MountPlan {
582 config: GraphqlConfig,
583 development: bool,
584 introspection: bool,
585 sdl: bool,
586 websocket: bool,
587 sse: bool,
588}
589
590fn route(method: &str, path: String, handler: &str, guard: Option<&str>) -> RouteInfo {
591 RouteInfo {
592 method: method.to_owned(),
593 path,
594 handler: format!("autumn_plugin_graphql::{handler}"),
595 classification: guard.map_or(RouteClassification::Public, |_| RouteClassification::Gated),
596 middleware: guard.map(str::to_owned).into_iter().collect(),
597 ..Default::default()
598 }
599}
600
601impl<E: Executor> Plugin for GraphqlPlugin<E> {
602 fn name(&self) -> Cow<'static, str> {
605 Cow::Owned(format!(
606 "{PLUGIN_NAME}@{}",
607 self.mount_plan().config.path.trim()
608 ))
609 }
610
611 fn contract(&self) -> Option<PluginContract> {
613 Some(
614 PluginContract::new(PLUGIN_NAME)
615 .plugin_version(env!("CARGO_PKG_VERSION"))
616 .autumn_web(SUPPORTED_AUTUMN_WEB),
617 )
618 }
619
620 fn build(mut self, app: AppBuilder) -> AppBuilder {
621 let app = match &self.config_source {
622 ConfigSource::Resolve(section) => app.config_section(section.clone()),
623 ConfigSource::Explicit(_) => app,
624 };
625 let plan = self.mount_plan();
626 let routes = self.route_infos();
627
628 let mut problems: Vec<String> = Vec::new();
629 if let Err(error) = self.resolved() {
630 problems.push(error.to_string());
631 }
632 let trusted = self.trusted_documents_for(&plan).unwrap_or_else(|error| {
633 problems.push(error);
634 TrustedDocuments::new()
635 });
636 let app = app.on_startup(move |_state| {
637 let problems = problems.clone();
638 async move {
639 if problems.is_empty() {
640 Ok(())
641 } else {
642 Err(autumn_web::AutumnError::internal_server_error_msg(format!(
643 "{PLUGIN_NAME}: {}",
644 problems.join("; ")
645 )))
646 }
647 }
648 });
649
650 let config = plan.config.clone();
651 if !config.enabled {
652 tracing::info!(path = %config.path, "GraphQL endpoint disabled by configuration");
653 return app;
654 }
655 let path = config.path.trim().to_owned();
656
657 let (executor, sdl) = match self.source.take() {
658 Some(Source::Ready { executor, sdl }) => (executor, sdl),
659 Some(Source::Deferred(materialize)) => materialize(&config),
660 None => return app,
661 };
662
663 let endpoint = Arc::new(self.endpoint(&plan, &path, executor, sdl, trusted));
664
665 let mut endpoint_router = Router::new().route(
666 "/",
667 post(http::post_graphql::<E>).get(http::get_graphql::<E>),
668 );
669 if endpoint.sdl.is_some() {
670 endpoint_router = endpoint_router.route("/sdl", get(http::sdl::<E>));
671 }
672 if plan.websocket {
673 endpoint_router = endpoint_router.route("/ws", get(ws::upgrade::<E>));
674 }
675 let layered = endpoint_router.layer(Extension(endpoint));
676 let guarded = match self.guard.take() {
678 Some(guard) => (guard.apply)(layered),
679 None => layered,
680 };
681
682 tracing::info!(
683 path = %path,
684 introspection = plan.introspection,
685 sdl = plan.sdl,
686 websocket = plan.websocket,
687 sse = plan.sse,
688 persisted_queries = ?config.persisted_queries.mode,
689 "mounting GraphQL endpoint"
690 );
691 let drain = self.drain.clone();
692 app.nest(&path, guarded)
693 .declare_plugin_routes(routes)
694 .on_shutdown(move || {
695 let drain = drain.clone();
696 async move { drain.drain() }
697 })
698 }
699}