Skip to content

Commit

Permalink
Add loader support (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
mcollina authored Apr 19, 2019
1 parent a5904e0 commit 84b8254
Show file tree
Hide file tree
Showing 6 changed files with 453 additions and 6 deletions.
78 changes: 73 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# fastify-gql

[![Greenkeeper badge](https://badges.greenkeeper.io/mcollina/fastify-gql.svg)](https://greenkeeper.io/)
[![Greenkeeper badge](https://badges.greenkeeper.io/mcollina/fastify-gql.svg)](https://greenkeeper.io/) [![Build Status](https://travis-ci.com/mcollina/fastify-gql.svg?branch=master)](https://travis-ci.com/mcollina/fastify-gql)

Fastify barebone GraphQL adapter.
Queries are cached on reuse to reduce the overhead of query parsing and
validation.
fastify-gql supports a Just-In-Time compiler via
[graphql-jit](http://npm.im/graphql-jit).

Features:

* Caching of query parsing and validation.
* Automatic loader integration to avoid 1 + N queries.
* Just-In-Time compiler via [graphql-jit](http://npm.im/graphql-jit).

## Install

Expand Down Expand Up @@ -96,6 +98,8 @@ __fastify-gql__ supports the following options:
definition](https://graphql.org/graphql-js/type/#graphqlschema). The graphql schema.
The string will be parsed.
* `resolvers`: Object. The graphql resolvers.
* `loaders`: Object. See [defineLoaders](#defineLoaders) for more
details.
* `graphiql`: boolean. Serve
[GraphiQL](https://www.npmjs.com/package/graphiql) on `/graphiql` if
`routes` is `true`.
Expand Down Expand Up @@ -248,6 +252,70 @@ async function run () {
run()
```

<a name="loaders"></a>
#### app.graphql.defineLoaders(loaders)

A loader is an utility to avoid the 1 + N query problem of GraphQL.
Each defined loader will register a resolver that coalesces each of the
request and combines them into a single, bulk query. Morever, it can
also cache the results, so that other parts of the GraphQL do not have
to fetch the same data.

Each loader function has the signature `loader(queries, context)`.
`queries` is an array of objects defined as `{ obj, params }` where
`obj` is the current object and `params` are the GraphQL params (those
are the first two parameters of a normal resolver). The `context` is the
GraphQL context, and it includes a `reply` object.

Example:


```js
const loaders = {
Dog: {
async owner (queries, { reply }) {
return queries.map(({ obj }) => owners[obj.name])
}
}
}

app.register(GQL, {
schema,
resolvers,
loaders
})
```

It is also possible disable caching with:

```js
const loaders = {
Dog: {
owner: {
async loader (queries, { reply }) {
return queries.map(({ obj }) => owners[obj.name])
},
opts: {
cache: false
}
}
}
}

app.register(GQL, {
schema,
resolvers,
loaders
})
```

Disabling caching has the advantage to avoid the serialization at
the cost of more objects to fetch in the resolvers.


Internally, it uses
[single-user-cache](http://npm.im/single-user-cache).

#### reply.graphql(source, context, variables, operationName)

Decorate [Reply](https://www.fastify.io/docs/latest/Reply/) with a
Expand Down
2 changes: 1 addition & 1 deletion example.js → examples/basic.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'

const Fastify = require('fastify')
const GQL = require('.')
const GQL = require('..')

const app = Fastify()

Expand Down
68 changes: 68 additions & 0 deletions examples/loaders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use strict'

const Fastify = require('fastify')
const GQL = require('..')

const app = Fastify()

const dogs = [{
name: 'Max'
}, {
name: 'Charlie'
}, {
name: 'Buddy'
}, {
name: 'Max'
}]

const owners = {
'Max': {
name: 'Jennifer'
},
'Charlie': {
name: 'Sarah'
},
'Buddy': {
name: 'Tracy'
}
}

const schema = `
type Human {
name: String!
}
type Dog {
name: String!
owner: Human
}
type Query {
dogs: [Dog]
}
`

const resolvers = {
Query: {
dogs (_, params, { reply }) {
return dogs
}
}
}

const loaders = {
Dog: {
async owner (queries, { reply }) {
return queries.map(({ obj }) => owners[obj.name])
}
}
}

app.register(GQL, {
schema,
resolvers,
loaders,
graphiql: true
})

app.listen(3000)
46 changes: 46 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const LRU = require('tiny-lru')
const routes = require('./routes')
const { BadRequest, MethodNotAllowed, InternalServerError } = require('http-errors')
const { compileQuery } = require('graphql-jit')
const { Factory } = require('single-user-cache')
const {
parse,
buildSchema,
Expand All @@ -21,6 +22,8 @@ const {
execute
} = require('graphql')

const kLoaders = Symbol('fastify-gql.loaders')

function buildCache (opts) {
if (opts.hasOwnProperty('cache')) {
if (opts.cache === false) {
Expand Down Expand Up @@ -132,10 +135,53 @@ module.exports = fp(async function (app, opts) {
}
}

let factory

fastifyGraphQl.defineLoaders = function (loaders) {
// set up the loaders factory
if (!factory) {
factory = new Factory()
app.decorateReply(kLoaders)
app.addHook('onRequest', async function (req, reply) {
reply[kLoaders] = factory.create({ req, reply })
})
}

function defineLoader (name) {
// async needed because of throw
return async function (obj, params, { reply }) {
if (!reply) {
throw new Error('loaders only work via reply.graphql()')
}
return reply[kLoaders][name]({ obj, params })
}
}

const resolvers = {}
for (const typeKey of Object.keys(loaders)) {
const type = loaders[typeKey]
resolvers[typeKey] = {}
for (const prop of Object.keys(type)) {
const name = typeKey + '-' + prop
resolvers[typeKey][prop] = defineLoader(name)
if (typeof type[prop] === 'function') {
factory.add(name, type[prop])
} else {
factory.add(name, type[prop].opts, type[prop].loader)
}
}
}
fastifyGraphQl.defineResolvers(resolvers)
}

if (opts.resolvers) {
fastifyGraphQl.defineResolvers(opts.resolvers)
}

if (opts.loaders) {
fastifyGraphQl.defineLoaders(opts.loaders)
}

async function fastifyGraphQl (source, context, variables, operationName) {
context = Object.assign({ app: this }, context)
const reply = context.reply
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"graphql": "^14.1.1",
"graphql-jit": "0.1.2",
"http-errors": "^1.7.2",
"single-user-cache": "^0.3.0",
"tiny-lru": "^6.0.0"
}
}
Loading

0 comments on commit 84b8254

Please sign in to comment.