ResolverExtension

Trait ResolverExtension 

Source
pub trait ResolverExtension: Sized + 'static {
    // Required methods
    fn new(
        subgraph_schemas: Vec<SubgraphSchema>,
        config: Configuration,
    ) -> Result<Self, Error>;
    fn resolve(
        &mut self,
        ctx: &AuthorizedOperationContext,
        prepared: &[u8],
        headers: SubgraphHeaders,
        variables: Variables,
    ) -> Result<Response, Error>;

    // Provided methods
    fn prepare(&mut self, field: ResolvedField<'_>) -> Result<Vec<u8>, Error> { ... }
    fn resolve_subscription<'s>(
        &'s mut self,
        ctx: &'s AuthorizedOperationContext,
        prepared: &'s [u8],
        headers: SubgraphHeaders,
        variables: Variables,
    ) -> Result<impl IntoSubscription<'s>, Error> { ... }
}
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::{AuthorizedOperationContext, Configuration, Error, ResolvedField, Response, SubgraphHeaders, SubgraphSchema, Variables},
};

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

// Configuration in the TOML for this extension
#[derive(serde::Deserialize)]
struct Config {
    #[serde(default)]
    key: Option<String>
}

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

    fn resolve(
        &mut self,
        ctx: &AuthorizedOperationContext,
        prepared: &[u8],
        headers: SubgraphHeaders,
        variables: Variables,
    ) -> Result<Response, Error> {
        // field which must be resolved. The prepared bytes can be customized to store anything you need in the operation cache.
        let field = ResolvedField::try_from(prepared)?;
        Ok(Response::null())
    }
}

§Configuration

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

[extensions.my-resolver.config]
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-resolver]
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 schema crate::types::Directive. The whole subgraph schema is also provided for each subgraph where this extension is used. While the latter can be accessed with ResolvedField::directive() in the resolve() method.

Required Methods§

Source

fn new( subgraph_schemas: Vec<SubgraphSchema>, 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 the complete subgraph schema is provided whenever this extension is used.

§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
}
for schema in subgraph_schemas {
    for directive in schema.directives() {
        match directive.name() {
            "httpEndpoint" => {
                 let http_endpoint: HttpEndpoint = directive.arguments()?;
            }
            _ => unreachable!()
        }
    }
}
Source

fn resolve( &mut self, ctx: &AuthorizedOperationContext, prepared: &[u8], headers: SubgraphHeaders, variables: Variables, ) -> Result<Response, Error>

Resolves the field with the provided prepared bytes, headers and variables. With the default prepare() you can retrieve all the relevant information with:

let field = ResolvedField::try_from(prepared)?;

If you’re not doing any data transformation it’s best to forward the JSON or CBOR bytes, with Response::json and Response::cbor respectively, directly to the gateway. The gateway will always validate the subgraph data and deal with error propagation. Otherwise use Response::data to use the fastest supported serialization format.

Provided Methods§

Source

fn prepare(&mut self, field: ResolvedField<'_>) -> Result<Vec<u8>, Error>

Prepares the field for resolution. The resulting byte array will be part of the operation cache. Backwards compatibility is not a concern as the cache is only re-used for the same schema and extension versions. By default the ResolvedField is cached for a simpler implementation.

Source

fn resolve_subscription<'s>( &'s mut self, ctx: &'s AuthorizedOperationContext, prepared: &'s [u8], headers: SubgraphHeaders, variables: Variables, ) -> Result<impl IntoSubscription<'s>, Error>

Resolves a subscription for the given prepared bytes, headers, and variables. Subscriptions must implement the Subscription trait. It’s also possible to provide a de-duplication key. If provided the gateway will first check if there is an existing subscription with the same key and if there is, re-use it for the new client. This greatly limits the impact on the upstream service. So you have two choices for the result type:

  • return a Ok(subscription) directly, without any de-duplication
  • return a Ok((key, callback)) with an optional de-duplication key and a callback function. The latter is only called if no existing subscription exists for the given key.

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§