@graphql-eslint/eslint-plugin 3.14.0-alpha-20221222211539-5e993f5 → 3.14.0-alpha-20221223011223-bd3e820

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.
Files changed (185) hide show
  1. package/cjs/configs/index.js +10 -10
  2. package/cjs/documents.js +5 -5
  3. package/cjs/estree-converter/converter.js +2 -2
  4. package/cjs/estree-converter/index.js +3 -3
  5. package/cjs/estree-converter/utils.js +2 -2
  6. package/cjs/flat-configs.js +36 -0
  7. package/cjs/index.js +16 -14
  8. package/cjs/parser.js +13 -13
  9. package/cjs/processor.js +2 -2
  10. package/cjs/rules/alphabetize.js +7 -7
  11. package/cjs/rules/graphql-js-validation.js +9 -9
  12. package/cjs/rules/index.js +66 -66
  13. package/cjs/rules/lone-executable-definition.js +4 -4
  14. package/cjs/rules/match-document-filename.js +4 -4
  15. package/cjs/rules/naming-convention.js +6 -6
  16. package/cjs/rules/no-anonymous-operations.js +2 -2
  17. package/cjs/rules/no-deprecated.js +2 -2
  18. package/cjs/rules/no-one-place-fragments.js +3 -4
  19. package/cjs/rules/no-root-type.js +3 -3
  20. package/cjs/rules/no-scalar-result-type-on-mutation.js +2 -2
  21. package/cjs/rules/no-unreachable-types.js +3 -3
  22. package/cjs/rules/no-unused-fields.js +3 -3
  23. package/cjs/rules/relay-arguments.js +2 -2
  24. package/cjs/rules/relay-edge-types.js +6 -6
  25. package/cjs/rules/relay-page-info.js +5 -5
  26. package/cjs/rules/require-deprecation-date.js +2 -2
  27. package/cjs/rules/require-deprecation-reason.js +2 -2
  28. package/cjs/rules/require-description.js +8 -8
  29. package/cjs/rules/require-field-of-type-query-in-mutation-result.js +3 -3
  30. package/cjs/rules/require-id-when-available.js +8 -8
  31. package/cjs/rules/selection-set-depth.js +5 -5
  32. package/cjs/rules/strict-id-in-types.js +7 -7
  33. package/cjs/rules/unique-fragment-name.js +4 -4
  34. package/cjs/rules/unique-operation-name.js +2 -2
  35. package/cjs/schema.js +2 -2
  36. package/esm/cache.js +25 -0
  37. package/esm/configs/base.js +4 -0
  38. package/esm/configs/index.js +12 -0
  39. package/esm/configs/operations-all.js +29 -0
  40. package/esm/configs/operations-recommended.js +53 -0
  41. package/esm/configs/relay.js +9 -0
  42. package/esm/configs/schema-all.js +22 -0
  43. package/esm/configs/schema-recommended.js +49 -0
  44. package/esm/documents.js +144 -0
  45. package/esm/estree-converter/converter.js +58 -0
  46. package/esm/estree-converter/index.js +3 -0
  47. package/esm/estree-converter/types.js +1 -0
  48. package/esm/estree-converter/utils.js +102 -0
  49. package/esm/flat-configs.js +33 -0
  50. package/esm/graphql-config.js +49 -0
  51. package/esm/index.js +9 -0
  52. package/esm/package.json +1 -0
  53. package/esm/parser.js +56 -0
  54. package/esm/processor.js +75 -0
  55. package/esm/rules/alphabetize.js +344 -0
  56. package/esm/rules/description-style.js +75 -0
  57. package/esm/rules/graphql-js-validation.js +498 -0
  58. package/esm/rules/index.js +71 -0
  59. package/esm/rules/input-name.js +133 -0
  60. package/esm/rules/lone-executable-definition.js +85 -0
  61. package/esm/rules/match-document-filename.js +232 -0
  62. package/esm/rules/naming-convention.js +307 -0
  63. package/esm/rules/no-anonymous-operations.js +64 -0
  64. package/esm/rules/no-case-insensitive-enum-values-duplicates.js +58 -0
  65. package/esm/rules/no-deprecated.js +121 -0
  66. package/esm/rules/no-duplicate-fields.js +109 -0
  67. package/esm/rules/no-hashtag-description.js +86 -0
  68. package/esm/rules/no-one-place-fragments.js +80 -0
  69. package/esm/rules/no-root-type.js +83 -0
  70. package/esm/rules/no-scalar-result-type-on-mutation.js +63 -0
  71. package/esm/rules/no-typename-prefix.js +62 -0
  72. package/esm/rules/no-unreachable-types.js +154 -0
  73. package/esm/rules/no-unused-fields.js +127 -0
  74. package/esm/rules/relay-arguments.js +118 -0
  75. package/esm/rules/relay-connection-types.js +104 -0
  76. package/esm/rules/relay-edge-types.js +186 -0
  77. package/esm/rules/relay-page-info.js +97 -0
  78. package/esm/rules/require-deprecation-date.js +120 -0
  79. package/esm/rules/require-deprecation-reason.js +53 -0
  80. package/esm/rules/require-description.js +190 -0
  81. package/esm/rules/require-field-of-type-query-in-mutation-result.js +69 -0
  82. package/esm/rules/require-id-when-available.js +196 -0
  83. package/esm/rules/require-nullable-fields-with-oneof.js +58 -0
  84. package/esm/rules/require-type-pattern-with-oneof.js +57 -0
  85. package/esm/rules/selection-set-depth.js +131 -0
  86. package/esm/rules/strict-id-in-types.js +159 -0
  87. package/esm/rules/unique-fragment-name.js +86 -0
  88. package/esm/rules/unique-operation-name.js +62 -0
  89. package/esm/schema.js +37 -0
  90. package/esm/testkit.js +181 -0
  91. package/esm/types.js +1 -0
  92. package/esm/utils.js +83 -0
  93. package/package.json +10 -1
  94. package/typings/estree-converter/converter.d.cts +1 -1
  95. package/typings/estree-converter/converter.d.ts +1 -1
  96. package/typings/estree-converter/index.d.cts +3 -3
  97. package/typings/estree-converter/index.d.ts +3 -3
  98. package/typings/estree-converter/types.d.cts +3 -3
  99. package/typings/estree-converter/types.d.ts +3 -3
  100. package/typings/estree-converter/utils.d.cts +2 -2
  101. package/typings/estree-converter/utils.d.ts +2 -2
  102. package/typings/flat-configs.d.cts +248 -0
  103. package/typings/flat-configs.d.ts +248 -0
  104. package/typings/graphql-config.d.cts +1 -1
  105. package/typings/graphql-config.d.ts +1 -1
  106. package/typings/index.d.cts +8 -7
  107. package/typings/index.d.ts +8 -7
  108. package/typings/parser.d.cts +1 -1
  109. package/typings/parser.d.ts +1 -1
  110. package/typings/rules/alphabetize.d.cts +1 -1
  111. package/typings/rules/alphabetize.d.ts +1 -1
  112. package/typings/rules/description-style.d.cts +1 -1
  113. package/typings/rules/description-style.d.ts +1 -1
  114. package/typings/rules/graphql-js-validation.d.cts +1 -1
  115. package/typings/rules/graphql-js-validation.d.ts +1 -1
  116. package/typings/rules/index.d.cts +45 -45
  117. package/typings/rules/index.d.ts +45 -45
  118. package/typings/rules/input-name.d.cts +1 -1
  119. package/typings/rules/input-name.d.ts +1 -1
  120. package/typings/rules/lone-executable-definition.d.cts +1 -1
  121. package/typings/rules/lone-executable-definition.d.ts +1 -1
  122. package/typings/rules/match-document-filename.d.cts +2 -2
  123. package/typings/rules/match-document-filename.d.ts +2 -2
  124. package/typings/rules/naming-convention.d.cts +1 -1
  125. package/typings/rules/naming-convention.d.ts +1 -1
  126. package/typings/rules/no-anonymous-operations.d.cts +1 -1
  127. package/typings/rules/no-anonymous-operations.d.ts +1 -1
  128. package/typings/rules/no-case-insensitive-enum-values-duplicates.d.cts +1 -1
  129. package/typings/rules/no-case-insensitive-enum-values-duplicates.d.ts +1 -1
  130. package/typings/rules/no-deprecated.d.cts +1 -1
  131. package/typings/rules/no-deprecated.d.ts +1 -1
  132. package/typings/rules/no-duplicate-fields.d.cts +1 -1
  133. package/typings/rules/no-duplicate-fields.d.ts +1 -1
  134. package/typings/rules/no-hashtag-description.d.cts +1 -1
  135. package/typings/rules/no-hashtag-description.d.ts +1 -1
  136. package/typings/rules/no-one-place-fragments.d.cts +1 -1
  137. package/typings/rules/no-one-place-fragments.d.ts +1 -1
  138. package/typings/rules/no-root-type.d.cts +1 -1
  139. package/typings/rules/no-root-type.d.ts +1 -1
  140. package/typings/rules/no-scalar-result-type-on-mutation.d.cts +1 -1
  141. package/typings/rules/no-scalar-result-type-on-mutation.d.ts +1 -1
  142. package/typings/rules/no-typename-prefix.d.cts +1 -1
  143. package/typings/rules/no-typename-prefix.d.ts +1 -1
  144. package/typings/rules/no-unreachable-types.d.cts +1 -1
  145. package/typings/rules/no-unreachable-types.d.ts +1 -1
  146. package/typings/rules/no-unused-fields.d.cts +1 -1
  147. package/typings/rules/no-unused-fields.d.ts +1 -1
  148. package/typings/rules/relay-arguments.d.cts +1 -1
  149. package/typings/rules/relay-arguments.d.ts +1 -1
  150. package/typings/rules/relay-connection-types.d.cts +1 -1
  151. package/typings/rules/relay-connection-types.d.ts +1 -1
  152. package/typings/rules/relay-edge-types.d.cts +1 -1
  153. package/typings/rules/relay-edge-types.d.ts +1 -1
  154. package/typings/rules/relay-page-info.d.cts +1 -1
  155. package/typings/rules/relay-page-info.d.ts +1 -1
  156. package/typings/rules/require-deprecation-date.d.cts +1 -1
  157. package/typings/rules/require-deprecation-date.d.ts +1 -1
  158. package/typings/rules/require-deprecation-reason.d.cts +1 -1
  159. package/typings/rules/require-deprecation-reason.d.ts +1 -1
  160. package/typings/rules/require-description.d.cts +1 -1
  161. package/typings/rules/require-description.d.ts +1 -1
  162. package/typings/rules/require-field-of-type-query-in-mutation-result.d.cts +1 -1
  163. package/typings/rules/require-field-of-type-query-in-mutation-result.d.ts +1 -1
  164. package/typings/rules/require-id-when-available.d.cts +1 -1
  165. package/typings/rules/require-id-when-available.d.ts +1 -1
  166. package/typings/rules/require-nullable-fields-with-oneof.d.cts +1 -1
  167. package/typings/rules/require-nullable-fields-with-oneof.d.ts +1 -1
  168. package/typings/rules/require-type-pattern-with-oneof.d.cts +1 -1
  169. package/typings/rules/require-type-pattern-with-oneof.d.ts +1 -1
  170. package/typings/rules/selection-set-depth.d.cts +1 -1
  171. package/typings/rules/selection-set-depth.d.ts +1 -1
  172. package/typings/rules/strict-id-in-types.d.cts +1 -1
  173. package/typings/rules/strict-id-in-types.d.ts +1 -1
  174. package/typings/rules/unique-fragment-name.d.cts +2 -2
  175. package/typings/rules/unique-fragment-name.d.ts +2 -2
  176. package/typings/rules/unique-operation-name.d.cts +1 -1
  177. package/typings/rules/unique-operation-name.d.ts +1 -1
  178. package/typings/schema.d.cts +1 -1
  179. package/typings/schema.d.ts +1 -1
  180. package/typings/testkit.d.cts +3 -3
  181. package/typings/testkit.d.ts +3 -3
  182. package/typings/types.d.cts +2 -2
  183. package/typings/types.d.ts +2 -2
  184. package/typings/utils.d.cts +2 -2
  185. package/typings/utils.d.ts +2 -2
