Repository: mercurius-js/mercurius
Pull request #322 (2020-10-17 07:13:38)

Add support for simplified api for custom directives
This is my second take on simpler api for custom directives.

aldis-ameriks: There seem to be pollingInterval tests failing on ci. Unable to reproduce locally.
aldis-ameriks: I noticed there's already a PR open adding the functionality, #321.
mcollina: Would you mind to meld some of the changes of https://github.com/mercurius-js/mercurius/pull/321 in this PR?

Specifically it is missing:

* [ ] docs
* [ ] `app.graphql.transformSchema()` https://github.com/mercurius-js/mercurius/pull/321/files#diff-e727e4bdf3657fd1d798edcd6b099d6e092f8573cba266154583a746bba0f346R347
* [ ] a test that shows that these new options are not really needed after all https://github.com/mercurius-js/mercurius/pull/321/files#diff-7e68be9d56871ccf792c451c73d81f7a8688e3b43e5ae1688252df9b02bbdbe1R100-R105.
aldis-ameriks: Should I keep the tests that use `graphql-constraint-directive`? Internally, the library does the same as what I included in tests with `length` directive.

> a test that shows that these new options are not really needed after all https://github.com/mercurius-js/mercurius/pull/321/files#diff-7e68be9d56871ccf792c451c73d81f7a8688e3b43e5ae1688252df9b02bbdbe1R100-R105.
Could you elaborate what you mean by that? Unless I misunderstand, you had already included tests that demonstrate directive usage without using those utility functions.
https://github.com/mercurius-js/mercurius/pull/321/files#diff-7e68be9d56871ccf792c451c73d81f7a8688e3b43e5ae1688252df9b02bbdbe1R216-R221
mcollina: > Should I keep the tests that use graphql-constraint-directive? Internally, the library does the same as what I included in tests with length directive.

No, please use your version, it's better.

