Getting started
Installation
Grafast is an alternative execution layer for GraphQL; we still need GraphQL.js
for building the schema, and parsing and validating requests. So the first thing
you need to do to get started is to install grafast
and graphql
:
- npm
- Yarn
- pnpm
npm install --save grafast@beta graphql
yarn add grafast@beta graphql
pnpm add grafast@beta graphql
We intend to write up a specification so that other languages may implement the Grafast execution strategy, but for now Grafast is JavaScript/TypeScript only.
If you have an existing GraphQL.js schema, you can run it through Grafast ─ see using with an existing schema.
TypeScript v5.0.0+ (optional)
We recommend that you use TypeScript for the best experience - auto-completion, inline documentation, etc.
You do not need to use TypeScript to use Grafast, but if you do then you
must use a version from TypeScript v5.0.0 upward and configure it to support
the exports
property in package.json
, you can do so by adding this to your
TypeScript configuration:
"moduleResolution": "node16", // Or "nodenext"
Our adherence to semver does not cover types - we may make breaking changes to TypeScript types in patch-level updates. The reason for this is that TypeScript itself is ever-changing, and the libraries we depend on often make breaking type changes, forcing us to do so too. Further, improvements to types are generally a good thing for developer experience, even if it might mean you have to spend a couple minutes after updating to address any issues.
However, we try and keep the TypeScript types as stable as possible, only making breaking changes when their benefits outweigh the costs (as determined by our maintainer), and we do our best to detail in the release notes how to deal with these changes (if any action is necessary).
My first plan
Let's build a simple GraphQL schema powered by Grafast plans and query it.
There are many ways to build a GraphQL schema, we're going to use the "schema first" approach in this example, but there's no reason that a Grafast schema couldn't be produced "code first" or "database first" or any other approach.
We have a playground you can use for experimenting with Grafast without having to install any software.
First, lets define our GraphQL schema. We're going to go with an incredibly simple schema with a single field that adds together its two arguments:
const typeDefs = /* GraphQL */ `
type Query {
addTwoNumbers(a: Int!, b: Int!): Int
}
`;
Now we need to define the plans
for the schema. The plan for our
Query.addTwoNumbers
field is to read the arguments, then use the
lambda step to add them together. The lambda step takes a list of other
steps, and then determines the result by calling the given callback for each set
of resulting values.
const { lambda } = require("grafast");
const plans = {
Query: {
addTwoNumbers(_, fieldArgs) {
const $a = fieldArgs.get("a");
const $b = fieldArgs.get("b");
return lambda([$a, $b], ([a, b]) => a + b);
},
},
};
lambda
is a bit of an escape hatch ─ it enables one-by-one processing of
values rather than the batched processing that Grafast prefers for efficiency.
It can be handy as a utility function when batching would confer no benefit,
but in general you should pick a more suitable step.
Making the callback function to lambda
a global (defined once) function
would enable Grafast to potentially detect multiple uses of it and
deduplicate them. This is important for performance if a similar lambda
callback is used in lots of places in a query.
To turn this into a schema, we can use the makeGrafastSchema
helper which will
stitch the typeDefs
and the plans
together:
const { makeGrafastSchema } = require("grafast");
const schema = makeGrafastSchema({
typeDefs,
plans,
});
Finally, we can run our query:
const { grafastSync } = require("grafast");
const result = grafastSync({
schema,
source: /* GraphQL */ `
{
addTwoNumbers(a: 40, b: 2)
}
`,
});
The result is what we'd expect:
{
"data": {
"addTwoNumbers": 42
}
}
Our schema is so simple the query could be ran synchronously with
grafastSync(...)
, but most queries in the real world should be executed
through grafast(...)
and will return either the result, a promise to the
result, or even an async iterable for @stream
, @defer
, subscriptions and
similar!
We could then serve this schema over HTTP using a server such as grafserv or any envelop-capable server.
My first step class
The building blocks of an operation plan are "steps." Steps are instances of
"step classes," and Grafast makes available a modest range of standard steps
that you can use; but when these aren't enough you can write your own.
Step classes extend the ExecutableStep
class. The only required method to
define is execute
, however most steps will also have a constructor
in which
they accept their arguments (some of which may be dependencies) and may also
have the various lifecycle methods.
Full details for doing so can be found in Step classes, but let's build
ourselves a simple one now to replace the lambda
usage above:
const { ExecutableStep } = require("grafast");
class AddStep extends ExecutableStep {
constructor($a, $b) {
super();
this.addDependency($a);
this.addDependency($b);
}
execute({ indexMap, values: [aDep, bDep] }) {
return indexMap((i) => {
const a = aDep.at(i);
const b = bDep.at(i);
return a + b;
});
}
}
By convention, we always define a function that constructs an instance of our class:
function add($a, $b) {
return new AddStep($a, $b);
}
There's multiple reasons for this, a simple one is to make the plan code
easier to read: we won't see the new
calls in our plan resolver functions,
nor the redundant Step
wording, resulting in a higher signal-to-noise ratio.
More importantly, though, is that the small layer of indirection allows us to
do some minor manipulations before handing off to the class constructor, and
makes the APIs more future-proof since we can have the function return
something different in future without having to refactor our plans in the
schema. And remember that this cost is only incurred at planning time (which is
generally cached and can be re-used for similar future requests), and each
field is only planned once, so the overhead of an additional function call is
negligible.
Now we can use this function to add our numbers, rather than the lambda plan:
const plans = {
Query: {
addTwoNumbers(_, args) {
const $a = args.get("a");
const $b = args.get("b");
- return lambda([$a, $b], ([a, b]) => a + b);
+ return add($a, $b);
},
},
};
You may well be able to write an entire Grafast schema using off-the-shelf step classes, but it's worth being aware of how step classes work in case you want to push your optimizations further. Read more about step classes, or continue through the documentation.