@graphql-eslint/eslint-plugin 4.3.0 → 4.3.1-alpha-20241207204625-6a4230707a78900a6339b03afe904b9dd6c31561
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/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/index.d.cts +18 -4
- 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/index.d.cts +18 -4
- 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.d.cts +4 -3
- package/cjs/rules/match-document-filename/index.js +63 -37
- package/cjs/rules/naming-convention/index.d.cts +6 -10
- package/cjs/rules/naming-convention/index.js +146 -57
- 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.d.cts +79 -13
- package/cjs/rules/require-description/index.js +67 -49
- 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/index.d.ts +19 -5
- 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/index.d.ts +19 -5
- 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.d.ts +4 -3
- package/esm/rules/match-document-filename/index.js +63 -37
- package/esm/rules/naming-convention/index.d.ts +6 -10
- package/esm/rules/naming-convention/index.js +146 -57
- 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.d.ts +79 -13
- package/esm/rules/require-description/index.js +67 -49
- 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/index.browser.js +1838 -1135
- package/package.json +1 -1
@@ -5,7 +5,7 @@ const schema = {
|
|
5
5
|
maxItems: 1,
|
6
6
|
items: {
|
7
7
|
type: "object",
|
8
|
-
additionalProperties:
|
8
|
+
additionalProperties: false,
|
9
9
|
required: ["disallow"],
|
10
10
|
properties: {
|
11
11
|
disallow: {
|
@@ -16,15 +16,16 @@ const schema = {
|
|
16
16
|
}
|
17
17
|
}
|
18
18
|
}
|
19
|
-
}
|
19
|
+
};
|
20
|
+
const rule = {
|
20
21
|
meta: {
|
21
22
|
type: "suggestion",
|
22
|
-
hasSuggestions:
|
23
|
+
hasSuggestions: true,
|
23
24
|
docs: {
|
24
25
|
category: "Schema",
|
25
26
|
description: "Disallow using root types `mutation` and/or `subscription`.",
|
26
27
|
url: "https://the-guild.dev/graphql/eslint/rules/no-root-type",
|
27
|
-
requiresSchema:
|
28
|
+
requiresSchema: true,
|
28
29
|
examples: [
|
29
30
|
{
|
30
31
|
title: "Incorrect",
|
@@ -56,12 +57,18 @@ const schema = {
|
|
56
57
|
schema
|
57
58
|
},
|
58
59
|
create(context) {
|
59
|
-
const schema2 = requireGraphQLSchema("no-root-type", context)
|
60
|
+
const schema2 = requireGraphQLSchema("no-root-type", context);
|
61
|
+
const disallow = new Set(context.options[0].disallow);
|
62
|
+
const rootTypeNames = [
|
60
63
|
disallow.has("mutation") && schema2.getMutationType(),
|
61
64
|
disallow.has("subscription") && schema2.getSubscriptionType()
|
62
65
|
].filter((v) => !!v).map((type) => type.name).join("|");
|
63
|
-
|
64
|
-
|
66
|
+
if (!rootTypeNames) {
|
67
|
+
return {};
|
68
|
+
}
|
69
|
+
const selector = `:matches(ObjectTypeDefinition, ObjectTypeExtension) > .name[value=/^(${rootTypeNames})$/]`;
|
70
|
+
return {
|
71
|
+
[selector](node) {
|
65
72
|
const typeName = node.value;
|
66
73
|
context.report({
|
67
74
|
node,
|
@@ -74,7 +81,7 @@ const schema = {
|
|
74
81
|
]
|
75
82
|
});
|
76
83
|
}
|
77
|
-
}
|
84
|
+
};
|
78
85
|
}
|
79
86
|
};
|
80
87
|
export {
|
@@ -1,14 +1,15 @@
|
|
1
1
|
import { isScalarType, Kind } from "graphql";
|
2
2
|
import { getNodeName, requireGraphQLSchema } from "../../utils.js";
|
3
|
-
const RULE_ID = "no-scalar-result-type-on-mutation"
|
3
|
+
const RULE_ID = "no-scalar-result-type-on-mutation";
|
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: "Avoid scalar result type on mutation type to make sure to return a valid state.",
|
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",
|
@@ -37,17 +38,24 @@ const RULE_ID = "no-scalar-result-type-on-mutation", rule = {
|
|
37
38
|
schema: []
|
38
39
|
},
|
39
40
|
create(context) {
|
40
|
-
const schema = requireGraphQLSchema(RULE_ID, context)
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
41
|
+
const schema = requireGraphQLSchema(RULE_ID, context);
|
42
|
+
const mutationType = schema.getMutationType();
|
43
|
+
if (!mutationType) {
|
44
|
+
return {};
|
45
|
+
}
|
46
|
+
const selector = [
|
47
|
+
`:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=${mutationType.name}]`,
|
48
|
+
"> FieldDefinition > .gqlType Name"
|
49
|
+
].join(" ");
|
50
|
+
return {
|
51
|
+
[selector](node) {
|
52
|
+
const typeName = node.value;
|
53
|
+
const graphQLType = schema.getType(typeName);
|
47
54
|
if (isScalarType(graphQLType)) {
|
48
55
|
let fieldDef = node.parent;
|
49
|
-
|
56
|
+
while (fieldDef.kind !== Kind.FIELD_DEFINITION) {
|
50
57
|
fieldDef = fieldDef.parent;
|
58
|
+
}
|
51
59
|
context.report({
|
52
60
|
node,
|
53
61
|
message: `Unexpected scalar result type \`${typeName}\` for ${getNodeName(fieldDef)}`,
|
@@ -60,7 +68,7 @@ const RULE_ID = "no-scalar-result-type-on-mutation", rule = {
|
|
60
68
|
});
|
61
69
|
}
|
62
70
|
}
|
63
|
-
}
|
71
|
+
};
|
64
72
|
}
|
65
73
|
};
|
66
74
|
export {
|
@@ -1,11 +1,12 @@
|
|
1
|
-
const NO_TYPENAME_PREFIX = "NO_TYPENAME_PREFIX"
|
1
|
+
const NO_TYPENAME_PREFIX = "NO_TYPENAME_PREFIX";
|
2
|
+
const rule = {
|
2
3
|
meta: {
|
3
4
|
type: "suggestion",
|
4
|
-
hasSuggestions:
|
5
|
+
hasSuggestions: true,
|
5
6
|
docs: {
|
6
7
|
category: "Schema",
|
7
8
|
description: "Enforces users to avoid using the type name in a field name while defining your schema.",
|
8
|
-
recommended:
|
9
|
+
recommended: true,
|
9
10
|
url: "https://the-guild.dev/graphql/eslint/rules/no-typename-prefix",
|
10
11
|
examples: [
|
11
12
|
{
|
@@ -40,26 +41,29 @@ const NO_TYPENAME_PREFIX = "NO_TYPENAME_PREFIX", rule = {
|
|
40
41
|
create(context) {
|
41
42
|
return {
|
42
43
|
"ObjectTypeDefinition, ObjectTypeExtension, InterfaceTypeDefinition, InterfaceTypeExtension"(node) {
|
43
|
-
const typeName = node.name.value
|
44
|
+
const typeName = node.name.value;
|
45
|
+
const lowerTypeName = typeName.toLowerCase();
|
44
46
|
for (const field of node.fields || []) {
|
45
47
|
const fieldName = field.name.value;
|
46
|
-
fieldName.toLowerCase().startsWith(lowerTypeName)
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
48
|
+
if (fieldName.toLowerCase().startsWith(lowerTypeName)) {
|
49
|
+
context.report({
|
50
|
+
data: {
|
51
|
+
fieldName,
|
52
|
+
typeName
|
53
|
+
},
|
54
|
+
messageId: NO_TYPENAME_PREFIX,
|
55
|
+
node: field.name,
|
56
|
+
suggest: [
|
57
|
+
{
|
58
|
+
desc: `Remove \`${fieldName.slice(0, typeName.length)}\` prefix`,
|
59
|
+
fix: (fixer) => fixer.replaceText(
|
60
|
+
field.name,
|
61
|
+
fieldName.replace(new RegExp(`^${typeName}`, "i"), "")
|
62
|
+
)
|
63
|
+
}
|
64
|
+
]
|
65
|
+
});
|
66
|
+
}
|
63
67
|
}
|
64
68
|
}
|
65
69
|
};
|
@@ -8,7 +8,8 @@ import {
|
|
8
8
|
import lowerCase from "lodash.lowercase";
|
9
9
|
import { ModuleCache } from "../../cache.js";
|
10
10
|
import { getTypeName, requireGraphQLSchema } from "../../utils.js";
|
11
|
-
const RULE_ID = "no-unreachable-types"
|
11
|
+
const RULE_ID = "no-unreachable-types";
|
12
|
+
const KINDS = [
|
12
13
|
Kind.DIRECTIVE_DEFINITION,
|
13
14
|
Kind.OBJECT_TYPE_DEFINITION,
|
14
15
|
Kind.OBJECT_TYPE_EXTENSION,
|
@@ -22,7 +23,9 @@ const RULE_ID = "no-unreachable-types", KINDS = [
|
|
22
23
|
Kind.UNION_TYPE_EXTENSION,
|
23
24
|
Kind.ENUM_TYPE_DEFINITION,
|
24
25
|
Kind.ENUM_TYPE_EXTENSION
|
25
|
-
]
|
26
|
+
];
|
27
|
+
const reachableTypesCache = new ModuleCache();
|
28
|
+
const RequestDirectiveLocations = /* @__PURE__ */ new Set([
|
26
29
|
DirectiveLocation.QUERY,
|
27
30
|
DirectiveLocation.MUTATION,
|
28
31
|
DirectiveLocation.SUBSCRIPTION,
|
@@ -34,20 +37,27 @@ const RULE_ID = "no-unreachable-types", KINDS = [
|
|
34
37
|
]);
|
35
38
|
function getReachableTypes(schema) {
|
36
39
|
const cachedValue = reachableTypesCache.get(schema);
|
37
|
-
if (process.env.NODE_ENV !== "test" && cachedValue)
|
40
|
+
if (process.env.NODE_ENV !== "test" && cachedValue) {
|
38
41
|
return cachedValue;
|
39
|
-
|
42
|
+
}
|
43
|
+
const reachableTypes = /* @__PURE__ */ new Set();
|
44
|
+
const collect = (node) => {
|
40
45
|
const typeName = getTypeName(node);
|
41
|
-
if (reachableTypes.has(typeName))
|
46
|
+
if (reachableTypes.has(typeName)) {
|
42
47
|
return;
|
48
|
+
}
|
43
49
|
reachableTypes.add(typeName);
|
44
50
|
const type = schema.getType(typeName) || schema.getDirective(typeName);
|
45
51
|
if (isInterfaceType(type)) {
|
46
52
|
const { objects, interfaces } = schema.getImplementations(type);
|
47
|
-
for (const { astNode } of [...objects, ...interfaces])
|
53
|
+
for (const { astNode } of [...objects, ...interfaces]) {
|
48
54
|
visit(astNode, visitor);
|
49
|
-
|
50
|
-
|
55
|
+
}
|
56
|
+
} else if (type?.astNode) {
|
57
|
+
visit(type.astNode, visitor);
|
58
|
+
}
|
59
|
+
};
|
60
|
+
const visitor = {
|
51
61
|
InterfaceTypeDefinition: collect,
|
52
62
|
ObjectTypeDefinition: collect,
|
53
63
|
InputValueDefinition: collect,
|
@@ -62,15 +72,21 @@ function getReachableTypes(schema) {
|
|
62
72
|
schema.getQueryType(),
|
63
73
|
schema.getMutationType(),
|
64
74
|
schema.getSubscriptionType()
|
65
|
-
])
|
66
|
-
|
67
|
-
|
75
|
+
]) {
|
76
|
+
if (type?.astNode) {
|
77
|
+
visit(type.astNode, visitor);
|
78
|
+
}
|
79
|
+
}
|
80
|
+
for (const node of schema.getDirectives()) {
|
68
81
|
if (node.locations.some((location) => RequestDirectiveLocations.has(location))) {
|
69
82
|
reachableTypes.add(node.name);
|
70
|
-
for (const arg of node.args)
|
83
|
+
for (const arg of node.args) {
|
71
84
|
reachableTypes.add(getNamedType(arg.type).name);
|
85
|
+
}
|
72
86
|
}
|
73
|
-
|
87
|
+
}
|
88
|
+
reachableTypesCache.set(schema, reachableTypes);
|
89
|
+
return reachableTypes;
|
74
90
|
}
|
75
91
|
const rule = {
|
76
92
|
meta: {
|
@@ -81,7 +97,7 @@ const rule = {
|
|
81
97
|
description: "Requires all types to be reachable at some level by root level fields.",
|
82
98
|
category: "Schema",
|
83
99
|
url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID}`,
|
84
|
-
requiresSchema:
|
100
|
+
requiresSchema: true,
|
85
101
|
examples: [
|
86
102
|
{
|
87
103
|
title: "Incorrect",
|
@@ -116,14 +132,15 @@ const rule = {
|
|
116
132
|
)
|
117
133
|
}
|
118
134
|
],
|
119
|
-
recommended:
|
135
|
+
recommended: true
|
120
136
|
},
|
121
137
|
type: "suggestion",
|
122
138
|
schema: [],
|
123
|
-
hasSuggestions:
|
139
|
+
hasSuggestions: true
|
124
140
|
},
|
125
141
|
create(context) {
|
126
|
-
const schema = requireGraphQLSchema(RULE_ID, context)
|
142
|
+
const schema = requireGraphQLSchema(RULE_ID, context);
|
143
|
+
const reachableTypes = getReachableTypes(schema);
|
127
144
|
return {
|
128
145
|
[`:matches(${KINDS}) > .name`](node) {
|
129
146
|
const typeName = node.value;
|
@@ -1,7 +1,8 @@
|
|
1
1
|
import { TypeInfo, visit, visitWithTypeInfo } from "graphql";
|
2
2
|
import { ModuleCache } from "../../cache.js";
|
3
3
|
import { eslintSelectorsTip, requireGraphQLOperations, requireGraphQLSchema } from "../../utils.js";
|
4
|
-
const RULE_ID = "no-unused-fields"
|
4
|
+
const RULE_ID = "no-unused-fields";
|
5
|
+
const RELAY_SCHEMA = (
|
5
6
|
/* GraphQL */
|
6
7
|
`
|
7
8
|
# Root Query Type
|
@@ -42,7 +43,8 @@ const RULE_ID = "no-unused-fields", RELAY_SCHEMA = (
|
|
42
43
|
endCursor: String
|
43
44
|
}
|
44
45
|
`
|
45
|
-
)
|
46
|
+
);
|
47
|
+
const RELAY_QUERY = (
|
46
48
|
/* GraphQL */
|
47
49
|
`
|
48
50
|
query {
|
@@ -60,20 +62,22 @@ const RULE_ID = "no-unused-fields", RELAY_SCHEMA = (
|
|
60
62
|
}
|
61
63
|
}
|
62
64
|
`
|
63
|
-
)
|
65
|
+
);
|
66
|
+
const RELAY_DEFAULT_IGNORED_FIELD_SELECTORS = [
|
64
67
|
"[parent.name.value=PageInfo][name.value=/(endCursor|startCursor|hasNextPage|hasPreviousPage)/]",
|
65
68
|
"[parent.name.value=/Edge$/][name.value=cursor]",
|
66
69
|
"[parent.name.value=/Connection$/][name.value=pageInfo]"
|
67
|
-
]
|
70
|
+
];
|
71
|
+
const schema = {
|
68
72
|
type: "array",
|
69
73
|
maxItems: 1,
|
70
74
|
items: {
|
71
75
|
type: "object",
|
72
|
-
additionalProperties:
|
76
|
+
additionalProperties: false,
|
73
77
|
properties: {
|
74
78
|
ignoredFieldSelectors: {
|
75
79
|
type: "array",
|
76
|
-
uniqueItems:
|
80
|
+
uniqueItems: true,
|
77
81
|
minItems: 1,
|
78
82
|
description: [
|
79
83
|
"Fields that will be ignored and are allowed to be unused.",
|
@@ -83,8 +87,7 @@ const RULE_ID = "no-unused-fields", RELAY_SCHEMA = (
|
|
83
87
|
JSON.stringify(RELAY_DEFAULT_IGNORED_FIELD_SELECTORS, null, 2),
|
84
88
|
"```",
|
85
89
|
eslintSelectorsTip
|
86
|
-
].join(
|
87
|
-
`),
|
90
|
+
].join("\n"),
|
88
91
|
items: {
|
89
92
|
type: "string",
|
90
93
|
pattern: "^\\[(.+)]$"
|
@@ -92,22 +95,33 @@ const RULE_ID = "no-unused-fields", RELAY_SCHEMA = (
|
|
92
95
|
}
|
93
96
|
}
|
94
97
|
}
|
95
|
-
}
|
98
|
+
};
|
99
|
+
const usedFieldsCache = new ModuleCache();
|
96
100
|
function getUsedFields(schema2, operations) {
|
97
101
|
const cachedValue = usedFieldsCache.get(schema2);
|
98
|
-
if (process.env.NODE_ENV !== "test" && cachedValue)
|
102
|
+
if (process.env.NODE_ENV !== "test" && cachedValue) {
|
99
103
|
return cachedValue;
|
100
|
-
|
104
|
+
}
|
105
|
+
const usedFields = /* @__PURE__ */ Object.create(null);
|
106
|
+
const typeInfo = new TypeInfo(schema2);
|
107
|
+
const visitor = visitWithTypeInfo(typeInfo, {
|
101
108
|
Field(node) {
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
109
|
+
const fieldDef = typeInfo.getFieldDef();
|
110
|
+
if (!fieldDef) {
|
111
|
+
return false;
|
112
|
+
}
|
113
|
+
const parentTypeName = typeInfo.getParentType().name;
|
114
|
+
const fieldName = node.name.value;
|
115
|
+
usedFields[parentTypeName] ??= /* @__PURE__ */ new Set();
|
116
|
+
usedFields[parentTypeName].add(fieldName);
|
106
117
|
}
|
107
|
-
})
|
108
|
-
|
118
|
+
});
|
119
|
+
const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
|
120
|
+
for (const { document } of allDocuments) {
|
109
121
|
visit(document, visitor);
|
110
|
-
|
122
|
+
}
|
123
|
+
usedFieldsCache.set(schema2, usedFields);
|
124
|
+
return usedFields;
|
111
125
|
}
|
112
126
|
const rule = {
|
113
127
|
meta: {
|
@@ -118,10 +132,10 @@ const rule = {
|
|
118
132
|
description: "Requires all fields to be used at some level by siblings operations.",
|
119
133
|
category: "Schema",
|
120
134
|
url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID}`,
|
121
|
-
requiresSiblings:
|
122
|
-
requiresSchema:
|
135
|
+
requiresSiblings: true,
|
136
|
+
requiresSchema: true,
|
123
137
|
// Requires documents to be set
|
124
|
-
isDisabledForAllConfig:
|
138
|
+
isDisabledForAllConfig: true,
|
125
139
|
examples: [
|
126
140
|
{
|
127
141
|
title: "Incorrect",
|
@@ -188,17 +202,26 @@ const rule = {
|
|
188
202
|
},
|
189
203
|
type: "suggestion",
|
190
204
|
schema,
|
191
|
-
hasSuggestions:
|
205
|
+
hasSuggestions: true
|
192
206
|
},
|
193
207
|
create(context) {
|
194
|
-
const schema2 = requireGraphQLSchema(RULE_ID, context)
|
208
|
+
const schema2 = requireGraphQLSchema(RULE_ID, context);
|
209
|
+
const siblingsOperations = requireGraphQLOperations(RULE_ID, context);
|
210
|
+
const usedFields = getUsedFields(schema2, siblingsOperations);
|
211
|
+
const { ignoredFieldSelectors } = context.options[0] || {};
|
212
|
+
const selector = (ignoredFieldSelectors || []).reduce(
|
213
|
+
(acc, selector2) => `${acc}:not(${selector2})`,
|
214
|
+
"FieldDefinition"
|
215
|
+
);
|
195
216
|
return {
|
196
|
-
[(
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
217
|
+
[selector](node) {
|
218
|
+
const fieldName = node.name.value;
|
219
|
+
const parentTypeName = node.parent.name.value;
|
220
|
+
const isUsed = usedFields[parentTypeName]?.has(fieldName);
|
221
|
+
if (isUsed) {
|
222
|
+
return;
|
223
|
+
}
|
224
|
+
context.report({
|
202
225
|
node: node.name,
|
203
226
|
messageId: RULE_ID,
|
204
227
|
data: { fieldName },
|
@@ -206,7 +229,10 @@ const rule = {
|
|
206
229
|
{
|
207
230
|
desc: `Remove \`${fieldName}\` field`,
|
208
231
|
fix(fixer) {
|
209
|
-
const sourceCode = context.getSourceCode()
|
232
|
+
const sourceCode = context.getSourceCode();
|
233
|
+
const tokenBefore = sourceCode.getTokenBefore(node);
|
234
|
+
const tokenAfter = sourceCode.getTokenAfter(node);
|
235
|
+
const isEmptyType = tokenBefore.type === "{" && tokenAfter.type === "}";
|
210
236
|
return fixer.remove(isEmptyType ? node.parent : node);
|
211
237
|
}
|
212
238
|
}
|
@@ -1,21 +1,24 @@
|
|
1
1
|
import { isScalarType, Kind } from "graphql";
|
2
2
|
import { requireGraphQLSchema } from "../../utils.js";
|
3
|
-
const RULE_ID = "relay-arguments"
|
3
|
+
const RULE_ID = "relay-arguments";
|
4
|
+
const MISSING_ARGUMENTS = "MISSING_ARGUMENTS";
|
5
|
+
const schema = {
|
4
6
|
type: "array",
|
5
7
|
maxItems: 1,
|
6
8
|
items: {
|
7
9
|
type: "object",
|
8
|
-
additionalProperties:
|
10
|
+
additionalProperties: false,
|
9
11
|
minProperties: 1,
|
10
12
|
properties: {
|
11
13
|
includeBoth: {
|
12
14
|
type: "boolean",
|
13
|
-
default:
|
15
|
+
default: true,
|
14
16
|
description: "Enforce including both forward and backward pagination arguments"
|
15
17
|
}
|
16
18
|
}
|
17
19
|
}
|
18
|
-
}
|
20
|
+
};
|
21
|
+
const rule = {
|
19
22
|
meta: {
|
20
23
|
type: "problem",
|
21
24
|
docs: {
|
@@ -34,8 +37,7 @@ const RULE_ID = "relay-arguments", MISSING_ARGUMENTS = "MISSING_ARGUMENTS", sche
|
|
34
37
|
"",
|
35
38
|
"- `last` takes a non-negative integer",
|
36
39
|
"- `before` takes the Cursor type"
|
37
|
-
].join(
|
38
|
-
`),
|
40
|
+
].join("\n"),
|
39
41
|
url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID}`,
|
40
42
|
examples: [
|
41
43
|
{
|
@@ -61,7 +63,7 @@ const RULE_ID = "relay-arguments", MISSING_ARGUMENTS = "MISSING_ARGUMENTS", sche
|
|
61
63
|
)
|
62
64
|
}
|
63
65
|
],
|
64
|
-
isDisabledForAllConfig:
|
66
|
+
isDisabledForAllConfig: true
|
65
67
|
},
|
66
68
|
messages: {
|
67
69
|
[MISSING_ARGUMENTS]: "A field that returns a Connection type must include forward pagination arguments (`first` and `after`), backward pagination arguments (`last` and `before`), or both."
|
@@ -69,15 +71,19 @@ const RULE_ID = "relay-arguments", MISSING_ARGUMENTS = "MISSING_ARGUMENTS", sche
|
|
69
71
|
schema
|
70
72
|
},
|
71
73
|
create(context) {
|
72
|
-
const schema2 = requireGraphQLSchema(RULE_ID, context)
|
74
|
+
const schema2 = requireGraphQLSchema(RULE_ID, context);
|
75
|
+
const { includeBoth = true } = context.options[0] || {};
|
73
76
|
return {
|
74
77
|
"FieldDefinition > .gqlType Name[value=/Connection$/]"(node) {
|
75
78
|
let fieldNode = node.parent;
|
76
|
-
|
79
|
+
while (fieldNode.kind !== Kind.FIELD_DEFINITION) {
|
77
80
|
fieldNode = fieldNode.parent;
|
81
|
+
}
|
78
82
|
const args = Object.fromEntries(
|
79
83
|
fieldNode.arguments?.map((argument) => [argument.name.value, argument]) || []
|
80
|
-
)
|
84
|
+
);
|
85
|
+
const hasForwardPagination = !!(args.first && args.after);
|
86
|
+
const hasBackwardPagination = !!(args.last && args.before);
|
81
87
|
if (!hasForwardPagination && !hasBackwardPagination) {
|
82
88
|
context.report({
|
83
89
|
node: fieldNode.name,
|
@@ -86,9 +92,14 @@ const RULE_ID = "relay-arguments", MISSING_ARGUMENTS = "MISSING_ARGUMENTS", sche
|
|
86
92
|
return;
|
87
93
|
}
|
88
94
|
function checkField(typeName, argumentName) {
|
89
|
-
const argument = args[argumentName]
|
95
|
+
const argument = args[argumentName];
|
96
|
+
const hasArgument = !!argument;
|
90
97
|
let type = argument;
|
91
|
-
if (hasArgument && type.gqlType.kind === Kind.NON_NULL_TYPE
|
98
|
+
if (hasArgument && type.gqlType.kind === Kind.NON_NULL_TYPE) {
|
99
|
+
type = type.gqlType;
|
100
|
+
}
|
101
|
+
const isAllowedNonNullType = hasArgument && type.gqlType.kind === Kind.NAMED_TYPE && (type.gqlType.name.value === typeName || typeName === "String" && isScalarType(schema2.getType(type.gqlType.name.value)));
|
102
|
+
if (!isAllowedNonNullType) {
|
92
103
|
const returnType = typeName === "String" ? "String or Scalar" : typeName;
|
93
104
|
context.report({
|
94
105
|
node: (argument || fieldNode).name,
|
@@ -96,7 +107,14 @@ const RULE_ID = "relay-arguments", MISSING_ARGUMENTS = "MISSING_ARGUMENTS", sche
|
|
96
107
|
});
|
97
108
|
}
|
98
109
|
}
|
99
|
-
(includeBoth || args.first || args.after)
|
110
|
+
if (includeBoth || args.first || args.after) {
|
111
|
+
checkField("Int", "first");
|
112
|
+
checkField("String", "after");
|
113
|
+
}
|
114
|
+
if (includeBoth || args.last || args.before) {
|
115
|
+
checkField("Int", "last");
|
116
|
+
checkField("String", "before");
|
117
|
+
}
|
100
118
|
}
|
101
119
|
};
|
102
120
|
}
|
@@ -1,5 +1,11 @@
|
|
1
1
|
import { Kind } from "graphql";
|
2
|
-
const MUST_BE_OBJECT_TYPE = "MUST_BE_OBJECT_TYPE"
|
2
|
+
const MUST_BE_OBJECT_TYPE = "MUST_BE_OBJECT_TYPE";
|
3
|
+
const MUST_CONTAIN_FIELD_EDGES = "MUST_CONTAIN_FIELD_EDGES";
|
4
|
+
const MUST_CONTAIN_FIELD_PAGE_INFO = "MUST_CONTAIN_FIELD_PAGE_INFO";
|
5
|
+
const MUST_HAVE_CONNECTION_SUFFIX = "MUST_HAVE_CONNECTION_SUFFIX";
|
6
|
+
const EDGES_FIELD_MUST_RETURN_LIST_TYPE = "EDGES_FIELD_MUST_RETURN_LIST_TYPE";
|
7
|
+
const PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE = "PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE";
|
8
|
+
const NON_OBJECT_TYPES = [
|
3
9
|
Kind.SCALAR_TYPE_DEFINITION,
|
4
10
|
Kind.UNION_TYPE_DEFINITION,
|
5
11
|
Kind.UNION_TYPE_EXTENSION,
|
@@ -9,7 +15,11 @@ const MUST_BE_OBJECT_TYPE = "MUST_BE_OBJECT_TYPE", MUST_CONTAIN_FIELD_EDGES = "M
|
|
9
15
|
Kind.ENUM_TYPE_EXTENSION,
|
10
16
|
Kind.INTERFACE_TYPE_DEFINITION,
|
11
17
|
Kind.INTERFACE_TYPE_EXTENSION
|
12
|
-
]
|
18
|
+
];
|
19
|
+
const notConnectionTypesSelector = `:matches(${NON_OBJECT_TYPES})[name.value=/Connection$/] > .name`;
|
20
|
+
const hasEdgesField = (node) => node.fields?.some((field) => field.name.value === "edges");
|
21
|
+
const hasPageInfoField = (node) => node.fields?.some((field) => field.name.value === "pageInfo");
|
22
|
+
const rule = {
|
13
23
|
meta: {
|
14
24
|
type: "problem",
|
15
25
|
docs: {
|
@@ -21,10 +31,9 @@ const MUST_BE_OBJECT_TYPE = "MUST_BE_OBJECT_TYPE", MUST_CONTAIN_FIELD_EDGES = "M
|
|
21
31
|
"- Connection type must be an Object type",
|
22
32
|
"- Connection type must contain a field `edges` that return a list type that wraps an edge type",
|
23
33
|
"- Connection type must contain a field `pageInfo` that return a non-null `PageInfo` Object type"
|
24
|
-
].join(
|
25
|
-
`),
|
34
|
+
].join("\n"),
|
26
35
|
url: "https://the-guild.dev/graphql/eslint/rules/relay-connection-types",
|
27
|
-
isDisabledForAllConfig:
|
36
|
+
isDisabledForAllConfig: true,
|
28
37
|
examples: [
|
29
38
|
{
|
30
39
|
title: "Incorrect",
|
@@ -69,16 +78,29 @@ const MUST_BE_OBJECT_TYPE = "MUST_BE_OBJECT_TYPE", MUST_CONTAIN_FIELD_EDGES = "M
|
|
69
78
|
context.report({ node, messageId: MUST_BE_OBJECT_TYPE });
|
70
79
|
},
|
71
80
|
":matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value!=/Connection$/]"(node) {
|
72
|
-
hasEdgesField(node) && hasPageInfoField(node)
|
81
|
+
if (hasEdgesField(node) && hasPageInfoField(node)) {
|
82
|
+
context.report({ node: node.name, messageId: MUST_HAVE_CONNECTION_SUFFIX });
|
83
|
+
}
|
73
84
|
},
|
74
85
|
":matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/]"(node) {
|
75
|
-
hasEdgesField(node)
|
86
|
+
if (!hasEdgesField(node)) {
|
87
|
+
context.report({ node: node.name, messageId: MUST_CONTAIN_FIELD_EDGES });
|
88
|
+
}
|
89
|
+
if (!hasPageInfoField(node)) {
|
90
|
+
context.report({ node: node.name, messageId: MUST_CONTAIN_FIELD_PAGE_INFO });
|
91
|
+
}
|
76
92
|
},
|
77
93
|
":matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=edges] > .gqlType"(node) {
|
78
|
-
node.kind === Kind.LIST_TYPE || node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.LIST_TYPE
|
94
|
+
const isListType = node.kind === Kind.LIST_TYPE || node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.LIST_TYPE;
|
95
|
+
if (!isListType) {
|
96
|
+
context.report({ node, messageId: EDGES_FIELD_MUST_RETURN_LIST_TYPE });
|
97
|
+
}
|
79
98
|
},
|
80
99
|
":matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=pageInfo] > .gqlType"(node) {
|
81
|
-
node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.NAMED_TYPE && node.gqlType.name.value === "PageInfo"
|
100
|
+
const isNonNullPageInfoType = node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.NAMED_TYPE && node.gqlType.name.value === "PageInfo";
|
101
|
+
if (!isNonNullPageInfoType) {
|
102
|
+
context.report({ node, messageId: PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE });
|
103
|
+
}
|
82
104
|
}
|
83
105
|
};
|
84
106
|
}
|