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.0Grafast 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:localThe 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 } }