Skip to main content

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 install --save grafast@beta graphql
note

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.

My first plan

Let's build a simple GraphQL schema powered by Grafast plans and query it.

info

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.

tip

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);
},
},
};
tip

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.

tip

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
}
}
info

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(count, [allA, allB]) {
const results = [];
for (let i = 0; i < count; i++) {
// `allA` and `allB` have the same length, which is `count`, and the
// entries at the same index in each list relate to each other.
const a = allA[i];
const b = allB[i];
results.push(a + b);
}
return results;
}
}
tip

The above example of execute is verbose to ease understanding, but could have leveraged the knowledge that there's exactly two dependencies and that the two lists have the same length in order to use a much more concise implementation:

  execute(count, [allA, allB]) {
return allA.map((a, i) => a + allB[i]);
}

By convention, we always define a function that constructs an instance of our class:

function add($a, $b) {
return new AddStep($a, $b);
}
note

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.