Repository: mcollina/fastify-gql
Pull request #189 (2020-06-24 12:52:14)

Add support for custom graphql validation rules
Adds support for passing optional custom validationRules. Should enable usage of libraries like graphql-query-complexity.

aldis-ameriks: I had few failing tests on clean master.

```
 FAIL  test/routes.js 2 failed of 107 3s
 ✖ schema issues
 ✖ schema issues

 FAIL  test/subscription.js 4 failed of 8 793.296ms
 ✖ schema issues
 ✖ schema issues
 ✖ Server is not running.
 ✖ Server is not running.
```

Patch:
diff --git a/README.md b/README.md
index c737cdc..9bc9be8 100644
--- a/README.md
+++ b/README.md
@@ -637,6 +637,7 @@ __fastify-gql__ supports the following options:
 * `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)._
 * `queryDepth`: `Integer`. The maximum depth allowed for a single query. _Note: GraphiQL IDE (or Playground IDE) sends an introspection query when it starts up. This query has a depth of 7 so when the `queryDepth` value is smaller than 7 this query will fail with a `Bad Request` error_
+* `validationRules`: `Function[]`. Optional additional validation rules that the queries must satisfy in addition to those defined by the GraphQL specification.
 * `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.
diff --git a/index.d.ts b/index.d.ts
index 0050f23..25a053f 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,5 +1,5 @@
 import fastify, { FastifyError, FastifyReply, FastifyRequest, RegisterOptions } from "fastify";
-import { DocumentNode, ExecutionResult, GraphQLSchema, Source, GraphQLResolveInfo, GraphQLIsTypeOfFn, GraphQLTypeResolver, GraphQLScalarType } from 'graphql';
+import { DocumentNode, ExecutionResult, GraphQLSchema, Source, GraphQLResolveInfo, GraphQLIsTypeOfFn, GraphQLTypeResolver, GraphQLScalarType, ValidationRule } from 'graphql';
 import { IncomingMessage, Server, ServerResponse } from "http";
 import { Http2Server, Http2ServerRequest, Http2ServerResponse } from 'http2';
 
@@ -110,6 +110,11 @@ declare namespace fastifyGQL {
      * The maximum depth allowed for a single query.
      */
     queryDepth?: number,
+    /**
+     * Optional additional validation rules.
+     * Queries must satisfy these rules in addition to those defined by the GraphQL specification.
+     */
+    validationRules?: ValidationRule[],
     context?: (request: FastifyRequest<HttpRequest>, reply: FastifyReply<HttpResponse>) => Promise<any>,
     /**
      * Enable subscription support when options are provided. [`emitter`](https://github.com/mcollina/mqemitter) property is required when subscriptions is an object. (Default false)
@@ -151,7 +156,7 @@ declare namespace fastifyGQL {
      * Custom additional properties of this error
      */
     extensions?: object
-  } 
+  }
 }
 
 declare module "fastify" {
@@ -262,4 +267,4 @@ type Request = {
   document: DocumentNode;
   variables: Record<string, any>;
   extensions?: Record<string, any>;
-}; 
+};
diff --git a/index.js b/index.js
index 5d05a69..5156635 100644
--- a/index.js
+++ b/index.js
@@ -20,6 +20,7 @@ const {
   extendSchema,
   validate,
   validateSchema,
+  specifiedRules,
   execute
 } = require('graphql')
 const { buildExecutionContext } = require('graphql/execution/execute')
@@ -80,6 +81,8 @@ const plugin = fp(async function (app, opts) {
   let subscriber
   let verifyClient
 
+  const validationRules = [...specifiedRules, ...(opts.validationRules || [])]
+
   if (typeof subscriptionOpts === 'object') {
     emitter = subscriptionOpts.emitter || mq()
     verifyClient = subscriptionOpts.verifyClient
@@ -316,7 +319,7 @@ const plugin = fp(async function (app, opts) {
       }
 
       // Validate
-      const validationErrors = validate(schema, document)
+      const validationErrors = validate(schema, document, validationRules)
 
       if (validationErrors.length > 0) {
         if (lruErrors) {


Test patch:
diff --git a/test/validation-rules.js b/test/validation-rules.js
new file mode 100644
index 0000000..6ddcbaa
--- /dev/null
+++ b/test/validation-rules.js
@@ -0,0 +1,113 @@
+'use strict'
+
+const { test } = require('tap')
+const Fastify = require('fastify')
+const { GraphQLError } = require('graphql')
+const GQL = require('..')
+
+const schema = `
+  type Query {
+    add(x: Int, y: Int): Int
+  }
+`
+
+const resolvers = {
+  Query: {
+    add: async (_, obj) => {
+      const { x, y } = obj
+      return x + y
+    }
+  }
+}
+
+const query = '{ add(x: 2, y: 2) }'
+
+test('validationRules - reports an error', async (t) => {
+  t.plan(2)
+  const app = Fastify()
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    validationRules: [
+      // validation rule that reports an error
+      function (context) {
+        return {
+          Document () {
+            context.reportError(new GraphQLError('Validation rule error'))
+          }
+        }
+      }
+    ]
+  })
+
+  // needed so that graphql is defined
+  await app.ready()
+
+  try {
+    await app.graphql(query)
+  } catch (e) {
+    t.equal(e.errors.length, 1)
+    t.equal(e.errors[0].message, 'Validation rule error')
+  }
+})
+
+test('validationRules - passes when no errors', async (t) => {
+  t.plan(1)
+  const app = Fastify()
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    validationRules: [
+      // validation rule that reports no errors
+      function (_context) {
+        return {
+          Document () {
+            return false
+          }
+        }
+      }
+    ]
+  })
+
+  // needed so that graphql is defined
+  await app.ready()
+
+  const res = await app.graphql(query)
+  t.deepEqual(res, { data: { add: 4 } })
+})
+
+test('validationRules - works with empty validationRules', async (t) => {
+  t.plan(1)
+  const app = Fastify()
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    validationRules: []
+  })
+
+  // needed so that graphql is defined
+  await app.ready()
+
+  const res = await app.graphql(query)
+  t.deepEqual(res, { data: { add: 4 } })
+})
+
+test('validationRules - works with missing validationRules', async (t) => {
+  t.plan(1)
+  const app = Fastify()
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    validationRules: undefined
+  })
+
+  // needed so that graphql is defined
+  await app.ready()
+
+  const res = await app.graphql(query)
+  t.deepEqual(res, { data: { add: 4 } })
+})