grafbase_sdk/extension/
resolver.rs

1use crate::{
2    component::AnyExtension,
3    types::{
4        Configuration, Error, FieldDefinitionDirective, FieldInputs, FieldOutputs, SchemaDirective, SubgraphHeaders,
5        SubscriptionOutput,
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::{SubgraphHeaders, FieldDefinitionDirective, FieldInputs, FieldOutputs, Configuration, Error, SchemaDirective}
25/// };
26///
27/// #[derive(ResolverExtension)]
28/// struct MyResolver {
29///   config: Config
30/// }
31///
32/// #[derive(serde::Deserialize)]
33/// struct Config {
34///   my_custom_key: String
35/// }
36///
37/// impl ResolverExtension for MyResolver {
38///    fn new(schema_directives: Vec<SchemaDirective>, config: Configuration) -> Result<Self, Error> {
39///        let config: Config = config.deserialize()?;
40///        Ok(Self { config })
41///    }
42///
43///    fn resolve_field(
44///        &mut self,
45///        headers: SubgraphHeaders,
46///        subgraph_name: &str,
47///        directive: FieldDefinitionDirective<'_>,
48///        inputs: FieldInputs<'_>,
49///    ) -> Result<FieldOutputs, Error> {
50///         unimplemented!()
51///    }
52/// }
53/// ```
54/// ## Configuration
55///
56/// The configuration provided in the `new` method is the one defined in the `grafbase.toml`
57/// file by the extension user:
58///
59/// ```toml
60/// [extensions.my-auth.config]
61/// my_custom_key = "value"
62/// ```
63///
64/// Once your business logic is written down you can compile your extension with:
65///
66/// ```bash
67/// grafbase extension build
68/// ```
69///
70/// It will generate all the necessary files in a `build` directory which you can specify in the
71/// `grafbase.toml` configuration with:
72///
73/// ```toml
74/// [extensions.my-auth]
75/// path = "<project path>/build"
76/// ```
77///
78/// ## Directives
79///
80/// In addition to the Rust extension, a `definitions.graphql` file will be also generated. It
81/// should define directives for subgraph owners and any necessary input types, scalars or enum
82/// necessary for those. Directives have two purposes for resolvers: define which fields can be
83/// resolved, providing the necessary metadata for it, and provide global metadata with schema
84/// directive.
85///
86/// A HTTP resolver extension could have the following directives for example:
87///
88/// ```graphql
89/// scalar URL
90///
91/// directive @httpEndpoint(name: String!, url: URL!) on SCHEMA
92///
93/// directive @http(endpoint: String!, path: String!) on FIELD_DEFINITION
94/// ```
95///
96/// The `@httpEndpoint` directive would be provided during the [new()](ResolverExtension::new())
97/// method as a [SchemaDirective]. While the latter would be provided as a [FieldDefinitionDirective] during
98/// the [resolve_field()](ResolverExtension::resolve_field()) method.
99///
100#[allow(unused_variables)]
101pub trait ResolverExtension: Sized + 'static {
102    /// Creates a new instance of the extension. The [Configuration] will contain all the
103    /// configuration defined in the `grafbase.toml` by the extension user in a serialized format.
104    /// Furthermore all schema directives from all subgraphs will be provided as
105    /// [SchemaDirective]s.
106    ///
107    /// # Configuration example
108    ///
109    /// The following TOML configuration:
110    /// ```toml
111    /// [extensions.my-auth.config]
112    /// my_custom_key = "value"
113    /// ```
114    ///
115    /// can be easily deserialized with:
116    ///
117    /// ```rust
118    /// # use grafbase_sdk::types::{Configuration, Error};
119    /// # fn dummy(config: Configuration) -> Result<(), Error> {
120    /// #[derive(serde::Deserialize)]
121    /// struct Config {
122    ///     my_custom_key: String
123    /// }
124    ///
125    /// let config: Config = config.deserialize()?;
126    /// # Ok(())
127    /// # }
128    /// ```
129    ///
130    /// # Directive example
131    ///
132    /// ```graphql
133    /// extend schema @httpEdnpoint(name: "example", url: "https://example.com")
134    /// ```
135    ///
136    /// can be easily deserialized with:
137    ///
138    /// ```rust
139    /// # use grafbase_sdk::types::{Error, SchemaDirective};
140    /// # fn dummy(schema_directives: Vec<SchemaDirective>) -> Result<(), Error> {
141    /// #[derive(serde::Deserialize)]
142    /// struct HttpEndpoint {
143    ///     name: String,
144    ///     url: String
145    /// }
146    ///
147    /// let config: Vec<HttpEndpoint> = schema_directives
148    ///     .into_iter()
149    ///     .map(|dir| dir.arguments())
150    ///     .collect::<Result<_, _>>()?;
151    /// # Ok(())
152    /// # }
153    /// ```
154    fn new(schema_directives: Vec<SchemaDirective>, config: Configuration) -> Result<Self, Error>;
155
156    /// Resolves a GraphQL field. This function receives a batch of inputs and is called at most once per
157    /// query field.
158    ///
159    /// Supposing we have the following directive applied on this schema:
160    ///
161    /// ```graphql
162    /// extend schema
163    ///  @link(
164    ///    url: "https://specs.grafbase.com/grafbase"
165    ///    import: ["FieldSet"]
166    ///  )
167    ///
168    /// directive @resolve(fields: FieldSet!) on FIELD_DEFINITION
169    /// ```
170    ///
171    /// ```graphql
172    /// type Query {
173    ///    users: [User] # from a different subgraph
174    /// }
175    ///
176    /// type User {
177    ///     id : ID!
178    ///     field: JSON @resolve(fields: "id")
179    /// }
180    /// ```
181    ///
182    /// and a query like:
183    ///
184    /// ```graphql
185    /// query {
186    ///    users {
187    ///       field
188    ///    }
189    /// }
190    /// ```
191    ///
192    /// The subgraph providing `Query.users` will return an arbitrary number N of users. Instead of
193    /// being called N times, this resolver will be called once with a [FieldInputs] containing N
194    /// [FieldInput](crate::types::FieldInput). This allows you to batch everything together at
195    /// once.
196    ///
197    /// ```rust
198    /// # use grafbase_sdk::types::{SubgraphHeaders, FieldDefinitionDirective, FieldInputs, FieldOutputs, Error};
199    /// # fn resolve_field(
200    /// #    headers: SubgraphHeaders,
201    /// #    subgraph_name: &str,
202    /// #    directive: FieldDefinitionDirective<'_>,
203    /// #    inputs: FieldInputs<'_>,
204    /// # ) -> Result<FieldOutputs, Error> {
205    /// // Static arguments passed on to the directive that do not depend on the response data.
206    /// #[derive(serde::Deserialize)]
207    /// struct StaticArguments<'a> {
208    ///     #[serde(borrow)]
209    ///     endpoint_name: &'a str,
210    /// }
211    /// let StaticArguments { endpoint_name } = directive.arguments()?;
212    ///
213    /// let mut builder = FieldOutputs::builder(inputs);
214    /// for input in inputs {
215    ///     // Field arguments that depend on response data.
216    ///     #[derive(serde::Deserialize)]
217    ///     struct ResponseArguments<'a> {
218    ///         #[serde(borrow)]
219    ///         id: &'a str,
220    ///     }
221    ///
222    ///     let ResponseArguments { id } = directive.arguments()?;
223    ///     builder.insert(input, "data");
224    /// }
225    ///
226    /// Ok(builder.build())
227    /// # }
228    /// ```
229    ///
230    /// [FieldOutputs] can also be initialized with a single error or a single data for convenience.
231    ///
232    /// We also want to support providing raw JSON and CBOR bytes directly for batched and
233    /// non-batched data later on, if it's of interested let us know!
234    ///
235    /// In addition to this the method also receives the subgraph `headers` after all the
236    /// subgraph-related header rules. And metadata the
237    /// [FieldDefinitionDirectiveSite](crate::types::FieldDefinitionDirectiveSite) is also
238    /// available with [directive.site()](crate::types::FieldDefinitionDirective::site()).
239    fn resolve_field(
240        &mut self,
241        headers: SubgraphHeaders,
242        subgraph_name: &str,
243        directive: FieldDefinitionDirective<'_>,
244        inputs: FieldInputs<'_>,
245    ) -> Result<FieldOutputs, Error>;
246
247    /// Resolves a subscription field by setting up a subscription handler.
248    ///
249    /// # Arguments
250    ///
251    /// * `headers` - The subgraph headers associated with this field resolution
252    /// * `directive` - The directive associated with this subscription field
253    /// * `definition` - The field definition containing metadata about the subscription
254    ///
255    /// # Returns
256    ///
257    /// Returns a `Result` containing either a boxed `Subscriber` implementation or an `Error`
258    fn resolve_subscription(
259        &mut self,
260        headers: SubgraphHeaders,
261        subgraph_name: &str,
262        directive: FieldDefinitionDirective<'_>,
263    ) -> Result<Box<dyn Subscription>, Error> {
264        unimplemented!()
265    }
266
267    /// Returns a key for a subscription field.
268    ///
269    /// This method is used to identify unique subscription channels or connections
270    /// when managing multiple active subscriptions. The returned key can be
271    /// used to track, manage, or deduplicate subscriptions.
272    ///
273    /// # Arguments
274    ///
275    /// * `headers` - The subgraph headers associated with this subscription
276    /// * `subgraph_name` - The name of the subgraph associated with this subscription
277    /// * `directive` - The directive associated with this subscription field
278    ///
279    /// # Returns
280    ///
281    /// Returns an `Option<Vec<u8>>` containing either a unique key for this
282    /// subscription or `None` if no deduplication is desired.
283    fn subscription_key(
284        &mut self,
285        headers: &SubgraphHeaders,
286        subgraph_name: &str,
287        directive: FieldDefinitionDirective<'_>,
288    ) -> Option<Vec<u8>> {
289        None
290    }
291}
292
293/// A trait for consuming field outputs from streams.
294///
295/// This trait provides an abstraction over different implementations
296/// of subscriptions to field output streams. Implementors should handle
297/// the details of their specific transport mechanism while providing a
298/// consistent interface for consumers.
299pub trait Subscription {
300    /// Retrieves the next field output from the subscription.
301    ///
302    /// Returns:
303    /// - `Ok(Some(FieldOutputs))` if a field output was available
304    /// - `Ok(None)` if the subscription has ended normally
305    /// - `Err(Error)` if an error occurred while retrieving the next field output
306    fn next(&mut self) -> Result<Option<SubscriptionOutput>, Error>;
307}
308
309#[doc(hidden)]
310pub fn register<T: ResolverExtension>() {
311    pub(super) struct Proxy<T: ResolverExtension>(T);
312
313    impl<T: ResolverExtension> AnyExtension for Proxy<T> {
314        fn resolve_field(
315            &mut self,
316            headers: SubgraphHeaders,
317            subgraph_name: &str,
318            directive: FieldDefinitionDirective<'_>,
319            inputs: FieldInputs<'_>,
320        ) -> Result<FieldOutputs, Error> {
321            ResolverExtension::resolve_field(&mut self.0, headers, subgraph_name, directive, inputs)
322        }
323        fn resolve_subscription(
324            &mut self,
325            headers: SubgraphHeaders,
326            subgraph_name: &str,
327            directive: FieldDefinitionDirective<'_>,
328        ) -> Result<Box<dyn Subscription>, Error> {
329            ResolverExtension::resolve_subscription(&mut self.0, headers, subgraph_name, directive)
330        }
331
332        fn subscription_key(
333            &mut self,
334            headers: &SubgraphHeaders,
335            subgraph_name: &str,
336            directive: FieldDefinitionDirective<'_>,
337        ) -> Result<Option<Vec<u8>>, Error> {
338            Ok(ResolverExtension::subscription_key(
339                &mut self.0,
340                headers,
341                subgraph_name,
342                directive,
343            ))
344        }
345    }
346
347    crate::component::register_extension(Box::new(|schema_directives, config| {
348        <T as ResolverExtension>::new(schema_directives, config)
349            .map(|extension| Box::new(Proxy(extension)) as Box<dyn AnyExtension>)
350    }))
351}