Repository: mcollina/fastify-gql
Pull request #129 (2020-03-19 15:35:56)

Support federation protocol
This PR adds enables federation support so the service can be deployed behind an Apollo Gateway.

Based on the [specification](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/) there are 4 requirements
- [x] Implementation of the federation schema specification
- [x] Support for fetching service capabilities
- [x] Implementation of stub type generation for references
- [x] Implementation of request resolving for entities

A fork of the original [federation-demo](https://github.com/apollographql/federation-demo) updated to use `fastify-gql` can be found [here](https://github.com/frikille/federation-demo/tree/fastify-example)



Patch:
diff --git a/README.md b/README.md
index 4e99fcc..4cd36ca 100644
--- a/README.md
+++ b/README.md
@@ -292,6 +292,137 @@ app.register(GQL, {
 })
 ```
 
+### Federation metadata support
+
+The signature of the method is the same as a standard resolver: `__resolveReference(source, args, context, info)` where the `source` will contain the reference object that needs to be resolved.
+
+```js
+'use strict'
+
+const Fastify = require('fastify')
+const GQL = require('fastify-gql')
+
+const users = {
+  1: {
+    id: '1',
+    name: 'John',
+    username: '@john'
+  },
+  2: {
+    id: '2',
+    name: 'Jane',
+    username: '@jane'
+  }
+}
+
+const app = 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]
+    }
+  }
+}
+
+app.register(GQL, {
+  schema,
+  resolvers,
+  federationMetadata: true
+})
+
+app.get('/', async function (req, reply) {
+  const query = '{ _service { sdl } }'
+  return app.graphql(query)
+})
+
+app.listen(3000)
+```
+
+### Federation with __resolveReference caching
+
+Just like standard resolvers, the `__resolveReference` resolver can be a performance bottleneck. To avoid this, the it is strongly recommended to define the `__resolveReference` function for an entity as a [loader](#defineLoaders).
+
+```js
+'use strict'
+
+const Fastify = require('fastify')
+const GQL = require('fastify-gql')
+
+const users = {
+  1: {
+    id: '1',
+    name: 'John',
+    username: '@john'
+  },
+  2: {
+    id: '2',
+    name: 'Jane',
+    username: '@jane'
+  }
+}
+
+const app = 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']
+    }
+  }
+}
+
+const loaders = {
+  User: {
+    async __resolveReference(queries, context) {
+      // This should be a bulk query to the database
+      return queries.map(({ obj }) => users[obj.id])
+    }
+  }
+}
+
+app.register(GQL, {
+  schema,
+  resolvers,
+  loaders,
+  federationMetadata: true
+})
+
+app.get('/', async function (req, reply) {
+  const query = '{ _service { sdl } }'
+  return app.graphql(query)
+})
+
+app.listen(3000)
+```
+
 ## API
 
 ### plugin options
@@ -321,6 +452,7 @@ __fastify-gql__ supports the following options:
 * `subscription`: Boolean | Object. Enable subscriptions. It is uses [mqemitter](https://github.com/mcollina/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.
+* `federationMetadata`: Boolean. Enable federation metadata support so the service can be deployed behind an Apollo Gateway
 
 #### queryDepth example
 ```
