@graphql-eslint/eslint-plugin 4.3.0 → 4.3.1-alpha-20241209185034-de2d7397da8c26620a8930dd12b6dff42e43f537
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{index.browser.js → browser.js} +1756 -1104
- package/cjs/cache.js +6 -2
- package/cjs/configs/operations-all.js +2 -2
- package/cjs/configs/schema-all.js +2 -2
- package/cjs/configs/schema-recommended.js +1 -1
- package/cjs/documents.js +13 -7
- package/cjs/estree-converter/converter.js +17 -8
- package/cjs/estree-converter/utils.js +22 -9
- package/cjs/graphql-config.js +13 -6
- package/cjs/meta.js +1 -1
- package/cjs/parser.js +36 -9
- package/cjs/processor.js +48 -20
- package/cjs/rules/alphabetize/index.js +99 -47
- package/cjs/rules/description-style/index.js +10 -6
- package/cjs/rules/graphql-js-validation.js +142 -108
- package/cjs/rules/input-name/index.js +51 -38
- package/cjs/rules/lone-executable-definition/index.js +15 -6
- package/cjs/rules/match-document-filename/index.js +57 -32
- package/cjs/rules/naming-convention/index.js +76 -37
- package/cjs/rules/no-anonymous-operations/index.js +8 -5
- package/cjs/rules/no-deprecated/index.js +27 -13
- package/cjs/rules/no-duplicate-fields/index.js +15 -8
- package/cjs/rules/no-hashtag-description/index.js +18 -10
- package/cjs/rules/no-one-place-fragments/index.js +17 -10
- package/cjs/rules/no-root-type/index.js +15 -8
- package/cjs/rules/no-scalar-result-type-on-mutation/index.js +20 -12
- package/cjs/rules/no-typename-prefix/index.js +25 -21
- package/cjs/rules/no-unreachable-types/index.js +34 -17
- package/cjs/rules/no-unused-fields/index.js +56 -30
- package/cjs/rules/relay-arguments/index.js +31 -13
- package/cjs/rules/relay-connection-types/index.js +31 -9
- package/cjs/rules/relay-edge-types/index.js +84 -41
- package/cjs/rules/relay-page-info/index.js +31 -14
- package/cjs/rules/require-deprecation-date/index.js +20 -9
- package/cjs/rules/require-deprecation-reason/index.js +8 -5
- package/cjs/rules/require-description/index.js +60 -42
- package/cjs/rules/require-field-of-type-query-in-mutation-result/index.js +21 -10
- package/cjs/rules/require-import-fragment/index.js +20 -11
- package/cjs/rules/require-nullable-fields-with-oneof/index.js +12 -5
- package/cjs/rules/require-nullable-result-in-root/index.js +32 -27
- package/cjs/rules/require-selections/index.js +88 -46
- package/cjs/rules/require-type-pattern-with-oneof/index.js +14 -10
- package/cjs/rules/selection-set-depth/index.js +19 -10
- package/cjs/rules/strict-id-in-types/index.js +32 -19
- package/cjs/rules/unique-enum-value-names/index.js +4 -3
- package/cjs/rules/unique-fragment-name/index.js +25 -18
- package/cjs/rules/unique-operation-name/index.js +5 -5
- package/cjs/schema.js +14 -8
- package/cjs/siblings.js +60 -32
- package/cjs/utils.js +23 -9
- package/esm/cache.js +6 -2
- package/esm/configs/operations-all.js +2 -2
- package/esm/configs/schema-all.js +2 -2
- package/esm/configs/schema-recommended.js +1 -1
- package/esm/documents.js +13 -7
- package/esm/estree-converter/converter.js +17 -8
- package/esm/estree-converter/utils.js +22 -9
- package/esm/graphql-config.js +13 -6
- package/esm/meta.js +1 -1
- package/esm/parser.js +36 -9
- package/esm/processor.js +48 -20
- package/esm/rules/alphabetize/index.js +99 -47
- package/esm/rules/description-style/index.js +10 -6
- package/esm/rules/graphql-js-validation.js +142 -108
- package/esm/rules/input-name/index.js +51 -38
- package/esm/rules/lone-executable-definition/index.js +15 -6
- package/esm/rules/match-document-filename/index.js +57 -32
- package/esm/rules/naming-convention/index.js +76 -37
- package/esm/rules/no-anonymous-operations/index.js +8 -5
- package/esm/rules/no-deprecated/index.js +27 -13
- package/esm/rules/no-duplicate-fields/index.js +15 -8
- package/esm/rules/no-hashtag-description/index.js +18 -10
- package/esm/rules/no-one-place-fragments/index.js +17 -10
- package/esm/rules/no-root-type/index.js +15 -8
- package/esm/rules/no-scalar-result-type-on-mutation/index.js +20 -12
- package/esm/rules/no-typename-prefix/index.js +25 -21
- package/esm/rules/no-unreachable-types/index.js +34 -17
- package/esm/rules/no-unused-fields/index.js +56 -30
- package/esm/rules/relay-arguments/index.js +31 -13
- package/esm/rules/relay-connection-types/index.js +31 -9
- package/esm/rules/relay-edge-types/index.js +84 -41
- package/esm/rules/relay-page-info/index.js +31 -14
- package/esm/rules/require-deprecation-date/index.js +20 -9
- package/esm/rules/require-deprecation-reason/index.js +8 -5
- package/esm/rules/require-description/index.js +60 -42
- package/esm/rules/require-field-of-type-query-in-mutation-result/index.js +21 -10
- package/esm/rules/require-import-fragment/index.js +20 -11
- package/esm/rules/require-nullable-fields-with-oneof/index.js +12 -5
- package/esm/rules/require-nullable-result-in-root/index.js +32 -27
- package/esm/rules/require-selections/index.js +88 -46
- package/esm/rules/require-type-pattern-with-oneof/index.js +14 -10
- package/esm/rules/selection-set-depth/index.js +19 -10
- package/esm/rules/strict-id-in-types/index.js +32 -19
- package/esm/rules/unique-enum-value-names/index.js +4 -3
- package/esm/rules/unique-fragment-name/index.js +25 -18
- package/esm/rules/unique-operation-name/index.js +5 -5
- package/esm/schema.js +15 -8
- package/esm/siblings.js +60 -32
- package/esm/utils.js +23 -9
- package/package.json +11 -1
@@ -8,57 +8,60 @@ import {
|
|
8
8
|
requireGraphQLSchema,
|
9
9
|
TYPES_KINDS
|
10
10
|
} from "../../utils.js";
|
11
|
-
const RULE_ID = "require-description"
|
11
|
+
const RULE_ID = "require-description";
|
12
|
+
const ALLOWED_KINDS = [
|
12
13
|
...TYPES_KINDS,
|
13
14
|
Kind.DIRECTIVE_DEFINITION,
|
14
15
|
Kind.FIELD_DEFINITION,
|
15
16
|
Kind.INPUT_VALUE_DEFINITION,
|
16
17
|
Kind.ENUM_VALUE_DEFINITION,
|
17
18
|
Kind.OPERATION_DEFINITION
|
18
|
-
]
|
19
|
+
];
|
20
|
+
const schema = {
|
19
21
|
type: "array",
|
20
22
|
minItems: 1,
|
21
23
|
maxItems: 1,
|
22
24
|
items: {
|
23
25
|
type: "object",
|
24
|
-
additionalProperties:
|
26
|
+
additionalProperties: false,
|
25
27
|
minProperties: 1,
|
26
28
|
properties: {
|
27
29
|
types: {
|
28
30
|
type: "boolean",
|
29
|
-
enum: [
|
31
|
+
enum: [true],
|
30
32
|
description: `Includes:
|
31
|
-
${TYPES_KINDS.map((kind) => `- \`${kind}\``).join(`
|
32
|
-
`)}`
|
33
|
+
${TYPES_KINDS.map((kind) => `- \`${kind}\``).join("\n")}`
|
33
34
|
},
|
34
35
|
rootField: {
|
35
36
|
type: "boolean",
|
36
|
-
enum: [
|
37
|
+
enum: [true],
|
37
38
|
description: "Definitions within `Query`, `Mutation`, and `Subscription` root types."
|
38
39
|
},
|
39
40
|
ignoredSelectors: {
|
40
41
|
...ARRAY_DEFAULT_OPTIONS,
|
41
|
-
description: ["Ignore specific selectors", eslintSelectorsTip].join(
|
42
|
-
`)
|
42
|
+
description: ["Ignore specific selectors", eslintSelectorsTip].join("\n")
|
43
43
|
},
|
44
44
|
...Object.fromEntries(
|
45
45
|
[...ALLOWED_KINDS].sort().map((kind) => {
|
46
46
|
let description = `> [!NOTE]
|
47
47
|
>
|
48
48
|
> Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`;
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
49
|
+
if (kind === Kind.OPERATION_DEFINITION) {
|
50
|
+
description += [
|
51
|
+
"",
|
52
|
+
"",
|
53
|
+
"> [!WARNING]",
|
54
|
+
">",
|
55
|
+
'> You must use only comment syntax `#` and not description syntax `"""` or `"`.'
|
56
|
+
].join("\n");
|
57
|
+
}
|
58
|
+
return [kind, { type: "boolean", description }];
|
57
59
|
})
|
58
60
|
)
|
59
61
|
}
|
60
62
|
}
|
61
|
-
}
|
63
|
+
};
|
64
|
+
const rule = {
|
62
65
|
meta: {
|
63
66
|
docs: {
|
64
67
|
category: "Schema",
|
@@ -67,7 +70,7 @@ ${TYPES_KINDS.map((kind) => `- \`${kind}\``).join(`
|
|
67
70
|
examples: [
|
68
71
|
{
|
69
72
|
title: "Incorrect",
|
70
|
-
usage: [{ types:
|
73
|
+
usage: [{ types: true, FieldDefinition: true }],
|
71
74
|
code: (
|
72
75
|
/* GraphQL */
|
73
76
|
`
|
@@ -79,7 +82,7 @@ ${TYPES_KINDS.map((kind) => `- \`${kind}\``).join(`
|
|
79
82
|
},
|
80
83
|
{
|
81
84
|
title: "Correct",
|
82
|
-
usage: [{ types:
|
85
|
+
usage: [{ types: true, FieldDefinition: true }],
|
83
86
|
code: (
|
84
87
|
/* GraphQL */
|
85
88
|
`
|
@@ -97,7 +100,7 @@ ${TYPES_KINDS.map((kind) => `- \`${kind}\``).join(`
|
|
97
100
|
},
|
98
101
|
{
|
99
102
|
title: "Correct",
|
100
|
-
usage: [{ OperationDefinition:
|
103
|
+
usage: [{ OperationDefinition: true }],
|
101
104
|
code: (
|
102
105
|
/* GraphQL */
|
103
106
|
`
|
@@ -110,7 +113,7 @@ ${TYPES_KINDS.map((kind) => `- \`${kind}\``).join(`
|
|
110
113
|
},
|
111
114
|
{
|
112
115
|
title: "Correct",
|
113
|
-
usage: [{ rootField:
|
116
|
+
usage: [{ rootField: true }],
|
114
117
|
code: (
|
115
118
|
/* GraphQL */
|
116
119
|
`
|
@@ -158,12 +161,12 @@ ${TYPES_KINDS.map((kind) => `- \`${kind}\``).join(`
|
|
158
161
|
],
|
159
162
|
configOptions: [
|
160
163
|
{
|
161
|
-
types:
|
162
|
-
[Kind.DIRECTIVE_DEFINITION]:
|
163
|
-
rootField:
|
164
|
+
types: true,
|
165
|
+
[Kind.DIRECTIVE_DEFINITION]: true,
|
166
|
+
rootField: true
|
164
167
|
}
|
165
168
|
],
|
166
|
-
recommended:
|
169
|
+
recommended: true
|
167
170
|
},
|
168
171
|
type: "suggestion",
|
169
172
|
messages: {
|
@@ -172,11 +175,18 @@ ${TYPES_KINDS.map((kind) => `- \`${kind}\``).join(`
|
|
172
175
|
schema
|
173
176
|
},
|
174
177
|
create(context) {
|
175
|
-
const { types, rootField, ignoredSelectors = [], ...restOptions } = context.options[0] || {}
|
176
|
-
|
177
|
-
|
178
|
+
const { types, rootField, ignoredSelectors = [], ...restOptions } = context.options[0] || {};
|
179
|
+
const kinds = new Set(types ? TYPES_KINDS : []);
|
180
|
+
for (const [kind, isEnabled] of Object.entries(restOptions)) {
|
181
|
+
if (isEnabled) {
|
182
|
+
kinds.add(kind);
|
183
|
+
} else {
|
184
|
+
kinds.delete(kind);
|
185
|
+
}
|
186
|
+
}
|
178
187
|
if (rootField) {
|
179
|
-
const schema2 = requireGraphQLSchema(RULE_ID, context)
|
188
|
+
const schema2 = requireGraphQLSchema(RULE_ID, context);
|
189
|
+
const rootTypeNames = getRootTypeNames(schema2);
|
180
190
|
kinds.add(
|
181
191
|
`:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/^(${[
|
182
192
|
...rootTypeNames
|
@@ -184,27 +194,35 @@ ${TYPES_KINDS.map((kind) => `- \`${kind}\``).join(`
|
|
184
194
|
);
|
185
195
|
}
|
186
196
|
let selector = `:matches(${[...kinds]})`;
|
187
|
-
for (const str of ignoredSelectors)
|
197
|
+
for (const str of ignoredSelectors) {
|
188
198
|
selector += `:not(${str})`;
|
199
|
+
}
|
189
200
|
return {
|
190
201
|
[selector](node) {
|
191
202
|
let description = "";
|
192
203
|
const isOperation = node.kind === Kind.OPERATION_DEFINITION;
|
193
204
|
if (isOperation) {
|
194
|
-
const rawNode = node.rawNode()
|
205
|
+
const rawNode = node.rawNode();
|
206
|
+
const { prev, line } = rawNode.loc.startToken;
|
195
207
|
if (prev?.kind === TokenKind.COMMENT) {
|
196
|
-
const value = prev.value.trim()
|
197
|
-
|
208
|
+
const value = prev.value.trim();
|
209
|
+
const linesBefore = line - prev.line;
|
210
|
+
if (!value.startsWith("eslint") && linesBefore === 1) {
|
211
|
+
description = value;
|
212
|
+
}
|
198
213
|
}
|
199
|
-
} else
|
214
|
+
} else {
|
200
215
|
description = node.description?.value.trim() || "";
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
216
|
+
}
|
217
|
+
if (description.length === 0) {
|
218
|
+
context.report({
|
219
|
+
loc: isOperation ? getLocation(node.loc.start, node.operation) : node.name.loc,
|
220
|
+
messageId: RULE_ID,
|
221
|
+
data: {
|
222
|
+
nodeName: getNodeName(node)
|
223
|
+
}
|
224
|
+
});
|
225
|
+
}
|
208
226
|
}
|
209
227
|
};
|
210
228
|
}
|
@@ -1,13 +1,14 @@
|
|
1
1
|
import { isObjectType } from "graphql";
|
2
2
|
import { getTypeName, requireGraphQLSchema } from "../../utils.js";
|
3
|
-
const RULE_ID = "require-field-of-type-query-in-mutation-result"
|
3
|
+
const RULE_ID = "require-field-of-type-query-in-mutation-result";
|
4
|
+
const rule = {
|
4
5
|
meta: {
|
5
6
|
type: "suggestion",
|
6
7
|
docs: {
|
7
8
|
category: "Schema",
|
8
9
|
description: "Allow the client in one round-trip not only to call mutation but also to get a wagon of data to update their application.\n> Currently, no errors are reported for result type `union`, `interface` and `scalar`.",
|
9
10
|
url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID}`,
|
10
|
-
requiresSchema:
|
11
|
+
requiresSchema: true,
|
11
12
|
examples: [
|
12
13
|
{
|
13
14
|
title: "Incorrect",
|
@@ -47,16 +48,26 @@ const RULE_ID = "require-field-of-type-query-in-mutation-result", rule = {
|
|
47
48
|
schema: []
|
48
49
|
},
|
49
50
|
create(context) {
|
50
|
-
const schema = requireGraphQLSchema(RULE_ID, context)
|
51
|
-
|
52
|
-
|
53
|
-
|
51
|
+
const schema = requireGraphQLSchema(RULE_ID, context);
|
52
|
+
const mutationType = schema.getMutationType();
|
53
|
+
const queryType = schema.getQueryType();
|
54
|
+
if (!mutationType || !queryType) {
|
55
|
+
return {};
|
56
|
+
}
|
57
|
+
const selector = `:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=${mutationType.name}] > FieldDefinition > .gqlType Name`;
|
58
|
+
return {
|
59
|
+
[selector](node) {
|
60
|
+
const typeName = node.value;
|
61
|
+
const graphQLType = schema.getType(typeName);
|
54
62
|
if (isObjectType(graphQLType)) {
|
55
63
|
const { fields } = graphQLType.astNode;
|
56
|
-
fields?.some((field) => getTypeName(field) === queryType.name)
|
57
|
-
|
58
|
-
|
59
|
-
|
64
|
+
const hasQueryType = fields?.some((field) => getTypeName(field) === queryType.name);
|
65
|
+
if (!hasQueryType) {
|
66
|
+
context.report({
|
67
|
+
node,
|
68
|
+
message: `Mutation result type "${graphQLType.name}" must contain field of type "${queryType.name}"`
|
69
|
+
});
|
70
|
+
}
|
60
71
|
}
|
61
72
|
}
|
62
73
|
};
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import path from "node:path";
|
2
2
|
import { requireGraphQLOperations, slash } from "../../utils.js";
|
3
|
-
const RULE_ID = "require-import-fragment"
|
3
|
+
const RULE_ID = "require-import-fragment";
|
4
|
+
const SUGGESTION_ID = "add-import-expression";
|
5
|
+
const rule = {
|
4
6
|
meta: {
|
5
7
|
type: "suggestion",
|
6
8
|
docs: {
|
@@ -64,9 +66,9 @@ const RULE_ID = "require-import-fragment", SUGGESTION_ID = "add-import-expressio
|
|
64
66
|
)
|
65
67
|
}
|
66
68
|
],
|
67
|
-
requiresSiblings:
|
69
|
+
requiresSiblings: true
|
68
70
|
},
|
69
|
-
hasSuggestions:
|
71
|
+
hasSuggestions: true,
|
70
72
|
messages: {
|
71
73
|
[RULE_ID]: 'Expected "{{fragmentName}}" fragment to be imported.',
|
72
74
|
[SUGGESTION_ID]: 'Add import expression for "{{fragmentName}}".'
|
@@ -74,24 +76,31 @@ const RULE_ID = "require-import-fragment", SUGGESTION_ID = "add-import-expressio
|
|
74
76
|
schema: []
|
75
77
|
},
|
76
78
|
create(context) {
|
77
|
-
const comments = context.getSourceCode().getAllComments()
|
79
|
+
const comments = context.getSourceCode().getAllComments();
|
80
|
+
const siblings = requireGraphQLOperations(RULE_ID, context);
|
81
|
+
const filePath = context.filename;
|
78
82
|
return {
|
79
83
|
"FragmentSpread > .name"(node) {
|
80
|
-
const fragmentName = node.value
|
84
|
+
const fragmentName = node.value;
|
85
|
+
const fragmentsFromSiblings = siblings.getFragment(fragmentName);
|
81
86
|
for (const comment of comments) {
|
82
|
-
if (comment.type !== "Line"
|
87
|
+
if (comment.type !== "Line") continue;
|
88
|
+
const isPossibleImported = new RegExp(
|
83
89
|
`^\\s*import\\s+(${fragmentName}\\s+from\\s+)?['"]`
|
84
|
-
).test(comment.value)
|
90
|
+
).test(comment.value);
|
91
|
+
if (!isPossibleImported) continue;
|
85
92
|
const extractedImportPath = comment.value.match(/(["'])((?:\1|.)*?)\1/)?.[2];
|
86
93
|
if (!extractedImportPath) continue;
|
87
94
|
const importPath = path.join(filePath, "..", extractedImportPath);
|
88
|
-
|
95
|
+
const hasInSiblings = fragmentsFromSiblings.some(
|
89
96
|
(source) => source.filePath === importPath
|
90
|
-
)
|
97
|
+
);
|
98
|
+
if (hasInSiblings) return;
|
91
99
|
}
|
92
|
-
|
100
|
+
const fragmentInSameFile = fragmentsFromSiblings.some(
|
93
101
|
(source) => source.filePath === filePath
|
94
|
-
)
|
102
|
+
);
|
103
|
+
if (fragmentInSameFile) return;
|
95
104
|
const suggestedFilePaths = fragmentsFromSiblings.length ? fragmentsFromSiblings.map(
|
96
105
|
(o) => (
|
97
106
|
// Use always forward slash for suggested import path
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { Kind } from "graphql";
|
2
2
|
import { getNodeName } from "../../utils.js";
|
3
|
-
const RULE_ID = "require-nullable-fields-with-oneof"
|
3
|
+
const RULE_ID = "require-nullable-fields-with-oneof";
|
4
|
+
const rule = {
|
4
5
|
meta: {
|
5
6
|
type: "suggestion",
|
6
7
|
docs: {
|
@@ -42,16 +43,22 @@ const RULE_ID = "require-nullable-fields-with-oneof", rule = {
|
|
42
43
|
create(context) {
|
43
44
|
return {
|
44
45
|
"Directive[name.value=oneOf]"({ parent }) {
|
45
|
-
|
46
|
+
const isTypeOrInput = [
|
46
47
|
Kind.OBJECT_TYPE_DEFINITION,
|
47
48
|
Kind.INPUT_OBJECT_TYPE_DEFINITION
|
48
|
-
].includes(parent.kind)
|
49
|
-
|
50
|
-
|
49
|
+
].includes(parent.kind);
|
50
|
+
if (!isTypeOrInput) {
|
51
|
+
return;
|
52
|
+
}
|
53
|
+
for (const field of parent.fields || []) {
|
54
|
+
if (field.gqlType.kind === Kind.NON_NULL_TYPE) {
|
55
|
+
context.report({
|
51
56
|
node: field.name,
|
52
57
|
messageId: RULE_ID,
|
53
58
|
data: { nodeName: getNodeName(field) }
|
54
59
|
});
|
60
|
+
}
|
61
|
+
}
|
55
62
|
}
|
56
63
|
};
|
57
64
|
}
|
@@ -1,14 +1,15 @@
|
|
1
1
|
import { Kind } from "graphql";
|
2
2
|
import { getNodeName, requireGraphQLSchema } from "../../utils.js";
|
3
|
-
const RULE_ID = "require-nullable-result-in-root"
|
3
|
+
const RULE_ID = "require-nullable-result-in-root";
|
4
|
+
const rule = {
|
4
5
|
meta: {
|
5
6
|
type: "suggestion",
|
6
|
-
hasSuggestions:
|
7
|
+
hasSuggestions: true,
|
7
8
|
docs: {
|
8
9
|
category: "Schema",
|
9
10
|
description: "Require nullable fields in root types.",
|
10
11
|
url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID}`,
|
11
|
-
requiresSchema:
|
12
|
+
requiresSchema: true,
|
12
13
|
examples: [
|
13
14
|
{
|
14
15
|
title: "Incorrect",
|
@@ -42,34 +43,38 @@ const RULE_ID = "require-nullable-result-in-root", rule = {
|
|
42
43
|
schema: []
|
43
44
|
},
|
44
45
|
create(context) {
|
45
|
-
const schema = requireGraphQLSchema(RULE_ID, context)
|
46
|
+
const schema = requireGraphQLSchema(RULE_ID, context);
|
47
|
+
const rootTypeNames = new Set(
|
46
48
|
[schema.getQueryType(), schema.getMutationType()].filter((v) => !!v).map((type) => type.name)
|
47
|
-
)
|
49
|
+
);
|
50
|
+
const sourceCode = context.getSourceCode();
|
48
51
|
return {
|
49
52
|
"ObjectTypeDefinition,ObjectTypeExtension"(node) {
|
50
|
-
if (rootTypeNames.has(node.name.value))
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
53
|
+
if (!rootTypeNames.has(node.name.value)) return;
|
54
|
+
for (const field of node.fields || []) {
|
55
|
+
if (field.gqlType.type !== Kind.NON_NULL_TYPE || field.gqlType.gqlType.type !== Kind.NAMED_TYPE)
|
56
|
+
continue;
|
57
|
+
const name = field.gqlType.gqlType.name.value;
|
58
|
+
const type = schema.getType(name);
|
59
|
+
const resultType = type?.astNode ? getNodeName(type.astNode) : type?.name;
|
60
|
+
context.report({
|
61
|
+
node: field.gqlType,
|
62
|
+
messageId: RULE_ID,
|
63
|
+
data: {
|
64
|
+
resultType: resultType || "",
|
65
|
+
rootType: getNodeName(node)
|
66
|
+
},
|
67
|
+
suggest: [
|
68
|
+
{
|
69
|
+
desc: `Make ${resultType} nullable`,
|
70
|
+
fix(fixer) {
|
71
|
+
const text = sourceCode.getText(field.gqlType);
|
72
|
+
return fixer.replaceText(field.gqlType, text.replace("!", ""));
|
69
73
|
}
|
70
|
-
|
71
|
-
|
72
|
-
}
|
74
|
+
}
|
75
|
+
]
|
76
|
+
});
|
77
|
+
}
|
73
78
|
}
|
74
79
|
};
|
75
80
|
}
|
@@ -15,7 +15,9 @@ import {
|
|
15
15
|
requireGraphQLOperations,
|
16
16
|
requireGraphQLSchema
|
17
17
|
} from "../../utils.js";
|
18
|
-
const RULE_ID = "require-selections"
|
18
|
+
const RULE_ID = "require-selections";
|
19
|
+
const DEFAULT_ID_FIELD_NAME = "id";
|
20
|
+
const schema = {
|
19
21
|
definitions: {
|
20
22
|
asString: {
|
21
23
|
type: "string"
|
@@ -26,7 +28,7 @@ const RULE_ID = "require-selections", DEFAULT_ID_FIELD_NAME = "id", schema = {
|
|
26
28
|
maxItems: 1,
|
27
29
|
items: {
|
28
30
|
type: "object",
|
29
|
-
additionalProperties:
|
31
|
+
additionalProperties: false,
|
30
32
|
properties: {
|
31
33
|
fieldName: {
|
32
34
|
oneOf: [{ $ref: "#/definitions/asString" }, { $ref: "#/definitions/asArray" }],
|
@@ -38,16 +40,17 @@ const RULE_ID = "require-selections", DEFAULT_ID_FIELD_NAME = "id", schema = {
|
|
38
40
|
}
|
39
41
|
}
|
40
42
|
}
|
41
|
-
}
|
43
|
+
};
|
44
|
+
const rule = {
|
42
45
|
meta: {
|
43
46
|
type: "suggestion",
|
44
|
-
hasSuggestions:
|
47
|
+
hasSuggestions: true,
|
45
48
|
docs: {
|
46
49
|
category: "Operations",
|
47
50
|
description: "Enforce selecting specific fields when they are available on the GraphQL type.",
|
48
51
|
url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID}`,
|
49
|
-
requiresSchema:
|
50
|
-
requiresSiblings:
|
52
|
+
requiresSchema: true,
|
53
|
+
requiresSiblings: true,
|
51
54
|
examples: [
|
52
55
|
{
|
53
56
|
title: "Incorrect",
|
@@ -98,34 +101,45 @@ const RULE_ID = "require-selections", DEFAULT_ID_FIELD_NAME = "id", schema = {
|
|
98
101
|
)
|
99
102
|
}
|
100
103
|
],
|
101
|
-
recommended:
|
104
|
+
recommended: true,
|
102
105
|
whenNotToUseIt: "Relay Compiler automatically adds an `id` field to any type that has an `id` field, even if it hasn't been explicitly requested. Requesting a field that is not used directly in the code can conflict with another Relay rule: `relay/unused-fields`."
|
103
106
|
},
|
104
107
|
messages: {
|
105
|
-
[RULE_ID]:
|
106
|
-
Include it in your selection set{{ addition }}.`
|
108
|
+
[RULE_ID]: "Field{{ pluralSuffix }} {{ fieldName }} must be selected when it's available on a type.\nInclude it in your selection set{{ addition }}."
|
107
109
|
},
|
108
110
|
schema
|
109
111
|
},
|
110
112
|
create(context) {
|
111
|
-
const schema2 = requireGraphQLSchema(RULE_ID, context)
|
113
|
+
const schema2 = requireGraphQLSchema(RULE_ID, context);
|
114
|
+
const siblings = requireGraphQLOperations(RULE_ID, context);
|
115
|
+
const { fieldName = DEFAULT_ID_FIELD_NAME, requireAllFields } = context.options[0] || {};
|
116
|
+
const idNames = asArray(fieldName);
|
117
|
+
const selector = "SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]";
|
118
|
+
const typeInfo = new TypeInfo(schema2);
|
112
119
|
function checkFragments(node) {
|
113
120
|
for (const selection of node.selections) {
|
114
|
-
if (selection.kind !== Kind.FRAGMENT_SPREAD)
|
121
|
+
if (selection.kind !== Kind.FRAGMENT_SPREAD) {
|
115
122
|
continue;
|
123
|
+
}
|
116
124
|
const [foundSpread] = siblings.getFragment(selection.name.value);
|
117
|
-
if (!foundSpread)
|
125
|
+
if (!foundSpread) {
|
118
126
|
continue;
|
119
|
-
|
127
|
+
}
|
128
|
+
const checkedFragmentSpreads = /* @__PURE__ */ new Set();
|
129
|
+
const visitor = visitWithTypeInfo(typeInfo, {
|
120
130
|
SelectionSet(node2, key, _parent) {
|
121
131
|
const parent = _parent;
|
122
|
-
parent.kind === Kind.FRAGMENT_DEFINITION
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
132
|
+
if (parent.kind === Kind.FRAGMENT_DEFINITION) {
|
133
|
+
checkedFragmentSpreads.add(parent.name.value);
|
134
|
+
} else if (parent.kind !== Kind.INLINE_FRAGMENT) {
|
135
|
+
checkSelections(
|
136
|
+
node2,
|
137
|
+
typeInfo.getType(),
|
138
|
+
selection.loc.start,
|
139
|
+
parent,
|
140
|
+
checkedFragmentSpreads
|
141
|
+
);
|
142
|
+
}
|
129
143
|
}
|
130
144
|
});
|
131
145
|
visit(foundSpread.document, visitor);
|
@@ -133,52 +147,74 @@ Include it in your selection set{{ addition }}.`
|
|
133
147
|
}
|
134
148
|
function checkSelections(node, type, loc, parent, checkedFragmentSpreads = /* @__PURE__ */ new Set()) {
|
135
149
|
const rawType = getBaseType(type);
|
136
|
-
if (rawType instanceof GraphQLObjectType || rawType instanceof GraphQLInterfaceType)
|
150
|
+
if (rawType instanceof GraphQLObjectType || rawType instanceof GraphQLInterfaceType) {
|
137
151
|
checkFields(rawType);
|
138
|
-
else if (rawType instanceof GraphQLUnionType)
|
152
|
+
} else if (rawType instanceof GraphQLUnionType) {
|
139
153
|
for (const selection of node.selections) {
|
140
154
|
const types = rawType.getTypes();
|
141
155
|
if (selection.kind === Kind.INLINE_FRAGMENT) {
|
142
156
|
const t = types.find((t2) => t2.name === selection.typeCondition.name.value);
|
143
|
-
|
157
|
+
if (t) {
|
158
|
+
checkFields(t);
|
159
|
+
}
|
144
160
|
} else if (selection.kind === Kind.FRAGMENT_SPREAD) {
|
145
161
|
const [foundSpread] = siblings.getFragment(selection.name.value);
|
146
162
|
if (!foundSpread) return;
|
147
|
-
const fragmentSpread = foundSpread.document
|
148
|
-
|
163
|
+
const fragmentSpread = foundSpread.document;
|
164
|
+
const t = fragmentSpread.typeCondition.name.value === rawType.name ? rawType : types.find((t2) => t2.name === fragmentSpread.typeCondition.name.value);
|
165
|
+
checkedFragmentSpreads.add(fragmentSpread.name.value);
|
166
|
+
checkSelections(fragmentSpread.selectionSet, t, loc, parent, checkedFragmentSpreads);
|
149
167
|
}
|
150
168
|
}
|
169
|
+
}
|
151
170
|
function checkFields(rawType2) {
|
152
171
|
const fields = rawType2.getFields();
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
172
|
+
const hasIdFieldInType = idNames.some((name) => fields[name]);
|
173
|
+
if (!hasIdFieldInType) {
|
174
|
+
return;
|
175
|
+
}
|
176
|
+
checkFragments(node);
|
177
|
+
if (requireAllFields) {
|
178
|
+
for (const idName of idNames) {
|
179
|
+
report([idName]);
|
180
|
+
}
|
181
|
+
} else {
|
182
|
+
report(idNames);
|
183
|
+
}
|
159
184
|
}
|
160
185
|
function report(idNames2) {
|
161
186
|
function hasIdField({ selections }) {
|
162
187
|
return selections.some((selection) => {
|
163
|
-
if (selection.kind === Kind.FIELD)
|
164
|
-
|
165
|
-
|
188
|
+
if (selection.kind === Kind.FIELD) {
|
189
|
+
if (selection.alias && idNames2.includes(selection.alias.value)) {
|
190
|
+
return true;
|
191
|
+
}
|
192
|
+
return idNames2.includes(selection.name.value);
|
193
|
+
}
|
194
|
+
if (selection.kind === Kind.INLINE_FRAGMENT) {
|
166
195
|
return hasIdField(selection.selectionSet);
|
196
|
+
}
|
167
197
|
if (selection.kind === Kind.FRAGMENT_SPREAD) {
|
168
198
|
const [foundSpread] = siblings.getFragment(selection.name.value);
|
169
199
|
if (foundSpread) {
|
170
200
|
const fragmentSpread = foundSpread.document;
|
171
|
-
|
201
|
+
checkedFragmentSpreads.add(fragmentSpread.name.value);
|
202
|
+
return hasIdField(fragmentSpread.selectionSet);
|
172
203
|
}
|
173
204
|
}
|
174
|
-
return
|
205
|
+
return false;
|
175
206
|
});
|
176
207
|
}
|
177
|
-
|
208
|
+
const hasId = hasIdField(node);
|
209
|
+
if (hasId) {
|
178
210
|
return;
|
211
|
+
}
|
179
212
|
const fieldName2 = englishJoinWords(
|
180
213
|
idNames2.map((name) => `\`${(parent.alias || parent.name).value}.${name}\``)
|
181
|
-
)
|
214
|
+
);
|
215
|
+
const pluralSuffix = idNames2.length > 1 ? "s" : "";
|
216
|
+
const addition = checkedFragmentSpreads.size === 0 ? "" : ` or add to used fragment${checkedFragmentSpreads.size > 1 ? "s" : ""} ${englishJoinWords([...checkedFragmentSpreads].map((name) => `\`${name}\``))}`;
|
217
|
+
const problem = {
|
182
218
|
loc,
|
183
219
|
messageId: RULE_ID,
|
184
220
|
data: {
|
@@ -187,19 +223,25 @@ Include it in your selection set{{ addition }}.`
|
|
187
223
|
addition
|
188
224
|
}
|
189
225
|
};
|
190
|
-
"type" in node
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
226
|
+
if ("type" in node) {
|
227
|
+
problem.suggest = idNames2.map((idName) => ({
|
228
|
+
desc: `Add \`${idName}\` selection`,
|
229
|
+
fix: (fixer) => {
|
230
|
+
let insertNode = node.selections[0];
|
231
|
+
insertNode = insertNode.kind === Kind.INLINE_FRAGMENT ? insertNode.selectionSet.selections[0] : insertNode;
|
232
|
+
return fixer.insertTextBefore(insertNode, `${idName} `);
|
233
|
+
}
|
234
|
+
}));
|
235
|
+
}
|
236
|
+
context.report(problem);
|
197
237
|
}
|
198
238
|
}
|
199
239
|
return {
|
200
240
|
[selector](node) {
|
201
241
|
const typeInfo2 = node.typeInfo();
|
202
|
-
|
242
|
+
if (typeInfo2.gqlType) {
|
243
|
+
checkSelections(node, typeInfo2.gqlType, node.loc.start, node.parent);
|
244
|
+
}
|
203
245
|
}
|
204
246
|
};
|
205
247
|
}
|