Plugins
The plugin system Graphile Build uses is provided by the graphile-config
module. This module isn't specific to any project (it's used by Graphile Build,
PostGraphile, Grafast, Grafserv, and more), and it handles the common
requirements of a plugin and preset system (composing plugins, ordering them,
handling dependencies, disabling plugins, etc) without worrying about the
details specific to any one use case. The specifics are handled by named
"scopes", which are root-level keys in the plugin object.
Graphile Build plugins are graphile-config plugins that have one or more of
the following scopes:
inflectiongatherschema
Plugins with none of the scopes listed above can still be included in a Graphile Build preset, they will just have no effect. This allows users to share a single configuration across many different projects.
Presets
graphile-config also provides the preset system used by Graphile Build. A
preset is simply a collection of other presets, plugins, and configuration
options to use. Presets can "extend from" other presets, and it's very common
to start your preset off by extending Graphile Build's defaultPreset:
import { defaultPreset } from "graphile-build";
export default {
extends: [defaultPreset],
};
Once you have a preset, you can feed it into the relevant Graphile Build methods, such as buildSchema:
import { buildSchema } from "graphile-build";
import { printSchema } from "graphql";
import preset from "./graphile.config.mjs";
const schema = await buildSchema(preset);
console.log(printSchema(schema));
Writing Plugins
A Graphile Build plugin is a simple object with a name, description,
version and entries for any of the scopes it wishes to implement.
Here's a plugin that does nothing:
const NoopPlugin = {
name: "NoopPlugin",
version: "0.0.0",
description: "Does nothing",
};
The scopes that a plugin might implement relate to the phases of schema
building. Schema building starts with the inflection phase, where the various
inflectors which control the naming of things are registered and customized.
Next comes the gather phase, where data is collected (for example by
performing introspection against your database). Finally we have the schema
phase where the behaviors of all the entities from the gather phase are
determined and then the schema is generated.
inflection scope
If a plugin wants to name things, or change how things are named, it would
implement the inflection scope.
This plugin replaces the builtin inflector so that the root types Query,
Mutation and Subscription are renamed to RootQuery, RootMutation and
RootSubscription:
const RootNamingPlugin = {
name: "RootNamingPlugin",
version: "0.0.0",
description: "Prefixes 'Root' to the root operation types",
inflection: {
replace: {
builtin(previous, options, text) {
if (["Query", "Mutation", "Subscription"].includes(text)) {
return `Root${text}`;
} else {
return previous(text);
}
},
},
},
};
gather scope
If a plugin needs to do something asynchronous, such as gather data from a
remote source, or read it from a file, then that work should be done via the
gather scope.
schema scope
Most commonly, Graphile Build plugins will want to implement the schema scope
in order to affect the GraphQL schema that is being built. The GraphQL schema
is built by hooking the various configuration objects that are passed to the
constructors in GraphQL.js (such as GraphQLObjectType,
GraphQLInputObjectType, GraphQLUnionType, etc) and some of their
configuration fields (such as GraphQLObjectType's fields or interfaces
configuration fields), and sometimes even the fields within that, or deeper
still. The deepest hook is GraphQLObjectType_fields_field_args_arg which is
used to manipulate a specific argument from the list of arguments from a
specific field from a list of fields on a specific GraphQL object type.
Every hook is passed three parameters, the first parameter is the entity
configuration being manipulated, the second is the build object
which is common to all hooks, and the third is the context object
specific to that hook, which gives a description (via the scope) of what is
actually being hooked.
Example 1
For example this plugin adds a field to the root
query type by hooking GraphQLObjectType_fields, and looking at the
context.scope.isRootQuery to determine if this is the object that we want to
modify:
import { constant } from "grafast";
const RootQueryFieldPlugin = {
name: "RootQueryFieldPlugin",
version: "0.0.0",
description: "Adds a field to the root Query type",
schema: {
hooks: {
GraphQLObjectType_fields(fields, build, context) {
// Only add the field to the root query type
if (!context.scope.isRootQuery) return fields;
// Add a field called `meaningOfLife`
fields.meaningOfLife = {
// It's an integer
type: build.graphql.GraphQLInt,
// When you call the field, you should always return the number '42'
plan() {
return constant(42);
},
};
return fields;
},
},
},
};
Example 2
This plugin will add a field random(sides: Int) to every GraphQLObjectType
that is generated:
// No imports required!
const MyRandomFieldPlugin = {
name: "MyRandomFieldPlugin",
version: "0.0.0",
schema: {
GraphQLObjectType_fields(fields, build, context) {
const {
extend,
graphql: { GraphQLInt },
options: { myDefaultMin = 1, myDefaultMax = 100 },
} = build;
return extend(fields, {
random: {
type: GraphQLInt,
args: {
sides: {
type: GraphQLInt,
},
},
plan(_, fieldArgs) {
const $sides = fieldArgs.get("sides");
return lambda(
$sides,
(sides) =>
Math.floor(
Math.random() * ((sides ?? myDefaultMax) - myDefaultMin + 1),
) + myDefaultMin,
);
},
},
});
},
},
};
First it registers a hook on GraphQLObjectType_fields which will be called for
the fields property of every GraphQLObjectType that is constructed.
The callback to this hook is passed the three standard options:
- input object,
fields, which is basically aGraphQLFieldConfigMapfrom graphql-js. Buildobject (from which we're usingextendandgraphql.GraphQLIntContextobject which it is ignoring; but if we wanted to filter which objects get therandomfield added this would be what we'd use
Finally we're returning a derivative of the fields that were input by adding
an additonal property field which is a GraphQL field config
GraphQLFieldConfig (see the GraphQL-js
documentation) but with
Grafast features mixed in - most notably the plan (rather than a resolve
method).