Skip to content

Commit c6bf0f5

Browse files
committed
Add support for Interface and Union types max complexity #26
1 parent 152b1bf commit c6bf0f5

File tree

4 files changed

+264
-14
lines changed

4 files changed

+264
-14
lines changed

src/QueryComplexity.ts

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
GraphQLField, isCompositeType, GraphQLCompositeType, GraphQLFieldMap,
2020
GraphQLSchema, DocumentNode, TypeInfo,
2121
visit, visitWithTypeInfo,
22-
GraphQLDirective,
22+
GraphQLDirective, isAbstractType,
2323
} from 'graphql';
2424
import {
2525
GraphQLUnionType,
@@ -48,6 +48,11 @@ export type ComplexityEstimator = (options: ComplexityEstimatorArgs) => number |
4848
// Complexity can be anything that is supported by the configured estimators
4949
export type Complexity = any;
5050

51+
// Map of complexities for possible types (of Union, Interface types)
52+
type ComplexityMap = {
53+
[typeName: string]: number,
54+
}
55+
5156
export interface QueryComplexityOptions {
5257
// The maximum allowed query complexity, queries above this threshold will be rejected
5358
maximumComplexity: number,
@@ -185,9 +190,19 @@ export default class QueryComplexity {
185190
if (typeDef instanceof GraphQLObjectType || typeDef instanceof GraphQLInterfaceType) {
186191
fields = typeDef.getFields();
187192
}
188-
return node.selectionSet.selections.reduce(
189-
(total: number, childNode: FieldNode | FragmentSpreadNode | InlineFragmentNode) => {
190-
let nodeComplexity = 0;
193+
194+
// Determine all possible types of the current node
195+
let possibleTypeNames: string[];
196+
if (isAbstractType(typeDef)) {
197+
possibleTypeNames = this.context.getSchema().getPossibleTypes(typeDef).map(t => t.name);
198+
} else {
199+
possibleTypeNames = [typeDef.name];
200+
}
201+
202+
// Collect complexities for all possible types individually
203+
const selectionSetComplexities: ComplexityMap = node.selectionSet.selections.reduce(
204+
(complexities: ComplexityMap, childNode: FieldNode | FragmentSpreadNode | InlineFragmentNode) => {
205+
// let nodeComplexity = 0;
191206

192207
let includeNode = true;
193208
let skipNode = false;
@@ -209,7 +224,7 @@ export default class QueryComplexity {
209224
});
210225

211226
if (!includeNode || skipNode) {
212-
return total;
227+
return complexities;
213228
}
214229

215230
switch (childNode.kind) {
@@ -247,7 +262,11 @@ export default class QueryComplexity {
247262
const tmpComplexity = estimator(estimatorArgs);
248263

249264
if (typeof tmpComplexity === 'number' && !isNaN(tmpComplexity)) {
250-
nodeComplexity = tmpComplexity;
265+
complexities = addComplexities(
266+
tmpComplexity,
267+
complexities,
268+
possibleTypeNames,
269+
);
251270
return true;
252271
}
253272

@@ -268,7 +287,22 @@ export default class QueryComplexity {
268287
const fragmentType = assertCompositeType(
269288
this.context.getSchema().getType(fragment.typeCondition.name.value)
270289
);
271-
nodeComplexity = this.nodeComplexity(fragment, fragmentType);
290+
const nodeComplexity = this.nodeComplexity(fragment, fragmentType);
291+
if (isAbstractType(fragmentType)) {
292+
// Add fragment complexity for all possible types
293+
complexities = addComplexities(
294+
nodeComplexity,
295+
complexities,
296+
this.context.getSchema().getPossibleTypes(fragmentType).map(t => t.name),
297+
);
298+
} else {
299+
// Add complexity for object type
300+
complexities = addComplexities(
301+
nodeComplexity,
302+
complexities,
303+
[fragmentType.name],
304+
);
305+
}
272306
break;
273307
}
274308
case Kind.INLINE_FRAGMENT: {
@@ -279,16 +313,41 @@ export default class QueryComplexity {
279313
);
280314
}
281315

282-
nodeComplexity = this.nodeComplexity(childNode, inlineFragmentType);
316+
const nodeComplexity = this.nodeComplexity(childNode, inlineFragmentType);
317+
if (isAbstractType(inlineFragmentType)) {
318+
// Add fragment complexity for all possible types
319+
complexities = addComplexities(
320+
nodeComplexity,
321+
complexities,
322+
this.context.getSchema().getPossibleTypes(inlineFragmentType).map(t => t.name),
323+
);
324+
} else {
325+
// Add complexity for object type
326+
complexities = addComplexities(
327+
nodeComplexity,
328+
complexities,
329+
[inlineFragmentType.name],
330+
);
331+
}
283332
break;
284333
}
285334
default: {
286-
nodeComplexity = this.nodeComplexity(childNode, typeDef);
335+
complexities = addComplexities(
336+
this.nodeComplexity(childNode, typeDef),
337+
complexities,
338+
possibleTypeNames,
339+
);
287340
break;
288341
}
289342
}
290-
return Math.max(nodeComplexity, 0) + total;
291-
}, 0);
343+
344+
return complexities;
345+
}, {});
346+
// Only return max complexity of all possible types
347+
if (!selectionSetComplexities) {
348+
return NaN;
349+
}
350+
return Math.max(...Object.values(selectionSetComplexities), 0);
292351
}
293352
return 0;
294353
}
@@ -306,3 +365,24 @@ export default class QueryComplexity {
306365
));
307366
}
308367
}
368+
369+
/**
370+
* Adds a complexity to the complexity map for all possible types
371+
* @param complexity
372+
* @param complexityMap
373+
* @param possibleTypes
374+
*/
375+
function addComplexities(
376+
complexity: number,
377+
complexityMap: ComplexityMap,
378+
possibleTypes: string[],
379+
): ComplexityMap {
380+
for (const type of possibleTypes) {
381+
if (complexityMap.hasOwnProperty(type)) {
382+
complexityMap[type] = complexityMap[type] + complexity;
383+
} else {
384+
complexityMap[type] = complexity;
385+
}
386+
}
387+
return complexityMap;
388+
}

