Trait ResolverExtension

Source
pub trait ResolverExtension: Sized + 'static {
    // Required methods
    fn new(
        schema_directives: Vec<SchemaDirective>,
        config: Configuration,
    ) -> Result<Self, Error>;
    fn resolve_field(
        &mut self,
        headers: SubgraphHeaders,
        subgraph_name: &str,
        directive: FieldDefinitionDirective<'_>,
        inputs: FieldInputs<'_>,
    ) -> Result<FieldOutputs, Error>;

    // Provided methods
    fn resolve_subscription(
        &mut self,
        headers: SubgraphHeaders,
        subgraph_name: &str,
        directive: FieldDefinitionDirective<'_>,
    ) -> Result<Box<dyn Subscription>, Error> { ... }
    fn subscription_key(
        &mut self,
        headers: &SubgraphHeaders,
        subgraph_name: &str,
        directive: FieldDefinitionDirective<'_>,
    ) -> Option<Vec<u8>> { ... }
}
Expand description

A resolver extension is called by the gateway to resolve a specific field.

§Example

You can initialize a new resolver extension with the Grafbase CLI:

grafbase extension init --type resolver my-resolver

This will generate the following:

use grafbase_sdk::{
    ResolverExtension,
    types::{SubgraphHeaders, FieldDefinitionDirective, FieldInputs, FieldOutputs, Configuration, Error, SchemaDirective}
};

#[derive(ResolverExtension)]
struct MyResolver {
  config: Config
}

#[derive(serde::Deserialize)]
struct Config {
  my_custom_key: String
}

impl ResolverExtension for MyResolver {
   fn new(schema_directives: Vec<SchemaDirective>, config: Configuration) -> Result<Self, Error> {
       let config: Config = config.deserialize()?;
       Ok(Self { config })
   }

   fn resolve_field(
       &mut self,
       headers: SubgraphHeaders,
       subgraph_name: &str,
       directive: FieldDefinitionDirective<'_>,
       inputs: FieldInputs<'_>,
   ) -> Result<FieldOutputs, Error> {
        unimplemented!()
   }
}

§Configuration

The configuration provided in the new method is the one defined in the grafbase.toml file by the extension user:

[extensions.my-auth.config]
my_custom_key = "value"

Once your business logic is written down you can compile your extension with:

grafbase extension build

It will generate all the necessary files in a build directory which you can specify in the grafbase.toml configuration with:

[extensions.my-auth]
path = "<project path>/build"

§Directives

In addition to the Rust extension, a definitions.graphql file will be also generated. It should define directives for subgraph owners and any necessary input types, scalars or enum necessary for those. Directives have two purposes for resolvers: define which fields can be resolved, providing the necessary metadata for it, and provide global metadata with schema directive.

A HTTP resolver extension could have the following directives for example:

scalar URL

directive @httpEndpoint(name: String!, url: URL!) on SCHEMA

directive @http(endpoint: String!, path: String!) on FIELD_DEFINITION

The @httpEndpoint directive would be provided during the new() method as a SchemaDirective. While the latter would be provided as a FieldDefinitionDirective during the resolve_field() method.

Required Methods§

Source

fn new( schema_directives: Vec<SchemaDirective>, config: Configuration, ) -> Result<Self, Error>

Creates a new instance of the extension. The Configuration will contain all the configuration defined in the grafbase.toml by the extension user in a serialized format. Furthermore all schema directives from all subgraphs will be provided as SchemaDirectives.

§Configuration example

The following TOML configuration:

[extensions.my-auth.config]
my_custom_key = "value"

can be easily deserialized with:

#[derive(serde::Deserialize)]
struct Config {
    my_custom_key: String
}

let config: Config = config.deserialize()?;
§Directive example
extend schema @httpEdnpoint(name: "example", url: "https://example.com")

can be easily deserialized with:

#[derive(serde::Deserialize)]
struct HttpEndpoint {
    name: String,
    url: String
}

let config: Vec<HttpEndpoint> = schema_directives
    .into_iter()
    .map(|dir| dir.arguments())
    .collect::<Result<_, _>>()?;
Source

fn resolve_field( &mut self, headers: SubgraphHeaders, subgraph_name: &str, directive: FieldDefinitionDirective<'_>, inputs: FieldInputs<'_>, ) -> Result<FieldOutputs, Error>

Resolves a GraphQL field. This function receives a batch of inputs and is called at most once per query field.

Supposing we have the following directive applied on this schema:

extend schema
 @link(
   url: "https://specs.grafbase.com/grafbase"
   import: ["FieldSet"]
 )

directive @resolve(fields: FieldSet!) on FIELD_DEFINITION
type Query {
   users: [User] # from a different subgraph
}

type User {
    id : ID!
    field: JSON @resolve(fields: "id")
}

and a query like:

query {
   users {
      field
   }
}

The subgraph providing Query.users will return an arbitrary number N of users. Instead of being called N times, this resolver will be called once with a FieldInputs containing N FieldInput. This allows you to batch everything together at once.

// Static arguments passed on to the directive that do not depend on the response data.
#[derive(serde::Deserialize)]
struct StaticArguments<'a> {
    #[serde(borrow)]
    endpoint_name: &'a str,
}
let StaticArguments { endpoint_name } = directive.arguments()?;

let mut builder = FieldOutputs::builder(inputs);
for input in inputs {
    // Field arguments that depend on response data.
    #[derive(serde::Deserialize)]
    struct ResponseArguments<'a> {
        #[serde(borrow)]
        id: &'a str,
    }

    let ResponseArguments { id } = directive.arguments()?;
    builder.insert(input, "data");
}

Ok(builder.build())

FieldOutputs can also be initialized with a single error or a single data for convenience.

We also want to support providing raw JSON and CBOR bytes directly for batched and non-batched data later on, if it’s of interested let us know!

In addition to this the method also receives the subgraph headers after all the subgraph-related header rules. And metadata the FieldDefinitionDirectiveSite is also available with directive.site().

Provided Methods§

Source

fn resolve_subscription( &mut self, headers: SubgraphHeaders, subgraph_name: &str, directive: FieldDefinitionDirective<'_>, ) -> Result<Box<dyn Subscription>, Error>

Resolves a subscription field by setting up a subscription handler.

§Arguments
  • headers - The subgraph headers associated with this field resolution
  • directive - The directive associated with this subscription field
  • definition - The field definition containing metadata about the subscription
§Returns

Returns a Result containing either a boxed Subscriber implementation or an Error

Source

fn subscription_key( &mut self, headers: &SubgraphHeaders, subgraph_name: &str, directive: FieldDefinitionDirective<'_>, ) -> Option<Vec<u8>>

Returns a key for a subscription field.

This method is used to identify unique subscription channels or connections when managing multiple active subscriptions. The returned key can be used to track, manage, or deduplicate subscriptions.

§Arguments
  • headers - The subgraph headers associated with this subscription
  • subgraph_name - The name of the subgraph associated with this subscription
  • directive - The directive associated with this subscription field
§Returns

Returns an Option<Vec<u8>> containing either a unique key for this subscription or None if no deduplication is desired.

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety", so this trait is not object safe.

Implementors§