grafbase_sdk/extension/
hooks.rs

1use crate::{
2    component::AnyExtension,
3    host_io::{event_queue::EventQueue, http::StatusCode},
4    types::{
5        AuthorizedOperationContext, Configuration, Error, ErrorResponse, GatewayHeaders, Headers, HttpRequestParts,
6        OnRequestOutput, RequestContext,
7    },
8};
9
10/// The Hooks extension allows you to hook into an incoming request or an outgoing response.
11///
12/// You have mutable access to the headers, and information about the request or response
13/// to decide whether to continue processing or not.
14///
15/// Keep in mind this is not meant for authentication purposes.
16///
17/// # Example
18///
19/// ```rust
20/// use grafbase_sdk::{
21///     HooksExtension,
22///     types::{GatewayHeaders, Headers, Configuration, Error, ErrorResponse, RequestContext},
23///     host_io::event_queue::EventQueue,
24/// };
25///
26/// #[derive(HooksExtension)]
27/// struct MyHooks {
28///     config: Config,
29/// }
30///
31/// #[derive(serde::Deserialize)]
32/// struct Config {
33///     // Define your configuration fields here. They are parsed from
34///     // the grafbase.toml configuration.
35///     something: String,
36/// }
37///
38/// impl HooksExtension for MyHooks {
39///     fn new(config: Configuration) -> Result<Self, Error> {
40///         let config = config.deserialize()?;
41///         Ok(Self { config })
42///     }
43///
44///     #[allow(refining_impl_trait)]
45///     fn on_request(&mut self, url: &str, method: http::Method, headers: &mut GatewayHeaders) -> Result<(), ErrorResponse> {
46///         // Implement your request hook logic here.
47///         Ok(())
48///     }
49///
50///     fn on_response(
51///         &mut self,
52///         ctx: &RequestContext,
53///         status: http::StatusCode,
54///         headers: &mut Headers,
55///         event_queue: EventQueue,
56///     ) -> Result<(), Error> {
57///         // Implement your response hook logic here.
58///         Ok(())
59///     }
60/// }
61/// ```
62#[allow(unused_variables)]
63pub trait HooksExtension: Sized + 'static {
64    /// Creates a new instance of the extension. The [`Configuration`] will contain all the
65    /// configuration defined in the `grafbase.toml` by the extension user in a serialized format.
66    ///
67    /// # Example
68    ///
69    /// The following TOML configuration:
70    /// ```toml
71    /// [extensions.my-hooks.config]
72    /// my_custom_key = "value"
73    /// ```
74    ///
75    /// can be easily deserialized with:
76    ///
77    /// ```rust
78    /// # use grafbase_sdk::types::{Configuration, Error};
79    /// # fn dummy(config: Configuration) -> Result<(), Error> {
80    /// #[derive(serde::Deserialize)]
81    /// struct Config {
82    ///     my_custom_key: String
83    /// }
84    ///
85    /// let config: Config = config.deserialize()?;
86    /// # Ok(())
87    /// # }
88    /// ```
89    fn new(config: Configuration) -> Result<Self, Error>;
90
91    /// Called immediately when a request is received, before entering the GraphQL engine.
92    ///
93    /// This hook can be used to modify the request headers before they are processed by the GraphQL engine, and provides a way to audit the headers, URL, and method before processing the operation.
94    ///
95    /// It can also be used to define the contract key to use for the contracts extension if you
96    /// have any configured:
97    ///
98    /// ```rust
99    /// # use grafbase_sdk::{host_io::http::Method, types::{ErrorResponse, GatewayHeaders, OnRequestOutput}};
100    /// # struct MyContract;
101    /// # impl MyContract {
102    /// #[allow(refining_impl_trait)]
103    /// fn on_request(&mut self, url: &str, method: Method, headers: &mut GatewayHeaders) -> Result<OnRequestOutput, ErrorResponse> {
104    ///     Ok(OnRequestOutput::new().contract_key("my-contract-key"))
105    /// }
106    /// # }
107    /// ```
108    fn on_request(
109        &mut self,
110        url: &str,
111        method: http::Method,
112        headers: &mut GatewayHeaders,
113    ) -> Result<impl IntoOnRequestOutput, ErrorResponse> {
114        Ok(())
115    }
116
117    /// Called right before the response is sent back to the client.
118    ///
119    /// This hook can be used to modify the response headers before the response is sent back to the client.
120    fn on_response(
121        &mut self,
122        ctx: &RequestContext,
123        status: http::StatusCode,
124        headers: &mut Headers,
125        event_queue: EventQueue,
126    ) -> Result<(), Error> {
127        Ok(())
128    }
129
130    /// Called when a GraphQL subgraph request is made, allowing you to modify the request parts before they are sent to the subgraph.
131    fn on_graphql_subgraph_request(
132        &mut self,
133        ctx: &AuthorizedOperationContext,
134        subgraph_name: &str,
135        parts: &mut HttpRequestParts,
136    ) -> Result<(), Error> {
137        Ok(())
138    }
139
140    /// Called when a virtual subgraph request is made through an extension, allowing you to modify the request headers before sending it to the extension.
141    fn on_virtual_subgraph_request(
142        &mut self,
143        ctx: &AuthorizedOperationContext,
144        subgraph_name: &str,
145        headers: &mut Headers,
146    ) -> Result<(), Error> {
147        Ok(())
148    }
149}
150
151pub trait IntoOnRequestOutput {
152    fn into_on_request_output(self) -> OnRequestOutput;
153}
154
155impl IntoOnRequestOutput for OnRequestOutput {
156    fn into_on_request_output(self) -> OnRequestOutput {
157        self
158    }
159}
160
161impl IntoOnRequestOutput for () {
162    fn into_on_request_output(self) -> OnRequestOutput {
163        OnRequestOutput::default()
164    }
165}
166
167#[doc(hidden)]
168pub fn register<T: HooksExtension>() {
169    pub(super) struct Proxy<T: HooksExtension>(T);
170
171    impl<T: HooksExtension> AnyExtension for Proxy<T> {
172        fn on_request(
173            &mut self,
174            url: &str,
175            method: http::Method,
176            headers: &mut Headers,
177        ) -> Result<OnRequestOutput, ErrorResponse> {
178            self.0
179                .on_request(url, method, headers)
180                .map(|output| output.into_on_request_output())
181        }
182
183        fn on_response(
184            &mut self,
185            ctx: &RequestContext,
186            status: StatusCode,
187            headers: &mut Headers,
188            event_queue: EventQueue,
189        ) -> Result<(), Error> {
190            self.0.on_response(ctx, status, headers, event_queue)
191        }
192
193        fn on_graphql_subgraph_request(
194            &mut self,
195            ctx: &AuthorizedOperationContext,
196            subgraph_name: &str,
197            parts: &mut HttpRequestParts,
198        ) -> Result<(), Error> {
199            self.0.on_graphql_subgraph_request(ctx, subgraph_name, parts)
200        }
201
202        fn on_virtual_subgraph_request(
203            &mut self,
204            ctx: &AuthorizedOperationContext,
205            subgraph_name: &str,
206            headers: &mut Headers,
207        ) -> Result<(), Error> {
208            self.0.on_virtual_subgraph_request(ctx, subgraph_name, headers)
209        }
210    }
211
212    crate::component::register_extension(Box::new(|_, config| {
213        <T as HooksExtension>::new(config).map(|extension| Box::new(Proxy(extension)) as Box<dyn AnyExtension>)
214    }))
215}