grafbase_sdk/extension/
resolver.rs

1use crate::{
2    component::AnyExtension,
3    types::{
4        AuthorizedOperationContext, Configuration, Error, IndexedSchema, ResolvedField, Response, SubgraphHeaders,
5        SubgraphSchema, SubscriptionItem, Variables,
6    },
7};
8
9/// A resolver extension is called by the gateway to resolve a specific field.
10///
11/// # Example
12///
13/// You can initialize a new resolver extension with the Grafbase CLI:
14///
15/// ```bash
16/// grafbase extension init --type resolver my-resolver
17/// ```
18///
19/// This will generate the following:
20///
21/// ```rust
22/// use grafbase_sdk::{
23///     ResolverExtension,
24///     types::{AuthorizedOperationContext, Configuration, Error, ResolvedField, Response, SubgraphHeaders, SubgraphSchema, Variables},
25/// };
26///
27/// #[derive(ResolverExtension)]
28/// struct MyResolver {
29///     config: Config
30/// }
31///
32/// // Configuration in the TOML for this extension
33/// #[derive(serde::Deserialize)]
34/// struct Config {
35///     #[serde(default)]
36///     key: Option<String>
37/// }
38///
39/// impl ResolverExtension for MyResolver {
40///     fn new(subgraph_schemas: Vec<SubgraphSchema>, config: Configuration) -> Result<Self, Error> {
41///         let config: Config = config.deserialize()?;
42///         Ok(Self { config })
43///     }
44///
45///     fn resolve(
46///         &mut self,
47///         ctx: &AuthorizedOperationContext,
48///         prepared: &[u8],
49///         headers: SubgraphHeaders,
50///         variables: Variables,
51///     ) -> Result<Response, Error> {
52///         // field which must be resolved. The prepared bytes can be customized to store anything you need in the operation cache.
53///         let field = ResolvedField::try_from(prepared)?;
54///         Ok(Response::null())
55///     }
56/// }
57/// ```
58/// ## Configuration
59///
60/// The configuration provided in the `new` method is the one defined in the `grafbase.toml`
61/// file by the extension user:
62///
63/// ```toml
64/// [extensions.my-resolver.config]
65/// key = "value"
66/// ```
67///
68/// Once your business logic is written down you can compile your extension with:
69///
70/// ```bash
71/// grafbase extension build
72/// ```
73///
74/// It will generate all the necessary files in a `build` directory which you can specify in the
75/// `grafbase.toml` configuration with:
76///
77/// ```toml
78/// [extensions.my-resolver]
79/// path = "<project path>/build"
80/// ```
81///
82/// ## Directives
83///
84/// In addition to the Rust extension, a `definitions.graphql` file will be also generated. It
85/// should define directives for subgraph owners and any necessary input types, scalars or enum
86/// necessary for those. Directives have two purposes for resolvers: define which fields can be
87/// resolved, providing the necessary metadata for it, and provide global metadata with schema
88/// directive.
89///
90/// A HTTP resolver extension could have the following directives for example:
91///
92/// ```graphql
93/// scalar URL
94///
95/// directive @httpEndpoint(name: String!, url: URL!) on SCHEMA
96///
97/// directive @http(endpoint: String!, path: String!) on FIELD_DEFINITION
98/// ```
99///
100/// The `@httpEndpoint` directive would be provided during the [new()](ResolverExtension::new())
101/// method as a schema [crate::types::Directive]. The whole subgraph schema is also provided for
102/// each subgraph where this extension is used. While the latter can be accessed with
103/// [ResolvedField::directive()] in the [resolve()](ResolverExtension::resolve()) method.
104///
105pub trait ResolverExtension: Sized + 'static {
106    /// Creates a new instance of the extension. The [Configuration] will contain all the
107    /// configuration defined in the `grafbase.toml` by the extension user in a serialized format.
108    /// Furthermore the complete subgraph schema is provided whenever this extension is used.
109    ///
110    /// # Configuration example
111    ///
112    /// The following TOML configuration:
113    /// ```toml
114    /// [extensions.my-auth.config]
115    /// my_custom_key = "value"
116    /// ```
117    ///
118    /// can be easily deserialized with:
119    ///
120    /// ```rust
121    /// # use grafbase_sdk::types::{Configuration, Error};
122    /// # fn dummy(config: Configuration) -> Result<(), Error> {
123    /// #[derive(serde::Deserialize)]
124    /// struct Config {
125    ///     my_custom_key: String
126    /// }
127    ///
128    /// let config: Config = config.deserialize()?;
129    /// # Ok(())
130    /// # }
131    /// ```
132    ///
133    /// # Directive example
134    ///
135    /// ```graphql
136    /// extend schema @httpEdnpoint(name: "example", url: "https://example.com")
137    /// ```
138    ///
139    /// can be easily deserialized with:
140    ///
141    /// ```rust
142    /// # use grafbase_sdk::types::{Error, SubgraphSchema};
143    /// # fn dummy(subgraph_schemas: Vec<SubgraphSchema>) -> Result<(), Error> {
144    /// #[derive(serde::Deserialize)]
145    /// struct HttpEndpoint {
146    ///     name: String,
147    ///     url: String
148    /// }
149    /// for schema in subgraph_schemas {
150    ///     for directive in schema.directives() {
151    ///         match directive.name() {
152    ///             "httpEndpoint" => {
153    ///                  let http_endpoint: HttpEndpoint = directive.arguments()?;
154    ///             }
155    ///             _ => unreachable!()
156    ///         }
157    ///     }
158    /// }
159    /// # Ok(())
160    /// # }
161    /// ```
162    fn new(subgraph_schemas: Vec<SubgraphSchema>, config: Configuration) -> Result<Self, Error>;
163
164    /// Prepares the field for resolution. The resulting byte array will be part of the operation
165    /// cache. Backwards compatibility is not a concern as the cache is only re-used for the same
166    /// schema and extension versions.
167    /// By default the [ResolvedField] is cached for a simpler implementation.
168    fn prepare(&mut self, field: ResolvedField<'_>) -> Result<Vec<u8>, Error> {
169        Ok(field.into())
170    }
171
172    /// Resolves the field with the provided prepared bytes, headers and variables. With the
173    /// default [prepare()](ResolverExtension::prepare()) you can retrieve all the relevant
174    /// information with:
175    /// ```rust
176    /// # use grafbase_sdk::types::{SubgraphHeaders, Variables, Response, Error, ResolvedField};
177    /// # fn resolve(prepared: &[u8], headers: SubgraphHeaders, variables: Variables) -> Result<Response, Error> {
178    /// let field = ResolvedField::try_from(prepared)?;
179    /// # Ok(Response::null())
180    /// # }
181    /// ```
182    ///
183    /// If you're not doing any data transformation it's best to forward the JSON or CBOR bytes,
184    /// with [Response::json] and [Response::cbor] respectively, directly to the gateway. The
185    /// gateway will always validate the subgraph data and deal with error propagation. Otherwise
186    /// use [Response::data] to use the fastest supported serialization format.
187    fn resolve(
188        &mut self,
189        ctx: &AuthorizedOperationContext,
190        prepared: &[u8],
191        headers: SubgraphHeaders,
192        variables: Variables,
193    ) -> Result<Response, Error>;
194
195    /// Resolves a subscription for the given prepared bytes, headers, and variables.
196    /// Subscriptions must implement the [Subscription] trait. It's also possible to provide a
197    /// de-duplication key. If provided the gateway will first check if there is an existing
198    /// subscription with the same key and if there is, re-use it for the new client. This greatly
199    /// limits the impact on the upstream service. So you have two choices for the result type:
200    /// - return a `Ok(subscription)` directly, without any de-duplication
201    /// - return a `Ok((key, callback))` with an optional de-duplication key and a callback
202    ///   function. The latter is only called if no existing subscription exists for the given key.
203    #[allow(unused_variables)]
204    fn resolve_subscription<'s>(
205        &'s mut self,
206        ctx: &'s AuthorizedOperationContext,
207        prepared: &'s [u8],
208        headers: SubgraphHeaders,
209        variables: Variables,
210    ) -> Result<impl IntoSubscription<'s>, Error> {
211        unimplemented!("Subscription resolution not implemented for this resolver extension");
212
213        // So that Rust doesn't complain about the unknown type
214        #[allow(unused)]
215        Ok(PhantomSubscription)
216    }
217}
218
219/// A trait for consuming field outputs from streams.
220///
221/// This trait provides an abstraction over different implementations
222/// of subscriptions to field output streams. Implementors should handle
223/// the details of their specific transport mechanism while providing a
224/// consistent interface for consumers.
225pub trait Subscription {
226    /// Retrieves the next field output from the subscription.
227    ///
228    /// Returns:
229    /// - `Ok(Some(Data))` if a field output was available
230    /// - `Ok(None)` if the subscription has ended normally
231    /// - `Err(Error)` if an error occurred while retrieving the next field output
232    fn next(&mut self) -> Result<Option<SubscriptionItem>, Error>;
233}
234
235pub type SubscriptionCallback<'s> = Box<dyn FnOnce() -> Result<Box<dyn Subscription + 's>, Error> + 's>;
236
237pub trait IntoSubscription<'s> {
238    fn into_deduplication_key_and_subscription_callback(self) -> (Option<Vec<u8>>, SubscriptionCallback<'s>);
239}
240
241impl<'s, S> IntoSubscription<'s> for S
242where
243    S: Subscription + 's,
244{
245    fn into_deduplication_key_and_subscription_callback(self) -> (Option<Vec<u8>>, SubscriptionCallback<'s>) {
246        (None, Box::new(move || Ok(Box::new(self))))
247    }
248}
249
250impl<'s, Callback, S> IntoSubscription<'s> for (Vec<u8>, Callback)
251where
252    Callback: FnOnce() -> Result<S, Error> + 's,
253    S: Subscription + 's,
254{
255    fn into_deduplication_key_and_subscription_callback(self) -> (Option<Vec<u8>>, SubscriptionCallback<'s>) {
256        (
257            Some(self.0),
258            Box::new(move || {
259                let s = (self.1)()?;
260                Ok(Box::new(s) as Box<dyn Subscription + 's>)
261            }),
262        )
263    }
264}
265
266impl<'s, Callback, S> IntoSubscription<'s> for (Option<Vec<u8>>, Callback)
267where
268    Callback: FnOnce() -> Result<S, Error> + 's,
269    S: Subscription + 's,
270{
271    fn into_deduplication_key_and_subscription_callback(self) -> (Option<Vec<u8>>, SubscriptionCallback<'s>) {
272        (
273            self.0,
274            Box::new(move || {
275                let s = (self.1)()?;
276                Ok(Box::new(s) as Box<dyn Subscription + 's>)
277            }),
278        )
279    }
280}
281
282impl<'s, Callback, S> IntoSubscription<'s> for (String, Callback)
283where
284    Callback: FnOnce() -> Result<S, Error> + 's,
285    S: Subscription + 's,
286{
287    fn into_deduplication_key_and_subscription_callback(self) -> (Option<Vec<u8>>, SubscriptionCallback<'s>) {
288        (
289            Some(self.0.into()),
290            Box::new(move || {
291                let s = (self.1)()?;
292                Ok(Box::new(s) as Box<dyn Subscription + 's>)
293            }),
294        )
295    }
296}
297
298impl<'s, Callback, S> IntoSubscription<'s> for (Option<String>, Callback)
299where
300    Callback: FnOnce() -> Result<S, Error> + 's,
301    S: Subscription + 's,
302{
303    fn into_deduplication_key_and_subscription_callback(self) -> (Option<Vec<u8>>, SubscriptionCallback<'s>) {
304        (
305            self.0.map(Into::into),
306            Box::new(move || {
307                let s = (self.1)()?;
308                Ok(Box::new(s) as Box<dyn Subscription + 's>)
309            }),
310        )
311    }
312}
313
314impl<'s, Callback, S> IntoSubscription<'s> for (Callback, Vec<u8>)
315where
316    Callback: FnOnce() -> Result<S, Error> + 's,
317    S: Subscription + 's,
318{
319    fn into_deduplication_key_and_subscription_callback(self) -> (Option<Vec<u8>>, SubscriptionCallback<'s>) {
320        (self.1, self.0).into_deduplication_key_and_subscription_callback()
321    }
322}
323
324impl<'s, Callback, S> IntoSubscription<'s> for (Callback, Option<Vec<u8>>)
325where
326    Callback: FnOnce() -> Result<S, Error> + 's,
327    S: Subscription + 's,
328{
329    fn into_deduplication_key_and_subscription_callback(self) -> (Option<Vec<u8>>, SubscriptionCallback<'s>) {
330        (self.1, self.0).into_deduplication_key_and_subscription_callback()
331    }
332}
333
334impl<'s, Callback, S> IntoSubscription<'s> for (Callback, String)
335where
336    Callback: FnOnce() -> Result<S, Error> + 's,
337    S: Subscription + 's,
338{
339    fn into_deduplication_key_and_subscription_callback(self) -> (Option<Vec<u8>>, SubscriptionCallback<'s>) {
340        (self.1, self.0).into_deduplication_key_and_subscription_callback()
341    }
342}
343
344impl<'s, Callback, S> IntoSubscription<'s> for (Callback, Option<String>)
345where
346    Callback: FnOnce() -> Result<S, Error> + 's,
347    S: Subscription + 's,
348{
349    fn into_deduplication_key_and_subscription_callback(self) -> (Option<Vec<u8>>, SubscriptionCallback<'s>) {
350        (self.1, self.0).into_deduplication_key_and_subscription_callback()
351    }
352}
353
354#[doc(hidden)]
355pub fn register<T: ResolverExtension>() {
356    pub(super) struct Proxy<T: ResolverExtension>(T);
357
358    impl<T: ResolverExtension> AnyExtension for Proxy<T> {
359        fn prepare(&mut self, field: ResolvedField<'_>) -> Result<Vec<u8>, Error> {
360            self.0.prepare(field)
361        }
362
363        fn resolve(
364            &mut self,
365            ctx: &AuthorizedOperationContext,
366
367            prepared: &[u8],
368            headers: SubgraphHeaders,
369            variables: Variables,
370        ) -> Response {
371            self.0.resolve(ctx, prepared, headers, variables).into()
372        }
373
374        fn resolve_subscription<'a>(
375            &'a mut self,
376            ctx: &'a AuthorizedOperationContext,
377
378            prepared: &'a [u8],
379            headers: SubgraphHeaders,
380            variables: Variables,
381        ) -> Result<(Option<Vec<u8>>, SubscriptionCallback<'a>), Error> {
382            let (key, callback) = self
383                .0
384                .resolve_subscription(ctx, prepared, headers, variables)?
385                .into_deduplication_key_and_subscription_callback();
386            Ok((key, callback))
387        }
388    }
389
390    crate::component::register_extension(Box::new(|subgraph_schemas, config| {
391        let schemas = subgraph_schemas
392            .into_iter()
393            .map(IndexedSchema::from)
394            .collect::<Vec<_>>();
395        <T as ResolverExtension>::new(schemas.into_iter().map(SubgraphSchema).collect(), config)
396            .map(|extension| Box::new(Proxy(extension)) as Box<dyn AnyExtension>)
397    }))
398}
399
400struct PhantomSubscription;
401
402impl Subscription for PhantomSubscription {
403    fn next(&mut self) -> Result<Option<SubscriptionItem>, Error> {
404        Ok(None) // No-op implementation for the phantom subscription
405    }
406}