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}