Repository: mcollina/fastify-gql
Pull request #193 (2020-07-03 17:41:31)

Allow using graphql request metadata when defining validationRules
Graphql validation context does not include variables and operationName, however those are often needed when defining custom validation rules.

These changes allow initializing custom validation rules and passing the the graphql request metadata in the function.
I'm using `params` as an object to make it easier to add new properties a bit easier in the future.

I'm a bit unsure if it's necessary to add extra validation for `opts.validationRules` to ensure it's a function and returned value an iterable.

aldis-ameriks: I ran into a problem with standard and the typescript tests.
Apparently to get standard working nicely with typescript, standardx should be used (https://standardjs.com/#typescript), which disables rules that cause problems with typescript.
But to work around the issue, I eslint ignored the unused type imports.

Patch:
diff --git a/README.md b/README.md
index 8e4b5cf..8e52e5d 100644
--- a/README.md
+++ b/README.md
@@ -637,7 +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.
+* `validationRules`: `Function` or `Function[]`. Optional additional validation rules that the queries must satisfy in addition to those defined by the GraphQL specification. When using `Function`, arguments include additional data from graphql request and the return value must be validation rules `Function[]`.
 * `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 6dcf30b..5c90ee6 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -118,7 +118,7 @@ declare namespace fastifyGQL {
      * Optional additional validation rules.
      * Queries must satisfy these rules in addition to those defined by the GraphQL specification.
      */
-    validationRules?: ValidationRule[],
+    validationRules?: ValidationRules,
     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)
@@ -272,3 +272,5 @@ type Request = {
   variables: Record<string, any>;
   extensions?: Record<string, any>;
 };
+
+type ValidationRules = ValidationRule[] | ((params: { source: string, variables?: Record<string, any>, operationName?: string }) => ValidationRule[])
diff --git a/index.js b/index.js
index 18ded13..c92dfa3 100644
--- a/index.js
+++ b/index.js
@@ -81,8 +81,6 @@ 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
@@ -321,7 +319,15 @@ const plugin = fp(async function (app, opts) {
       }
 
       // Validate
-      const validationErrors = validate(fastifyGraphQl.schema, document, validationRules)
+      let validationRules = []
+      if (opts.validationRules) {
+        if (Array.isArray(opts.validationRules)) {
+          validationRules = opts.validationRules
+        } else {
+          validationRules = opts.validationRules({ source, variables, operationName })
+        }
+      }
+      const validationErrors = validate(fastifyGraphQl.schema, document, [...specifiedRules, ...validationRules])
 
       if (validationErrors.length > 0) {
         if (lruErrors) {


Test patch:
diff --git a/test/types/index.ts b/test/types/index.ts
index 60953e9..cc59c37 100644
--- a/test/types/index.ts
+++ b/test/types/index.ts
@@ -1,4 +1,6 @@
 import Fastify from 'fastify'
+// eslint-disable-next-line no-unused-vars
+import { ValidationContext, ValidationRule } from 'graphql'
 import GQL, { ErrorWithProps } from '../..'
 
 const app = Fastify()
@@ -109,4 +111,15 @@ function makeGraphqlServer (options: GQL.Options) {
   return app
 }
 
+const customValidationRule: ValidationRule = (_context: ValidationContext) => {
+  return {
+    Document () {
+      return false
+    }
+  }
+}
+
 makeGraphqlServer({ schema, resolvers })
+makeGraphqlServer({ schema, resolvers, validationRules: [] })
+makeGraphqlServer({ schema, resolvers, validationRules: [customValidationRule] })
+makeGraphqlServer({ schema, resolvers, validationRules: ({ variables, operationName, source }) => [customValidationRule] })
diff --git a/test/types/tsconfig.json b/test/types/tsconfig.json
index 3d53f6c..cc771bc 100644
--- a/test/types/tsconfig.json
+++ b/test/types/tsconfig.json
@@ -4,7 +4,7 @@
     "module": "commonjs",
     "noEmit": true,
     "strict": true,
-    "esModuleInterop": true,
+    "esModuleInterop": true
   },
   "exclude": ["node_modules", "**/node_modules/*"],
   "files": [
diff --git a/test/validation-rules.js b/test/validation-rules.js
index 6ddcbaa..0b21a1c 100644
--- a/test/validation-rules.js
+++ b/test/validation-rules.js
@@ -22,7 +22,7 @@ const resolvers = {
 
 const query = '{ add(x: 2, y: 2) }'
 
-test('validationRules - reports an error', async (t) => {
+test('validationRules array - reports an error', async (t) => {
   t.plan(2)
   const app = Fastify()
 
@@ -52,7 +52,7 @@ test('validationRules - reports an error', async (t) => {
   }
 })
 
-test('validationRules - passes when no errors', async (t) => {
+test('validationRules array - passes when no errors', async (t) => {
   t.plan(1)
   const app = Fastify()
 
@@ -78,7 +78,7 @@ test('validationRules - passes when no errors', async (t) => {
   t.deepEqual(res, { data: { add: 4 } })
 })
 
-test('validationRules - works with empty validationRules', async (t) => {
+test('validationRules array - works with empty validationRules', async (t) => {
   t.plan(1)
   const app = Fastify()
 
@@ -95,6 +95,79 @@ test('validationRules - works with empty validationRules', async (t) => {
   t.deepEqual(res, { data: { add: 4 } })
 })
 
+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()
@@ -111,3 +184,40 @@ test('validationRules - works with missing validationRules', async (t) => {
   const res = await app.graphql(query)
   t.deepEqual(res, { data: { add: 4 } })
 })
+
+test('validationRules - includes graphql request metadata', async (t) => {
+  t.plan(4)
+  const app = Fastify()
+
+  const query = `
+    query Add ($x: Int!, $y: Int!) {
+      add(x: $x, y: $y)
+    }
+  `
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    validationRules: function ({ source, variables, operationName }) {
+      t.equal(source, query)
+      t.deepEqual(variables, { x: 2, y: 2 })
+      t.deepEqual(operationName, 'Add')
+      return [
+        // 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, null, { x: 2, y: 2 }, 'Add')
+  t.deepEqual(res, { data: { add: 4 } })
+})