Skip to content

Commit 44dc2e7

Browse files
committed
Merge branch 'feature/catch-invalid-graphql-query' into develop
2 parents 34cb872 + a925469 commit 44dc2e7

File tree

24 files changed

+3315
-638
lines changed

24 files changed

+3315
-638
lines changed

.circleci/config.yml

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,28 @@
22
#
33
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
44
#
5-
version: 2
5+
version: 2.1
6+
7+
workflows:
8+
all-tests:
9+
jobs:
10+
- test-and-build:
11+
# Override graphql-version to test against specific versions. Type checking is disabled due missing
12+
# definitions for field extensions in older @types/graphql versions
13+
matrix:
14+
parameters:
15+
graphql-version: ["~0.13", "~14.0", "~14.5", "~14.6", "~15.0"]
16+
- test-and-build:
17+
# Leave graphql-version unspecified to respect the lockfile and also run tsc
18+
name: test-and-build-with-typecheck
19+
620
jobs:
7-
build:
21+
test-and-build:
22+
parameters:
23+
graphql-version:
24+
type: string
25+
default: ""
26+
827
docker:
928
# specify the version you desire here
1029
- image: circleci/node:latest
@@ -17,16 +36,24 @@ jobs:
1736
# Download and cache dependencies
1837
- restore_cache:
1938
keys:
20-
- v1-dependencies-{{ checksum "package.json" }}
39+
- v1-dependencies-{{ checksum "package.json" }}-<< parameters.graphql-version >>
2140
# fallback to using the latest cache if no exact match is found
2241
- v1-dependencies-
2342

24-
- run: yarn
43+
- when:
44+
condition: << parameters.graphql-version >>
45+
steps:
46+
- run: yarn install --ignore-scripts
47+
- run: yarn --ignore-scripts add --dev graphql@<< parameters.graphql-version >>
48+
- unless:
49+
condition: << parameters.graphql-version >>
50+
steps:
51+
- run: yarn install --frozen-lockfile
2552

2653
- save_cache:
2754
paths:
2855
- node_modules
29-
key: v1-dependencies-{{ checksum "package.json" }}
56+
key: v1-dependencies-{{ checksum "package.json" }}-<< parameters.graphql-version >>
3057

3158
# run tests!
32-
- run: yarn test
59+
- run: yarn test

