Skip to main content

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}