@@ -570,7 +702,7 @@ async function run () {
 run()
 ```
 
-<a name="loaders"></a>
+<a name="defineLoaders"></a>
 #### app.graphql.defineLoaders(loaders)
 
 A loader is an utility to avoid the 1 + N query problem of GraphQL.
diff --git a/index.d.ts b/index.d.ts
index 72b8822..6356f5d 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -121,7 +121,11 @@ declare namespace fastifyGQL {
         info: object,
         next: (result: boolean) => void
       ) => void
-    }
+    },
+    /**
+     * Enable federation metadata support so the service can be deployed behind an Apollo Gateway
+     */
+    federationMetadata?: boolean
   }
 }
 
diff --git a/index.js b/index.js
index 32033d8..c562f1f 100644
--- a/index.js
+++ b/index.js
@@ -22,6 +22,7 @@ const {
   execute
 } = require('graphql')
 const queryDepth = require('./lib/queryDepth')
+const buildFederationSchema = require('./lib/federation')
 
 const kLoaders = Symbol('fastify-gql.loaders')
 
@@ -60,7 +61,11 @@ module.exports = fp(async function (app, opts) {
   let schema = opts.schema
 
   if (typeof schema === 'string') {
-    schema = buildSchema(schema)
+    if (opts.federationMetadata) {
+      schema = buildFederationSchema(schema)
+    } else {
+      schema = buildSchema(schema)
+    }
   } else if (!opts.schema) {
     schema = new GraphQLSchema({
       query: new GraphQLObjectType({
@@ -94,7 +99,8 @@ module.exports = fp(async function (app, opts) {
       path: opts.path,
       context: opts.context,
       schema,
-      subscription: opts.subscription
+      subscription: opts.subscription,
+      federationMetadata: opts.federationMetadata
     })
   }
 
@@ -146,6 +152,8 @@ module.exports = fp(async function (app, opts) {
               ...fields[prop],
               ...resolver[prop]
             }
+          } else if (prop === '__resolveReference') {
+            type.resolveReference = resolver[prop]
           } else {
             fields[prop].resolve = resolver[prop]
           }
diff --git a/lib/federation.js b/lib/federation.js
new file mode 100644
index 0000000..38065b2
--- /dev/null
+++ b/lib/federation.js
@@ -0,0 +1,227 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2016-2020 Meteor Development Group, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+'use strict'
+
+const {
+  GraphQLSchema,
+  GraphQLObjectType,
+  Kind,
+  extendSchema,
+  parse,
+  isTypeDefinitionNode,
+  isTypeExtensionNode,
+  isObjectType
+} = require('graphql')
+
+const BASE_FEDERATION_TYPES = `
+  scalar _Any
+  scalar _FieldSet
+
+  type _Service {
+    sdl: String
+  }
+
+  directive @external on FIELD_DEFINITION
+  directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
+  directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
+  directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
+`
+
+const extensionKindToDefinitionKind = {
+  [Kind.SCALAR_TYPE_EXTENSION]: Kind.SCALAR_TYPE_DEFINITION,
+  [Kind.OBJECT_TYPE_EXTENSION]: Kind.OBJECT_TYPE_DEFINITION,
+  [Kind.INTERFACE_TYPE_EXTENSION]: Kind.INTERFACE_TYPE_DEFINITION,
+  [Kind.UNION_TYPE_EXTENSION]: Kind.UNION_TYPE_DEFINITION,
+  [Kind.ENUM_TYPE_EXTENSION]: Kind.ENUM_TYPE_DEFINITION,
+  [Kind.INPUT_OBJECT_TYPE_EXTENSION]: Kind.INPUT_OBJECT_TYPE_DEFINITION
+}
+
+function getStubTypes (schemaDefinitions) {
+  const definitionsMap = {}
+  const extensionsMap = {}
+
+  for (const definition of schemaDefinitions) {
+    const typeName = definition.name.value
+
+    if (isTypeDefinitionNode(definition)) {
+      definitionsMap[typeName] = typeName
+    } else if (isTypeExtensionNode(definition)) {
+      extensionsMap[typeName] = {
+        kind: extensionKindToDefinitionKind[definition.kind],
+        name: definition.name
+      }
+    }
+  }
+
+  return Object.keys(extensionsMap)
+    .filter(extensionTypeName => !definitionsMap[extensionTypeName])
+    .map(extensionTypeName => extensionsMap[extensionTypeName])
+}
+
+function gatherDirectives (type) {
+  let directives = []
+  for (const node of (type.extensionASTNodes || [])) {
+    /* istanbul ignore else we are not interested in nodes that does not have directives */
+    if (node.directives) {
+      directives = directives.concat(node.directives)
+    }
+  }
+
+  if (type.astNode && type.astNode.directives) {
+    directives = directives.concat(type.astNode.directives)
+  }
+
+  return directives
+}
+
+function typeIncludesDirective (type, directiveName) {
+  const directives = gatherDirectives(type)
+  return directives.some(directive => directive.name.value === directiveName)
+}
+
+function addTypeNameToResult (result, typename) {
+  /* istanbul ignore else when result is null or not an object we return original result */
+  if (result !== null && typeof result === 'object') {
+    Object.defineProperty(result, '__typename', {
+      value: typename
+    })
+  }
+  return result
+}
+
+function addEntitiesResolver (schema) {
+  const entityTypes = Object.values(schema.getTypeMap()).filter(
+    type => isObjectType(type) && typeIncludesDirective(type, 'key')
+  )
+
+  if (entityTypes.length > 0) {
+    schema = extendSchema(schema, parse(`
+      union _Entity = ${entityTypes.join(' | ')}
+
+      extend type Query {
+        _entities(representations: [_Any!]!): [_Entity]!
+      }
+    `), {
+      assumeValid: true
+    })
+
+    const query = schema.getType('Query')
+    const queryFields = query.getFields()
+    queryFields._entities = {
+      ...queryFields._entities,
+      resolve: (_source, { representations }, context, info) => {
+        return representations.map((reference) => {
+          const { __typename } = reference
+
+          const type = info.schema.getType(__typename)
+          if (!type || !isObjectType(type)) {
+            throw new Error(
+              `The _entities resolver tried to load an entity for type "${__typename}", but no object type of that name was found in the schema`
+            )
+          }
+
+          const resolveReference = type.resolveReference
+            ? type.resolveReference
+            : function defaultResolveReference () {
+              return reference
+            }
+
+          const result = resolveReference(reference, {}, context, info)
+
+          if (result && 'then' in result && typeof result.then === 'function') {
+            return result.then(x =>
+              addTypeNameToResult(x, __typename)
+            )
+          }
+
+          return addTypeNameToResult(result, __typename)
+        })
+      }
+    }
+  }
+
+  return schema
+}
+
+function addServiceResolver (schema, originalSchemaSDL) {
+  schema = extendSchema(schema, parse(`
+    extend type Query {
+      _service: _Service!
+    }
+  `), {
+    assumeValid: true
+  })
+  const query = schema.getType('Query')
+
+  const queryFields = query.getFields()
+  queryFields._service = {
+    ...queryFields._service,
+    resolve: () => ({ sdl: originalSchemaSDL })
+  }
+
+  return schema
+}
+
+function buildFederationSchema (schema) {
+  let federationSchema = new GraphQLSchema({
+    query: undefined
+  })
+
+  federationSchema = extendSchema(federationSchema, parse(BASE_FEDERATION_TYPES))
+
+  const parsedOriginalSchema = parse(schema)
+  const stubTypeDefinitions = getStubTypes(parsedOriginalSchema.definitions)
+
+  federationSchema = extendSchema(federationSchema, {
+    kind: Kind.DOCUMENT,
+    definitions: [
+      ...stubTypeDefinitions
+    ]
+  })
+
+  federationSchema = extendSchema(federationSchema, parsedOriginalSchema)
+
+  if (!federationSchema.getType('Query')) {
+    federationSchema = new GraphQLSchema({
+      ...federationSchema.toConfig(),
+      query: new GraphQLObjectType({
+        name: 'Query',
+        fields: {}
+      })
+    })
+  }
+
+  federationSchema = addEntitiesResolver(federationSchema)
+  federationSchema = addServiceResolver(federationSchema, schema)
+
+  return new GraphQLSchema({
+    ...federationSchema.toConfig(),
+    query: federationSchema.getType('Query'),
+    mutation: federationSchema.getType('Mutation'),
+    subscription: federationSchema.getType('Subscription')
+  })
+}
+
+module.exports = buildFederationSchema


Test patch:
diff --git a/tap-snapshots/test-federation.js-TAP.test.js b/tap-snapshots/test-federation.js-TAP.test.js
new file mode 100644
index 0000000..0c280d4
--- /dev/null
+++ b/tap-snapshots/test-federation.js-TAP.test.js
@@ -0,0 +1,45 @@
+/* IMPORTANT
+ * This snapshot file is auto-generated, but designed for humans.
+ * It should be checked into source control and tracked carefully.
+ * Re-generate by setting TAP_SNAPSHOT=1 and running tests.
+ * Make sure to inspect the output below.  Do not ignore changes!
+ */
+'use strict'
+exports['test/federation.js TAP buildFederationSchema function adds stub types > must match snapshot 1'] = `
+directive @external on FIELD_DEFINITION
+
+directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
+
+directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
+
+directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
+
+directive @customdir on FIELD_DEFINITION
+
+scalar _Any
+
+union _Entity = User | Product
+
+scalar _FieldSet
+
+type _Service {
+  sdl: String
+}
+
+type Product {
+  sku: String
+}
+
+type Query {
+  me: User
+  _entities(representations: [_Any!]!): [_Entity]!
+  _service: _Service!
+}
+
+type User {
+  id: ID!
+  name: String
+  username: String
+}
+
+`
diff --git a/test/federation.js b/test/federation.js
new file mode 100644
index 0000000..186438c
--- /dev/null
+++ b/test/federation.js
@@ -0,0 +1,794 @@
+'use strict'
+
+const { test } = require('tap')
+const Fastify = require('fastify')
+const { printSchema } = require('graphql')
+const WebSocket = require('ws')
+const mq = require('mqemitter')
+const GQL = require('..')
+const buildFederationSchema = require('../lib/federation')
+
+test('basic federation support', async (t) => {
+  const app = Fastify()
+  const schema = `
+    extend type Query {
+      me: User
+    }
+
+    type User @key(fields: "id") {
+      id: ID!
+      name: String
+      username: String
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      me: () => {
+        return {
+          id: '1',
+          name: 'John',
+          username: '@john'
+        }
+      }
+    },
+    User: {
+      __resolveReference: (object) => {
+        return {
+          id: object.id,
+          name: 'John',
+          username: '@john'
+        }
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    federationMetadata: true
+  })
+
+  await app.ready()
+
+  const query = '{ _service { sdl } }'
+  const res = await app.inject({
+    method: 'GET',
+    url: `/graphql?query=${query}`
+  })
+
+  t.deepEqual(JSON.parse(res.body), {
+    data: {
+      _service: {
+        sdl: schema
+      }
+    }
+  })
+})
+
+test('a normal schema can be run in federated mode', async (t) => {
+  const app = Fastify()
+  const schema = `
+    extend type Query {
+      me: User
+    }
+
+    type User {
+      id: ID!
+      name: String
+      username: String
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      me: () => {
+        return {
+          id: '1',
+          name: 'John',
+          username: '@john'
+        }
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    federationMetadata: true
+  })
+
+  await app.ready()
+
+  const query = '{ _service { sdl } }'
+  const res = await app.inject({
+    method: 'GET',
+    url: `/graphql?query=${query}`
+  })
+
+  t.deepEqual(JSON.parse(res.body), {
+    data: {
+      _service: {
+        sdl: schema
+      }
+    }
+  })
+})
+
+test('a schema can be run in federated mode when Query is not defined', async (t) => {
+  const app = Fastify()
+  const schema = `
+    type User @key(fields: "id") {
+      id: ID!
+      name: String
+      username: String
+    }
+  `
+
+  const resolvers = {
+    User: {
+      __resolveReference: (object) => {
+        return {
+          id: object.id,
+          name: 'John',
+          username: '@john'
+        }
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    federationMetadata: true
+  })
+
+  await app.ready()
+
+  const query = '{ _service { sdl } }'
+  const res = await app.inject({
+    method: 'GET',
+    url: `/graphql?query=${query}`
+  })
+
+  t.deepEqual(JSON.parse(res.body), {
+    data: {
+      _service: {
+        sdl: schema
+      }
+    }
+  })
+})
+
+test('entities resolver returns correct value', async (t) => {
+  const app = Fastify()
+  const schema = `
+    extend type Query {
+      me: User
+    }
+
+    type User @key(fields: "id") {
+      id: ID!
+      name: String
+      username: String
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      me: () => {
+        return {
+          id: '1',
+          name: 'John',
+          username: '@john'
+        }
+      }
+    },
+    User: {
+      __resolveReference: (reference) => {
+        return {
+          id: reference.id,
+          name: 'John',
+          username: '@john'
+        }
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    federationMetadata: true
+  })
+
+  await app.ready()
+
+  const query = `
+  {
+    _entities(representations: [{ __typename: "User", id: "1"}]) {
+      __typename
+      ... on User {
+        id
+        username
+        name
+      }
+    }
+  }
+  `
+  const res = await app.inject({
+    method: 'GET',
+    url: `/graphql?query=${query}`
+  })
+
+  t.deepEqual(JSON.parse(res.body), {
+    data: {
+      _entities: [{
+        __typename: 'User',
+        id: '1',
+        username: '@john',
+        name: 'John'
+      }]
+    }
+  })
+})
+
+test('entities resolver returns correct value with async resolver', async (t) => {
+  const app = Fastify()
+  const schema = `
+    extend type Query {
+      me: User
+    }
+
+    type User @key(fields: "id") {
+      id: ID!
+      name: String
+      username: String
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      me: () => {
+        return {
+          id: '1',
+          name: 'John',
+          username: '@john'
+        }
+      }
+    },
+    User: {
+      __resolveReference: (reference) => {
+        return Promise.resolve({
+          id: reference.id,
+          name: 'John',
+          username: '@john'
+        })
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    federationMetadata: true
+  })
+
+  await app.ready()
+
+  const query = `
+  {
+    _entities(representations: [{ __typename: "User", id: "1"}]) {
+      __typename
+      ... on User {
+        id
+        username
+        name
+      }
+    }
+  }
+  `
+  const res = await app.inject({
+    method: 'GET',
+    url: `/graphql?query=${query}`
+  })
+
+  t.deepEqual(JSON.parse(res.body), {
+    data: {
+      _entities: [{
+        __typename: 'User',
+        id: '1',
+        username: '@john',
+        name: 'John'
+      }]
+    }
+  })
+})
+
+test('entities resolver returns user default resolver if resolveReference is not set', async (t) => {
+  const app = Fastify()
+  const schema = `
+    extend type Query {
+      me: User
+    }
+
+    type User @key(fields: "id") {
+      id: ID!
+      name: String
+      username: String
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      me: () => {
+        return {
+          id: '1',
+          name: 'John',
+          username: '@john'
+        }
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    federationMetadata: true
+  })
+
+  await app.ready()
+
+  const query = `
+  {
+    _entities(representations: [{ __typename: "User", id: "1"}]) {
+      __typename
+      ... on User {
+        id
+        username
+        name
+      }
+    }
+  }
+  `
+  const res = await app.inject({
+    method: 'GET',
+    url: `/graphql?query=${query}`
+  })
+
+  t.deepEqual(JSON.parse(res.body), {
+    data: {
+      _entities: [{
+        __typename: 'User',
+        id: '1',
+        username: null,
+        name: null
+      }]
+    }
+  })
+})
+
+test('entities resolver throws an error if reference type name not in schema', async (t) => {
+  const app = Fastify()
+  const schema = `
+    extend type Query {
+      me: User
+    }
+
+    type User @key(fields: "id") {
+      id: ID!
+      name: String
+      username: String
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      me: () => {
+        return {
+          id: '1',
+          name: 'John',
+          username: '@john'
+        }
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    federationMetadata: true
+  })
+
+  await app.ready()
+
+  const query = `
+  {
+    _entities(representations: [{ __typename: "Account", id: "1"}]) {
+      __typename
+      ... on User {
+        id
+        username
+        name
+      }
+    }
+  }
+  `
+  const res = await app.inject({
+    method: 'GET',
+    url: `/graphql?query=${query}`
+  })
+
+  t.equal(JSON.parse(res.body).errors[0].message, 'The _entities resolver tried to load an entity for type "Account", but no object type of that name was found in the schema')
+})
+
+test('buildFederationSchema function adds stub types', async (t) => {
+  const schema = `
+    extend type Query {
+      me: User
+    }
+
+    type User @key(fields: "id") {
+      id: ID!
+      name: String
+      username: String
+    }
+
+    extend type Product @key(fields: "sku") {
+      sku: String
+    }
+
+    directive @customdir on FIELD_DEFINITION
+  `
+
+  const federationSchema = buildFederationSchema(schema)
+
+  t.matchSnapshot(printSchema(federationSchema))
+})
+
+test('mutation works with federation support', async (t) => {
+  const app = Fastify()
+  const schema = `
+    extend type Query {
+      me: User
+    }
+
+    type User @key(fields: "id") {
+      id: ID!
+      name: String
+      username: String
+    }
+
+    extend type Mutation {
+      add(a: Int, b: Int): Int
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      me: () => {
+        return {
+          id: '1',
+          name: 'John',
+          username: '@john'
+        }
+      }
+    },
+    Mutation: {
+      add: (root, { a, b }) => {
+        return a + b
+      }
+    },
+    User: {
+      __resolveReference: (object) => {
+        return {
+          id: object.id,
+          name: 'John',
+          username: '@john'
+        }
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    federationMetadata: true
+  })
+
+  await app.ready()
+
+  const query = 'mutation { add(a: 11 b: 19) }'
+  const res = await app.inject({
+    method: 'POST',
+    url: '/graphql',
+    body: {
+      query
+    }
+  })
+
+  t.deepEqual(JSON.parse(res.body), {
+    data: {
+      add: 30
+    }
+  })
+})
+
+test('subscription server sends update to subscriptions', t => {
+  const app = Fastify()
+  t.tearDown(() => app.close())
+
+  const sendTestQuery = () => {
+    app.inject({
+      method: 'POST',
+      url: '/graphql',
+      body: {
+        query: `
+          query {
+            notifications {
+              id
+              message
+            }
+          }
+        `
+      }
+    }, () => {
+      sendTestMutation()
+    })
+  }
+
+  const sendTestMutation = () => {
+    app.inject({
+      method: 'POST',
+      url: '/graphql',
+      body: {
+        query: `
+          mutation {
+            addNotification(message: "Hello World") {
+              id
+            }
+          }
+        `
+      }
+    }, () => {})
+  }
+
+  const emitter = mq()
+  const schema = `
+    type Notification @key(fields: "id") {
+      id: ID!
+      message: String
+    }
+
+    extend type Query {
+      notifications: [Notification]
+    }
+
+    extend type Mutation {
+      addNotification(message: String): Notification
+    }
+
+    extend type Subscription {
+      notificationAdded: Notification
+    }
+  `
+
+  let idCount = 1
+  const notifications = [{
+    id: idCount,
+    message: 'Notification message'
+  }]
+
+  const resolvers = {
+    Query: {
+      notifications: () => notifications
+    },
+    Mutation: {
+      addNotification: async (_, { message }) => {
+        const id = idCount++
+        const notification = {
+          id,
+          message
+        }
+        notifications.push(notification)
+        await emitter.emit({
+          topic: 'NOTIFICATION_ADDED',
+          payload: {
+            notificationAdded: notification
+          }
+        })
+
+        return notification
+      }
+    },
+    Subscription: {
+      notificationAdded: {
+        subscribe: (root, args, { pubsub }) => pubsub.subscribe('NOTIFICATION_ADDED')
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    subscription: {
+      emitter
+    },
+    federationMetadata: true
+  })
+
+  app.listen(0, err => {
+    t.error(err)
+
+    const ws = new WebSocket('ws://localhost:' + (app.server.address()).port + '/graphql', 'graphql-ws')
+    const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8', objectMode: true })
+    t.tearDown(client.destroy.bind(client))
+    client.setEncoding('utf8')
+
+    client.write(JSON.stringify({
+      type: 'connection_init'
+    }))
+
+    client.write(JSON.stringify({
+      id: 1,
+      type: 'start',
+      payload: {
+        query: `
+          subscription {
+            notificationAdded {
+              id
+              message
+            }
+          }
+        `
+      }
+    }))
+
+    client.write(JSON.stringify({
+      id: 2,
+      type: 'start',
+      payload: {
+        query: `
+          subscription {
+            notificationAdded {
+              id
+              message
+            }
+          }
+        `
+      }
+    }))
+
+    client.write(JSON.stringify({
+      id: 2,
+      type: 'stop'
+    }))
+
+    client.on('data', chunk => {
+      const data = JSON.parse(chunk)
+
+      if (data.id === 1 && data.type === 'data') {
+        t.equal(chunk, JSON.stringify({
+          type: 'data',
+          id: 1,
+          payload: {
+            data: {
+              notificationAdded: {
+                id: '1',
+                message: 'Hello World'
+              }
+            }
+          }
+        }))
+
+        client.end()
+        t.end()
+      } else if (data.id === 2 && data.type === 'complete') {
+        sendTestQuery()
+      }
+    })
+  })
+})
+
+test('federation supports loader for __resolveReference function', async (t) => {
+  const app = Fastify()
+  const users = {
+    1: {
+      id: 1,
+      name: 'John',
+      username: '@john'
+    },
+    2: {
+      id: 2,
+      name: 'Jane',
+      username: '@jane'
+    }
+  }
+  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']
+      }
+    }
+  }
+
+  const loaders = {
+    User: {
+      async __resolveReference (queries, { reply }) {
+        t.deepEqual(queries, [{
+          obj: {
+            __typename: 'User',
+            id: '1'
+          },
+          params: {}
+        }, {
+          obj: {
+            __typename: 'User',
+            id: '2'
+          },
+          params: {}
+        }])
+        return queries.map(({ obj }) => users[obj.id])
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    loaders,
+    federationMetadata: true
+  })
+
+  await app.ready()
+
+  const query = `
+  {
+    _entities(representations: [{ __typename: "User", id: "1" }, { __typename: "User", id: "2" }, { __typename: "User", id: "1" }]) {
+      ... on User {
+        id
+        username
+        name
+      }
+    }
+  }
+  `
+  const res = await app.inject({
+    method: 'POST',
+    url: '/graphql',
+    body: {
+      query
+    }
+  })
+
+  t.deepEqual(JSON.parse(res.body), {
+    data: {
+      _entities: [{
+        id: '1',
+        username: '@john',
+        name: 'John'
+      },
+      {
+        id: '2',
+        username: '@jane',
+        name: 'Jane'
+      },
+      {
+        id: '1',
+        username: '@john',
+        name: 'John'
+      }]
+    }
+  })
+})