.eslintrc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
{
2-
"parser": "typescript-eslint-parser",
2+
"parser": "@typescript-eslint/parser",
33

44
"plugins": [
5-
"typescript"
5+
"@typescript-eslint"
66
],
77

88
"extends": [
9-
"eslint:recommended"
9+
"eslint:recommended",
10+
"plugin:@typescript-eslint/recommended"
1011
],
1112

1213
"env": {
@@ -187,7 +188,7 @@
187188
"no-unneeded-ternary": 2,
188189
"no-unreachable": 2,
189190
"no-unused-expressions": 2,
190-
"no-unused-vars": [2, {"vars": "all", "args": "after-used"}],
191+
"no-unused-vars": 2,
191192
"no-use-before-define": 0,
192193
"no-useless-call": 2,
193194
"no-var": 2,

README.md

Lines changed: 115 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# GraphQL Query Complexity Analysis for graphql-js
22

3+
[![npm](https://img.shields.io/npm/dm/graphql-query-complexity)](https://www.npmjs.com/package/graphql-query-complexity)
4+
[![npm version](https://badge.fury.io/js/graphql-query-complexity.svg)](https://badge.fury.io/js/graphql-query-complexity)
5+
[![CircleCI](https://circleci.com/gh/slicknode/graphql-query-complexity.svg?style=shield)](https://circleci.com/gh/slicknode/graphql-query-complexity)
6+
[![Twitter Follow](https://img.shields.io/twitter/follow/slicknode?style=social)](https://twitter.com/slicknode)
7+
38
This library provides GraphQL query analysis to reject complex queries to your GraphQL server.
49
This can be used to protect your GraphQL servers against resource exhaustion and DoS attacks.
510

@@ -19,83 +24,92 @@ npm install -S graphql-query-complexity
1924
Create the rule with a maximum query complexity:
2025

2126
```javascript
27+
import queryComplexity, {
28+
simpleEstimator
29+
} from 'graphql-query-complexity';
30+
2231
const rule = queryComplexity({
2332
// The maximum allowed query complexity, queries above this threshold will be rejected
2433
maximumComplexity: 1000,
2534

2635
// The query variables. This is needed because the variables are not available
2736
// in the visitor of the graphql-js library
2837
variables: {},
38+
39+
// specify operation name only when pass multi-operation documents
40+
operationName?: string,
2941

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

3547
// Optional function to create a custom error
3648
createError: (max: number, actual: number) => {
3749
return new GraphQLError(`Query is too complex: ${actual}. Maximum allowed complexity: ${max}`);
38-
}
50+
},
51+
52+
// Add any number of estimators. The estimators are invoked in order, the first
53+
// numeric value that is being returned by an estimator is used as the field complexity.
54+
// If no estimator returns a value, an exception is raised.
55+
estimators: [
56+
// Add more estimators here...
57+
58+
// This will assign each field a complexity of 1 if no other estimator
59+
// returned a value.
60+
simpleEstimator({
61+
defaultComplexity: 1
62+
})
63+
]
3964
});
4065
```
4166

42-
## Customizing complexity calculation
67+
## Configuration / Complexity Estimators
4368

44-
By default, every field gets a complexity of 1. Let's look at the following example query:
69+
The complexity calculation of a GraphQL query can be customized with so called complexity estimators.
70+
A complexity estimator is a simple function that calculates the complexity for a field. You can add
71+
any number of complexity estimators to the rule, which are then executed one after another.
72+
The first estimator that returns a numeric complexity value determines the complexity for that field.
4573

46-
```graphql
47-
query {
48-
posts(count: 10) {
49-
title
50-
text
51-
}
52-
}
53-
```
74+
At least one estimator has to return a complexity value, otherwise an exception is raised. You can
75+
for example use the [simpleEstimator](./src/estimators/simple/README.md) as the last estimator
76+
in your chain to define a default value.
5477

55-
This would result in a complexity of 3. The fields `posts`, `title` and `text` each add a complexity of 1.
56-
If we assume that the posts field returns a list of 10 posts, the complexity estimation is pretty inaccurate.
78+
You can use any of the available estimators to calculate the complexity of a field
79+
or write your own:
5780

58-
When defining your fields, you have a two options to customize the calculation.
81+
* **[`simpleEstimator`](src/estimators/simple/README.md):** The simple estimator returns a fixed complexity for each field. Can be used as
82+
last estimator in the chain for a default value.
83+
* **[`directiveEstimator`](src/estimators/directive/README.md):** Set the complexity via a directive in your
84+
schema definition (for example via GraphQL SDL)
85+
* **[`fieldExtensionsEstimator`](src/estimators/fieldExtensions/README.md):** The field extensions estimator lets you set a numeric value or a custom estimator
86+
function in the field config extensions of your schema.
87+
* PRs welcome...
5988

60-
You can set a custom complexity in the field config:
89+
Consult the documentation of each estimator for information about how to use them.
6190

62-
```javascript
63-
const Post = new GraphQLObjectType({
64-
name: 'Post',
65-
fields: () => ({
66-
title: { type: GraphQLString },
67-
text: { type: GraphQLString, complexity: 5 },
68-
}),
69-
});
70-
```
71-
The same query would now result in a complexity of 7.
72-
5 for the `text` field and 1 for each of the other fields.
91+
## Creating Custom Estimators
7392

74-
You can also pass a calculation function in the field config to determine a custom complexity.
75-
This function will provide the complexity of the child nodes as well as the field input arguments.
93+
An estimator has the following function signature:
7694

77-
That way you can make a more realistic estimation of individual field complexity values:
95+
```typescript
96+
type ComplexityEstimatorArgs = {
97+
// The composite type (interface, object, union) that the evaluated field belongs to
98+
type: GraphQLCompositeType,
99+
100+
// The GraphQLField that is being evaluated
101+
field: GraphQLField<any, any>,
102+
103+
// The input arguments of the field
104+
args: {[key: string]: any},
105+
106+
// The complexity of all child selections for that field
107+
childComplexity: number
108+
}
78109

79-
```javascript
80-
const Query = new GraphQLObjectType({
81-
name: 'Query',
82-
fields: () => ({
83-
posts: {
84-
type: new GraphQLList(Post),
85-
complexity: (args, childComplexity) => childComplexity * args.count,
86-
args: {
87-
count: {
88-
type: GraphQLInt,
89-
defaultValue: 10
90-
}
91-
}
92-
},
93-
}),
94-
});
110+
type ComplexityEstimator = (options: ComplexityEstimatorArgs) => number | void;
95111
```
96112

97-
This would result in a complexity of 60 since the `childComplexity` of posts (`text` 5, `title` 1) is multiplied by the
98-
number of posts (`args.count`).
99113

100114
## Usage with express-graphql
101115

@@ -111,15 +125,60 @@ import schema from './schema';
111125
const app = express();
112126
app.use('/api', graphqlHTTP(async (request, response, {variables}) => ({
113127
schema,
114-
validationRules: [ queryComplexity({
115-
maximumComplexity: 1000,
116-
variables,
117-
onComplete: (complexity: number) => {console.log('Query Complexity:', complexity);},
118-
}) ]
128+
validationRules: [
129+
queryComplexity({
130+
estimators: [
131+
// Configure your estimators
132+
simpleEstimator({defaultComplexity: 1})
133+
],
134+
maximumComplexity: 1000,
135+
variables,
136+
onComplete: (complexity: number) => {console.log('Query Complexity:', complexity);},
137+
})
138+
]
119139
})));
120140
```
121141

122-
## Credits
142+
## Calculate query complexity
143+
144+
If you want to calculate the complexity of a GraphQL query outside of the validation phase, for example to
145+
return the complexity value in a resolver, you can calculate the complexity via `getComplexity`:
146+
147+
```javascript
148+
import { getComplexity, simpleEstimator } from 'graphql-query-complexity';
149+
import { parse } from 'graphql';
150+
151+
// Import your schema or get it form the info object in your resolver
152+
import schema from './schema';
153+
154+
// You can also use gql template tag to get the parsed query
155+
const query = parse(`
156+
query Q($count: Int) {
157+
some_value
158+
some_list(count: $count) {
159+
some_child_value
160+
}
161+
}
162+
`);
163+
164+
const complexity = getComplexity({
165+
estimators: [
166+
simpleEstimator({defaultComplexity: 1})
167+
],
168+
schema,
169+
query,
170+
variables: {
171+
count: 10,
172+
},
173+
});
174+
175+
console.log(complexity); // Output: 3
176+
```
177+
178+
179+
## Prior Art
180+
181+
This project is inspired by the following prior projects:
123182

124-
This project is heavily inspired by the query complexity analysis in the
125-
[Sangria GraphQL](http://sangria-graphql.org/) implementation.
183+
- Query complexity analysis in the [Sangria GraphQL](http://sangria-graphql.org/) implementation.
184+
- [graphql-cost-analysis](https://github.com/pa-bru/graphql-cost-analysis) - Multipliers and directiveEstimator

package.json

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,57 @@
11
{
22
"name": "graphql-query-complexity",
3-
"version": "0.1.2",
3+
"version": "0.7.1",
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",
9+
"lint:fix": "eslint --fix src/**/*.ts",
810
"clean": "rimraf dist/*",
911
"build": "tsc",
1012
"test": "npm run lint && npm run testonly",
11-
"testonly": "mocha --check-leaks --exit --full-trace --require ts-node/register 'src/**/__tests__/**/*-test.{ts,tsx}'",
13+
"testonly": "mocha --check-leaks --exit --full-trace --require ts-node/register/transpile-only 'src/**/__tests__/**/*-test.{ts,tsx}'",
1214
"dist": "npm run clean && tsc && npm run build",
1315
"prepublish": "npm run clean && npm run dist"
1416
},
1517
"directories": {
1618
"lib": "./dist"
1719
},
18-
"dependencies": {},
20+
"dependencies": {
21+
"lodash.get": "^4.4.2"
22+
},
1923
"peerDependencies": {
20-
"graphql": "^0.13.2"
24+
"graphql": "^0.13.0 || ^14.0.0 || ^15.0.0"
2125
},
2226
"files": [
2327
"dist",
2428
"README.md",
2529
"LICENSE"
2630
],
27-
"repository": "ivome/graphql-query-complexity",
31+
"repository": "slicknode/graphql-query-complexity",
2832
"keywords": [
2933
"graphql",
30-
"query complexity"
34+
"query",
35+
"validation",
36+
"cost",
37+
"complexity",
38+
"analysis"
3139
],
3240
"author": "Ivo Meißner",
3341
"license": "MIT",
3442
"devDependencies": {
35-
"@types/assert": "^0.0.31",
36-
"@types/chai": "^4.1.4",
37-
"@types/graphql": "^0.13.4",
38-
"@types/mocha": "^5.2.5",
39-
"chai": "^4.1.0",
40-
"eslint": "^5.4.0",
41-
"eslint-plugin-typescript": "^0.12.0",
42-
"graphql": "^0.13.2",
43-
"mocha": "^5.2.0",
44-
"rimraf": "^2.6.1",
45-
"ts-node": "^7.0.1",
46-
"typescript": "^3.0.1",
47-
"typescript-eslint-parser": "^18.0.0"
43+
"@types/assert": "^1.4.6",
44+
"@types/chai": "^4.2.11",
45+
"@types/lodash.get": "^4.4.6",
46+
"@types/mocha": "^7.0.2",
47+
"@typescript-eslint/eslint-plugin": "^2.27.0",
48+
"@typescript-eslint/parser": "^2.27.0",
49+
"chai": "^4.2.0",
50+
"eslint": "^6.8.0",
51+
"graphql": "^14.5.0 || ^15.0.0",
52+
"mocha": "^7.1.1",
53+
"rimraf": "^3.0.2",
54+
"ts-node": "^8.8.2",
55+
"typescript": "^3.8.3"
4856
}
4957
}

0 commit comments

Comments
 (0)