Skip to content

Commit 2de6a00

Browse files
committed
Added fieldConfigEstimator / legacyEstimator tests
1 parent 16291df commit 2de6a00

File tree

15 files changed

+1397
-13
lines changed

15 files changed

+1397
-13
lines changed

README.md

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ npm install -S graphql-query-complexity
1919
Create the rule with a maximum query complexity:
2020

2121
```javascript
22+
import queryComplexity, {
23+
simpleEstimator
24+
} from 'graphql-query-complexity';
25+
2226
const rule = queryComplexity({
2327
// The maximum allowed query complexity, queries above this threshold will be rejected
2428
maximumComplexity: 1000,
@@ -35,10 +39,69 @@ const rule = queryComplexity({
3539
// Optional function to create a custom error
3640
createError: (max: number, actual: number) => {
3741
return new GraphQLError(`Query is too complex: ${actual}. Maximum allowed complexity: ${max}`);
38-
}
42+
},
43+
44+
// Add any number of estimators. The estimators are invoked in order, the first
45+
// numeric value that is being returned by an estimator is used as the field complexity.
46+
// If no estimator returns a value, an exception is raised.
47+
estimators: [
48+
// Add more estimators here...
49+
50+
// This will assign each field a complexity of 1 if no other estimator
51+
// returned a value.
52+
simpleEstimator({
53+
defaultComplexity: 1
54+
})
55+
]
3956
});
4057
```
4158

59+
## Configuration / Complexity Estimators
60+
61+
The complexity calculation of a GraphQL query can be customized with so called complexity estimators.
62+
A complexity estimator is a simple function that calculates the complexity for a field. You can add
63+
any number of complexity estimators to the rule, which are then executed one after another.
64+
The first estimator that returns a numeric complexity value determines the complexity for that field.
65+
66+
At least one estimator has to return a complexity value, otherwise an exception is raised. You can
67+
for example use the [simpleEstimator](./src/estimators/simple/README.md) as the last estimator
68+
in your chain to define a default value.
69+
70+
You can use any of the available estimators to calculate the complexity of a field
71+
or write your own:
72+
73+
* **[`simpleEstimator`](src/estimators/simple/README.md):** The simple estimator returns a fixed complexity for each field. Can be used as
74+
last estimator in the chain for a default value.
75+
* **[`fieldConfigEstimator`](src/estimators/simple/README.md):** The field config estimator lets you set a numeric value or a custom estimator
76+
function in the field config of your schema.
77+
* **[`legacyEstimator`](src/estimators/legacy/README.md):** The legacy estimator implements the logic of previous versions. Can be used
78+
to gradually migrate your codebase to new estimators.
79+
* PR welcome...
80+
81+
82+
## Creating Custom Estimators
83+
84+
An estimator has the following function signature:
85+
86+
```typescript
87+
type ComplexityEstimatorArgs = {
88+
// The composite type (interface, object, union) that the evaluated field belongs to
89+
type: GraphQLCompositeType,
90+
91+
// The GraphQLField that is being evaluated
92+
field: GraphQLField<any, any>,
93+
94+
// The input arguments of the field
95+
args: {[key: string]: any},
96+
97+
// The complexity of all child selections for that field
98+
childComplexity: number
99+
}
100+
101+
type ComplexityEstimator = (options: ComplexityEstimatorArgs) => number | void;
102+
```
103+
104+
42105
## Customizing complexity calculation
43106

