autumn_plugin_graphql/context.rs
1//! What every resolver can read from its [`async_graphql::Context`].
2//!
3//! The plugin inserts, into every operation's context data:
4//!
5//! - the request's [`AppState`] — so `ctx.data::<AppState>()?.pool()`,
6//! `.extension::<T>()` and everything else a route handler can reach is
7//! reachable from a resolver;
8//! - a [`GraphqlRequestInfo`] describing the transport request (method, path,
9//! headers, request id, transport kind);
10//! - whatever the app's [`context hooks`](crate::GraphqlPlugin::context) add,
11//! typically the authenticated principal and per-request `DataLoader`s.
12//!
13//! Hooks receive the request's [`Parts`], so any Autumn/axum extractor that
14//! implements `FromRequestParts<AppState>` — sessions, the current user,
15//! tenant resolution — works unchanged:
16//!
17//! ```ignore
18//! GraphqlPlugin::new(schema).context(|parts, state, data| {
19//! Box::pin(async move {
20//! let user = CurrentUser::from_request_parts(parts, state).await?;
21//! data.insert(user);
22//! Ok(())
23//! })
24//! })
25//! ```
26
27use std::sync::Arc;
28
29use async_graphql::Data;
30use autumn_web::{AppState, AutumnResult};
31use http::request::Parts;
32use http::{HeaderMap, Method, Uri};
33
34use crate::persisted::BoxFuture;
35
36/// Which transport carried an operation.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum Transport {
40 /// `POST {path}` (JSON, `application/graphql`, or multipart).
41 HttpPost,
42 /// `GET {path}?query=…` — queries only.
43 HttpGet,
44 /// `GET {path}/ws` upgraded to a WebSocket.
45 WebSocket,
46 /// A `text/event-stream` response (GraphQL over SSE).
47 Sse,
48}
49
50impl Transport {
51 /// Label used in logs and metrics.
52 #[must_use]
53 pub const fn as_str(self) -> &'static str {
54 match self {
55 Self::HttpPost => "http_post",
56 Self::HttpGet => "http_get",
57 Self::WebSocket => "websocket",
58 Self::Sse => "sse",
59 }
60 }
61}
62
63/// The transport request an operation arrived on. Available to every
64/// resolver as `ctx.data::<GraphqlRequestInfo>()`.
65#[derive(Debug, Clone)]
66#[non_exhaustive]
67pub struct GraphqlRequestInfo {
68 /// HTTP method of the request (for a WebSocket, of the upgrade request).
69 pub method: Method,
70 /// Request URI.
71 pub uri: Uri,
72 /// Request headers.
73 pub headers: HeaderMap,
74 /// Autumn's request id, when the request-id middleware assigned one.
75 pub request_id: Option<String>,
76 /// Which transport carried the operation.
77 pub transport: Transport,
78 /// The mount path of the endpoint that served it.
79 pub endpoint: Arc<str>,
80}
81
82impl GraphqlRequestInfo {
83 pub(crate) fn from_parts(parts: &Parts, transport: Transport, endpoint: &Arc<str>) -> Self {
84 let request_id = parts
85 .extensions
86 .get::<autumn_web::middleware::RequestId>()
87 .map(ToString::to_string)
88 .or_else(|| {
89 parts
90 .headers
91 .get("x-request-id")
92 .and_then(|v| v.to_str().ok())
93 .map(str::to_owned)
94 });
95 Self {
96 method: parts.method.clone(),
97 uri: parts.uri.clone(),
98 headers: parts.headers.clone(),
99 request_id,
100 transport,
101 endpoint: Arc::clone(endpoint),
102 }
103 }
104}
105
106/// A per-request context hook.
107///
108/// It adds data to the operation's context from the request's parts and
109/// the app state. Returning `Err` refuses the request
110/// with that error's HTTP status (for example `401`) before anything
111/// executes.
112pub trait ContextHook: Send + Sync + 'static {
113 /// Populate `data` for one request.
114 fn extend<'a>(
115 &'a self,
116 parts: &'a mut Parts,
117 state: &'a AppState,
118 data: &'a mut Data,
119 ) -> BoxFuture<'a, AutumnResult<()>>;
120}
121
122impl<F> ContextHook for F
123where
124 F: for<'a> Fn(&'a mut Parts, &'a AppState, &'a mut Data) -> BoxFuture<'a, AutumnResult<()>>
125 + Send
126 + Sync
127 + 'static,
128{
129 fn extend<'a>(
130 &'a self,
131 parts: &'a mut Parts,
132 state: &'a AppState,
133 data: &'a mut Data,
134 ) -> BoxFuture<'a, AutumnResult<()>> {
135 self(parts, state, data)
136 }
137}
138
139/// Handles a WebSocket client's `connection_init` payload.
140///
141/// Browser clients put credentials there, since they cannot set headers on the
142/// upgrade). The returned data joins the connection's context; returning
143/// `Err` closes the connection with `4403 Forbidden`.
144pub trait WsInitHook: Send + Sync + 'static {
145 /// Validate the payload and produce connection data.
146 fn on_init(
147 &self,
148 payload: serde_json::Value,
149 state: AppState,
150 ) -> BoxFuture<'static, AutumnResult<Data>>;
151}
152
153impl<F, Fut> WsInitHook for F
154where
155 F: Fn(serde_json::Value, AppState) -> Fut + Send + Sync + 'static,
156 Fut: std::future::Future<Output = AutumnResult<Data>> + Send + 'static,
157{
158 fn on_init(
159 &self,
160 payload: serde_json::Value,
161 state: AppState,
162 ) -> BoxFuture<'static, AutumnResult<Data>> {
163 Box::pin(self(payload, state))
164 }
165}
166
167/// Run every hook in order against one request.
168pub(crate) async fn run_hooks(
169 hooks: &[Arc<dyn ContextHook>],
170 parts: &mut Parts,
171 state: &AppState,
172 data: &mut Data,
173) -> AutumnResult<()> {
174 for hook in hooks {
175 hook.extend(parts, state, data).await?;
176 }
177 Ok(())
178}