Patch:
diff --git a/docs/api/options.md b/docs/api/options.md
index a5c51ea..980c344 100644
--- a/docs/api/options.md
+++ b/docs/api/options.md
@@ -15,6 +15,7 @@
       - [app.graphql.extendSchema(schema) and app.graphql.defineResolvers(resolvers)](#appgraphqlextendschemaschema-and-appgraphqldefineresolversresolvers)
       - [app.graphql.replaceSchema(schema)](#appgraphqlreplaceschemaschema)
       - [app.graphql.schema](#appgraphqlschema)
+      - [app.graphql.transformSchema(transforms)](#appgraphqltransformschematransforms)
       - [app.graphql.defineLoaders(loaders)](#appgraphqldefineloadersloaders)
       - [reply.graphql(source, context, variables, operationName)](#replygraphqlsource-context-variables-operationname)
 
@@ -24,12 +25,13 @@
 
 **mercurius** supports the following options:
 
-- `schema`: String or [schema
+- `schema`: String, String[] or [schema
   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.
+- `schemaTransforms`: Array of schema-transformation functions. Accept a schema as an argument and return a schema.
 - `graphiql`: boolean | string. Serve
   [GraphiQL](https://www.npmjs.com/package/graphiql) on `/graphiql` if `true` or `'graphiql'`, or
   [GraphQL IDE](https://www.npmjs.com/package/graphql-playground-react) on `/playground` if `'playground'`
@@ -334,6 +336,17 @@ async function run() {
 run()
 ```
 
+#### app.graphql.transformSchema(transforms)
+
+`transforms` can be an array of functions or a single function that accept the schema and returns a schema.
+It is an utility function that calls `replaceSchema` underneath.
+
+```js
+app.graphql.extendSchema(typeDefs)
+app.graphql.defineResolvers(resolvers)
+app.graphql.transformSchema(directive()) // or [directive()]
+```
+
 #### app.graphql.schema
 
 Provides access to the built `GraphQLSchema` object that `mercurius` will use to execute queries. This property will reflect any updates made by `extendSchema` or `replaceSchema` as well.
diff --git a/index.d.ts b/index.d.ts
index 50464b7..7989705 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -8,7 +8,6 @@ import {
   DocumentNode,
   ExecutionResult,
   GraphQLSchema,
-  GraphQLError,
   Source,
   GraphQLResolveInfo,
   GraphQLIsTypeOfFn,
@@ -86,7 +85,7 @@ export interface MercuriusSchemaOptions {
   /**
    * The GraphQL schema. String schema will be parsed
    */
-  schema: GraphQLSchema | string;
+  schema: GraphQLSchema | string | string[];
   /**
    * Object with resolver functions
    */
@@ -107,6 +106,10 @@ export interface MercuriusSchemaOptions {
       ) => any;
     };
   };
+  /**
+   * Schema transformation function or an array of schema transformation functions
+   */
+  schemaTransforms?: ((originalSchema: GraphQLSchema) => GraphQLSchema) | Array<(originalSchema: GraphQLSchema) => GraphQLSchema>;
 }
 
 export interface MercuriusCommonOptions {
diff --git a/index.js b/index.js
index 7317a35..2043157 100644
--- a/index.js
+++ b/index.js
@@ -126,6 +126,10 @@ const plugin = fp(async function (app, opts) {
     throw new Error('Adding "schema", "resolvers" or "loaders" to plugin options when plugin is running in gateway mode is not allowed')
   }
 
+  if (Array.isArray(schema)) {
+    schema = schema.join('\n')
+  }
+
   if (typeof schema === 'string') {
     if (opts.federationMetadata) {
       schema = buildFederationSchema(schema)
@@ -340,6 +344,16 @@ const plugin = fp(async function (app, opts) {
     fastifyGraphQl.defineResolvers(resolvers)
   }
 
+  fastifyGraphQl.transformSchema = function (schemaTransforms) {
+    if (!Array.isArray(schemaTransforms)) {
+      schemaTransforms = [schemaTransforms]
+    }
+
+    for (const transformFn of schemaTransforms) {
+      fastifyGraphQl.replaceSchema(transformFn(fastifyGraphQl.schema))
+    }
+  }
+
   if (opts.resolvers) {
     fastifyGraphQl.defineResolvers(opts.resolvers)
   }
@@ -348,6 +362,10 @@ const plugin = fp(async function (app, opts) {
     fastifyGraphQl.defineLoaders(opts.loaders)
   }
 
+  if (opts.schemaTransforms) {
+    fastifyGraphQl.transformSchema(opts.schemaTransforms)
+  }
+
   async function fastifyGraphQl (source, context, variables, operationName) {
     context = Object.assign({ app: this, lruGatewayResolvers }, context)
     const reply = context.reply


Test patch:
diff --git a/test/directives.js b/test/directives.js
new file mode 100644
index 0000000..f990757
--- /dev/null
+++ b/test/directives.js
@@ -0,0 +1,635 @@
+'use strict'
+
+const { test } = require('tap')
+const Fastify = require('fastify')
+const mercurius = require('..')
+const { defaultFieldResolver, GraphQLScalarType, isNonNullType, isScalarType } = require('graphql')
+const {
+  MapperKind,
+  mapSchema,
+  getDirectives,
+  makeExecutableSchema,
+  printSchemaWithDirectives,
+  getResolversFromSchema,
+  mergeResolvers
+} = require('graphql-tools')
+const buildFederationSchema = require('../lib/federation')
+
+class ValidationError extends Error {
+  constructor (message, extensions) {
+    super(message)
+    this.extensions = extensions
+    this.message = message
+  }
+}
+
+class LimitedLengthType extends GraphQLScalarType {
+  constructor (type, maxLength) {
+    super({
+      name: `${type.name}WithLengthAtMost${maxLength.toString()}`,
+      serialize (value) {
+        const newValue = type.serialize(value)
+        if (newValue.length > maxLength) {
+          throw new ValidationError(`expected length ${newValue.length} to be at most ${maxLength}`, { foo: 'bar' })
+        }
+        return newValue
+      },
+      parseValue (value) {
+        const newValue = type.parseValue(value)
+        if (newValue.length > maxLength) {
+          throw new ValidationError(`expected length ${newValue.length} to be at most ${maxLength}`, { foo: 'bar' })
+        }
+        return newValue
+      },
+      parseLiteral (ast) {
+        const newValue = type.parseLiteral(ast, {})
+        if (newValue.length > maxLength) {
+          throw new ValidationError(`expected length ${newValue.length} to be at most ${maxLength}`, { foo: 'bar' })
+        }
+        return newValue
+      }
+    })
+  }
+}
+
+function getLimitedLengthType (type, maxLength) {
+  const limitedLengthTypes = {}
+  const limitedLengthTypesByTypeName = limitedLengthTypes[type.name]
+  if (!limitedLengthTypesByTypeName) {
+    const newType = new LimitedLengthType(type, maxLength)
+    limitedLengthTypes[type.name] = {}
+    limitedLengthTypes[type.name][maxLength] = newType
+    return newType
+  }
+  const limitedLengthType = limitedLengthTypesByTypeName[maxLength]
+  if (!limitedLengthType) {
+    const newType = new LimitedLengthType(type, maxLength)
+    limitedLengthTypesByTypeName[maxLength] = newType
+    return newType
+  }
+  return limitedLengthType
+}
+
+function wrapType (fieldConfig, directiveArgumentMap) {
+  if (isNonNullType(fieldConfig.type) && isScalarType(fieldConfig.type.ofType)) {
+    fieldConfig.type = getLimitedLengthType(fieldConfig.type.ofType, directiveArgumentMap.max)
+  } else if (isScalarType(fieldConfig.type)) {
+    fieldConfig.type = getLimitedLengthType(fieldConfig.type, directiveArgumentMap.max)
+  } else {
+    throw new Error(`Not a scalar type: ${fieldConfig.type.toString()}`)
+  }
+}
+
+const lengthDirectiveTypeDefs = 'directive @length(max: Int) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION'
+function lengthDirectiveTransformer (schema) {
+  return mapSchema(schema, {
+    [MapperKind.FIELD]: (fieldConfig) => {
+      const directives = getDirectives(schema, fieldConfig)
+      const directiveArgumentMap = directives.length
+      if (directiveArgumentMap) {
+        wrapType(fieldConfig, directiveArgumentMap)
+        return fieldConfig
+      }
+    }
+  })
+}
+
+const upperDirectiveTypeDefs = 'directive @upper on FIELD_DEFINITION'
+function upperDirectiveTransformer (schema) {
+  return mapSchema(schema, {
+    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
+      const directives = getDirectives(schema, fieldConfig)
+      if (directives.upper) {
+        const { resolve = defaultFieldResolver } = fieldConfig
+        fieldConfig.resolve = async function (source, args, context, info) {
+          const result = await resolve(source, args, context, info)
+          if (typeof result === 'string') {
+            return result.toUpperCase()
+          }
+          return result
+        }
+        return fieldConfig
+      }
+    }
+  })
+}
+
+test('custom directives should work', async (t) => {
+  const app = Fastify()
+  const schema = `
+    ${upperDirectiveTypeDefs}
+    
+    type Query {
+      foo: String @upper
+      user: User
+    }
+    
+    type User {
+      id: ID!
+      name: String @upper
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      foo: () => 'bar',
+      user: () => ({ id: '1' })
+    }
+  }
+
+  const loaders = {
+    User: {
+      name: async (queries) => queries.map(() => 'name')
+    }
+  }
+
+  app.register(mercurius, {
+    schema,
+    resolvers,
+    loaders,
+    schemaTransforms: [upperDirectiveTransformer]
+  })
+
+  await app.ready()
+
+  let query = 'query { foo }'
+  let res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), { data: { foo: 'BAR' } })
+
+  query = 'query { user { id name } }'
+  res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), { data: { user: { id: '1', name: 'NAME' } } })
+})
+
+test('custom directives should work with single transform function', async (t) => {
+  const app = Fastify()
+  const schema = `
+    ${upperDirectiveTypeDefs}
+    
+    type Query {
+      foo: String @upper
+      user: User
+    }
+    
+    type User {
+      id: ID!
+      name: String @upper
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      foo: () => 'bar',
+      user: () => ({ id: '1' })
+    }
+  }
+
+  const loaders = {
+    User: {
+      name: async (queries) => queries.map(() => 'name')
+    }
+  }
+
+  app.register(mercurius, {
+    schema,
+    resolvers,
+    loaders,
+    schemaTransforms: upperDirectiveTransformer
+  })
+
+  await app.ready()
+
+  let query = 'query { foo }'
+  let res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), { data: { foo: 'BAR' } })
+
+  query = 'query { user { id name } }'
+  res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), { data: { user: { id: '1', name: 'NAME' } } })
+})
+
+test('custom directives should work with executable schema', async (t) => {
+  const app = Fastify()
+  const schema = `
+    ${upperDirectiveTypeDefs}
+    
+    type Query {
+      foo: String @upper
+      user: User
+    }
+    
+    type User {
+      id: ID!
+      name: String @upper
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      foo: () => 'bar',
+      user: () => ({ id: '1' })
+    }
+  }
+
+  const loaders = {
+    User: {
+      name: async (queries) => queries.map(() => 'name')
+    }
+  }
+
+  const executableSchema = makeExecutableSchema({
+    typeDefs: schema,
+    resolvers
+  })
+
+  app.register(mercurius, {
+    schema: executableSchema,
+    loaders,
+    schemaTransforms: [upperDirectiveTransformer]
+  })
+
+  await app.ready()
+
+  let query = 'query { foo }'
+  let res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), { data: { foo: 'BAR' } })
+
+  query = 'query { user { id name } }'
+  res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), { data: { user: { id: '1', name: 'NAME' } } })
+})
+
+test('directives with extendSchema', async (t) => {
+  const app = Fastify()
+
+  const todos = []
+  const typeDefs = `
+   type AddTodoResponse {
+     todos: [String]
+   }
+   extend type Query {
+     todos: [String]
+   }
+   extend type Mutation {
+     addTodo (input: TodoInput): AddTodoResponse
+   }
+   input TodoInput {
+     todo: String! @length(max: 3)
+   }
+ `
+
+  const resolvers = {
+    Query: {
+      todos: async () => todos
+    },
+    Mutation: {
+      addTodo: async (_, { input: { todo } }) => {
+        todos.push(todo)
+        return { todos }
+      }
+    }
+  }
+
+  await app.register(mercurius, { defineMutation: true })
+
+  app.graphql.extendSchema(lengthDirectiveTypeDefs)
+  app.graphql.extendSchema(typeDefs)
+  app.graphql.defineResolvers(resolvers)
+  app.graphql.schema = lengthDirectiveTransformer(app.graphql.schema)
+
+  const query = 'mutation { addTodo(input: { todo: "too-long" }) { todos } }'
+
+  const res = await app.inject({
+    method: 'POST',
+    url: '/graphql',
+    body: {
+      query
+    }
+  })
+
+  t.equal(res.statusCode, 400)
+  t.deepEqual(JSON.parse(res.body), {
+    data: null,
+    errors: [{
+      message: 'Expected value of type "StringWithLengthAtMost3", found "too-long"; expected length 8 to be at most 3',
+      locations: [{ line: 1, column: 35 }],
+      extensions: { foo: 'bar' }
+    }]
+  })
+})
+
+test('directives with transformSchema', async (t) => {
+  const app = Fastify()
+
+  const todos = []
+  const typeDefs = `
+   type AddTodoResponse {
+     todos: [String]
+   }
+   extend type Query {
+     todos: [String]
+   }
+   extend type Mutation {
+     addTodo (input: TodoInput): AddTodoResponse
+   }
+   input TodoInput {
+     todo: String! @length(max: 3)
+   }
+ `
+
+  const resolvers = {
+    Query: {
+      todos: async () => todos
+    },
+    Mutation: {
+      addTodo: async (_, { input: { todo } }) => {
+        todos.push(todo)
+        return { todos }
+      }
+    }
+  }
+
+  await app.register(mercurius, { defineMutation: true })
+
+  app.graphql.extendSchema(lengthDirectiveTypeDefs)
+  app.graphql.extendSchema(typeDefs)
+  app.graphql.defineResolvers(resolvers)
+  app.graphql.transformSchema(lengthDirectiveTransformer)
+
+  const query = 'mutation { addTodo(input: { todo: "too-long" }) { todos } }'
+
+  const res = await app.inject({
+    method: 'POST',
+    url: '/graphql',
+    body: {
+      query
+    }
+  })
+
+  t.equal(res.statusCode, 400)
+  t.deepEqual(JSON.parse(res.body), {
+    data: null,
+    errors: [{
+      message: 'Expected value of type "StringWithLengthAtMost3", found "too-long"; expected length 8 to be at most 3',
+      locations: [{ line: 1, column: 35 }],
+      extensions: { foo: 'bar' }
+    }]
+  })
+})
+
+test('federation support and custom directives', async (t) => {
+  const app = Fastify()
+  const schema = `
+    ${upperDirectiveTypeDefs}
+    
+    type Query {
+      foo: String @upper
+      user: User
+    }
+    
+    type User {
+      id: ID!
+      name: String @upper
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      foo: () => 'bar',
+      user: () => ({ id: '1' })
+    }
+  }
+
+  const loaders = {
+    User: {
+      name: async (queries) => queries.map(() => 'name')
+    }
+  }
+
+  app.register(mercurius, {
+    schema,
+    resolvers,
+    loaders,
+    federationMetadata: true,
+    schemaTransforms: [upperDirectiveTransformer]
+  })
+
+  await app.ready()
+
+  let query = '{ _service { sdl } }'
+  let res = await app.inject({ method: 'GET', url: `/graphql?query=${query}` })
+  t.deepEqual(JSON.parse(res.body), { data: { _service: { sdl: schema } } })
+
+  query = 'query { foo }'
+  res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), { data: { foo: 'BAR' } })
+
+  query = 'query { user { id name } }'
+  res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), { data: { user: { id: '1', name: 'NAME' } } })
+})
+
+test('federation support using schema from buildFederationSchema and custom directives', async (t) => {
+  const app = Fastify()
+  const schema = `
+    ${upperDirectiveTypeDefs}
+    
+    type Query {
+      foo: String @upper
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      foo: () => 'bar'
+    }
+  }
+
+  const federationSchema = buildFederationSchema(schema)
+
+  const executableSchema = makeExecutableSchema({
+    typeDefs: printSchemaWithDirectives(federationSchema),
+    resolvers: mergeResolvers([getResolversFromSchema(federationSchema), resolvers])
+  })
+
+  app.register(mercurius, {
+    schema: executableSchema,
+    schemaTransforms: [upperDirectiveTransformer]
+  })
+
+  await app.ready()
+
+  let query = '{ _service { sdl } }'
+  let res = await app.inject({ method: 'GET', url: `/graphql?query=${query}` })
+  t.deepEqual(JSON.parse(res.body), { data: { _service: { sdl: schema } } })
+
+  query = 'query { foo }'
+  res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), { data: { foo: 'BAR' } })
+})
+
+test('max length directive validation works', async (t) => {
+  const app = Fastify()
+  const schema = `
+  ${lengthDirectiveTypeDefs}
+    type Query {
+      foo(value: String!): String @length(max: 5)
+      user(id: ID!): User
+    }
+    
+    type Mutation {
+      createUser(input: CreateUserInput!): User!
+    }
+    
+    type User {
+      id: ID!
+      name: String @length(max: 5)
+    }
+    
+    input CreateUserInput {
+      id: ID!
+      name: String! @length(max: 3)
+    }
+  `
+
+  const users = [
+    { id: '1', name: 'foo' },
+    { id: '2', name: 'too-long' }
+  ]
+
+  const resolvers = {
+    Query: {
+      foo: (root, { value }) => value,
+      user: (root, { id }) => users.find(user => user.id === id)
+    },
+    Mutation: {
+      createUser: (root, args) => {
+        users.push({ id: args.input.id, name: args.input.name })
+        return { id: args.input.id }
+      }
+    }
+  }
+
+  const loaders = {
+    User: {
+      name: async (queries) => queries.map((query) => users.find(user => user.id === query.obj.id).name)
+    }
+  }
+
+  app.register(mercurius, {
+    schema,
+    resolvers,
+    loaders,
+    schemaTransforms: [lengthDirectiveTransformer]
+  })
+
+  await app.ready()
+
+  let query = 'query { foo(value: "bar") }'
+  let res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), { data: { foo: 'bar' } })
+
+  query = 'query { foo(value: "bar-too-long") }'
+  res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), {
+    data: { foo: null },
+    errors: [{
+      message: 'expected length 12 to be at most 5',
+      locations: [{ line: 1, column: 9 }],
+      path: ['foo'],
+      extensions: { foo: 'bar' }
+    }]
+  })
+
+  query = 'query { user(id: "1") { id name } }'
+  res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), { data: { user: { id: '1', name: 'foo' } } })
+
+  query = 'query { user(id: "2") { id name } }'
+  res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), {
+    data: { user: { id: 2, name: null } },
+    errors: [{
+      message: 'expected length 8 to be at most 5',
+      locations: [{ line: 1, column: 28 }],
+      path: ['user', 'name'],
+      extensions: { foo: 'bar' }
+    }]
+  })
+
+  query = 'mutation { createUser(input: {id: "3", name: "bar"}) { id name } }'
+  res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), { data: { createUser: { id: '3', name: 'bar' } } })
+  t.ok(users.find(user => user.id === '3'))
+
+  query = 'mutation { createUser(input: {id: "4", name: "too-long"}) { id name } }'
+  res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
+  t.deepEqual(JSON.parse(res.body), {
+    data: null,
+    errors: [{
+      message: 'Expected value of type "StringWithLengthAtMost3", found "too-long"; expected length 8 to be at most 3',
+      locations: [{ line: 1, column: 46 }],
+      extensions: { foo: 'bar' }
+    }]
+  })
+  t.notOk(users.find(user => user.id === '4'))
+})
+
+test('directives with array of typeDefs in schema option', async (t) => {
+  const app = Fastify()
+
+  const todos = []
+  const typeDefs = `
+   type AddTodoResponse {
+     todos: [String]
+   }
+   type Query {
+     todos: [String]
+   }
+   type Mutation {
+     addTodo (input: TodoInput): AddTodoResponse
+   }
+   input TodoInput {
+     todo: String! @length(max: 3)
+   }
+ `
+
+  const resolvers = {
+    Query: {
+      todos: async () => todos
+    },
+    Mutation: {
+      addTodo: async (_, { input: { todo } }) => {
+        todos.push(todo)
+        return { todos }
+      }
+    }
+  }
+
+  app.register(mercurius, {
+    graphiql: true,
+    schema: [lengthDirectiveTypeDefs, typeDefs],
+    schemaTransforms: [lengthDirectiveTransformer],
+    resolvers
+  })
+
+  const query = 'mutation { addTodo(input: { todo: "too-long" }) { todos } }'
+
+  const res = await app.inject({
+    method: 'POST',
+    url: '/graphql',
+    body: {
+      query
+    }
+  })
+
+  t.equal(res.statusCode, 400)
+  t.deepEqual(JSON.parse(res.body), {
+    data: null,
+    errors: [{
+      message: 'Expected value of type "StringWithLengthAtMost3", found "too-long"; expected length 8 to be at most 3',
+      locations: [{ line: 1, column: 35 }],
+      extensions: { foo: 'bar' }
+    }]
+  })
+})
diff --git a/test/federation.js b/test/federation.js
index 5ad0c2d..ad609f1 100644
--- a/test/federation.js
+++ b/test/federation.js
@@ -2,8 +2,7 @@
 
 const { test } = require('tap')
 const Fastify = require('fastify')
