Skip to content

Latest commit

 

History

History
463 lines (376 loc) · 11.2 KB

federation.md

File metadata and controls

463 lines (376 loc) · 11.2 KB

mercurius

Federation

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.

'use strict'

const Fastify = require('fastify')
const mercurius = require('mercurius')

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(mercurius, {
  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.

'use strict'

const Fastify = require('fastify')
const mercurius = require('mercurius')

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(mercurius, {
  schema,
  resolvers,
  loaders,
  federationMetadata: true
})

app.get('/', async function (req, reply) {
  const query = '{ _service { sdl } }'
  return app.graphql(query)
})

app.listen(3000)

Use GraphQL server as a Gateway for federated schemas

A GraphQL server can act as a Gateway that composes the schemas of the underlying services into one federated schema and executes queries across the services. Every underlying service must be a GraphQL server that supports the federation.

In Gateway mode the following options are not allowed (the plugin will throw an error if any of them are defined):

  • schema
  • resolvers
  • loaders

Also, using the following decorator methods will throw:

  • app.graphql.defineResolvers
  • app.graphql.defineLoaders
  • app.graphql.extendSchema
const gateway = Fastify()
const mercurius = require('mercurius')

gateway.register(mercurius, {
  gateway: {
    services: [
      {
        name: 'user',
        url: 'http://localhost:4001/graphql',
        rewriteHeaders: (headers, context) => {
          if (headers.authorization) {
            return {
              authorization: headers.authorization
            }
          }

          return {
            'x-api-key': 'secret-api-key'
          }
        },
        setResponseHeaders: (reply) => {
          reply.header('set-cookie', 'sessionId=12345')
        }
      },
      {
        name: 'post',
        url: 'http://localhost:4002/graphql'
      }
    ]
  }
})

await gateway.listen(4000)

Periodically refresh federated schemas in Gateway mode

The Gateway service can obtain new versions of federated schemas automatically within a defined polling interval using the following configuration:

  • gateway.pollingInterval defines the interval (in milliseconds) the gateway should use in order to look for schema changes from the federated services. If the received schema is unchanged, the previously cached version will be reused.
const gateway = Fastify()
const mercurius = require('mercurius')

gateway.register(mercurius, {
  gateway: {
    services: [
      {
        name: 'user',
        url: `http://localhost:3000/graphql`
      }
    ],
    pollingInterval: 2000
  }
})

gateway.listen(3001)

Programmatically refresh federated schemas in Gateway mode

The service acting as the Gateway can manually trigger re-fetching the federated schemas programmatically by calling the application.graphql.gateway.refresh() method. The method either returns the newly generated schema or null if no changes have been discovered.

const Fastify = require('fastify')
const mercurius = require('mercurius')

const server = Fastify()

server.register(mercurius, {
  graphiql: true,
  gateway: {
    services: [
      {
        name: 'user',
        url: 'http://localhost:3000/graphql'
      },
      {
        name: 'company',
        url: 'http://localhost:3001/graphql'
      }
    ]
  }
})

server.listen(3002)

setTimeout(async () => {
  const schema = await server.graphql.gateway.refresh()

  if (schema !== null) {
    server.graphql.replaceSchema(schema)
  }
}, 10000)

Using Gateway mode with a schema registry

The service acting as the Gateway can use supplied schema definitions instead of relying on the gateway to query each service. These can be updated using application.graphql.gateway.serviceMap.serviceName.setSchema() and then refreshing and replacing the schema.

const Fastify = require('fastify')
const mercurius = require('mercurius')

const server = Fastify()

server.register(mercurius, {
  graphiql: true,
  gateway: {
    services: [
      {
        name: 'user',
        url: 'http://localhost:3000/graphql',
        schema: `
          extend type Query {
            me: User
          }

          type User @key(fields: "id") {
            id: ID!
            name: String
          }
        `
      },
      {
        name: 'company',
        url: 'http://localhost:3001/graphql',
        schema: `
          extend type Query {
            company: Company
          }

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

await server.listen(3002)

server.graphql.gateway.serviceMap.user.setSchema(`
  extend type Query {
    me: User
  }

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

const schema = await server.graphql.gateway.refresh()

if (schema !== null) {
  server.graphql.replaceSchema(schema)
}

Flag service as mandatory in Gateway mode

Gateway service can handle federated services in 2 different modes, mandatory or not by utilizing the gateway.services.mandatory configuration flag. If a service is not mandatory, creating the federated schema will succeed even if the service isn't capable of delivering a schema. By default, services are not mandatory. Note: At least 1 service is necessary to create a valid federated schema.

const Fastify = require('fastify')
const mercurius = require('mercurius')

const server = Fastify()

server.register(mercurius, {
  graphiql: true,
  gateway: {
    services: [
      {
        name: 'user',
        url: 'http://localhost:3000/graphql',
        mandatory: true
      },
      {
        name: 'company',
        url: 'http://localhost:3001/graphql'
      }
    ]
  },
  pollingInterval: 2000
})

server.listen(3002)

Batched Queries to services

To fully leverage the DataLoader pattern we can tell the Gateway which of its services support batched queries.
In this case the service will receive a request body with an array of queries to execute.
Enabling batched queries for a service that doesn't support it will generate errors.

const Fastify = require('fastify')
const mercurius = require('mercurius')

const server = Fastify()

server.register(mercurius, {
  graphiql: true,
  gateway: {
    services: [
      {
        name: 'user',
        url: 'http://localhost:3000/graphql'  
        allowBatchedQueries: true             
      },
      {
        name: 'company',
        url: 'http://localhost:3001/graphql', 
        allowBatchedQueries: false            
      }
    ]
  },
  pollingInterval: 2000
})

server.listen(3002)

Using a custom errorHandler for handling downstream service errors in Gateway mode

Service which uses Gateway mode can process different types of issues that can be obtained from remote services (for example, Network Error, Downstream Error, etc.). A developer can provide a function (gateway.errorHandler) that can process these errors.

const Fastify = require('fastify')
const mercurius = require('mercurius')

const server = Fastify()

server.register(mercurius, {
  graphiql: true,
  gateway: {
    services: [
      {
        name: 'user',
        url: 'http://localhost:3000/graphql',
        mandatory: true
      },
      {
        name: 'company',
        url: 'http://localhost:3001/graphql'
      }
    ],
    errorHandler: (error, service) => {
      if (service.mandatory) {
        server.log.error(error)
      }
    },
  },
  pollingInterval: 2000
})

server.listen(3002)

Note: The default behavior of errorHandler is call errorFormatter to send the result. When is provided an errorHandler make sure to call errorFormatter manually if needed.

Securely parse service responses in Gateway mode

Gateway service responses can be securely parsed using the useSecureParse flag. By default, the target service is considered trusted and thus this flag is set to false. If there is a need to securely parse the JSON response from a service, this flag can be set to true and it will use the secure-json-parse library.

const Fastify = require('fastify')
const mercurius = require('mercurius')

const server = Fastify()

server.register(mercurius, {
  graphiql: true,
  gateway: {
    services: [
      {
        name: 'user',
        url: 'http://localhost:3000/graphql',
        useSecureParse: true
      },
      {
        name: 'company',
        url: 'http://localhost:3001/graphql'
      }
    ]
  },
  pollingInterval: 2000
})

server.listen(3002)