src/__tests__/QueryComplexity-test.ts

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ describe('QueryComplexity analysis', () => {
408408
);
409409
});
410410

411-
it('should return NaN when no astNode available on field when use directiveEstimator', () => {
411+
it('should return NaN when no astNode available on field when using directiveEstimator', () => {
412412
const ast = parse(`
413413
query {
414414
_service {
@@ -510,4 +510,151 @@ describe('QueryComplexity analysis', () => {
510510
});
511511
expect(complexity).to.equal(3);
512512
});
513+
514+
it('should calculate max complexity for nested fragment on union type', () => {
515+
const query = parse(`
516+
query Primary {
517+
union {
518+
...on Union {
519+
...on Item {
520+
complexScalar1: complexScalar
521+
}
522+
}
523+
...on SecondItem {
524+
scalar
525+
}
526+
...on Item {
527+
complexScalar2: complexScalar
528+
}
529+
}
530+
}
531+
`);
532+
533+
const complexity = getComplexity({
534+
estimators: [
535+
fieldExtensionsEstimator(),
536+
simpleEstimator({defaultComplexity: 0})
537+
],
538+
schema,
539+
query,
540+
});
541+
expect(complexity).to.equal(40);
542+
});
543+
544+
it('should calculate max complexity for nested fragment on union type + named fragment', () => {
545+
const query = parse(`
546+
query Primary {
547+
union {
548+
...F
549+
...on SecondItem {
550+
scalar
551+
}
552+
...on Item {
553+
complexScalar2: complexScalar
554+
}
555+
}
556+
}
557+
fragment F on Union {
558+
...on Item {
559+
complexScalar1: complexScalar
560+
}
561+
}
562+
`);
563+
564+
const complexity = getComplexity({
565+
estimators: [
566+
fieldExtensionsEstimator(),
567+
simpleEstimator({defaultComplexity: 0})
568+
],
569+
schema,
570+
query,
571+
});
572+
expect(complexity).to.equal(40);
573+
});
574+
575+
it('should calculate max complexity for multiple interfaces', () => {
576+
const query = parse(`
577+
query Primary {
578+
interface {
579+
...on Query {
580+
complexScalar
581+
}
582+
...on SecondItem {
583+
name
584+
name2: name
585+
}
586+
}
587+
}
588+
`);
589+
590+
const complexity = getComplexity({
591+
estimators: [
592+
fieldExtensionsEstimator(),
593+
simpleEstimator({defaultComplexity: 1})
594+
],
595+
schema,
596+
query,
597+
});
598+
expect(complexity).to.equal(21);
599+
});
600+
601+
it('should calculate max complexity for multiple interfaces with nesting', () => {
602+
const query = parse(`
603+
query Primary {
604+
interface {
605+
...on Query {
606+
complexScalar
607+
...on Query {
608+
a: complexScalar
609+
}
610+
}
611+
...on SecondItem {
612+
name
613+
name2: name
614+
}
615+
}
616+
}
617+
`);
618+
619+
const complexity = getComplexity({
620+
estimators: [
621+
fieldExtensionsEstimator(),
622+
simpleEstimator({defaultComplexity: 1})
623+
],
624+
schema,
625+
query,
626+
});
627+
expect(complexity).to.equal(41); // 1 for interface, 20 * 2 for complexScalar
628+
});
629+
630+
it('should calculate max complexity for multiple interfaces with nesting + named fragment', () => {
631+
const query = parse(`
632+
query Primary {
633+
interface {
634+
...F
635+
...on SecondItem {
636+
name
637+
name2: name
638+
}
639+
}
640+
}
641+
642+
fragment F on Query {
643+
complexScalar
644+
...on Query {
645+
a: complexScalar
646+
}
647+
}
648+
`);
649+
650+
const complexity = getComplexity({
651+
estimators: [
652+
fieldExtensionsEstimator(),
653+
simpleEstimator({defaultComplexity: 1})
654+
],
655+
schema,
656+
query,
657+
});
658+
expect(complexity).to.equal(41); // 1 for interface, 20 * 2 for complexScalar
659+
});
513660
});

src/__tests__/fixtures/schema.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ const NameInterface = new GraphQLInterfaceType({
6363
resolveType: () => Item
6464
});
6565

66+
const UnionInterface = new GraphQLInterfaceType({
67+
name: 'UnionInterface',
68+
fields: () => ({
69+
union: { type: Union }
70+
}),
71+
resolveType: () => Item
72+
});
73+
6674
const SecondItem = new GraphQLObjectType({
6775
name: 'SecondItem',
6876
fields: () => ({
@@ -92,7 +100,15 @@ const SDLInterface = new GraphQLInterfaceType({
92100
fields: {
93101
sdl: { type: GraphQLString }
94102
},
95-
resolveType: () => '"SDL"'
103+
resolveType: () => 'SDL'
104+
});
105+
106+
const SDL = new GraphQLObjectType({
107+
name: 'SDL',
108+
fields: {
109+
sdl: { type: GraphQLString }
110+
},
111+
interfaces: () => [SDLInterface],
96112
});
97113

98114
const Query = new GraphQLObjectType({
@@ -145,6 +161,12 @@ const Query = new GraphQLObjectType({
145161
},
146162
_service: {type: SDLInterface},
147163
}),
164+
interfaces: () => [NameInterface, UnionInterface,]
148165
});
149166

150-
export default new GraphQLSchema({ query: Query });
167+
export default new GraphQLSchema({
168+
query: Query,
169+
types: [
170+
SDL,
171+
],
172+
});

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
},
1818
"lib": [
1919
"es2015",
20+
"es2018",
2021
"esnext.asynciterable",
2122
"dom"
2223
]

0 commit comments

Comments
 (0)