Code Monkey home page Code Monkey logo

fastify-gql's Introduction

fastify-gql

Greenkeeper badge Build Status

Fastify barebone GraphQL adapter.

Features:

  • Caching of query parsing and validation.
  • Automatic loader integration to avoid 1 + N queries.
  • Just-In-Time compiler via graphql-jit.

Install

npm i fastify fastify-gql

Example

'use strict'

const Fastify = require('fastify')
const GQL = require('fastify-gql')

const app = Fastify()

const schema = `
  type Query {
    add(x: Int, y: Int): Int
  }
`

const resolvers = {
  Query: {
    add: async (_, { x, y }) => x + y
  }
}

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

app.get('/', async function (req, reply) {
  const query = '{ add(x: 2, y: 2) }'
  return reply.graphql(query)
})

app.listen(3000)

See test.js for more examples, docs are coming.

makeExecutableSchema support

'use strict'

const Fastify = require('fastify')
const GQL = require('fastify-gql')
const { makeExecutableSchema } = require('graphql-tools')

const app = Fastify()

const typeDefs = `
  type Query {
    add(x: Int, y: Int): Int
  }
`

const resolvers = {
  Query: {
    add: async (_, { x, y }) => x + y
  }
}

app.register(GQL, {
  schema: makeExecutableSchema({ typeDefs, resolvers })
})

app.get('/', async function (req, reply) {
  const query = '{ add(x: 2, y: 2) }'
  return reply.graphql(query)
})

app.listen(3000)

Access app context in resolver

...

const resolvers = {
  Query: {
    add: async (_, { x, y }, context) => {
      // do you need the request object?
      console.log(context.reply.request)
      return x + y
    }
  }
}

...

Build a custom GraphQL context object

...
const resolvers = {
  Query: {
    me: async (obj, args, ctx) => {
      // access user_id in ctx
      console.log(ctx.user_id)
    }
  }
}
app.register(GQL, {
  schema: makeExecutableSchema({ typeDefs, resolvers }),
  context: (request, reply) => {
    // Return an object that will be available in your GraphQL resolvers
    return {
        user_id: 1234
    }
  }
})
...

Subscription support (simple)

const schema = `
  type Notification {
    id: ID!
    message: String
  }

  type Query {
    notifications: [Notification]
  }

  type Mutation {
    addNotification(message: String): Notification
  }

  type Subscription {
    notificationAdded: Notification
  }
`

let idCount = 1
const notifications = [{
  id: idCount,
  message: 'Notification message'
}]

const resolvers = {
  Query: {
    notifications: () => notifications
  },
  Mutation: {
    addNotification: async (_, { message }, { pubsub }) => {
      const id = idCount++
      const notification = {
        id,
        message
      }
      notifications.push(notification)
      await pubsub.publish({
        topic: 'NOTIFICATION_ADDED',
        payload: {
          notificationAdded: notification
        }
      })

      return notification
    }
  },
  Subscription: {
    notificationAdded: {
      subscribe: async (root, args, { pubsub }) => await pubsub.subscribe('NOTIFICATION_ADDED')
    }
  }
}

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

Subscription support (with redis)

const redis = require('mqemitter-redis')
const emitter = redis({
  port: 6579,
  host: '127.0.0.1'
})

const schema = `
  type Vote {
    id: ID!
    title: String!
    ayes: Int
    noes: Int
  }

  type Query {
    votes: [Vote]
  }

  type Mutation {
    voteAye(voteId: ID!): Vote
    voteNo(voteId: ID!): Vote
  }

  type Subscription {
    voteAdded(voteId: ID!): Vote
  }
`
const votes = []
const VOTE_ADDED = 'VOTE_ADDED';

const resolvers = {
  Query: {
    votes: async () => votes
  },
  Mutation: {
    voteAye: async (_, { voteId }, { pubsub }) => {
      if (voteId <= votes.length) {
        votes[voteId - 1].ayes++;
        await pubsub.publish(
          {
            topic: `VOTE_ADDED_${voteId}`,
            payload: {
              voteAdded: votes[voteId - 1]
            }
          }
        );

        return votes[voteId - 1];
      }

      throw new Error('Invalid vote id');
    },
    voteNo: async (_, { voteId }, { pubsub }) => {
      if (voteId <= votes.length) {
        votes[voteId - 1].noes++;
        await pubsub.publish(
          {
            topic: `VOTE_ADDED_${voteId}`,
            payload: {
              voteAdded: votes[voteId - 1]
            }
          }
        );

        return votes[voteId - 1];
      }

      throw new Error('Invalid vote id');
    }
  },
  Subscription: {
    voteAdded: {
      subscribe: async (root, { voteId }, { pubsub }) => {
        // subscribe only for a vote with a given id
        return await pubsub.subscribe(`VOTE_ADDED_${voteId}`);
      }
    }
  }
};

app.register(GQL, {
  schema,
  resolvers,
  subscription: {
    emitter,
    verifyClient: (info, next) => {
      if (info.req.headers['x-fastify-header'] !== 'fastify is awesome !') {
        return next(false) // the connection is not allowed
      }
      next(true) // the connection is allowed
    }
  }
})

