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}