Skip to content

Commit b7bc4df

Browse files
committed
Add multiple complexity estimator support
1 parent 34cb872 commit b7bc4df

File tree

7 files changed

+83
-22
lines changed

7 files changed

+83
-22
lines changed

.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@
187187
"no-unneeded-ternary": 2,
188188
"no-unreachable": 2,
189189
"no-unused-expressions": 2,
190-
"no-unused-vars": [2, {"vars": "all", "args": "after-used"}],
190+
"typescript/no-unused-vars": 2,
191191
"no-use-before-define": 0,
192192
"no-useless-call": 2,
193193
"no-var": 2,

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const rule = queryComplexity({
2828
variables: {},
2929

3030
// Optional callback function to retrieve the determined query complexity
31-
// Will be invoked weather the query is rejected or not
31+
// Will be invoked whether the query is rejected or not
3232
// This can be used for logging or to implement rate limiting
3333
onComplete: (complexity: number) => {console.log('Determined query complexity: ', complexity)},
3434

@@ -119,7 +119,9 @@ app.use('/api', graphqlHTTP(async (request, response, {variables}) => ({
119119
})));
120120
```
121121

122-
## Credits
122+
## Prior Art
123+
124+
This project is inspired by the following prior projects:
125+
126+
- Query complexity analysis in the [Sangria GraphQL](http://sangria-graphql.org/) implementation.
123127

124-
This project is heavily inspired by the query complexity analysis in the
125-
[Sangria GraphQL](http://sangria-graphql.org/) implementation.

src/QueryComplexity.ts

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
FragmentSpreadNode,
1515
InlineFragmentNode,
1616
assertCompositeType,
17-
GraphQLField,
17+
GraphQLField, isCompositeType, GraphQLCompositeType,
1818
} from 'graphql';
1919
import {
2020
GraphQLUnionType,
@@ -24,22 +24,49 @@ import {
2424
getNamedType,
2525
GraphQLError
2626
} from 'graphql';
27+
import {simpleEstimator} from './estimators';
2728

28-
type ComplexityResolver = (args: any, complexity: number) => number;
29+
/**
30+
* @deprecated Use new complexity resolver
31+
*/
32+
type SimpleComplexityEstimator = (args: any, complexity: number) => number;
33+
34+
export type ComplexityEstimatorArgs = {
35+
type: GraphQLCompositeType,
36+
field: GraphQLField<any, any>,
37+
args: {[key: string]: any},
38+
childComplexity: number
39+
}
40+
41+
export type ComplexityEstimator = (options: ComplexityEstimatorArgs) => number | void;
2942

3043
type ComplexityGraphQLField<TSource, TContext> = GraphQLField<TSource, TContext> & {
31-
complexity?: ComplexityResolver | number | undefined
44+
complexity?: SimpleComplexityEstimator | number | undefined
3245
}
3346

3447
type ComplexityGraphQLFieldMap<TSource, TContext> = {
3548
[key: string]: ComplexityGraphQLField<TSource, TContext>
3649
}
3750

3851
export interface QueryComplexityOptions {
52+
// The maximum allowed query complexity, queries above this threshold will be rejected
3953
maximumComplexity: number,
54+
55+
// The query variables. This is needed because the variables are not available
56+
// in the visitor of the graphql-js library
4057
variables?: Object,
58+
59+
// Optional callback function to retrieve the determined query complexity
60+
// Will be invoked whether the query is rejected or not
61+
// This can be used for logging or to implement rate limiting
4162
onComplete?: (complexity: number) => void,
42-
createError?: (max: number, actual: number) => GraphQLError
63+
64+
// Optional function to create a custom error
65+
createError?: (max: number, actual: number) => GraphQLError,
66+
67+
// An array of complexity estimators to use if no estimator or value is defined
68+
// in the field configuration
69+
estimators?: Array<ComplexityEstimator>;
4370
}
4471

4572
function queryComplexityMessage(max: number, actual: number): string {
@@ -53,8 +80,8 @@ export default class QueryComplexity {
5380
context: ValidationContext;
5481
complexity: number;
5582
options: QueryComplexityOptions;
56-
fragments: {[name: string]: FragmentDefinitionNode};
5783
OperationDefinition: Object;
84+
estimators: Array<ComplexityEstimator>;
5885

5986
constructor(
6087
context: ValidationContext,
@@ -67,6 +94,9 @@ export default class QueryComplexity {
6794
this.context = context;
6895
this.complexity = 0;
6996
this.options = options;
97+
this.estimators = options.estimators || [
98+
simpleEstimator()
99+
];
70100

71101
this.OperationDefinition = {
72102
enter: this.onOperationDefinitionEnter,
@@ -135,7 +165,7 @@ export default class QueryComplexity {
135165
const fieldType = getNamedType(field.type);
136166

137167
// Get arguments
138-
let args;
168+
let args: {[key: string]: any};
139169
try {
140170
args = getArgumentValues(field, childNode, this.options.variables || {});
141171
} catch (e) {
@@ -144,11 +174,7 @@ export default class QueryComplexity {
144174

145175
// Check if we have child complexity
146176
let childComplexity = 0;
147-
if (
148-
fieldType instanceof GraphQLObjectType ||
149-
fieldType instanceof GraphQLInterfaceType ||
150-
fieldType instanceof GraphQLUnionType
151-
) {
177+
if (isCompositeType(fieldType)) {
152178
childComplexity = this.nodeComplexity(childNode, fieldType);
153179
}
154180

@@ -158,7 +184,31 @@ export default class QueryComplexity {
158184
} else if (typeof field.complexity === 'function') {
159185
nodeComplexity = field.complexity(args, childComplexity);
160186
} else {
161-
nodeComplexity = this.getDefaultComplexity(args, childComplexity);
187+
// Run estimators one after another and return first valid complexity
188+
// score
189+
const estimatorArgs: ComplexityEstimatorArgs = {
190+
childComplexity,
191+
args,
192+
field,
193+
type: typeDef
194+
};
195+
const validScore = this.estimators.find(estimator => {
196+
const tmpComplexity = estimator(estimatorArgs);
197+
198+
if (typeof tmpComplexity === 'number') {
199+
nodeComplexity = tmpComplexity;
200+
return true;
201+
}
202+
203+
return false;
204+
});
205+
if (!validScore) {
206+
throw new Error(
207+
`No complexity could be calculated for field ${typeDef.astNode}.${field.name}. ` +
208+
'Make sure you always have at least one estimator configured that returns a value.'
209+
);
210+
}
211+
// nodeComplexity = this.getDefaultComplexity(args, childComplexity);
162212
}
163213
break;
164214
}
@@ -205,8 +255,4 @@ export default class QueryComplexity {
205255
this.complexity
206256
));
207257
}
208-
209-
getDefaultComplexity(args: Object, childScore: number): number {
210-
return 1 + childScore;
211-
}
212258
}

src/estimators/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {default as simpleEstimator} from './simple';

src/estimators/simple.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {ComplexityEstimator, ComplexityEstimatorArgs} from '../QueryComplexity';
2+
3+
export default function (options?: {defaultComplexity?: number}): ComplexityEstimator {
4+
const defaultComplexity = options && typeof options.defaultComplexity === 'number' ? options.defaultComplexity : 1;
5+
return (args: ComplexityEstimatorArgs) => {
6+
return defaultComplexity + args.childComplexity;
7+
};
8+
}

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import { ValidationContext } from 'graphql';
77
import QueryComplexity from './QueryComplexity';
88
import { QueryComplexityOptions } from './QueryComplexity';
9+
10+
export * from './estimators';
11+
912
export default function createQueryComplexityValidator(options: QueryComplexityOptions): Function {
1013
return (context: ValidationContext): QueryComplexity => {
1114
return new QueryComplexity(context, options);

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
},
1717
"lib": [
1818
"es2015",
19-
"esnext.asynciterable"
19+
"esnext.asynciterable",
20+
"dom"
2021
]
2122
},
2223
"include": [

0 commit comments

Comments
 (0)