Skip to content

Commit e83084c

Browse files
committed
Root Level Limit: different take with onValidate
1 parent 181d7ed commit e83084c

File tree

4 files changed

+296
-238
lines changed

4 files changed

+296
-238
lines changed

packages/plugins/root-level-limitation/__tests__/root-level-limitation.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,51 @@ describe('root-level-limitation', () => {
6464
});
6565
expect(res.status).toBe(200);
6666
});
67+
68+
it('should not allow requests with max root level query and nested fragments', async () => {
69+
const res = await yoga.fetch('http://yoga/graphql', {
70+
method: 'POST',
71+
body: JSON.stringify({
72+
query: /* GraphQL */ `
73+
fragment QueryFragment on Query {
74+
topBooks {
75+
id
76+
}
77+
topProducts {
78+
id
79+
}
80+
}
81+
{
82+
...QueryFragment
83+
}
84+
`,
85+
}),
86+
headers: {
87+
'Content-Type': 'application/json',
88+
},
89+
});
90+
})
91+
92+
it('should allow requests with max root level query in comments', async () => {
93+
const res = await yoga.fetch('http://yoga/graphql', {
94+
method: 'POST',
95+
body: JSON.stringify({
96+
query: /* GraphQL */ `
97+
{
98+
# topBooks {
99+
# id
100+
# }
101+
topProducts {
102+
id
103+
}
104+
}
105+
`,
106+
}),
107+
headers: {
108+
'Content-Type': 'application/json',
109+
},
110+
});
111+
112+
expect(res.status).toBe(200);
113+
})
67114
});

packages/plugins/root-level-limitation/package.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,14 @@
3838
},
3939
"peerDependencies": {
4040
"@graphql-tools/utils": "^10.1.0",
41-
"@whatwg-node/server": "^0.9.32",
42-
"graphql": "^15.2.0 || ^16.0.0",
43-
"graphql-yoga": "^5.3.0"
41+
"@envelop/core": "^5.0.0",
42+
"graphql": "^15.2.0 || ^16.0.0"
4443
},
4544
"dependencies": {
4645
"tslib": "^2.5.2"
4746
},
4847
"devDependencies": {
49-
"@whatwg-node/fetch": "^0.9.17",
48+
"@envelop/core": "^5.0.0",
5049
"graphql": "^16.6.0",
5150
"graphql-yoga": "5.3.0"
5251
},
Lines changed: 28 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,47 @@
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';
25

3-
interface GraphQLParams {
4-
operationName?: string;
5-
query?: string;
6+
export interface RootLevelQueryLimitOptions {
7+
maxRootLevelFields: number;
68
}
79

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) {
2722
throw createGraphQLError('Query is too complex.', {
2823
extensions: {
2924
http: {
3025
spec: false,
3126
status: 400,
32-
headers: {
33-
Allow: 'POST',
34-
},
3527
},
3628
},
3729
});
3830
}
3931
}
40-
}
41-
42-
return true;
43-
},
32+
},
33+
};
4434
};
45-
}
4635

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+
}
6737

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+
)
7545
}
7646
}
77-
78-
return formattedString;
7947
}

0 commit comments

Comments
 (0)