-const { printSchema, defaultFieldResolver } = require('graphql')
-const { MapperKind, mapSchema, getDirectives, makeExecutableSchema, printSchemaWithDirectives, getResolversFromSchema, mergeResolvers } = require('graphql-tools')
+const { printSchema } = require('graphql')
 const WebSocket = require('ws')
 const mq = require('mqemitter')
 const GQL = require('..')
@@ -129,64 +128,6 @@ test('federation support using schema from buildFederationSchema', async (t) =>
   t.deepEqual(JSON.parse(res.body), { data: { add: 30 } })
 })
 
-test('federation support using schema from buildFederationSchema and custom directives', async (t) => {
-  const app = Fastify()
-  const schema = `
-    directive @upper on FIELD_DEFINITION
-    
-    type Query {
-      foo: String @upper
-    }
-  `
-
-  const resolvers = {
-    Query: {
-      foo: () => 'bar'
-    }
-  }
-
-  function upperDirectiveTransformer (schema) {
-    return mapSchema(schema, {
-      [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
-        const directives = getDirectives(schema, fieldConfig)
-        if (directives.upper) {
-          const { resolve = defaultFieldResolver } = fieldConfig
-          fieldConfig.resolve = async function (source, args, context, info) {
-            const result = await resolve(source, args, context, info)
-            if (typeof result === 'string') {
-              return result.toUpperCase()
-            }
-            return result
-          }
-          return fieldConfig
-        }
-      }
-    })
-  }
-
-  const federationSchema = buildFederationSchema(schema)
-
-  const executableSchema = makeExecutableSchema({
-    typeDefs: printSchemaWithDirectives(federationSchema),
-    resolvers: mergeResolvers([getResolversFromSchema(federationSchema), resolvers]),
-    schemaTransforms: [upperDirectiveTransformer]
-  })
-
-  app.register(GQL, {
-    schema: executableSchema
-  })
-
-  await app.ready()
-
-  let query = '{ _service { sdl } }'
-  let res = await app.inject({ method: 'GET', url: `/graphql?query=${query}` })
-  t.deepEqual(JSON.parse(res.body), { data: { _service: { sdl: schema } } })
-
-  query = 'query { foo }'
-  res = await app.inject({ method: 'POST', url: '/graphql', body: { query } })
-  t.deepEqual(JSON.parse(res.body), { data: { foo: 'BAR' } })
-})
-
 test('a normal schema can be run in federated mode', async (t) => {
   const app = Fastify()
   const schema = `
diff --git a/test/types/index.ts b/test/types/index.ts
index 0e56444..fff99ea 100644
--- a/test/types/index.ts
+++ b/test/types/index.ts
@@ -5,7 +5,7 @@ import Fastify from 'fastify'
 import mercurius, { MercuriusOptions } from '../..'
 // eslint-disable-next-line no-unused-vars
 import { ValidationContext, ValidationRule } from 'graphql'
-import { makeExecutableSchema } from 'graphql-tools'
+import { makeExecutableSchema, mapSchema } from 'graphql-tools'
 import mq from 'mqemitter'
 
 const app = Fastify()
@@ -141,6 +141,7 @@ makeGraphqlServer({ schema, resolvers, validationRules: [customValidationRule] }
 makeGraphqlServer({ schema, resolvers, validationRules: ({ variables, operationName, source }: { source: string, variables?: Record<string, any>, operationName?: string }) => [customValidationRule] })
 makeGraphqlServer({ schema, errorFormatter: mercurius.defaultErrorFormatter })
 makeGraphqlServer({ schema: mercurius.buildFederationSchema(schema) })
+makeGraphqlServer({ schema: [schema, 'extend type Query { foo: String }'] })
 
 // Gateway mode
 
@@ -227,3 +228,21 @@ app.register(mercurius, {
     emitter
   }
 })
+
+app.register(mercurius, {
+  schema,
+  resolvers,
+  schemaTransforms: []
+})
+
+app.register(mercurius, {
+  schema,
+  resolvers,
+  schemaTransforms: [(schema) => mapSchema(schema)]
+})
+
+app.register(mercurius, {
+  schema,
+  resolvers,
+  schemaTransforms: (schema) => mapSchema(schema)
+})