forked from ashfurrow/graphql-depth-limit
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
109 lines (100 loc) · 3.66 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
const {
GraphQLError,
Kind
} = require('graphql')
const arrify = require('arrify')
/**
* 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 {Array<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 {
const { definitions } = validationContext.getDocument()
const fragments = getFragments(definitions)
const queries = getQueriesAndMutations(definitions)
const queryDepths = {}
for (let name in queries) {
queryDepths[name] = determineDepth(queries[name], fragments, 0, maxDepth, validationContext, name, options)
}
callback(queryDepths)
return validationContext
} catch (err) {
/* istanbul ignore next */ { // eslint-disable-line no-lone-blocks
console.error(err)
throw err
}
}
}
module.exports = depthLimit
function getFragments(definitions) {
return definitions.reduce((map, definition) => {
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
map[definition.name.value] = definition
}
return map
}, {})
}
// this will actually get both queries and mutations. we can basically treat those the same
function getQueriesAndMutations(definitions) {
return definitions.reduce((map, definition) => {
if (definition.kind === Kind.OPERATION_DEFINITION) {
map[definition.name ? definition.name.value : ''] = definition
}
return map
}, {})
}
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 ])
)
}
switch (node.kind) {
case Kind.FIELD:
// 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
}
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, 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, options)
))
/* istanbul ignore next */
default:
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
/* istanbul ignore next */
default:
throw new Error(`Invalid ignore option: ${rule}`)
}
}
return false
}