Grafast plugin

A plugin for building schemas with Grafast plans instead of resolvers

Experimental Package

This package is experimental.

Plugins that add runtime behavior by wrapping resolvers do not apply that behavior to Grafast plans.

Install

npm install --save @pothos/plugin-grafast grafast graphql@^16.10.0

Grafast 1.0 requires GraphQL 16. Use a GraphQL version supported by your Grafast release.

Setup

import SchemaBuilder from '@pothos/core';
import GrafastPlugin from '@pothos/plugin-grafast';
import { get, inhibitOnNull, lambda, loadOne, type Step } from 'grafast';

type RequestContext = { requestId: string };

declare global {
  namespace Grafast {
    // Define the Context type used by grafast
    interface Context extends RequestContext {}
  }
}

type BuilderTypes = {
  // This tells the builder to expect plans instead of resolvers
  InferredFieldOptionsKind: 'Grafast';
  Context: RequestContext;
};

const builder = new SchemaBuilder<BuilderTypes>({
  plugins: [GrafastPlugin],
});

Usage

For documentation on how to write plans, see the Grafast documentation.

Adding plans to fields

builder.queryType({
  fields: (t) => ({
    addTwoNumbers: t.int({
      args: {
        a: t.arg.int({ required: true }),
        b: t.arg.int({ required: true }),
      },
      plan: (_, { $a, $b }) => {
        return lambda([$a, $b], ([a, b]) => a + b);
      },
    }),
  }),
});

Using resolvers

As an alternative to the previous query definition, you can write a resolver, but you will not have access to the 4th GraphqlResolveInfo argument:

builder.queryType({
  fields: (t) => ({
    addTwoNumbers: t.int({
      args: {
        a: t.arg.int({ required: true }),
        b: t.arg.int({ required: true }),
      },
      resolve: (_, { a, b }) => {
        return a + b;
      },
    }),
  }),
});

Resolvers should not be used to load data, but can make it easier to define a field that would otherwise use a simple lambda plan.

Abstract types

Abstract types (Unions and Interfaces) may require defining a plan to resolve to the correct type. For more details on how polymorphic types work in Grafast, see the Grafast documentation.

Interfaces

To implement an interface, you can implement it as you normally would in Pothos, and then call the .withPlan method on the interface ref to provide a plan for resolving the correct type.

interface AnimalData {
  id: string;
  kind: 'Dog' | 'Cat';
}

export const Animal = builder.interfaceRef<AnimalData>('Animal').withPlan({
  planType: ($record) => ({
    $__typename: get($record, 'kind'),
  }),
});

export const Dog = builder.objectRef<AnimalData>('Dog').implement({
  interfaces: [Animal],
});
export const Cat = builder.objectRef<AnimalData>('Cat').implement({
  interfaces: [Animal],
});

Animal.implement({
  fields: (t) => ({
    id: t.exposeID('id'),
  }),
});

You can now define a query to resolve this interface:

export const Animals = [
  {
    id: '1',
    kind: 'Dog',
  },
  {
    id: '2',
    kind: 'Cat',
  },
] satisfies AnimalData[];

function getAnimalsById(ids: readonly string[]): (AnimalData | null)[] {
  return ids.map((id) => Animals.find((entity) => entity.id === id) ?? null);
}

builder.queryFields((t) => ({
  animal: t.field({
    type: Animal,
    args: {
      id: t.arg.string({ required: true }),
    },
    plan: (_, $args) => loadOne($args.$id, getAnimalsById),
  }),
}));

Unions

Add another object type and combine it with Cat and Dog in a union:

interface AlienData {
  id: string;
  kind: 'Alien';
}

export const Alien = builder.objectRef<AlienData>('Alien').implement({
  fields: (t) => ({
    id: t.exposeID('id'),
  }),
});

export const Entity = builder
  .unionType('Entity', {
    types: [Cat, Dog, Alien],
  })
  .withPlan({
    planType: ($record) => ({
      $__typename: get($record, 'kind'),
    }),
  });

planForType

When planning polymorphic types, Grafast allows you to provide a planForType function that allows you to load the correct data for the current type.

This also enables changing the type of plan required for fields that return the abstract type:

planForType is not entirely type-safe, and will allow plans that resolve to data for the wrong type.

This replaces the previous Entity definition. The lookup uses the same animals and one alien:

function getEntitiesById(ids: readonly string[]): (AnimalData | AlienData | null)[] {
  const entities: (AnimalData | AlienData)[] = [...Animals, { id: '3', kind: 'Alien' }];
  return ids.map((id) => entities.find((entity) => entity.id === id) ?? null);
}

export const Entity = builder
  .unionType('Entity', {
    types: [Cat, Dog, Alien],
  })
  .withPlan({
    planType: (
      // Provide an explicit type so that the query field only needs to return the ID
      $specifier: Step<string>,
    ) => {
      const $record = inhibitOnNull(loadOne($specifier, getEntitiesById));
      return {
        $__typename: get($record, 'kind'),
        planForType: () => $record,
      };
    },
  });

builder.queryFields((t) => ({
  entity: t.field({
    type: Entity,
    args: {
      id: t.arg.string({ required: true }),
    },
    // Because our Entity plan loads the record, we can just return the ID here
    plan: (_, $args) => $args.$id,
  }),
}));

Run the plans locally

The browser playground executes GraphQL resolvers with graphql(). This example requires the Grafast executor to run its plans. The complete local example combines the setup, addition query, animal interface, and planForType union shown above.

With Node.js 22 or newer, run from a checkout of the Pothos repository:

pnpm install --frozen-lockfile
pnpm --dir website check:local

The local examples use the checkout's built Pothos packages. Their separate locked installation uses GraphQL 16, as required by Grafast 1.0; it does not change the repository's GraphQL version.

grafast/check.ts executes the schema with grafast() and checks addition, both animal types, the alien union member, and a missing entity. It also changes the arguments and checks the new sum. To try your own values, edit the query or variableValues in that file and update its expected result.

The execution call has this form:

import { grafast } from 'grafast';
import { schema } from './schema';

const result = await grafast({
  schema,
  source: '{ addTwoNumbers(a: 2, b: 3) }',
  contextValue: { requestId: 'example' },
});
console.log(result); // { data: { addTwoNumbers: 5 } }