API

plugin options

fastify-gql supports the following options:

  • schema: String or schema definition. The graphql schema. The string will be parsed.
  • resolvers: Object. The graphql resolvers.
  • loaders: Object. See defineLoaders for more details.
  • graphiql: boolean | string. Serve GraphiQL on /graphiql if true or 'graphiql', or GraphQL IDE on /playground if 'playground' if routes is true.
  • jit: Integer. The minimum number of execution a query needs to be executed before being jit'ed.
  • routes: boolean. Serves the Default: true. A graphql endpoint is exposed at /graphql.
  • context: Function. Result of function is passed to resolvers as a custom GraphQL context. The function receives the request and reply as parameters. It is only called when routes options is true
  • prefix: String. Change the route prefix of the graphql endpoint if enabled.
  • defineMutation: Boolean. Add the empty Mutation definition if schema is not defined (Default: false).
  • errorHandler: Functionย  or boolean. Change the default error handler (Default: true). Note: If a custom error handler is defined, it should return the standardized response format according to GraphQL spec.
  • queryDepth: Integer. The maximum depth allowed for a single query.
  • subscription: Boolean | Object. Enable subscriptions. It is uses mqemitter when it is true. To use a custom emitter set the value to an object containing the emitter.
    • subscription.emitter: Custom emitter
    • subscription.verifyClient: Function A function which can be used to validate incoming connections.

queryDepth example

query {
  dogs {
    name
    owner {
      name
      pet {
        name
        owner {
          name
          pet {
            name
          }
        }
      }
    }
  }
}

A queryDepth of 6 would allow this query. 5 or less would throw with the error - unnamedQuery query exceeds the query depth limit of 5

HTTP endpoints

GET /graphql

Executed the GraphQL query passed via query string parameters. The supported query string parameters are:

  • query, the GraphQL query.
  • operationName, the operation name to execute contained in the query.
  • variables, a JSON object containing the variables for the query.

POST /graphql

Executes the GraphQL query or mutation described in the body. The payload must conform to the following JSON schema:

{
  type: 'object',
  properties: {
    query: {
      type: 'string',
      description: 'the GraphQL query'
    },
    operationName: {
      type: 'string'
    },
    variables: {
      type: ['object', 'null'],
      additionalProperties: true
    }
  }
}

GET /graphiql

Serves GraphiQL if enabled by the options.

GET /playground

Serves GraphQL IDE if enabled by the options.

decorators

fastify-gql adds the following decorators.

app.graphql(source, context, variables, operationName)

Decorate Server with a graphql method. It calls the upstream graphql() method with the defined schema, and it adds { app } to the context.

const Fastify = require('fastify')
const GQL = require('fastify-gql')

const app = Fastify()
const schema = `
  type Query {
    add(x: Int, y: Int): Int
  }
`

const resolvers = {
  Query: {
    add: async (_, { x, y }) => x + y
  }
}

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

async function run () {
  // needed so that graphql is defined
  await app.ready()

  const query = '{ add(x: 2, y: 2) }'
  const res = await app.graphql(query)

  console.log(res)
  // prints:
  //
  // {
  //   data: {
  //      add: 4
  //   }
  // }
}

run()

app.graphql.extendSchema(schema) and app.graphql.defineResolvers(resolvers)

It is possible to add schemas and resolvers in separate fastify plugins, like so:

const Fastify = require('fastify')
const GQL = require('fastify-gql')

const app = Fastify()
const schema = `
  extend type Query {
    add(x: Int, y: Int): Int
  }
`

const resolvers = {
  Query: {
    add: async (_, { x, y }) => x + y
  }
}

app.register(GQL)

app.register(async function (app) {
  app.graphql.extendSchema(schema)
  app.graphql.defineResolvers(resolvers)
})

async function run () {
  // needed so that graphql is defined
  await app.ready()

  const query = '{ add(x: 2, y: 2) }'
  const res = await app.graphql(query)

  console.log(res)
  // prints:
  //
  // {
  //   data: {
  //      add: 4
  //   }
  // }
}

run()

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:

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:

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.

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

Decorate Reply with a graphql method. It calls the upstream graphql() function with the defined schema, and it adds { app, reply } to the context.

const Fastify = require('fastify')
const GQL = require('fastify-gql')

const app = Fastify()
const schema = `
  type Query {
    add(x: Int, y: Int): Int
  }
`

const resolvers = {
  add: async ({ x, y }) => x + y
}

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

app.get('/', async function (req, reply) {
  const query = '{ add(x: 2, y: 2) }'
  return reply.graphql(query)
})

async function run () {
  const res = await app.inject({
    method: 'GET',
    url: '/'
  })

  console.log(JSON.parse(res.body), {
    data: {
      add: 4
    }
  })
}

run()

License

MIT

fastify-gql's People

Contributors

bhamon avatar cnt000 avatar frikille avatar greenkeeper[bot] avatar john-behan avatar mafintosh avatar mcollina avatar pacodu avatar skellla avatar strongermyself avatar temsa avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.