mercurius-js / mercurius-gateway

Mercurius federation support plugin
MIT License
16 stars 11 forks source link
hacktoberfest

@mercurius/gateway

A module to create an Apollo Federation v1 gateway with mercurius.

Quick start

npm i fastify @mercuriusjs/federation @mercuriusjs/gateway

Create the user service

'use strict'

const Fastify = require('fastify')
const { mercuriusFederationPlugin } = require('@mercuriusjs/federation')

const users = {
  1: {
    id: '1',
    name: 'John',
    username: '@john'
  },
  2: {
    id: '2',
    name: 'Jane',
    username: '@jane'
  }
}

const service = Fastify()
const schema = `
  extend type Query {
    me: User
  }

  type User @key(fields: "id") {
    id: ID!
    name: String
    username: String
  }
`

const resolvers = {
  Query: {
    me: () => {
      return users['1']
    }
  },
  User: {
    __resolveReference: (source, args, context, info) => {
      return users[source.id]
    }
  }
}

service.register(mercuriusFederationPlugin, {
  schema,
  resolvers
})

service.listen({ port: 4001 })

Create the post service

'use strict'

const Fastify = require('fastify')
const { mercuriusFederationPlugin } = require('@mercuriusjs/federation')

const posts = {
  p1: {
    pid: 'p1',
    title: 'Post 1',
    content: 'Content 1',
    authorId: 'u1'
  },
  p2: {
    pid: 'p2',
    title: 'Post 2',
    content: 'Content 2',
    authorId: 'u2'
  }
}

const service = Fastify()
const schema = `
  extend type Query {
    topPosts(count: Int): [Post]
  }

  type Post @key(fields: "pid") {
    pid: ID!
    title: String
    content: String
    author: User @requires(fields: "pid title")
  }
`

const resolvers = {
  Query: {
    topPosts: (root, { count = 2 }) => Object.values(posts).slice(0, count)
  },
  Post: {
    __resolveReference: post => {
      return posts[post.pid]
    },
    author: post => {
      return {
        __typename: 'User',
        id: post.authorId
      }
    }
  }
}

service.register(mercuriusFederationPlugin, {
  schema,
  resolvers
})

service.listen({ port: 4002 })

Create the gateway

'use strict'

const Fastify = require('fastify')
const mercuriusGateway = require('@mercuriusjs/gateway')

const gateway = Fastify()
gateway.register(mercuriusGateway, {
  gateway: {
    services: [
      {
        name: 'user',
        url: 'http://localhost:4001/graphql'
      },
      {
        name: 'post',
        url: 'http://localhost:4002/graphql'
      }
    ]
  }
})

gateway.listen({ port: 3000 })

API

mercuriusGateway

Register the gateway in fastify.

const mercuriusGateway = require('@mercurius/gateway')

const schema = ...
const resolvers = ...
const app = Fastify()

app.register(mercuriusGateway, {
  gateway: [
    services: [
      {
        name: 'user',
        url: 'http://localhost:4001/graphql'
      },
      {
        name: 'post',
        url: 'http://localhost:4002/graphql'
      }
    ]
  }

})

options

Hooks

Hooks are registered with the fastify.graphqlGateway.addHook method and allow you to listen to specific events in the GraphQL request/response lifecycle. You have to register a hook before the event is triggered, otherwise the event is lost.

By using hooks you can interact directly with the GraphQL lifecycle of Mercurius gateway. There are GraphQL Request and Subscription hooks:

Notice: these hooks are only supported with async/await or returning a Promise.

Lifecycle

The schema of the internal lifecycle of Mercurius gateway.

On the right branch of every section there is the next phase of the lifecycle, on the left branch there is the corresponding GraphQL error(s) that will be generated if the parent throws an error (note that all the errors are automatically handled by Mercurius).

Gateway lifecycle

How the gateway lifecycle works integrated with the Mercurius lifecycle.

Incoming GraphQL Request
  │
  └─▶ Routing
           │
  errors ◀─┴─▶ preParsing Hook
                  │
         errors ◀─┴─▶ Parsing
                        │
               errors ◀─┴─▶ preValidation Hook
                               │
                      errors ◀─┴─▶ Validation
                                     │
                            errors ◀─┴─▶ preExecution Hook
                                            │
                                   errors ◀─┴─▶ Execution
                                                  │
                                                  └─▶ preGatewayExecution Hook(s) (appends errors only)
                                                         │
                                                errors ◀─┴─▶ GatewayExecution(s)
                                                               │
                                                      errors ◀─┴─▶ Resolution (once all GatewayExecutions have finished)
                                                                     │
                                                                     └─▶ onResolution Hook

Gateway subscription lifecycle

How the gateway subscription lifecycle works integrated with the Mercurius lifecycle.

