Skip to content

Commit b093f88

Browse files
committed
NEW RULE: selection-set-depth
1 parent e26a5d4 commit b093f88

File tree

8 files changed

+311
-2
lines changed

8 files changed

+311
-2
lines changed

.changeset/curvy-baboons-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': minor
3+
---
4+
5+
NEW RULE: selection-set-depth

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- [`no-operation-name-suffix`](./rules/no-operation-name-suffix.md)
1111
- [`require-deprecation-reason`](./rules/require-deprecation-reason.md)
1212
- [`avoid-operation-name-prefix`](./rules/avoid-operation-name-prefix.md)
13+
- [`selection-set-depth`](./rules/selection-set-depth.md)
1314
- [`no-case-insensitive-enum-values-duplicates`](./rules/no-case-insensitive-enum-values-duplicates.md)
1415
- [`require-description`](./rules/require-description.md)
1516
- [`require-id-when-available`](./rules/require-id-when-available.md)

docs/rules/selection-set-depth.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# `selection-set-depth`
2+
3+
- Category: `Best Practices`
4+
- Rule name: `@graphql-eslint/selection-set-depth`
5+
- Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
6+
- Requires GraphQL Operations: `true` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
7+
8+
Limit the complexity of the GraphQL operations solely by their depth. Based on https://github.com/stems/graphql-depth-limit .
9+
10+
## Usage Examples
11+
12+
### Incorrect
13+
14+
```graphql
15+
# eslint @graphql-eslint/selection-set-depth: ["error", [{"maxDepth":1}]]
16+
17+
query deep2 {
18+
viewer { # Level 0
19+
albums { # Level 1
20+
title # Level 2
21+
}
22+
}
23+
}
24+
```
25+
26+
### Correct
27+
28+
```graphql
29+
# eslint @graphql-eslint/selection-set-depth: ["error", [{"maxDepth":4}]]
30+
31+
query deep2 {
32+
viewer { # Level 0
33+
albums { # Level 1
34+
title # Level 2
35+
}
36+
}
37+
}
38+
```
39+
40+
### Correct (ignored field)
41+
42+
```graphql
43+
# eslint @graphql-eslint/selection-set-depth: ["error", [{"maxDepth":1,"ignore":["albums"]}]]
44+
45+
query deep2 {
46+
viewer { # Level 0
47+
albums { # Level 1
48+
title # Level 2
49+
}
50+
}
51+
}
52+
```
53+
54+
## Config Schema
55+
56+
The schema defines the following properties:

