|
1 | | -import { createGraphQLError } from '@graphql-tools/utils'; |
| 1 | +import { createGraphQLError, getRootTypes } from '@graphql-tools/utils'; |
| 2 | +import { Plugin } from '@envelop/core'; |
| 3 | +import type { ValidationRule } from 'graphql/validation/ValidationContext'; |
| 4 | +import { isObjectType } from 'graphql'; |
2 | 5 |
|
3 | | -interface GraphQLParams { |
4 | | - operationName?: string; |
5 | | - query?: string; |
| 6 | +export interface RootLevelQueryLimitOptions { |
| 7 | + maxRootLevelFields: number; |
6 | 8 | } |
7 | 9 |
|
8 | | -export function rootLevelQueryLimit({ maxRootLevelFields }: { maxRootLevelFields: number }) { |
9 | | - return { |
10 | | - onParams({ params }: unknown) { |
11 | | - const { query, operationName } = params as GraphQLParams; |
12 | | - |
13 | | - if (operationName?.includes('IntrospectionQuery')) return true; |
14 | | - |
15 | | - const newQuery = formatQuery(query || ''); |
16 | | - const linesArray = newQuery.split('\n'); |
17 | | - |
18 | | - let countLeadingSpacesTwo = 0; |
19 | | - |
20 | | - for (const line of linesArray) { |
21 | | - const leadingSpaces = line?.match(/^\s*/)?.[0]?.length || 0; |
22 | | - |
23 | | - if (leadingSpaces === 4 && line[leadingSpaces] !== ')') { |
24 | | - countLeadingSpacesTwo++; |
25 | | - |
26 | | - if (countLeadingSpacesTwo > maxRootLevelFields * 2) { |
| 10 | +export function createRootLevelQueryLimitRule(opts: RootLevelQueryLimitOptions): ValidationRule { |
| 11 | + const { maxRootLevelFields } = opts; |
| 12 | + |
| 13 | + return function rootLevelQueryLimitRule (context) { |
| 14 | + const rootTypes = getRootTypes(context.getSchema()); |
| 15 | + let rootFieldCount = 0; |
| 16 | + return { |
| 17 | + Field() { |
| 18 | + const parentType = context.getParentType(); |
| 19 | + if (isObjectType(parentType) && rootTypes.has(parentType)) { |
| 20 | + rootFieldCount++; |
| 21 | + if (rootFieldCount > maxRootLevelFields) { |
27 | 22 | throw createGraphQLError('Query is too complex.', { |
28 | 23 | extensions: { |
29 | 24 | http: { |
30 | 25 | spec: false, |
31 | 26 | status: 400, |
32 | | - headers: { |
33 | | - Allow: 'POST', |
34 | | - }, |
35 | 27 | }, |
36 | 28 | }, |
37 | 29 | }); |
38 | 30 | } |
39 | 31 | } |
40 | | - } |
41 | | - |
42 | | - return true; |
43 | | - }, |
| 32 | + }, |
| 33 | + }; |
44 | 34 | }; |
45 | | -} |
46 | 35 |
|
47 | | -function formatQuery(queryString: string) { |
48 | | - queryString = queryString.replace(/^\s+/gm, ''); |
49 | | - |
50 | | - let indentLevel = 0; |
51 | | - let formattedString = ''; |
52 | | - |
53 | | - for (let i = 0; i < queryString.length; i++) { |
54 | | - const char = queryString[i]; |
55 | | - |
56 | | - if (char === '{' || char === '(') { |
57 | | - formattedString += char; |
58 | | - indentLevel++; |
59 | | - // formattedString += ' '.repeat(indentLevel * 4); |
60 | | - } else if (char === '}' || char === ')') { |
61 | | - indentLevel--; |
62 | | - |
63 | | - if (formattedString[formattedString.length - 1] !== '\n') |
64 | | - formattedString = formattedString.trim().replace(/\n$/, ''); |
65 | | - |
66 | | - if (char === ')') formattedString += char; |
| 36 | +} |
67 | 37 |
|
68 | | - if (char === '}') formattedString += '\n' + ' '.repeat(indentLevel * 4) + char; |
69 | | - } else if (char === '\n') { |
70 | | - if (queryString[i + 1] !== '\n' && queryString[i + 1] !== undefined) { |
71 | | - formattedString += char + ' '.repeat(indentLevel * 4); |
72 | | - } |
73 | | - } else { |
74 | | - formattedString += char; |
| 38 | +export function rootLevelQueryLimit(opts: RootLevelQueryLimitOptions): Plugin { |
| 39 | + const rootLevelQueryLimitRule = createRootLevelQueryLimitRule(opts); |
| 40 | + return { |
| 41 | + onValidate({ addValidationRule }) { |
| 42 | + addValidationRule( |
| 43 | + rootLevelQueryLimitRule |
| 44 | + ) |
75 | 45 | } |
76 | 46 | } |
77 | | - |
78 | | - return formattedString; |
79 | 47 | } |
0 commit comments