Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 46706d4

Browse files
authored
Add maxQueryNodes limit (#98)
1 parent 81803a8 commit 46706d4

File tree

3 files changed

+109
-4
lines changed

3 files changed

+109
-4
lines changed

‎README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131

3232
const rule = createComplexityRule({
3333
// The maximum allowed query complexity, queries above this threshold will be rejected
34-
maximumComplexity: 1000,
34+
maximumComplexity: 1_000,
3535

3636
// The query variables. This is needed because the variables are not available
3737
// in the visitor of the graphql-js library
@@ -40,9 +40,16 @@ const rule = createComplexityRule({
4040
// The context object for the request (optional)
4141
context: {}
4242

43-
// specify operation name only when pass multi-operation documents
43+
// Specify operation name when evaluating multi-operation documents
4444
operationName?: string,
4545

46+
// The maximum number of query nodes to evaluate (fields, fragments, composite types).
47+
// If a query contains more than the specified number of nodes, the complexity rule will
48+
// throw an error, regardless of the complexity of the query.
49+
//
50+
// Default: 10_000
51+
maxQueryNodes?: 10_000,
52+
4653
// Optional callback function to retrieve the determined query complexity
4754
// Will be invoked whether the query is rejected or not
4855
// This can be used for logging or to implement rate limiting

‎src/QueryComplexity.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ export interface QueryComplexityOptions {
8585

8686
// Pass request context to the estimators via estimationContext
8787
context?: Record<string, any>;
88+
89+
// The maximum number of nodes to evaluate. If this is set, the query will be
90+
// rejected if it exceeds this number. (Includes fields, fragments, inline fragments, etc.)
91+
// Defaults to 10_000.
92+
maxQueryNodes?: number;
8893
}
8994

9095
function queryComplexityMessage(max: number, actual: number): string {
@@ -101,6 +106,7 @@ export function getComplexity(options: {
101106
variables?: Record<string, any>;
102107
operationName?: string;
103108
context?: Record<string, any>;
109+
maxQueryNodes?: number;
104110
}): number {
105111
const typeInfo = new TypeInfo(options.schema);
106112

@@ -118,6 +124,7 @@ export function getComplexity(options: {
118124
variables: options.variables,
119125
operationName: options.operationName,
120126
context: options.context,
127+
maxQueryNodes: options.maxQueryNodes,
121128
});
122129

123130
visit(options.query, visitWithTypeInfo(typeInfo, visitor));
@@ -140,6 +147,8 @@ export default class QueryComplexity {
140147
skipDirectiveDef: GraphQLDirective;
141148
variableValues: Record<string, any>;
142149
requestContext?: Record<string, any>;
150+
evaluatedNodes: number;
151+
maxQueryNodes: number;
143152

144153
constructor(context: ValidationContext, options: QueryComplexityOptions) {
145154
if (
@@ -154,7 +163,8 @@ export default class QueryComplexity {
154163
this.context = context;
155164
this.complexity = 0;
156165
this.options = options;
157-
166+
this.evaluatedNodes = 0;
167+
this.maxQueryNodes = options.maxQueryNodes ?? 10_000;
158168
this.includeDirectiveDef = this.context.getSchema().getDirective('include');
159169
this.skipDirectiveDef = this.context.getSchema().getDirective('skip');
160170
this.estimators = options.estimators;
@@ -274,7 +284,12 @@ export default class QueryComplexity {
274284
complexities: ComplexityMap,
275285
childNode: FieldNode | FragmentSpreadNode | InlineFragmentNode
276286
): ComplexityMap => {
277-
// let nodeComplexity = 0;
287+
this.evaluatedNodes++;
288+
if (this.evaluatedNodes >= this.maxQueryNodes) {
289+
throw new GraphQLError(
290+
'Query exceeds the maximum allowed number of nodes.'
291+
);
292+
}
278293
let innerComplexities = complexities;
279294

280295
let includeNode = true;

‎src/__tests__/QueryComplexity-test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,4 +939,87 @@ describe('QueryComplexity analysis', () => {
939939

940940
expect(errors).to.have.length(0);
941941
});
942+
943+
it('should reject queries that exceed the maximum number of fragment nodes', () => {
944+
const query = parse(`
945+
query {
946+
...F
947+
...F
948+
}
949+
fragment F on Query {
950+
scalar
951+
}
952+
`);
953+
954+
expect(() =>
955+
getComplexity({
956+
estimators: [simpleEstimator({ defaultComplexity: 1 })],
957+
schema,
958+
query,
959+
maxQueryNodes: 1,
960+
variables: {},
961+
})
962+
).to.throw('Query exceeds the maximum allowed number of nodes.');
963+
});
964+
965+
it('should reject queries that exceed the maximum number of field nodes', () => {
966+
const query = parse(`
967+
query {
968+
scalar
969+
scalar1: scalar
970+
scalar2: scalar
971+
scalar3: scalar
972+
scalar4: scalar
973+
scalar5: scalar
974+
scalar6: scalar
975+
scalar7: scalar
976+
}
977+
`);
978+
979+
expect(() =>
980+
getComplexity({
981+
estimators: [simpleEstimator({ defaultComplexity: 1 })],
982+
schema,
983+
query,
984+
maxQueryNodes: 1,
985+
variables: {},
986+
})
987+
).to.throw('Query exceeds the maximum allowed number of nodes.');
988+
});
989+
990+
it('should limit the number of query nodes to 10_000 by default', () => {
991+
const failingQuery = parse(`
992+
query {
993+
${Array.from({ length: 10_000 }, (_, i) => `scalar${i}: scalar`).join(
994+
'\n'
995+
)}
996+
}
997+
`);
998+
999+
expect(() =>
1000+
getComplexity({
1001+
estimators: [simpleEstimator({ defaultComplexity: 1 })],
1002+
schema,
1003+
query: failingQuery,
1004+
variables: {},
1005+
})
1006+
).to.throw('Query exceeds the maximum allowed number of nodes.');
1007+
1008+
const passingQuery = parse(`
1009+
query {
1010+
${Array.from({ length: 9999 }, (_, i) => `scalar${i}: scalar`).join(
1011+
'\n'
1012+
)}
1013+
}
1014+
`);
1015+
1016+
expect(() =>
1017+
getComplexity({
1018+
estimators: [simpleEstimator({ defaultComplexity: 1 })],
1019+
schema,
1020+
query: passingQuery,
1021+
variables: {},
1022+
})
1023+
).not.to.throw();
1024+
});
9421025
});

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /