Repository: mcollina/fastify-gql
Pull request #116 (2020-02-10 14:30:09)

updates the way query depth is computed
@mcollina Not really sure if this new way of computing query depth covers all cases.
In the proposed test I used the following query
```
flavors {
      id
      name 
      nutrition {
        calories,
        description {
          body
          other {
            body
          }
        }
      }
      recipes {
        name
        ingredients {
          name
        }
      }
      seasons {
        name
      }
    }
```
which have a depth of 4 (`flavors -> nutrition -> description -> other`)
Fixes: #105 


Patch:
diff --git a/lib/queryDepth.js b/lib/queryDepth.js
index 7c8ab9a..279a5c7 100644
--- a/lib/queryDepth.js
+++ b/lib/queryDepth.js
@@ -14,16 +14,15 @@ const { Kind } = require('graphql')
 function queryDepth (definitions, queryDepthLimit) {
   const queries = getQueriesAndMutations(definitions)
   const queryDepth = {}
+
   for (const name in queries) {
     queryDepth[name] = determineDepth(queries[name])
   }
-  const errors = []
 
+  const errors = []
   if (typeof queryDepthLimit === 'number') {
     for (const query of Object.keys(queryDepth)) {
-      const totalDepth = Object.values(queryDepth[query]).reduce((acc, curr) => {
-        return acc + curr
-      })
+      const totalDepth = queryDepth[query]
       if (totalDepth > queryDepthLimit) {
         const queryDepthError = new Error(`${query} query exceeds the query depth limit of ${queryDepthLimit}`)
         errors.push(queryDepthError)
@@ -33,7 +32,15 @@ function queryDepth (definitions, queryDepthLimit) {
 
   return errors
 }
-
+function determineDepth (node, current = 0) {
+  let result = current
+  if (node.selectionSet) {
+    for (const selection of node.selectionSet.selections) {
+      result = determineDepth(selection, current++)
+    }
+  }
+  return result
+}
 function getQueriesAndMutations (definitions) {
   return definitions.reduce((map, definition) => {
     if (definition.kind === Kind.OPERATION_DEFINITION) {
@@ -43,19 +50,4 @@ function getQueriesAndMutations (definitions) {
   }, {})
 }
 
-function determineDepth (node, totals = {}) {
-  if (node.selectionSet) {
-    const name = node.name ? node.name.value : 'unnamedNode'
-    if (totals[name]) {
-      totals[name] += 1
-    } else {
-      totals[name] = 1
-    }
-    node.selectionSet.selections.forEach(selection => {
-      determineDepth(selection, totals)
-    })
-  }
-  return totals
-}
-
 module.exports = queryDepth
diff --git a/lib/routes.js b/lib/routes.js
index 62de2f2..b6ca2ee 100644
--- a/lib/routes.js
+++ b/lib/routes.js
@@ -215,13 +215,13 @@ module.exports = async function (app, opts) {
     return reply.graphql(query, { pubsub: subscriber, ...context }, variables, operationName)
   })
 
-  if (opts.ide) {
+  if (opts.ide || opts.graphiql) {
     app.register(Static, {
       root: join(__dirname, '../static'),
       wildcard: false,
       serve: false
     })
-    if (opts.ide === true || opts.ide === 'graphiql') {
+    if (opts.ide === true || opts.ide === 'graphiql' || opts.graphiql === true) {
       app.get('/graphiql', (req, reply) => {
         reply.sendFile('graphiql.html')
       })
diff --git a/package.json b/package.json
index 48efe8b..4a9e83b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "fastify-gql",
-  "version": "3.0.3",
+  "version": "3.0.4",
   "description": "Fastify barebone GraphQL adapter",
   "main": "index.js",
   "types": "index.d.ts",
@@ -45,7 +45,7 @@
     "graphql": "^14.5.0",
     "graphql-jit": "^0.4.0",
     "http-errors": "^1.7.3",
-    "mqemitter": "^3.0.0",
+    "mqemitter": "^4.0.0",
     "readable-stream": "^3.5.0",
     "single-user-cache": "^0.3.0",
     "tiny-lru": "^7.0.0"


Test patch:
diff --git a/test/query-depth.js b/test/query-depth.js
index c47023a..5337209 100644
--- a/test/query-depth.js
+++ b/test/query-depth.js
@@ -144,7 +144,7 @@ test('queryDepth - test total depth is within queryDepth parameter', async (t) =
   app.register(GQL, {
     schema,
     resolvers,
-    queryDepth: 6
+    queryDepth: 5
   })
 
   // needed so that graphql is defined
@@ -156,19 +156,20 @@ test('queryDepth - test total depth is within queryDepth parameter', async (t) =
 })
 
 test('queryDepth - test total depth is over queryDepth parameter', async (t) => {
+  t.plan(1)
   const app = Fastify()
 
   app.register(GQL, {
     schema,
     resolvers,
-    queryDepth: 5
+    queryDepth: 3
   })
 
   // needed so that graphql is defined
   await app.ready()
 
   const err = new BadRequest()
-  const queryDepthError = new Error('unnamedQuery query exceeds the query depth limit of 5')
+  const queryDepthError = new Error('unnamedQuery query exceeds the query depth limit of 3')
   err.errors = [queryDepthError]
 
   try {
@@ -232,3 +233,200 @@ test('queryDepth - definition.kind and definition.name change', async (t) => {
 
   t.deepEqual(res, goodResponse)
 })
+
+test('queryDepth - ensure query depth is correctly calculated', async (t) => {
+  const schema = `
+  type Nutrition {
+      flavorId: ID
+      calories: Int
+      fat: Int
+      sodium: Int,
+      description: NutritionDescription
+    }
+
+  type NutritionDescription {
+    body: String
+    other: AnotherType
+  }
+  type AnotherType {
+    body: String
+  }
+
+  type Recipe {
+    name: String
+    ingredients: [Ingredient]
+  }
+
+  type Ingredient {
+    name: String,
+  }
+  type Flavor {
+    id: ID
+    name: String
+    description: String
+    nutrition: Nutrition
+    recipes: [Recipe]
+    seasons: [Season]
+  }
+
+  type Season {
+    name: String
+  }
+  type Query {
+    flavors: [Flavor],
+    otherFlavors: [Flavor]
+  }
+`
+  const resolvers = {
+    Query: {
+      otherFlavors (params, { reply }) {
+        return [
+          {
+            id: 2,
+            name: 'Blueberry',
+            seasons: [
+              { name: 'Spring' },
+              { name: 'Fall' }
+            ]
+          },
+          {
+            id: 3,
+            name: 'Blackberry',
+            seasons: [
+              { name: 'Winter' }
+            ]
+          }
+        ]
+      },
+      flavors (params, { reply }) {
+        return [
+          {
+            id: 1,
+            name: 'Strawberry',
+            seasons: [
+              { name: 'Spring' },
+              { name: 'Summer' }
+            ]
+          }
+        ]
+      }
+    },
+    Flavor: {
+      nutrition (params) {
+        return {
+          flavorId: 1,
+          calories: 123,
+          sodium: 10,
+          fat: 1
+        }
+      },
+      recipes (params) {
+        return [
+          { name: 'Strawberry Cake' },
+          { name: 'Strawberry Ice Cream' }
+        ]
+      }
+    },
+    Recipe: {
+      ingredients (params) {
+        return [
+          { name: 'milk' },
+          { name: 'sugar' },
+          { name: 'butter' }
+        ]
+      }
+    },
+    Nutrition: {
+      description (params) {
+        return {
+          body: 'lorem ipsum'
+        }
+      }
+    },
+    NutritionDescription: {
+      other (params) {
+        return {
+          body: 'another string'
+        }
+      }
+    }
+  }
+
+  const query = `{
+    flavors {
+      id
+      name
+      nutrition {
+        calories,
+        description {
+          body
+          other {
+            body
+          }
+        }
+      }
+      recipes {
+        name
+        ingredients {
+          name
+        }
+      }
+      seasons {
+        name
+      }
+    }
+  }`
+  const app = Fastify()
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    queryDepth: 5
+  })
+
+  // needed so that graphql is defined
+  await app.ready()
+
+  const res = await app.graphql(query)
+  t.deepEqual(res, {
+    data: {
+      flavors: [
+        {
+          id: '1',
+          name: 'Strawberry',
+          nutrition: {
+            calories: 123,
+            description: {
+              body: 'lorem ipsum',
+              other: {
+                body: 'another string'
+              }
+            }
+          },
+          recipes: [
+            {
+              name: 'Strawberry Cake',
+              ingredients: [
+                { name: 'milk' },
+                { name: 'sugar' },
+                { name: 'butter' }
+              ]
+            },
+            {
+              name: 'Strawberry Ice Cream',
+              ingredients: [
+                { name: 'milk' },
+                { name: 'sugar' },
+                { name: 'butter' }
+              ]
+            }
+          ],
+          seasons: [
+            { name: 'Spring' },
+            { name: 'Summer' }
+          ]
+        }
+      ]
+    }
+  })
+})
diff --git a/test/routes.js b/test/routes.js
index 09e6c63..a91d5c1 100644
--- a/test/routes.js
+++ b/test/routes.js
@@ -1323,3 +1323,22 @@ test('disable GET graphiql if ide is not "graphiql" or "playground"', async (t)
   })
   t.strictEqual(res2.statusCode, 404)
 })
+
+test('render graphiql if graphiql: true', async (t) => {
+  const app = Fastify()
+  app.register(GQL, {
+    graphiql: true
+  })
+
+  const res = await app.inject({
+    method: 'GET',
+    url: '/graphiql'
+  })
+  t.strictEqual(res.statusCode, 200)
+
+  const res2 = await app.inject({
+    method: 'GET',
+    url: '/playground'
+  })
+  t.strictEqual(res2.statusCode, 404)
+})