packages/plugin/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
"@graphql-tools/code-file-loader": "~6.2.6",
2525
"@graphql-tools/url-loader": "~6.7.0",
2626
"@graphql-tools/graphql-tag-pluck": "~6.3.0",
27-
"graphql-config": "^3.2.0"
27+
"graphql-config": "^3.2.0",
28+
"graphql-depth-limit": "1.1.0"
2829
},
2930
"devDependencies": {
31+
"@types/graphql-depth-limit": "1.1.2",
3032
"@types/eslint": "7.2.6",
3133
"bob-the-bundler": "1.1.0",
3234
"graphql-config": "3.2.0",

packages/plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import uniqueFragmentName from './unique-fragment-name';
1414
import uniqueOperationName from './unique-operation-name';
1515
import noDeprecated from './no-deprecated';
1616
import noHashtagDescription from './no-hashtag-description';
17+
import selectionSetDepth from './selection-set-depth';
1718
import { GraphQLESLintRule } from '../types';
1819
import { GRAPHQL_JS_VALIDATIONS } from './graphql-js-validation';
1920

@@ -27,6 +28,7 @@ export const rules: Record<string, GraphQLESLintRule> = {
2728
'no-operation-name-suffix': noOperationNameSuffix,
2829
'require-deprecation-reason': requireDeprecationReason,
2930
'avoid-operation-name-prefix': avoidOperationNamePrefix,
31+
'selection-set-depth': selectionSetDepth,
3032
'no-case-insensitive-enum-values-duplicates': noCaseInsensitiveEnumValuesDuplicates,
3133
'require-description': requireDescription,
3234
'require-id-when-available': requireIdWhenAvailable,
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { GraphQLESLintRule } from '../types';
2+
import depthLimit from 'graphql-depth-limit';
3+
import { DocumentNode, FragmentDefinitionNode, GraphQLError, Kind, OperationDefinitionNode } from 'graphql';
4+
import { GraphQLESTreeNode } from '../estree-parser';
5+
import { requireSiblingsOperations } from '../utils';
6+
import { SiblingOperations } from '../sibling-operations';
7+
8+
type SelectionSetDepthRuleConfig = [{ maxDepth: number; ignore?: string[] }];
9+
10+
const rule: GraphQLESLintRule<SelectionSetDepthRuleConfig> = {
11+
meta: {
12+
docs: {
13+
category: 'Best Practices',
14+
description: `Limit the complexity of the GraphQL operations solely by their depth. Based on https://github.com/stems/graphql-depth-limit .`,
15+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/selection-set-depth.md`,
16+
requiresSchema: false,
17+
requiresSiblings: true,
18+
examples: [
19+
{
20+
title: 'Incorrect',
21+
usage: [{ maxDepth: 1 }],
22+
code: `
23+
query deep2 {
24+
viewer { # Level 0
25+
albums { # Level 1
26+
title # Level 2
27+
}
28+
}
29+
}
30+
`,
31+
},
32+
{
33+
title: 'Correct',
34+
usage: [{ maxDepth: 4 }],
35+
code: `
36+
query deep2 {
37+
viewer { # Level 0
38+
albums { # Level 1
39+
title # Level 2
40+
}
41+
}
42+
}
43+
`,
44+
},
45+
{
46+
title: 'Correct (ignored field)',
47+
usage: [{ maxDepth: 1, ignore: ['albums'] }],
48+
code: `
49+
query deep2 {
50+
viewer { # Level 0
51+
albums { # Level 1
52+
title # Level 2
53+
}
54+
}
55+
}
56+
`,
57+
},
58+
],
59+
},
60+
type: 'suggestion',
61+
schema: {
62+
type: 'array',
63+
additionalItems: false,
64+
minItems: 1,
65+
maxItems: 1,
66+
items: {
67+
type: 'object',
68+
require: ['maxDepth'],
69+
properties: {
70+
maxDepth: {
71+
type: 'number',
72+
},
73+
ignore: {
74+
type: 'array',
75+
items: {
76+
type: 'string',
77+
},
78+
},
79+
},
80+
},
81+
},
82+
},
83+
create(context) {
84+
let siblings: SiblingOperations | null = null;
85+
86+
try {
87+
siblings = requireSiblingsOperations('selection-set-depth', context);
88+
} catch (e) {
89+
// eslint-disable-next-line no-console
90+
console.warn(
91+
`Rule "selection-set-depth" works best with sibligns operations loaded. For more info: http://bit.ly/graphql-eslint-operations`
92+
);
93+
}
94+
95+
const maxDepth = context.options[0].maxDepth;
96+
const ignore = context.options[0].ignore || [];
97+
const checkFn = depthLimit(maxDepth, { ignore });
98+
99+
return ['OperationDefinition', 'FragmentDefinition'].reduce((prev, nodeType) => {
100+
return {
101+
...prev,
102+
[nodeType]: (node: GraphQLESTreeNode<OperationDefinitionNode> | GraphQLESTreeNode<FragmentDefinitionNode>) => {
103+
try {
104+
const rawNode = node.rawNode();
105+
const fragmentsInUse = siblings ? siblings.getFragmentsInUse(rawNode, true) : [];
106+
const document: DocumentNode = {
107+
kind: Kind.DOCUMENT,
108+
definitions: [rawNode, ...fragmentsInUse],
109+
};
110+
111+
checkFn({
112+
getDocument: () => document,
113+
reportError: (error: GraphQLError) => {
114+
context.report({
115+
loc: error.locations[0],
116+
message: error.message,
117+
});
118+
},
119+
});
120+
} catch (e) {
121+
// eslint-disable-next-line no-console
122+
console.warn(
123+
`Rule "selection-set-depth" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`,
124+
e
125+
);
126+
}
127+
},
128+
};
129+
}, {});
130+
},
131+
};
132+
133+
export default rule;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { GraphQLRuleTester } from '../src/testkit';
2+
import rule from '../src/rules/selection-set-depth';
3+
import { ParserOptions } from '../src/types';
4+
5+
const WITH_SIBLINGS = {
6+
parserOptions: <ParserOptions>{
7+
operations: [
8+
`fragment AlbumFields on Album {
9+
id
10+
}`,
11+
],
12+
},
13+
};
14+
15+
const ruleTester = new GraphQLRuleTester();
16+
17+
ruleTester.runGraphQLTests('selection-set-depth', rule, {
18+
valid: [
19+
{
20+
options: [{ maxDepth: 2 }],
21+
code: /* GraphQL */ `
22+
query deep2 {
23+
viewer {
24+
# Level 0
25+
albums {
26+
# Level 1
27+
title # Level 2
28+
}
29+
}
30+
}
31+
`,
32+
},
33+
{
34+
...WITH_SIBLINGS,
35+
options: [{ maxDepth: 2 }],
36+
code: /* GraphQL */ `
37+
query deep2 {
38+
viewer {
39+
albums {
40+
...AlbumFields
41+
}
42+
}
43+
}
44+
`,
45+
},
46+
{
47+
...WITH_SIBLINGS,
48+
options: [{ maxDepth: 1, ignore: ['albums'] }],
49+
code: /* GraphQL */ `
50+
query deep2 {
51+
viewer {
52+
albums {
53+
...AlbumFields
54+
}
55+
}
56+
}
57+
`,
58+
},
59+
],
60+
invalid: [
61+
{
62+
options: [{ maxDepth: 1 }],
63+
errors: [{ message: `'deep2' exceeds maximum operation depth of 1` }],
64+
code: /* GraphQL */ `
65+
query deep2 {
66+
viewer {
67+
albums {
68+
title
69+
}
70+
}
71+
}
72+
`,
73+
},
74+
{
75+
...WITH_SIBLINGS,
76+
options: [{ maxDepth: 1 }],
77+
errors: [{ message: `'deep2' exceeds maximum operation depth of 1` }],
78+
code: /* GraphQL */ `
79+
query deep2 {
80+
viewer {
81+
albums {
82+
...AlbumFields
83+
}
84+
}
85+
}
86+
`,
87+
},
88+
],
89+
});

yarn.lock

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,13 @@
10851085
dependencies:
10861086
"@types/node" "*"
10871087

1088+
1089+
version "1.1.2"
1090+
resolved "https://registry.npmjs.org/@types/graphql-depth-limit/-/graphql-depth-limit-1.1.2.tgz#8e8a7b68548d703a3c3bd7f0531f314b3051f556"
1091+
integrity sha512-CJoghYUfE5/IKrqexgSsTECP0RcP2Ii+ulv/BjjFniABNAMgfwCTKFnCkkRskg9Sr3WZeJrSLjwBhNFetP5Tyw==
1092+
dependencies:
1093+
graphql "^14.5.3"
1094+
10881095
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
10891096
version "2.0.3"
10901097
resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@@ -3234,6 +3241,13 @@ [email protected], graphql-config@^3.2.0:
32343241
string-env-interpolation "1.0.1"
32353242
tslib "^2.0.0"
32363243

3244+
3245+
version "1.1.0"
3246+
resolved "https://registry.npmjs.org/graphql-depth-limit/-/graphql-depth-limit-1.1.0.tgz#59fe6b2acea0ab30ee7344f4c75df39cc18244e8"
3247+
integrity sha512-+3B2BaG8qQ8E18kzk9yiSdAa75i/hnnOwgSeAxVJctGQPvmeiLtqKOYF6HETCyRjiF7Xfsyal0HbLlxCQkgkrw==
3248+
dependencies:
3249+
arrify "^1.0.1"
3250+
32373251
graphql-tag@^2.11.0:
32383252
version "2.11.0"
32393253
resolved "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.11.0.tgz#1deb53a01c46a7eb401d6cb59dec86fa1cccbffd"
@@ -3260,6 +3274,13 @@ [email protected]:
32603274
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.4.0.tgz#e459dea1150da5a106486ba7276518b5295a4347"
32613275
integrity sha512-EB3zgGchcabbsU9cFe1j+yxdzKQKAbGUWRb13DsrsMN1yyfmmIq+2+L5MqVWcDCE4V89R5AyUOi7sMOGxdsYtA==
32623276

3277+
graphql@^14.5.3:
3278+
version "14.7.0"
3279+
resolved "https://registry.npmjs.org/graphql/-/graphql-14.7.0.tgz#7fa79a80a69be4a31c27dda824dc04dac2035a72"
3280+
integrity sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA==
3281+
dependencies:
3282+
iterall "^1.2.2"
3283+
32633284
growly@^1.3.0:
32643285
version "1.3.0"
32653286
resolved "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
@@ -3788,7 +3809,7 @@ istanbul-reports@^3.0.2:
37883809
html-escaper "^2.0.0"
37893810
istanbul-lib-report "^3.0.0"
37903811

3791-
iterall@^1.2.1:
3812+
iterall@^1.2.1, iterall@^1.2.2:
37923813
version "1.3.0"
37933814
resolved "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea"
37943815
integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==

0 commit comments

Comments
 (0)