diff --git a/README.md b/README.md index f701ce9..97355f3 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,14 @@ const ComplexityLimitRule = createComplexityLimitRule(1000, { }); ``` +By default, the validation rule applies a custom, lower cost factor for lists of introspection types, to prevent introspection queries from having unreasonably high costs. You can adjust this by setting `introspectionListFactor` on the configuration object. + +```js +const ComplexityLimitRule = createComplexityLimitRule(1000, { + introspectionListFactor: 10, // Default is 2. +}); +``` + [build-badge]: https://img.shields.io/travis/4Catalyzer/graphql-validation-complexity/master.svg [build]: https://travis-ci.org/4Catalyzer/graphql-validation-complexity diff --git a/src/index.js b/src/index.js index c26aef4..aa05c8c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ import { getVisitFn, GraphQLError, GraphQLNonNull, GraphQLList, GraphQLObjectType, } from 'graphql'; +import * as IntrospectionTypes from 'graphql/type/introspection'; export class CostCalculator { constructor() { @@ -46,15 +47,20 @@ export class ComplexityVisitor { scalarCost = 1, objectCost = 0, listFactor = 10, + + // Special list factor to make schema queries not have huge costs. + introspectionListFactor = 2, }) { this.context = context; this.scalarCost = scalarCost; this.objectCost = objectCost; this.listFactor = listFactor; + this.introspectionListFactor = introspectionListFactor; this.currentFragment = null; this.listDepth = 0; + this.introspectionListDepth = 0; this.rootCalculator = new CostCalculator(); this.fragmentCalculators = Object.create(null); @@ -80,7 +86,12 @@ export class ComplexityVisitor { if (type instanceof GraphQLNonNull) { this.enterType(type.ofType); } else if (type instanceof GraphQLList) { - ++this.listDepth; + if (this.isIntrospectionList(type)) { + ++this.introspectionListDepth; + } else { + ++this.listDepth; + } + this.enterType(type.ofType); } else { const fieldCost = type instanceof GraphQLObjectType ? @@ -89,13 +100,25 @@ export class ComplexityVisitor { } } + isIntrospectionList({ ofType }) { + let type = ofType; + if (type instanceof GraphQLNonNull) { + type = type.ofType; + } + + return IntrospectionTypes[type.name] === type; + } + getCalculator() { return this.currentFragment === null ? this.rootCalculator : this.fragmentCalculators[this.currentFragment]; } getDepthFactor() { - return this.listFactor ** this.listDepth; + return ( + this.listFactor ** this.listDepth * + this.introspectionListFactor ** this.introspectionListDepth + ); } leaveField() { @@ -106,7 +129,12 @@ export class ComplexityVisitor { if (type instanceof GraphQLNonNull) { this.leaveType(type.ofType); } else if (type instanceof GraphQLList) { - --this.listDepth; + if (this.isIntrospectionList(type)) { + --this.introspectionListDepth; + } else { + --this.listDepth; + } + this.leaveType(type.ofType); } } diff --git a/test/ComplexityVisitor.test.js b/test/ComplexityVisitor.test.js index c8ad450..3f0bfa8 100644 --- a/test/ComplexityVisitor.test.js +++ b/test/ComplexityVisitor.test.js @@ -1,5 +1,11 @@ -import { parse, TypeInfo, ValidationContext, visit, visitWithTypeInfo } - from 'graphql'; +import { + introspectionQuery, + parse, + TypeInfo, + ValidationContext, + visit, + visitWithTypeInfo, +} from 'graphql'; import { ComplexityVisitor } from '../src'; @@ -128,4 +134,16 @@ describe('ComplexityVisitor', () => { expect(visitor.getCost()).toBe(54); }); }); + + describe('introspection query', () => { + it('should calculate a reduced cost for the introspection query', () => { + const ast = parse(introspectionQuery); + + const context = new ValidationContext(schema, ast, typeInfo); + const visitor = new ComplexityVisitor(context, {}); + + visit(ast, visitWithTypeInfo(typeInfo, visitor)); + expect(visitor.getCost()).toBeLessThan(1000); + }); + }); });