Plan resolvers
Before executing a GraphQL request, Grafast must have an "operation plan" that details the actions ("steps") that are needed to satisfy the included operation (query, mutation, subscription). Plans are cached so they can be re-used for all sufficiently similar GraphQL requests in future — plan once, execute many times.
The first time Grafast sees a particular operation, it traverses the operation in a breadth-first manner, calling the associated plan resolvers and building out a tree of steps that form the beginnings of the operation plan. This is then optimized, finalized, cached, and executed. To maximize plan reuse, planning does not have access to the raw input values, instead representing them as steps to be populated at execution time for each request.
Field plan resolvers
// Simplified types
type FieldPlanResolver = (
$source: Step,
fieldArgs: FieldArgs, // See "FieldArgs" below
info: FieldInfo, // See "FieldInfo" below
) => Step;
"Field plan resolvers" are the functions responsible for detailing actions sufficient to resolve an individual field as part of the operation plan.
When calling a field's plan resolver, Grafast will pass:
- the
$sourcestep representing the object the field belongs to - a
FieldArgsobject to retrieve the field's arguments - a
FieldInfoobject with additional planning info (rarely needed)
The plan resolver must return a Step that represents the result of the field.
A field without a plan resolver will use the default plan resolver.
Source step
The $source step represents the data from the object that the field is being
resolved against (i.e. the GraphQL object type that the field belongs to). For
the root selection set, the parent object represents either the GraphQL
rootValue, or for subscription operations the event. For all other selection
sets the source object will be derived from the parent field:
- for a list type, the "source step" will represent an item in this list
- for an object type, the "source step" is the step the parent field returned
- for an abstract type, the "source step" will be the step representing the matching concrete object type
Default plan resolver
If a field does not have a plan resolver (and traditional resolvers are not being emulated for the field) then the default plan resolver will apply:
const defaultPlanResolver: FieldPlanResolver = ($source, fieldArgs, info) =>
get($source, info.fieldName);
The default plan resolver uses get()
to access the property of the $source object with the same name as the field.
FieldArgs
// Simplified type
type FieldArgs = {
getRaw(path: string | ReadonlyArray<string | number>): Step;
// Shortcuts for getRaw for each argument:
[`$${string}`]: Step;
// -- Advanced features --
autoApply($target: Step): void;
apply(
$target: ApplyableStep,
path?: ReadonlyArray<string | number>,
getTargetFromParent?: (parent: any, inputValue: any) => object | undefined,
): void;
getBaked(path: string | ReadonlyArray<string | number>): Step;
};
The "field arguments" (fieldArgs) is an object giving
ways of interacting with the values passed as arguments to a field.
Accessing argument values
You can retrieve a step representing a field argument's value either via the
matching $-prefixed property of the fieldArgs object, or via the .getRaw()
method.
Consider this schema:
input BookFilter {
author: String
publishedAfter: Int
}
type Query {
bookCount(search: String, filter: BookFilter): Int!
}
You can access the argument steps using the $-prefixed properties:
function bookCount($parent, fieldArgs) {
const { $search, $filter } = fieldArgs;
const { $author, $publishedAfter } = $filter;
}
or, equivalently, via .getRaw():
function bookCount($parent, fieldArgs) {
const $search = fieldArgs.getRaw("search");
const $filter = fieldArgs.getRaw("filter");
const $author = fieldArgs.getRaw(["filter", "author"]);
const $publishedAfter = fieldArgs.getRaw(["filter", "publishedAfter"]);
}
Early auto-apply
This is an advanced topic typically useful for programmatic schema manipulation; if you're writing a schema by hand you're unlikely to need it.
Arguments can have their own plan resolvers, see argument plan resolvers below. Grafast will invoke these automatically once the field plan resolver returns, but if you want to wrap a field plan resolver with a higher order function, you might want all of the arguments to have already been applied before your wrapper plan's logic continues.
You can trigger the auto-application early with the
fieldArgs.autoApply($target) method:
const oldPlan = usersField.extensions.grafast.plan;
usersField.extensions.grafast.plan = function ($source, fieldArgs, info) {
// Call the old plan method
const $target = oldPlan($source, fieldArgs, info);
// Perform the auto-application of arguments early:
fieldArgs.autoApply($target);
// Now do whatever logic we need to do:
if (!$target.getFirst()) {
$target.setFirst(constant(10));
}
return $target;
};
Handling complex inputs
This is a very advanced topic typically useful for autogenerated schemas; if you're writing a schema by hand you're unlikely to need it.
.getBaked() and .apply() are documented in the Handling complex
inputs article - these are advanced features you're
unlikely to need when writing your schema by hand, but they can be helpful
when generating schemas automatically or if you have particularly complex
input structures (advanced filters, ordering, pagination, etc).
FieldInfo
interface FieldInfo {
fieldName: string;
field: GraphQLField<any, any, any>;
schema: GraphQLSchema;
}
The info object contains information about the context in which the plan resolver is called:
fieldName: the name of the field being resolved (not its alias)field: the field itselfschema: the fullGraphQLSchemaobject the request is being executed against
It's very rare for hand-written plan resolvers to need this but it's useful for libraries that generate plan resolvers or when using the same plan resolver with multiple fields, as in the case of the default plan resolver.
Example
Given the following schema fragment:
type User {
friends(limit: Int): [User!]!
}
the plan resolver for the User.friends field might look like this:
function User_friends_plan(
$source: Step, // < Represents the User
fieldArgs: FieldArgs,
): Step {
// Read the 'limit' argument
const { $limit } = fieldArgs;
// Get the user's ID from the source step
const $userId = get($source, "id");
// Load the friends for this user
const $friends = loadMany($userId, friendsByUserId);
// Tweak the `$friends` plan to apply the limit
$friends.setParam("limit", $limit);
return $friends;
}
By convention, when a variable represents a step the variable's name starts
with a $; this helps remind us that this is not the actual value, but a
placeholder that represents the value that will be filled at execution time for
each request that uses this plan.
Traditional resolvers
Although Grafast uses an alternative execution model to the reference implementation (GraphQL.js), to make it easy for people to adopt Grafast it has support for emulating GraphQL.js' resolvers ("traditional resolvers") via a set of built in plan classes. This support is pretty good — sufficient to pass the integration tests of the GraphQL.js test suite — though it does have a few limitations (see Using with an existing schema for more details).
Using traditional resolvers fails to capture the benefits of Grafast (since traditional resolvers execute at execution time, we cannot optimize the operation at planning time) and the emulation creates additional overhead so it is discouraged for new schemas — if you're starting from scratch you should build a pure (plan-only) Grafast schema.
Remember: everything that can be done in a traditional resolver can be done instead via a step.
If a field has both a plan resolver and a traditional resolver, then the plan
resolver will run first, and the result of the plan resolver will be provided to
the traditional resolver as the source argument (the first argument), giving
users the ability to port a legacy schema to Grafast on a field-by-field
basis. We recommend starting with the fields that would yield the greatest
benefit, and those that consume them.
If a field with a traditional resolver is invoked, then Grafast will enter
"resolver emulation mode" for that tree, and will remain in resolver emulation
until a field with a plan is met; the default plan resolver will not be used
in this mode, instead the traditional defaultFieldResolver will be
emulated.
Specifying a field plan resolver
When building a GraphQL schema programatically, plan resolvers are stored into
extensions.grafast.plan of the field; for a raw GraphQL.js object type this
would look like:
import { GraphQLSchema, GraphQLObjectType, GraphQLInt } from "graphql";
import { constant } from "grafast";
const Query = new GraphQLObjectType({
name: "Query",
fields: {
meaningOfLife: {
type: GraphQLInt,
extensions: {
grafast: {
plan() {
return constant(42);
},
},
},
},
},
});
export const schema = new GraphQLSchema({
query: Query,
});
If you are using makeGrafastSchema then the field plan resolver for the field
fieldName on the object type typeName would be indicated via the
objects[typeName].plans[fieldName] property:
import { makeGrafastSchema, constant } from "grafast";
export const schema = makeGrafastSchema({
typeDefs: /* GraphQL */ `
type Query {
meaningOfLife: Int
}
`,
objects: {
Query: {
plans: {
meaningOfLife() {
return constant(42);
},
},
},
},
});
For GraphQL Modules you must
specify the resolver as an object, and then populate the extensions property:
import { createModule, gql } from 'graphql-modules';
import { constant } from "grafast";;
export const myModule = createModule({
id: 'my-module',
dirname: __dirname,
typeDefs: [
gql`
type Query {
meaningOfLife: Int!
}
`,
],
resolvers: {
Query: {
meaningOfLife: {
extensions: {
grafast: {
plan() {
return constant(42);
},
},
},
},
},
},
});
Argument plan resolvers
You wouldn't typically use this if you're writing your schema by hand, but it can be helpful if you're using automatic schema generation as it allows arguments to handle their own logic without having to "wrap" the underlying field plan resolver.
Sometimes rather than fetching and using the raw argument value directly, you
want to apply the argument to your field plan. This allows you to keep your
argument logic separate from your field plan logic. For this, your argument
would have an applyPlan method defined on it:
const schema = makeGrafastSchema({
typeDefs: /* GraphQL */ `
type Query {
users(first: Int, offset: Int): [User!]!
}
`,
objects: {
Query: {
plans: {
users: {
// The (simple) plan for the field
plan($query, fieldArgs) {
const $allUsers = users.find();
return $allUsers;
},
// These become `applyPlan` methods on the arguments:
args: {
// $target will be the return result of the field plan, i.e.
// `$allUsers` above
first($query, $target, val) {
const $first = val.getRaw();
$target.setFirst($first);
},
offset($query, $target, val) {
const $offset = val.getRaw();
$target.setOffset($offset);
},
},
},
},
},
},
});
Grafast will automatically call each of the arguments' plans once the field plan
resolver has returned, passing the step yielded from the field plan as the
$target for the arguments.
If Grafast can determine statically that your argument will not be passed at
runtime (i.e. will be undefined) then it may choose to skip calling the
applyPlan() method for that argument.
Asserting an object type's step
Sometimes a field plan resolver expects the source step to support specific methods.
For example, imagine a Post type that represents a row from a posts database
table. In the post list page on your website, you may want to fetch a truncated
version of the post body to include with each post. Fetching the entire body for
each post and truncating in the application layer would be inefficient; instead
you might use a custom SQL expression via, for example,
$post.select(sql`left(body, 200)`). For this to work, $post must be a Step
class that implements the .select(SQL) method. If a step without this method
were passed, the plan resolver would throw an error.
To ensure the steps passed to a particular object type are compatible with the
expectations of the plan resolvers, you can add an assertStep method
(objectType.extensions.grafast.assertStep). The value can either be a step
class (in which case it will be asserted that each step is an instanceof this
class) or an assertion function (which will be called and should throw an error
if the step is not acceptable).
assertStep is optional, but highly recommended when your plan resolvers rely
on methods that only exist on a specific step class to ensure errors in plans
are caught early.
Example - step class
Here assertStep asserts $post is a PgSelectSingleStep, therefore it's safe
to use .select():
import { makeGrafastSchema } from "grafast";
import { PgSelectSingleStep, sql, TYPES } from "@dataplan/pg";
const schema = makeGrafastSchema({
objects: {
Post: {
assertStep: PgSelectSingleStep,
plans: {
truncatedBody($post: PgSelectSingleStep) {
return $post.select(sql`left(body, 200)`, TYPES.text);
},
},
},
},
typeDefs: /* GraphQL */ `
type Post {
truncatedBody: String
}
# ...
`,
});
Example - assertion function
Using an assertion function allows you to accept a wider range of steps, perform duck typing, or throw more helpful error messages.
import { makeGrafastSchema } from "grafast";
import { PgSelectSingleStep, sql, TYPES } from "@dataplan/pg";
const schema = makeGrafastSchema({
objects: {
Post: {
assertStep($step) {
if ($step instanceof PgSelectSingleStep) return;
throw new Error(
`Type 'Post' expects PgSelectSingleStep; received ${$step.constructor.name}`,
);
},
plans: {
truncatedBody($post: PgSelectSingleStep) {
return $post.select(sql`left(body, 200)`, TYPES.text);
},
},
},
},
// ...
});
Example - extensions
If using raw GraphQL.js objects, the assertStep method goes inside of
extensions.grafast:
import { GraphQLObjectType, GraphQLString } from "graphql";
import { PgSelectSingleStep, sql, TYPES } from "@dataplan/pg";
const Post = new GraphQLObjectType({
name: "Post",
extensions: {
grafast: {
assertStep: PgSelectSingleStep,
},
},
fields: {
truncatedBody: {
type: GraphQLString,
extensions: {
grafast: {
plan($post: PgSelectSingleStep) {
return $post.select(sql`left(body, 200)`, TYPES.text);
},
},
},
},
},
});
Execution order & side effects
Grafast is declarative: steps form a directed acyclic graph (DAG) and only dependencies determine order — there is no implicit procedural sequencing. Two key rules:
- Dependencies before dependents.
- Steps implicitly depend on the most recent side effect step, if any.
This can be surprising during mutations: steps created before a mutation might execute after the mutation unless one of the rules above says otherwise.
Example
const $before = users.get({ id: $rowId });
const $valueBefore = $before.get("value1");
const $after = updateUser($rowId);
const $valueAfter = $after.get("value1");
const $log = sideEffect(
[$valueBefore, $valueAfter],
([before, after]) => void console.log({ before, after }),
);
Default graph (no extra side effects):
The engine may execute $after (mutation) before $valueBefore, because
nothing forbids it according to the two rules above.
Force “read-before-write” by marking $valueBefore as a side effect:
const $before = users.get({ id: $rowId });
const $valueBefore = $before.get("value1");
+$valueBefore.hasSideEffect = true;
const $after = updateUser($rowId);
const $valueAfter = $after.get("value1");
const $log = sideEffect(
[$valueBefore, $valueAfter],
([before, after]) => void console.log({ before, after })
);
This adds an implicit edge from $valueBefore to later steps (including $after):
Now $valueBefore must run before $after, so your log shows the true “before” and “after”.
When to use which
- Prefer explicit data deps when possible (e.g. make the read feed the write).
- Use
hasSideEffect = truewhen you need ordering without data flow (logging, metrics, authorization gates, idempotency checks). - Don’t sprinkle
hasSideEffecton hot paths unnecessarily; it reduces reordering freedom.