44107
By default, every field gets a complexity of 1. Let's look at the following example query:

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "0.1.2",
44
"description": "Validation rule for GraphQL query complexity analysis",
55
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
67
"scripts": {
78
"lint": "eslint src/**/*.ts",
89
"clean": "rimraf dist/*",

src/QueryComplexity.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
FragmentSpreadNode,
1515
InlineFragmentNode,
1616
assertCompositeType,
17-
GraphQLField, isCompositeType, GraphQLCompositeType,
17+
GraphQLField, isCompositeType, GraphQLCompositeType, GraphQLFieldMap,
1818
} from 'graphql';
1919
import {
2020
GraphQLUnionType,
@@ -29,22 +29,23 @@ import {
2929
legacyEstimator
3030
} from './estimators';
3131

32+
declare module 'graphql/type/definition' {
33+
export interface GraphQLField<TSource, TContext, TArgs = { [argName: string]: any }> {
34+
complexity?: Complexity;
35+
}
36+
}
37+
3238
export type ComplexityEstimatorArgs = {
3339
type: GraphQLCompositeType,
34-
field: ComplexityGraphQLField<any, any>,
40+
field: GraphQLField<any, any>,
3541
args: {[key: string]: any},
3642
childComplexity: number
3743
}
3844

3945
export type ComplexityEstimator = (options: ComplexityEstimatorArgs) => number | void;
4046

41-
type ComplexityGraphQLField<TSource, TContext> = GraphQLField<TSource, TContext> & {
42-
complexity?: any
43-
}
44-
45-
type ComplexityGraphQLFieldMap<TSource, TContext> = {
46-
[key: string]: ComplexityGraphQLField<TSource, TContext>
47-
}
47+
// Complexity can be anything that is supported by the configured estimators
48+
export type Complexity = any;
4849

4950
export interface QueryComplexityOptions {
5051
// The maximum allowed query complexity, queries above this threshold will be rejected
@@ -62,8 +63,7 @@ export interface QueryComplexityOptions {
6263
// Optional function to create a custom error
6364
createError?: (max: number, actual: number) => GraphQLError,
6465

65-
// An array of complexity estimators to use if no estimator or value is defined
66-
// in the field configuration
66+
// An array of complexity estimators to use for estimating the complexity
6767
estimators?: Array<ComplexityEstimator>;
6868
}
6969

@@ -146,7 +146,7 @@ export default class QueryComplexity {
146146
complexity: number = 0
147147
): number {
148148
if (node.selectionSet) {
149-
let fields:ComplexityGraphQLFieldMap<any, any> = {};
149+
let fields:GraphQLFieldMap<any, any> = {};
150150
if (typeDef instanceof GraphQLObjectType || typeDef instanceof GraphQLInterfaceType) {
151151
fields = typeDef.getFields();
152152
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Field Config Estimator
2+
3+
The `fieldConfigEstimator` lets you define a numeric value or a custom estimator
4+
in the field config of your GraphQL schema. If no complexity is set in the field config,
5+
the estimator does not return any value and the next estimator in the chain is executed.
6+
7+
## Usage
8+
9+
````typescript
10+
import queryComplexity, {fieldConfigEstimator} from 'graphql-query-complexity';
11+
12+
const rule = queryComplexity({
13+
estimators: [
14+
fieldConfigEstimator()
15+
]
16+
// ... other config
17+
});
18+
````
19+
20+
You can set a custom complexity as a numeric value in the field config:
21+
22+
```javascript
23+
const Post = new GraphQLObjectType({
24+
name: 'Post',
25+
fields: () => ({
26+
title: { type: GraphQLString },
27+
text: { type: GraphQLString, complexity: 5 },
28+
}),
29+
});
30+
```
31+
32+
**Example Query:**
33+
34+
```graphql
35+
query {
36+
posts(count: 10) {
37+
title
38+
text
39+
}
40+
}
41+
```
42+
43+
This query would result in a complexity of 7.
44+
5 for the `text` field and 1 for each of the other fields.
45+
46+
You can also pass an estimator in the field config to determine a custom complexity.
47+
This function will provide the complexity of the child nodes as well as the field input arguments.
48+
49+
The function signature is the same as for the main estimator which lets you reuse estimators:
50+
51+
```typescript
52+
type ComplexityEstimatorArgs = {
53+
type: GraphQLCompositeType,
54+
field: GraphQLField<any, any>,
55+
args: {[key: string]: any},
56+
childComplexity: number
57+
}
58+
59+
type ComplexityEstimator = (options: ComplexityEstimatorArgs) => number | void;
60+
```
61+
62+
That way you can make a more realistic estimation of individual field complexity values:
63+
64+
```javascript
65+
const Query = new GraphQLObjectType({
66+
name: 'Query',
67+
fields: () => ({
68+
posts: {
69+
type: new GraphQLList(Post),
70+
complexity: ({args, childComplexity}) => childComplexity * args.count,
71+
args: {
72+
count: {
73+
type: GraphQLInt,
74+
defaultValue: 10
75+
}
76+
}
77+
},
78+
}),
79+
});
80+
```
81+
82+
This would result in a complexity of 60 since the `childComplexity` of posts (`text` 5, `title` 1) is multiplied by the
83+
number of posts (`args.count`).

0 commit comments

Comments
 (0)