Incoming GraphQL Websocket subscription data
  │
  └─▶ Routing
           │
  errors ◀─┴─▶ preSubscriptionParsing Hook
                  │
         errors ◀─┴─▶ Subscription Parsing
                        │
               errors ◀─┴─▶ preSubscriptionExecution Hook
                              │
                     errors ◀─┴─▶ Subscription Execution
                                    │
                                    │
                                    └─▶ preGatewaySubscriptionExecution Hook(s)
                                            │
                                   errors ◀─┴─▶ Gateway Subscription Execution(s)
                                                  │
                                      wait for subscription data
                                                  │
                   subscription closed on error ◀─┴─▶ Subscription Resolution (when subscription data is received)
                                                        │
                                                        └─▶ onSubscriptionResolution Hook
                                                              │
                                              keeping processing until subscription ended
                                                              │
                               subscription closed on error ◀─┴─▶ Subscription End (when subscription stop is received)
                                                                    │
                                                                    └─▶ onSubscriptionEnd Hook

GraphQL Request Hooks

It is pretty easy to understand where each hook is executed by looking at the lifecycle definition.

preGatewayExecution

In the preGatewayExecution hook, you can modify the following items by returning them in the hook definition:

This hook will only be triggered in gateway mode. When in gateway mode, each hook definition will trigger multiple times in a single request just before executing remote GraphQL queries on the federated services.

Note, this hook contains service metadata in the service parameter:

fastify.graphqlGateway.addHook('preGatewayExecution', async (schema, document, context, service) => {
  const { modifiedDocument, errors } = await asyncMethod(document)

  return {
    document: modifiedDocument,
    errors
  }
})

Manage Errors from a request hook

If you get an error during the execution of your hook, you can just throw an error. The preGatewayExecution hook, which will continue execution of the rest of the query and append the error to the errors array in the response.

fastify.graphqlGateway.addHook('preGatewayExecution', async (schema, document, context, service) => {
  throw new Error('Some error')
})

Add errors to the GraphQL response from a hook

The preGatewayExecution hook support adding errors to the GraphQL response.

fastify.graphqlGateway.addHook('preGatewayExecution', async (schema, document, context) => {
  return {
    errors: [new Error('foo')]
  }
})

Note, the original query will still execute. Adding the above will result in the following response:

{
  "data": {
    "foo": "bar"
  },
  "errors": [
    {
      "message": "foo"
    }
  ]
}

GraphQL Subscription Hooks

It is pretty easy to understand where each hook is executed by looking at the lifecycle definition.

preGatewaySubscriptionExecution

This hook will only be triggered in gateway mode. When in gateway mode, each hook definition will trigger when creating a subscription with a federated service.

Note, this hook contains service metadata in the service parameter:

fastify.graphqlGateway.addHook('preGatewaySubscriptionExecution', async (schema, document, context, service) => {
  await asyncMethod()
})

Manage Errors from a subscription hook

If you get an error during the execution of your subscription hook, you can just throw an error and Mercurius will send the appropriate errors to the user along the websocket.`

fastify.graphqlGateway.addHook('preSubscriptionParsing', async (schema, source, context) => {
  throw new Error('Some error')
})

GraphQL Application lifecycle Hooks

There is one hook that you can use in a GraphQL application.

onGatewayReplaceSchema

When the Gateway service obtains new versions of federated schemas within a defined polling interval, the onGatewayReplaceSchema hook will be triggered every time a new schema is built. It is called just before the old schema is replaced with the new one.

It has the following parameters:

fastify.graphqlGateway.addHook('onGatewayReplaceSchema', async (instance, schema) => {
  await someSchemaTraversalFn()
})

If this hook throws, the error will be caught and logged using the FastifyInstance logger. Subsequent onGatewayReplaceSchema hooks that are registered will not be run for this interval.

Collectors

Collectors gather additional information about the response from the services that are part of the gateway and adds them to the context.collectors object.

Depending on the configuration it may have the following fields:

Each collector stores data in the same format:

  {
    "foo": { // query id
      "service": "bar", // name of the service
      "data": {
         "foo": "bar" // example: statusCode: 500
      }
    }
  }

It is possible to access and manipulate context.collectors via onResolution Hook

  app.graphql.addHook('onResolution', async function (execution, context) {
    console.log(context.collectors)

    // {
    //   statusCodes: {
    //     topPosts: {
    //       service: 'post',
    //       data: {
    //         statusCode: 500
    //       }
    //     },
    //     me: {
    //       service: 'user',
    //       data: {
    //         statusCode: 404
    //       }
    //     }
    //   },
    //   responseHeaders: {...},
    //   extensions: {...},
    // }

    execution.collectors = context.collectors // add collectors to response
  })