Repository: mcollina/fastify-gql
Pull request #30 (2019-02-08 13:59:10)

feat: add union and interface support
This PR adds support for union and interface, and related tests !


Patch:
diff --git a/README.md b/README.md
index d5570e8..2a74772 100644
--- a/README.md
+++ b/README.md
@@ -105,7 +105,6 @@ __fastify-gql__ supports the following options:
   exposed at `/graphql`.
 * `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](https://graphql.org/learn/serving-over-http/#response)._
 
 ### HTTP endpoints
 
diff --git a/index.js b/index.js
index a00d58e..975b09a 100644
--- a/index.js
+++ b/index.js
@@ -12,6 +12,8 @@ const {
   GraphQLObjectType,
   GraphQLScalarType,
   GraphQLEnumType,
+  GraphQLInterfaceType,
+  GraphQLUnionType,
   GraphQLSchema,
   extendSchema,
   validate,
@@ -79,7 +81,6 @@ module.exports = fp(async function (app, opts) {
 
   if (opts.routes !== false) {
     app.register(routes, {
-      errorHandler: opts.errorHandler,
       graphiql: opts.graphiql,
       prefix: opts.prefix
     })
@@ -109,6 +110,10 @@ module.exports = fp(async function (app, opts) {
       } else if (type instanceof GraphQLObjectType) {
         const fields = type.getFields()
         const resolver = resolvers[name]
+        if (resolver.isTypeOf) {
+          type.isTypeOf = resolver.isTypeOf
+          delete resolver.isTypeOf
+        }
         for (const prop of Object.keys(resolver)) {
           fields[prop].resolve = resolver[prop]
         }
@@ -117,6 +122,9 @@ module.exports = fp(async function (app, opts) {
         for (const prop of Object.keys(resolver)) {
           type[prop] = resolver[prop]
         }
+      } else if (type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType) {
+        const resolver = resolvers[name]
+        type.resolveType = resolver.resolveType
       } else {
         throw new Error(`Cannot find type ${name}`)
       }
@@ -178,13 +186,13 @@ module.exports = fp(async function (app, opts) {
       const operationAST = getOperationAST(document, operationName)
       if (operationAST.operation !== 'query') {
         const err = new MethodNotAllowed()
-        err.errors = [ new Error('Operation cannot be perfomed via a GET request') ]
+        err.errors = { errors: ['Operation cannot be perfomed via a GET request'] }
         throw err
       }
     }
 
     // minJit is 0 by default
-    if (cached && cached.count++ === minJit) {
+    if (cached && ++cached.count === minJit) {
       cached.jit = compileQuery(schema, document, operationName)
     }
 
diff --git a/package.json b/package.json
index 047ec8c..f71cc22 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "fastify-gql",
-  "version": "0.8.0",
+  "version": "0.7.0",
   "description": "Fastify barebone GraphQL adapter",
   "main": "index.js",
   "scripts": {
@@ -17,18 +17,18 @@
   },
   "homepage": "https://github.com/mcollina/fastify-gql#readme",
   "devDependencies": {
-    "autocannon": "^3.2.0",
-    "fastify": "^1.13.4",
-    "graphql-tools": "^4.0.3",
+    "autocannon": "^3.0.0",
+    "fastify": "^1.11.2",
+    "graphql-tools": "^4.0.0",
     "pre-commit": "^1.2.2",
     "snazzy": "^8.0.0",
     "standard": "^12.0.1",
-    "tap": "^12.1.1"
+    "tap": "^12.0.1"
   },
   "dependencies": {
-    "fastify-plugin": "^1.4.0",
+    "fastify-plugin": "^1.2.0",
     "fastify-static": "^1.1.0",
-    "graphql": "^14.1.1",
+    "graphql": "^14.0.2",
     "graphql-jit": "0.0.6",
     "http-errors": "^1.7.1",
     "tiny-lru": "^5.0.1"
diff --git a/routes.js b/routes.js
index 7fbcda3..66fc609 100644
--- a/routes.js
+++ b/routes.js
@@ -3,7 +3,6 @@
 const { join } = require('path')
 const Static = require('fastify-static')
 const { BadRequest } = require('http-errors')
-const { formatError, GraphQLError } = require('graphql')
 
 const responseSchema = {
   '2xx': {
@@ -12,66 +11,27 @@ const responseSchema = {
       data: {
         type: 'object',
         additionalProperties: true
-      },
-      errors: {
-        type: 'object',
-        properties: {
-          message: { type: 'string' },
-          locations: {
-            type: 'array',
-            items: {
-              type: 'object',
-              properties: {
-                line: { type: 'integer' },
-                column: { type: 'integer' }
-              }
-            }
-          },
-          path: {
-            type: 'array',
-            items: { type: 'string' }
-          }
-        }
       }
+      // TODO make this include errors
     }
   }
 }
 
-async function defaultErrorHandler (err, request, reply) {
-  if (!err.statusCode) {
-    throw err
-  }
-
-  reply.code(err.statusCode)
-  if (err.errors) {
-    const errors = err.errors.map(error => {
-      return error instanceof GraphQLError ? formatError(error) : { message: error.message }
-    })
-
-    return { errors }
-  } else {
-    return {
-      errors: [
-        { message: err.message }
-      ]
+module.exports = async function (app, opts) {
+  app.setErrorHandler(async function (err, request, reply) {
+    if (!err.statusCode) {
+      throw err
     }
-  }
-}
 
-function validationHandler (validationError) {
-  if (validationError) {
-    const err = new BadRequest()
-    err.errors = [validationError]
-    throw err
-  }
-}
-
-module.exports = async function (app, opts) {
-  if (typeof opts.errorHandler === 'function') {
-    app.setErrorHandler(opts.errorHandler)
-  } else if (opts.errorHandler === true || opts.errorHandler === undefined) {
-    app.setErrorHandler(defaultErrorHandler)
-  }
+    reply.code(err.statusCode)
+    if (err.errors) {
+      return { errors: err.errors }
+    } else {
+      return {
+        message: err.message
+      }
+    }
+  })
 
   app.get('/graphql', {
     schema: {
@@ -90,11 +50,8 @@ module.exports = async function (app, opts) {
         }
       },
       response: responseSchema
-    },
-    attachValidation: true
+    }
   }, function (request, reply) {
-    validationHandler(request.validationError)
-
     let {
       query,
       variables,
@@ -133,11 +90,8 @@ module.exports = async function (app, opts) {
         }
       },
       response: responseSchema
-    },
-    attachValidation: true
-  }, async function (request, reply) {
-    validationHandler(request.validationError)
-
+    }
+  }, function (request, reply) {
     const {
       query,
       variables,


Test patch:
diff --git a/.github/tests_checker.yml b/.github/tests_checker.yml
deleted file mode 100644
index 3092680..0000000
--- a/.github/tests_checker.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-comment: 'Could you please add tests to make sure this change works as expected?',
-fileExtensions: ['.php', '.ts', '.js', '.c', '.cs', '.cpp', '.rb', '.java']
-testDir: 'test'
diff --git a/tap-snapshots/test-routes.js-TAP.test.js b/tap-snapshots/test-routes.js-TAP.test.js
index 6acd216..9b8345e 100644
--- a/tap-snapshots/test-routes.js-TAP.test.js
+++ b/tap-snapshots/test-routes.js-TAP.test.js
@@ -61,10 +61,10 @@ exports[`test/routes.js TAP POST return 400 on error > undefined 1`] = `
 
 exports[`test/routes.js TAP mutation with GET errors > undefined 1`] = `
 {
-  "errors": [
-    {
-      "message": "Operation cannot be perfomed via a GET request"
-    }
-  ]
+  "errors": {
+    "errors": [
+      "Operation cannot be perfomed via a GET request"
+    ]
+  }
 }
 `