@@ -0,0 +1,190 @@
1
+ import { Kind, TokenKind } from 'graphql';
2
+ import { getLocation, requireGraphQLSchemaFromContext, TYPES_KINDS } from '../utils.js';
3
+ import { getRootTypeNames } from '@graphql-tools/utils';
4
+ const RULE_ID = 'require-description';
5
+ const ALLOWED_KINDS = [
6
+ ...TYPES_KINDS,
7
+ Kind.DIRECTIVE_DEFINITION,
8
+ Kind.FIELD_DEFINITION,
9
+ Kind.INPUT_VALUE_DEFINITION,
10
+ Kind.ENUM_VALUE_DEFINITION,
11
+ Kind.OPERATION_DEFINITION,
12
+ ];
13
+ function getNodeName(node) {
14
+ const DisplayNodeNameMap = {
15
+ [Kind.OBJECT_TYPE_DEFINITION]: 'type',
16
+ [Kind.INTERFACE_TYPE_DEFINITION]: 'interface',
17
+ [Kind.ENUM_TYPE_DEFINITION]: 'enum',
18
+ [Kind.SCALAR_TYPE_DEFINITION]: 'scalar',
19
+ [Kind.INPUT_OBJECT_TYPE_DEFINITION]: 'input',
20
+ [Kind.UNION_TYPE_DEFINITION]: 'union',
21
+ [Kind.DIRECTIVE_DEFINITION]: 'directive',
22
+ };
23
+ switch (node.kind) {
24
+ case Kind.OBJECT_TYPE_DEFINITION:
25
+ case Kind.INTERFACE_TYPE_DEFINITION:
26
+ case Kind.ENUM_TYPE_DEFINITION:
27
+ case Kind.SCALAR_TYPE_DEFINITION:
28
+ case Kind.INPUT_OBJECT_TYPE_DEFINITION:
29
+ case Kind.UNION_TYPE_DEFINITION:
30
+ return `${DisplayNodeNameMap[node.kind]} ${node.name.value}`;
31
+ case Kind.DIRECTIVE_DEFINITION:
32
+ return `${DisplayNodeNameMap[node.kind]} @${node.name.value}`;
33
+ case Kind.FIELD_DEFINITION:
34
+ case Kind.INPUT_VALUE_DEFINITION:
35
+ case Kind.ENUM_VALUE_DEFINITION:
36
+ return `${node.parent.name.value}.${node.name.value}`;
37
+ case Kind.OPERATION_DEFINITION:
38
+ return node.name ? `${node.operation} ${node.name.value}` : node.operation;
39
+ }
40
+ }
41
+ const schema = {
42
+ type: 'array',
43
+ minItems: 1,
44
+ maxItems: 1,
45
+ items: {
46
+ type: 'object',
47
+ additionalProperties: false,
48
+ minProperties: 1,
49
+ properties: {
50
+ types: {
51
+ type: 'boolean',
52
+ description: `Includes:\n${TYPES_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
53
+ },
54
+ rootField: {
55
+ type: 'boolean',
56
+ description: 'Definitions within `Query`, `Mutation`, and `Subscription` root types.',
57
+ },
58
+ ...Object.fromEntries([...ALLOWED_KINDS].sort().map(kind => {
59
+ let description = `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`;
60
+ if (kind === Kind.OPERATION_DEFINITION) {
61
+ description +=
62
+ '\n> You must use only comment syntax `#` and not description syntax `"""` or `"`.';
63
+ }
64
+ return [kind, { type: 'boolean', description }];
65
+ })),
66
+ },
67
+ },
68
+ };
69
+ export const rule = {
70
+ meta: {
71
+ docs: {
72
+ category: 'Schema',
73
+ description: 'Enforce descriptions in type definitions and operations.',
74
+ url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
75
+ examples: [
76
+ {
77
+ title: 'Incorrect',
78
+ usage: [{ types: true, FieldDefinition: true }],
79
+ code: /* GraphQL */ `
80
+ type someTypeName {
81
+ name: String
82
+ }
83
+ `,
84
+ },
85
+ {
86
+ title: 'Correct',
87
+ usage: [{ types: true, FieldDefinition: true }],
88
+ code: /* GraphQL */ `
89
+ """
90
+ Some type description
91
+ """
92
+ type someTypeName {
93
+ """
94
+ Name description
95
+ """
96
+ name: String
97
+ }
98
+ `,
99
+ },
100
+ {
101
+ title: 'Correct',
102
+ usage: [{ OperationDefinition: true }],
103
+ code: /* GraphQL */ `
104
+ # Create a new user
105
+ mutation createUser {
106
+ # ...
107
+ }
108
+ `,
109
+ },
110
+ {
111
+ title: 'Correct',
112
+ usage: [{ rootField: true }],
113
+ code: /* GraphQL */ `
114
+ type Mutation {
115
+ "Create a new user"
116
+ createUser: User
117
+ }
118
+
119
+ type User {
120
+ name: String
121
+ }
122
+ `,
123
+ },
124
+ ],
125
+ configOptions: [
126
+ {
127
+ types: true,
128
+ [Kind.DIRECTIVE_DEFINITION]: true,
129
+ // rootField: true TODO enable in graphql-eslint v4
130
+ },
131
+ ],
132
+ recommended: true,
133
+ },
134
+ type: 'suggestion',
135
+ messages: {
136
+ [RULE_ID]: 'Description is required for `{{ nodeName }}`.',
137
+ },
138
+ schema,
139
+ },
140
+ create(context) {
141
+ const { types, rootField, ...restOptions } = context.options[0] || {};
142
+ const kinds = new Set(types ? TYPES_KINDS : []);
143
+ for (const [kind, isEnabled] of Object.entries(restOptions)) {
144
+ if (isEnabled) {
145
+ kinds.add(kind);
146
+ }
147
+ else {
148
+ kinds.delete(kind);
149
+ }
150
+ }
151
+ if (rootField) {
152
+ const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
153
+ const rootTypeNames = getRootTypeNames(schema);
154
+ kinds.add(`:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/^(${[
155
+ ...rootTypeNames,
156
+ ].join(',')})$/] > FieldDefinition`);
157
+ }
158
+ const selector = [...kinds].join(',');
159
+ return {
160
+ [selector](node) {
161
+ var _a;
162
+ let description = '';
163
+ const isOperation = node.kind === Kind.OPERATION_DEFINITION;
164
+ if (isOperation) {
165
+ const rawNode = node.rawNode();
166
+ const { prev, line } = rawNode.loc.startToken;
167
+ if (prev.kind === TokenKind.COMMENT) {
168
+ const value = prev.value.trim();
169
+ const linesBefore = line - prev.line;
170
+ if (!value.startsWith('eslint') && linesBefore === 1) {
171
+ description = value;
172
+ }
173
+ }
174
+ }
175
+ else {
176
+ description = ((_a = node.description) === null || _a === void 0 ? void 0 : _a.value.trim()) || '';
177
+ }
178
+ if (description.length === 0) {
179
+ context.report({
180
+ loc: isOperation ? getLocation(node.loc.start, node.operation) : node.name.loc,
181
+ messageId: RULE_ID,
182
+ data: {
183
+ nodeName: getNodeName(node),
184
+ },
185
+ });
186
+ }
187
+ },
188
+ };
189
+ },
190
+ };
@@ -0,0 +1,69 @@
1
+ import { isObjectType } from 'graphql';
2
+ import { requireGraphQLSchemaFromContext, getTypeName } from '../utils.js';
3
+ const RULE_ID = 'require-field-of-type-query-in-mutation-result';
4
+ export const rule = {
5
+ meta: {
6
+ type: 'suggestion',
7
+ docs: {
8
+ category: 'Schema',
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`.',
10
+ url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
11
+ requiresSchema: true,
12
+ examples: [
13
+ {
14
+ title: 'Incorrect',
15
+ code: /* GraphQL */ `
16
+ type User { ... }
17
+
18
+ type Mutation {
19
+ createUser: User!
20
+ }
21
+ `,
22
+ },
23
+ {
24
+ title: 'Correct',
25
+ code: /* GraphQL */ `
26
+ type User { ... }
27
+
28
+ type Query { ... }
29
+
30
+ type CreateUserPayload {
31
+ user: User!
32
+ query: Query!
33
+ }
34
+
35
+ type Mutation {
36
+ createUser: CreateUserPayload!
37
+ }
38
+ `,
39
+ },
40
+ ],
41
+ },
42
+ schema: [],
43
+ },
44
+ create(context) {
45
+ const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
46
+ const mutationType = schema.getMutationType();
47
+ const queryType = schema.getQueryType();
48
+ if (!mutationType || !queryType) {
49
+ return {};
50
+ }
51
+ const selector = `:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=${mutationType.name}] > FieldDefinition > .gqlType Name`;
52
+ return {
53
+ [selector](node) {
54
+ const typeName = node.value;
55
+ const graphQLType = schema.getType(typeName);
56
+ if (isObjectType(graphQLType)) {
57
+ const { fields } = graphQLType.astNode;
58
+ const hasQueryType = fields.some(field => getTypeName(field) === queryType.name);
59
+ if (!hasQueryType) {
60
+ context.report({
61
+ node,
62
+ message: `Mutation result type "${graphQLType.name}" must contain field of type "${queryType.name}"`,
63
+ });
64
+ }
65
+ }
66
+ },
67
+ };
68
+ },
69
+ };
@@ -0,0 +1,196 @@
1
+ import { GraphQLInterfaceType, GraphQLObjectType, Kind, TypeInfo, visit, visitWithTypeInfo, } from 'graphql';
2
+ import { asArray } from '@graphql-tools/utils';
3
+ import { ARRAY_DEFAULT_OPTIONS, requireGraphQLSchemaFromContext, requireSiblingsOperations, englishJoinWords, } from '../utils.js';
4
+ import { getBaseType } from '../estree-converter/index.js';
5
+ const RULE_ID = 'require-id-when-available';
6
+ const DEFAULT_ID_FIELD_NAME = 'id';
7
+ const schema = {
8
+ definitions: {
9
+ asString: {
10
+ type: 'string',
11
+ },
12
+ asArray: ARRAY_DEFAULT_OPTIONS,
13
+ },
14
+ type: 'array',
15
+ maxItems: 1,
16
+ items: {
17
+ type: 'object',
18
+ additionalProperties: false,
19
+ properties: {
20
+ fieldName: {
21
+ oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asArray' }],
22
+ default: DEFAULT_ID_FIELD_NAME,
23
+ },
24
+ },
25
+ },
26
+ };
27
+ export const rule = {
28
+ meta: {
29
+ type: 'suggestion',
30
+ hasSuggestions: true,
31
+ docs: {
32
+ category: 'Operations',
33
+ description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
34
+ url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
35
+ requiresSchema: true,
36
+ requiresSiblings: true,
37
+ examples: [
38
+ {
39
+ title: 'Incorrect',
40
+ code: /* GraphQL */ `
41
+ # In your schema
42
+ type User {
43
+ id: ID!
44
+ name: String!
45
+ }
46
+
47
+ # Query
48
+ query {
49
+ user {
50
+ name
51
+ }
52
+ }
53
+ `,
54
+ },
55
+ {
56
+ title: 'Correct',
57
+ code: /* GraphQL */ `
58
+ # In your schema
59
+ type User {
60
+ id: ID!
61
+ name: String!
62
+ }
63
+
64
+ # Query
65
+ query {
66
+ user {
67
+ id
68
+ name
69
+ }
70
+ }
71
+
72
+ # Selecting \`id\` with an alias is also valid
73
+ query {
74
+ user {
75
+ id: name
76
+ }
77
+ }
78
+ `,
79
+ },
80
+ ],
81
+ recommended: true,
82
+ },
83
+ messages: {
84
+ [RULE_ID]: "Field{{ pluralSuffix }} {{ fieldName }} must be selected when it's available on a type.\nInclude it in your selection set{{ addition }}.",
85
+ },
86
+ schema,
87
+ },
88
+ create(context) {
89
+ const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
90
+ const siblings = requireSiblingsOperations(RULE_ID, context);
91
+ const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
92
+ const idNames = asArray(fieldName);
93
+ // Check selections only in OperationDefinition,
94
+ // skip selections of OperationDefinition and InlineFragment
95
+ const selector = 'OperationDefinition SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]';
96
+ const typeInfo = new TypeInfo(schema);
97
+ function checkFragments(node) {
98
+ for (const selection of node.selections) {
99
+ if (selection.kind !== Kind.FRAGMENT_SPREAD) {
100
+ continue;
101
+ }
102
+ const [foundSpread] = siblings.getFragment(selection.name.value);
103
+ if (!foundSpread) {
104
+ continue;
105
+ }
106
+ const checkedFragmentSpreads = new Set();
107
+ const visitor = visitWithTypeInfo(typeInfo, {
108
+ SelectionSet(node, key, parent) {
109
+ if (parent.kind === Kind.FRAGMENT_DEFINITION) {
110
+ checkedFragmentSpreads.add(parent.name.value);
111
+ }
112
+ else if (parent.kind !== Kind.INLINE_FRAGMENT) {
113
+ checkSelections(node, typeInfo.getType(), selection.loc.start, parent, checkedFragmentSpreads);
114
+ }
115
+ },
116
+ });
117
+ visit(foundSpread.document, visitor);
118
+ }
119
+ }
120
+ function checkSelections(node, type,
121
+ // Fragment can be placed in separate file
122
+ // Provide actual fragment spread location instead of location in fragment
123
+ loc,
124
+ // Can't access to node.parent in GraphQL AST.Node, so pass as argument
125
+ parent, checkedFragmentSpreads = new Set()) {
126
+ const rawType = getBaseType(type);
127
+ const isObjectType = rawType instanceof GraphQLObjectType;
128
+ const isInterfaceType = rawType instanceof GraphQLInterfaceType;
129
+ if (!isObjectType && !isInterfaceType) {
130
+ return;
131
+ }
132
+ const fields = rawType.getFields();
133
+ const hasIdFieldInType = idNames.some(name => fields[name]);
134
+ if (!hasIdFieldInType) {
135
+ return;
136
+ }
137
+ function hasIdField({ selections }) {
138
+ return selections.some(selection => {
139
+ if (selection.kind === Kind.FIELD) {
140
+ if (selection.alias && idNames.includes(selection.alias.value)) {
141
+ return true;
142
+ }
143
+ return idNames.includes(selection.name.value);
144
+ }
145
+ if (selection.kind === Kind.INLINE_FRAGMENT) {
146
+ return hasIdField(selection.selectionSet);
147
+ }
148
+ if (selection.kind === Kind.FRAGMENT_SPREAD) {
149
+ const [foundSpread] = siblings.getFragment(selection.name.value);
150
+ if (foundSpread) {
151
+ const fragmentSpread = foundSpread.document;
152
+ checkedFragmentSpreads.add(fragmentSpread.name.value);
153
+ return hasIdField(fragmentSpread.selectionSet);
154
+ }
155
+ }
156
+ return false;
157
+ });
158
+ }
159
+ const hasId = hasIdField(node);
160
+ checkFragments(node);
161
+ if (hasId) {
162
+ return;
163
+ }
164
+ const pluralSuffix = idNames.length > 1 ? 's' : '';
165
+ const fieldName = englishJoinWords(idNames.map(name => `\`${(parent.alias || parent.name).value}.${name}\``));
166
+ const addition = checkedFragmentSpreads.size === 0
167
+ ? ''
168
+ : ` or add to used fragment${checkedFragmentSpreads.size > 1 ? 's' : ''} ${englishJoinWords([...checkedFragmentSpreads].map(name => `\`${name}\``))}`;
169
+ const problem = {
170
+ loc,
171
+ messageId: RULE_ID,
172
+ data: {
173
+ pluralSuffix,
174
+ fieldName,
175
+ addition,
176
+ },
177
+ };
178
+ // Don't provide suggestions for selections in fragments as fragment can be in a separate file
179
+ if ('type' in node) {
180
+ problem.suggest = idNames.map(idName => ({
181
+ desc: `Add \`${idName}\` selection`,
182
+ fix: fixer => fixer.insertTextBefore(node.selections[0], `${idName} `),
183
+ }));
184
+ }
185
+ context.report(problem);
186
+ }
187
+ return {
188
+ [selector](node) {
189
+ const typeInfo = node.typeInfo();
190
+ if (typeInfo.gqlType) {
191
+ checkSelections(node, typeInfo.gqlType, node.loc.start, node.parent);
192
+ }
193
+ },
194
+ };
195
+ },
196
+ };
@@ -0,0 +1,58 @@
1
+ import { Kind } from 'graphql';
2
+ const RULE_ID = 'require-nullable-fields-with-oneof';
3
+ export const rule = {
4
+ meta: {
5
+ type: 'suggestion',
6
+ docs: {
7
+ category: 'Schema',
8
+ description: 'Require are `input` or `type` fields be non nullable with `@oneOf` directive.',
9
+ url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
10
+ examples: [
11
+ {
12
+ title: 'Incorrect',
13
+ code: /* GraphQL */ `
14
+ input Input @oneOf {
15
+ foo: String!
16
+ b: Int
17
+ }
18
+ `,
19
+ },
20
+ {
21
+ title: 'Correct',
22
+ code: /* GraphQL */ `
23
+ input Input @oneOf {
24
+ foo: String
25
+ bar: Int
26
+ }
27
+ `,
28
+ },
29
+ ],
30
+ },
31
+ messages: {
32
+ [RULE_ID]: 'Field `{{fieldName}}` must be nullable.',
33
+ },
34
+ schema: [],
35
+ },
36
+ create(context) {
37
+ return {
38
+ 'Directive[name.value=oneOf]'({ parent, }) {
39
+ const isTypeOrInput = [
40
+ Kind.OBJECT_TYPE_DEFINITION,
41
+ Kind.INPUT_OBJECT_TYPE_DEFINITION,
42
+ ].includes(parent.kind);
43
+ if (!isTypeOrInput) {
44
+ return;
45
+ }
46
+ for (const field of parent.fields) {
47
+ if (field.gqlType.kind === Kind.NON_NULL_TYPE) {
48
+ context.report({
49
+ node: field.name,
50
+ messageId: RULE_ID,
51
+ data: { fieldName: field.name.value },
52
+ });
53
+ }
54
+ }
55
+ },
56
+ };
57
+ },
58
+ };
@@ -0,0 +1,57 @@
1
+ const RULE_ID = 'require-type-pattern-with-oneof';
2
+ export const rule = {
3
+ meta: {
4
+ type: 'suggestion',
5
+ docs: {
6
+ category: 'Schema',
7
+ description: 'Enforce types with `@oneOf` directive have `error` and `ok` fields.',
8
+ url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
9
+ examples: [
10
+ {
11
+ title: 'Correct',
12
+ code: /* GraphQL */ `
13
+ type Mutation {
14
+ doSomething: DoSomethingMutationResult!
15
+ }
16
+
17
+ interface Error {
18
+ message: String!
19
+ }
20
+
21
+ type DoSomethingMutationResult @oneOf {
22
+ ok: DoSomethingSuccess
23
+ error: Error
24
+ }
25
+
26
+ type DoSomethingSuccess {
27
+ # ...
28
+ }
29
+ `,
30
+ },
31
+ ],
32
+ },
33
+ messages: {
34
+ [RULE_ID]: 'Type `{{typeName}}` should have `{{fieldName}}` field.',
35
+ },
36
+ schema: [],
37
+ },
38
+ create(context) {
39
+ return {
40
+ 'Directive[name.value=oneOf][parent.kind=ObjectTypeDefinition]'({ parent, }) {
41
+ const requiredFields = ['error', 'ok'];
42
+ for (const fieldName of requiredFields) {
43
+ if (!parent.fields.some(field => field.name.value === fieldName)) {
44
+ context.report({
45
+ node: parent.name,
46
+ messageId: RULE_ID,
47
+ data: {
48
+ typeName: parent.name.value,
49
+ fieldName,
50
+ },
51
+ });
52
+ }
53
+ }
54
+ },
55
+ };
56
+ },
57
+ };