graphql-depth-limit
Advanced tools
Comparing version 1.0.0 to 1.1.0
62
index.js
@@ -5,4 +5,13 @@ const { | ||
} = require('graphql') | ||
const arrify = require('arrify') | ||
module.exports = (maxDepth, callback) => validationContext => { | ||
/** | ||
* Creates a validator for the GraphQL query depth | ||
* @param {Number} maxDepth - The maximum allowed depth for any operation in a GraphQL document. | ||
* @param {Object} [options] | ||
* @param {String|RegExp|Function} options.ignore - Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that reaturns a boolean. | ||
* @param {Function} [callback] - Called each time validation runs. Receives an Object which is a map of the depths for each operation. | ||
* @returns {Function} The validator function for GraphQL validation phase. | ||
*/ | ||
const depthLimit = (maxDepth, options = {}, callback = () => {}) => validationContext => { | ||
try { | ||
@@ -14,5 +23,5 @@ const { definitions } = validationContext.getDocument() | ||
for (let name in queries) { | ||
queryDepths[name] = determineDepth(queries[name], fragments, 0, maxDepth, validationContext, name) | ||
queryDepths[name] = determineDepth(queries[name], fragments, 0, maxDepth, validationContext, name, options) | ||
} | ||
callback && callback(queryDepths) | ||
callback(queryDepths) | ||
return validationContext | ||
@@ -25,2 +34,4 @@ } catch (err) { | ||
module.exports = depthLimit | ||
function getFragments(definitions) { | ||
@@ -45,19 +56,28 @@ return definitions.reduce((map, definition) => { | ||
function determineDepth(node, fragments, depthSoFar, maxDepth, context, operationName) { | ||
function determineDepth(node, fragments, depthSoFar, maxDepth, context, operationName, options) { | ||
if (depthSoFar > maxDepth) { | ||
return context.reportError(new GraphQLError(`'${operationName}' exceeds maximum operation depth of ${maxDepth}`, [ node ])) | ||
return context.reportError( | ||
new GraphQLError(`'${operationName}' exceeds maximum operation depth of ${maxDepth}`, [ node ]) | ||
) | ||
} | ||
switch (node.kind) { | ||
case Kind.FIELD: | ||
if (!node.selectionSet) { | ||
// by default, ignore the introspection fields which begin with double underscores | ||
const shouldIgnore = /^__/.test(node.name.value) || seeIfIgnored(node, options.ignore) | ||
if (shouldIgnore || !node.selectionSet) { | ||
return 0 | ||
} else { | ||
return 1 + Math.max(...node.selectionSet.selections.map(selection => determineDepth(selection, fragments, depthSoFar + 1, maxDepth, context, operationName))) | ||
} | ||
return 1 + Math.max(...node.selectionSet.selections.map(selection => | ||
determineDepth(selection, fragments, depthSoFar + 1, maxDepth, context, operationName, options) | ||
)) | ||
case Kind.FRAGMENT_SPREAD: | ||
return determineDepth(fragments[node.name.value], fragments, depthSoFar, maxDepth, context, operationName) | ||
return determineDepth(fragments[node.name.value], fragments, depthSoFar, maxDepth, context, operationName, options) | ||
case Kind.INLINE_FRAGMENT: | ||
case Kind.FRAGMENT_DEFINITION: | ||
case Kind.OPERATION_DEFINITION: | ||
return Math.max(...node.selectionSet.selections.map(selection => determineDepth(selection, fragments, depthSoFar, maxDepth, context, operationName))) | ||
return Math.max(...node.selectionSet.selections.map(selection => | ||
determineDepth(selection, fragments, depthSoFar, maxDepth, context, operationName, options) | ||
)) | ||
default: | ||
@@ -68,1 +88,23 @@ throw new Error('uh oh! depth crawler cannot handle: ' + node.kind) | ||
function seeIfIgnored(node, ignore) { | ||
for (let rule of arrify(ignore)) { | ||
const fieldName = node.name.value | ||
switch (rule.constructor) { | ||
case Function: | ||
if (rule(fieldName)) { | ||
return true | ||
} | ||
break | ||
case String: | ||
case RegExp: | ||
if (fieldName.match(rule)) { | ||
return true | ||
} | ||
break | ||
default: | ||
throw new Error(`Invalid ignore option: ${rule}`) | ||
} | ||
} | ||
return false | ||
} | ||
{ | ||
"name": "graphql-depth-limit", | ||
"version": "1.0.0", | ||
"version": "1.1.0", | ||
"description": "Limit the complexity of your GraphQL queries based on depth.", | ||
@@ -10,4 +10,12 @@ "engines": { | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
"docs": "sed -n '/Documentation/q;p' README.md > README.bak.md; echo '## Documentation' >> README.bak.md; jsdoc2md --heading-depth 3 index.js >> README.bak.md && mv -f README.bak.md README.md", | ||
"lint": "eslint index.js", | ||
"test": "ava test.js" | ||
}, | ||
"ava": { | ||
"verbose": true | ||
}, | ||
"files": [ | ||
"index.js" | ||
], | ||
"repository": { | ||
@@ -32,3 +40,13 @@ "type": "git", | ||
"graphql": "*" | ||
}, | ||
"devDependencies": { | ||
"ava": "^0.21.0", | ||
"eslint": "^4.4.1", | ||
"eslint-config-airbnb-base": "^11.3.1", | ||
"graphql": "^0.10.5", | ||
"jsdoc-to-markdown": "^3.0.0" | ||
}, | ||
"dependencies": { | ||
"arrify": "^1.0.1" | ||
} | ||
} |
@@ -48,3 +48,11 @@ GraphQL Depth Limit | ||
album { | ||
# and so on... | ||
songs { | ||
album { | ||
songs { | ||
album { | ||
# and so on... | ||
} | ||
} | ||
} | ||
} | ||
} | ||
@@ -175,9 +183,18 @@ } | ||
schema, | ||
validationRules: [ depthLimit(10, depths => console.log(depths)) ] | ||
validationRules: [ depthLimit(10) ] | ||
}))) | ||
``` | ||
The first argument is the total depth limit. This will throw a validation error for queries (or mutations) with a depth of 11 or more. | ||
The second argument is a callback which receives an `Object` which is a map of the depths for each operation. | ||
The first argument is the total depth limit. This will throw a validation error for queries (or mutations) with a depth of 11 or more.<br/> | ||
The second argument is an options object, where you can do things like specify ignored fields. Introspection fields are ignored by default.<br/> | ||
The third argument is a callback which receives an `Object` which is a map of the depths for each operation.<br/> | ||
```js | ||
depthLimit( | ||
10, | ||
{ ignore: [ /_trusted$/, 'idontcare' ] }, | ||
depths => console.log(depths) | ||
) | ||
``` | ||
Now the evil query from before will tell the client this: | ||
@@ -201,1 +218,21 @@ | ||
## Future Work | ||
- [ ] Type-specific sub-depth limits, e.g. you can only descend 3 levels from an `Album` type, 5 levels from the `User` type, etc. | ||
- [ ] More customization options, like custom errors. | ||
## Documentation | ||
<a name="depthLimit"></a> | ||
### depthLimit(maxDepth, [options], [callback]) ⇒ <code>function</code> | ||
Creates a validator for the GraphQL query depth | ||
**Kind**: global function | ||
**Returns**: <code>function</code> - The validator function for GraphQL validation phase. | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| maxDepth | <code>Number</code> | The maximum allowed depth for any operation in a GraphQL document. | | ||
| [options] | <code>Object</code> | | | ||
| options.ignore | <code>String</code> \| <code>RegExp</code> \| <code>function</code> | Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that reaturns a boolean. | | ||
| [callback] | <code>function</code> | Called each time validation runs. Receives an Object which is a map of the depths for each operation. | | ||
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
11483
96
1
236
2
5
+ Addedarrify@^1.0.1
+ Addedarrify@1.0.1(transitive)