diff --git a/test/app-decorator.js b/test/app-decorator.js
index 5d649ee..78eda0b 100644
--- a/test/app-decorator.js
+++ b/test/app-decorator.js
@@ -464,3 +464,282 @@ test('enum should be supported', async (t) => {
     }
   })
 })
+
+test('interfaces should be supported with isTypeOf', async (t) => {
+  const app = Fastify()
+  const schema = `
+    interface Geometry {
+      type: String!
+    }
+
+    type Polygon implements Geometry {
+      type: String!
+      coordinates: String
+    }
+
+    type MultiPolygon implements Geometry {
+      type: String!
+      coordinates: Int
+    }
+
+    type Query {
+      getGeometryPolygon: Geometry
+      getGeometryMultiPolygon: Geometry
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      getGeometryPolygon: async () => {
+        return {
+          type: 'Polygon',
+          coordinates: 'test'
+        }
+      },
+      getGeometryMultiPolygon: async () => {
+        return {
+          type: 'MultiPolygon',
+          coordinates: 1
+        }
+      }
+    },
+    Polygon: {
+      isTypeOf (geometry) {
+        return geometry.type === 'Polygon'
+      }
+    },
+    MultiPolygon: {
+      isTypeOf (geometry) {
+        return geometry.type === 'MultiPolygon'
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers
+  })
+
+  // needed so that graphql is defined
+  await app.ready()
+
+  const queryPoly = `{
+    getGeometryPolygon {
+      type
+      ... on Polygon {
+        coordinates
+      }
+    }
+  }`
+  const resPoly = await app.graphql(queryPoly)
+
+  t.deepEqual(resPoly, {
+    data: {
+      getGeometryPolygon: {
+        type: 'Polygon',
+        coordinates: 'test'
+      }
+    }
+  })
+
+  const queryMultiPoly = `{
+    getGeometryMultiPolygon {
+      type
+      ... on MultiPolygon {
+        coordinates
+      }
+    }
+  }`
+  const resMultiPoly = await app.graphql(queryMultiPoly)
+
+  t.deepEqual(resMultiPoly, {
+    data: {
+      getGeometryMultiPolygon: {
+        type: 'MultiPolygon',
+        coordinates: 1
+      }
+    }
+  })
+})
+
+test('interfaces should be supported with resolveType', async (t) => {
+  const app = Fastify()
+  const schema = `
+    interface Geometry {
+      type: String!
+    }
+
+    type Polygon implements Geometry {
+      type: String!
+      coordinates: String
+    }
+
+    type MultiPolygon implements Geometry {
+      type: String!
+      coordinates: Int
+    }
+
+    type Query {
+      getGeometryPolygon: Geometry
+      getGeometryMultiPolygon: Geometry
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      getGeometryPolygon: async () => {
+        return {
+          type: 'Polygon',
+          coordinates: 'test'
+        }
+      },
+      getGeometryMultiPolygon: async () => {
+        return {
+          type: 'MultiPolygon',
+          coordinates: 1
+        }
+      }
+    },
+    Geometry: {
+      resolveType (geometry) {
+        return geometry.type
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers
+  })
+
+  // needed so that graphql is defined
+  await app.ready()
+
+  const queryPoly = `{
+    getGeometryPolygon {
+      type
+      ... on Polygon {
+        coordinates
+      }
+    }
+  }`
+  const resPoly = await app.graphql(queryPoly)
+
+  t.deepEqual(resPoly, {
+    data: {
+      getGeometryPolygon: {
+        type: 'Polygon',
+        coordinates: 'test'
+      }
+    }
+  })
+
+  const queryMultiPoly = `{
+    getGeometryMultiPolygon {
+      type
+      ... on MultiPolygon {
+        coordinates
+      }
+    }
+  }`
+  const resMultiPoly = await app.graphql(queryMultiPoly)
+
+  t.deepEqual(resMultiPoly, {
+    data: {
+      getGeometryMultiPolygon: {
+        type: 'MultiPolygon',
+        coordinates: 1
+      }
+    }
+  })
+})
+
+test('union should be supported with resolveType', async (t) => {
+  const app = Fastify()
+  const schema = `
+    union Geometry = Polygon | MultiPolygon
+
+    type Polygon {
+      type: String!
+      coordinates: String
+    }
+
+    type MultiPolygon {
+      type: String!
+      coordinates: Int
+    }
+
+    type Query {
+      getGeometryPolygon: Geometry
+      getGeometryMultiPolygon: Geometry
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      getGeometryPolygon: async () => {
+        return {
+          type: 'Polygon',
+          coordinates: 'test'
+        }
+      },
+      getGeometryMultiPolygon: async () => {
+        return {
+          type: 'MultiPolygon',
+          coordinates: 1
+        }
+      }
+    },
+    Geometry: {
+      resolveType (geometry) {
+        return geometry.type
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers
+  })
+
+  // needed so that graphql is defined
+  await app.ready()
+
+  const queryPoly = `{
+    getGeometryPolygon {
+      ... on Polygon {
+        type
+        coordinates
+      }
+    }
+  }`
+  const resPoly = await app.graphql(queryPoly)
+
+  t.deepEqual(resPoly, {
+    data: {
+      getGeometryPolygon: {
+        type: 'Polygon',
+        coordinates: 'test'
+      }
+    }
+  })
+
+  const queryMultiPoly = `{
+    getGeometryMultiPolygon {
+      ... on MultiPolygon {
+        type
+        coordinates
+      }
+    }
+  }`
+  const resMultiPoly = await app.graphql(queryMultiPoly)
+
+  t.deepEqual(resMultiPoly, {
+    data: {
+      getGeometryMultiPolygon: {
+        type: 'MultiPolygon',
+        coordinates: 1
+      }
+    }
+  })
+})
diff --git a/test/routes.js b/test/routes.js
index 524acb8..8d49b61 100644
--- a/test/routes.js
+++ b/test/routes.js
@@ -655,136 +655,3 @@ test('GET graphql endpoint with prefixed wrapper', async (t) => {
 
   t.strictEqual(res.statusCode, 200)
 })
-
-test('Custom error handler', async (t) => {
-  const app = Fastify()
-  const schema = `
-    type Query {
-      add(x: Int, y: Int): Int
-    }
-  `
-
-  const resolvers = {
-    add: async ({ x, y }) => t.fail('should never get called')
-  }
-
-  function errorHandler (error, req, reply) {
-    app.log.error(error)
-    reply.code(403)
-    reply.send()
-  }
-
-  app.register(GQL, {
-    schema,
-    resolvers,
-    errorHandler
-  })
-
-  // Invalid query, should throw 400 from fastify-gql but catched by user handler and set to 403
-  const res = await app.inject({
-    method: 'GET',
-    url: '/graphql?query={add(x:"2",y:2)}'
-  })
-
-  t.strictEqual(res.statusCode, 403)
-})
-
-test('Error handler set to true should not change default behavior', async (t) => {
-  const app = Fastify()
-  const schema = `
-    type Query {
-      add(x: Int, y: Int): Int
-    }
-  `
-
-  const resolvers = {
-    add: async ({ x, y }) => t.fail('should never get called')
-  }
-
-  app.register(GQL, {
-    schema,
-    resolvers,
-    errorHandler: true
-  })
-
-  // Invalid query
-  const res = await app.inject({
-    method: 'GET',
-    url: '/graphql?query={add(x:"2",y:2)}'
-  })
-
-  const expectedResult = {
-    errors: [{
-      message: 'Expected type Int, found "2".',
-      locations: [{
-        line: 1,
-        column: 8
-      }]
-    }]
-  }
-
-  t.strictEqual(res.statusCode, 400)
-  t.strictDeepEqual(JSON.parse(res.body), expectedResult)
-})
-
-test('Error handler set to false should pass error to higher handler', async (t) => {
-  const app = Fastify()
-  const schema = `
-    type Query {
-      add(x: Int, y: Int): Int
-    }
-  `
-
-  const resolvers = {
-    add: async ({ x, y }) => t.fail('should never get called')
-  }
-
-  app.register(GQL, {
-    schema,
-    resolvers,
-    errorHandler: false
-  })
-
-  app.setErrorHandler((error, req, reply) => {
-    app.log.error(error)
-    reply.code(403)
-    reply.send()
-  })
-
-  // Invalid query
-  const res = await app.inject({
-    method: 'GET',
-    url: '/graphql?query={add(x:"2",y:2)}'
-  })
-
-  t.strictEqual(res.statusCode, 403)
-})
-
-test('route validation is catched and parsed to graphql error', async (t) => {
-  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
-  })
-
-  // Invalid query
-  const res = await app.inject({
-    method: 'POST',
-    url: '/graphql'
-  })
-
-  const expectedResult = { errors: [{ message: 'body should be object' }] }
-
-  t.strictEqual(res.statusCode, 400)
-  t.strictDeepEqual(JSON.parse(res.body), expectedResult)
-})