Skip to content

Commit d05b3f6

Browse files
authored
Merge pull request #33 from slicknode/feature/union-interface-support
Return max complexity for UnionType / Interfaces
2 parents 8afd2dd + c6bf0f5 commit d05b3f6

File tree

4 files changed

+293
-17
lines changed

4 files changed

+293
-17
lines changed

src/QueryComplexity.ts

Lines changed: 92 additions & 14 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,
@@ -179,16 +184,25 @@ export default class QueryComplexity {
179184
nodeComplexity(
180185
node: FieldNode | FragmentDefinitionNode | InlineFragmentNode | OperationDefinitionNode,
181186
typeDef: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType,
182-
complexity: number = 0
183187
): number {
184188
if (node.selectionSet) {
185189
let fields:GraphQLFieldMap<any, any> = {};
186190
if (typeDef instanceof GraphQLObjectType || typeDef instanceof GraphQLInterfaceType) {
187191
fields = typeDef.getFields();
188192
}
189-
return complexity + node.selectionSet.selections.reduce(
190-
(total: number, childNode: FieldNode | FragmentSpreadNode | InlineFragmentNode) => {
191-
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;
192206

193207
let includeNode = true;
194208
let skipNode = false;
@@ -210,7 +224,7 @@ export default class QueryComplexity {
210224
});
211225

212226
if (!includeNode || skipNode) {
213-
return total;
227+
return complexities;
214228
}
215229

216230
switch (childNode.kind) {
@@ -248,7 +262,11 @@ export default class QueryComplexity {
248262
const tmpComplexity = estimator(estimatorArgs);
249263

250264
if (typeof tmpComplexity === 'number' && !isNaN(tmpComplexity)) {
251-
nodeComplexity = tmpComplexity;
265+
complexities = addComplexities(
266+
tmpComplexity,
267+
complexities,
268+
possibleTypeNames,
269+
);
252270
return true;
253271
}
254272

@@ -269,30 +287,69 @@ export default class QueryComplexity {
269287
const fragmentType = assertCompositeType(
270288
this.context.getSchema().getType(fragment.typeCondition.name.value)
271289
);
272-
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+
}
273306
break;
274307
}
275308
case Kind.INLINE_FRAGMENT: {
276309
let inlineFragmentType = typeDef;
277310
if (childNode.typeCondition && childNode.typeCondition.name) {
278-
// $FlowFixMe: Not sure why flow thinks this can still be NULL
279311
inlineFragmentType = assertCompositeType(
280312
this.context.getSchema().getType(childNode.typeCondition.name.value)
281313
);
282314
}
283315

284-
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+
}
285332
break;
286333
}
287334
default: {
288-
nodeComplexity = this.nodeComplexity(childNode, typeDef);
335+
complexities = addComplexities(
336+
this.nodeComplexity(childNode, typeDef),
337+
complexities,
338+
possibleTypeNames,
339+
);
289340
break;
290341
}
291342
}
292-
return Math.max(nodeComplexity, 0) + total;
293-
}, complexity);
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);
294351
}
295-
return complexity;
352+
return 0;
296353
}
297354

298355
createError(): GraphQLError {
@@ -308,3 +365,24 @@ export default class QueryComplexity {
308365
));
309366
}
310367
}
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: 176 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 {
@@ -482,4 +482,179 @@ describe('QueryComplexity analysis', () => {
482482
});
483483
expect(complexity2).to.equal(20);
484484
});
485+
486+
it('should calculate max complexity for fragment on union type', () => {
487+
const query = parse(`
488+
query Primary {
489+
union {
490+
...on Item {
491+
scalar
492+
}
493+
...on SecondItem {
494+
scalar
495+
}
496+
...on SecondItem {
497+
scalar
498+
}
499+
}
500+
}
501+
`);
502+
503+
const complexity = getComplexity({
504+
estimators: [
505+
fieldExtensionsEstimator(),
506+
simpleEstimator({defaultComplexity: 1})
507+
],
508+
schema,
509+
query,
510+
});
511+
expect(complexity).to.equal(3);
512+
});
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+
});
485660
});

0 commit comments

Comments
 (0)