@graphql-eslint/eslint-plugin 3.14.0-alpha-20221222211539-5e993f5 → 3.14.0-alpha-20221223011700-eed94bd

Sign up to get free protection for your applications and to get access to all the features.
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,118 @@
1
+ import { isScalarType, Kind } from 'graphql';
2
+ import { requireGraphQLSchemaFromContext } from '../utils.js';
3
+ const RULE_ID = 'relay-arguments';
4
+ const MISSING_ARGUMENTS = 'MISSING_ARGUMENTS';
5
+ const schema = {
6
+ type: 'array',
7
+ maxItems: 1,
8
+ items: {
9
+ type: 'object',
10
+ additionalProperties: false,
11
+ minProperties: 1,
12
+ properties: {
13
+ includeBoth: {
14
+ type: 'boolean',
15
+ default: true,
16
+ description: 'Enforce including both forward and backward pagination arguments',
17
+ },
18
+ },
19
+ },
20
+ };
21
+ export const rule = {
22
+ meta: {
23
+ type: 'problem',
24
+ docs: {
25
+ category: 'Schema',
26
+ description: [
27
+ 'Set of rules to follow Relay specification for Arguments.',
28
+ '',
29
+ '- A field that returns a Connection type must include forward pagination arguments (`first` and `after`), backward pagination arguments (`last` and `before`), or both',
30
+ '',
31
+ 'Forward pagination arguments',
32
+ '',
33
+ '- `first` takes a non-negative integer',
34
+ '- `after` takes the Cursor type',
35
+ '',
36
+ 'Backward pagination arguments',
37
+ '',
38
+ '- `last` takes a non-negative integer',
39
+ '- `before` takes the Cursor type',
40
+ ].join('\n'),
41
+ url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
42
+ examples: [
43
+ {
44
+ title: 'Incorrect',
45
+ code: /* GraphQL */ `
46
+ type User {
47
+ posts: PostConnection
48
+ }
49
+ `,
50
+ },
51
+ {
52
+ title: 'Correct',
53
+ code: /* GraphQL */ `
54
+ type User {
55
+ posts(after: String, first: Int, before: String, last: Int): PostConnection
56
+ }
57
+ `,
58
+ },
59
+ ],
60
+ isDisabledForAllConfig: true,
61
+ },
62
+ messages: {
63
+ [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.',
64
+ },
65
+ schema,
66
+ },
67
+ create(context) {
68
+ const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
69
+ const { includeBoth = true } = context.options[0] || {};
70
+ return {
71
+ 'FieldDefinition > .gqlType Name[value=/Connection$/]'(node) {
72
+ let fieldNode = node.parent;
73
+ while (fieldNode.kind !== Kind.FIELD_DEFINITION) {
74
+ fieldNode = fieldNode.parent;
75
+ }
76
+ const args = Object.fromEntries(fieldNode.arguments.map(argument => [argument.name.value, argument]));
77
+ const hasForwardPagination = Boolean(args.first && args.after);
78
+ const hasBackwardPagination = Boolean(args.last && args.before);
79
+ if (!hasForwardPagination && !hasBackwardPagination) {
80
+ context.report({
81
+ node: fieldNode.name,
82
+ messageId: MISSING_ARGUMENTS,
83
+ });
84
+ return;
85
+ }
86
+ function checkField(typeName, argumentName) {
87
+ const argument = args[argumentName];
88
+ const hasArgument = Boolean(argument);
89
+ let type = argument;
90
+ if (hasArgument && type.gqlType.kind === Kind.NON_NULL_TYPE) {
91
+ type = type.gqlType;
92
+ }
93
+ const isAllowedNonNullType = hasArgument &&
94
+ type.gqlType.kind === Kind.NAMED_TYPE &&
95
+ (type.gqlType.name.value === typeName ||
96
+ (typeName === 'String' && isScalarType(schema.getType(type.gqlType.name.value))));
97
+ if (!isAllowedNonNullType) {
98
+ const returnType = typeName === 'String' ? 'String or Scalar' : typeName;
99
+ context.report({
100
+ node: (argument || fieldNode).name,
101
+ message: hasArgument
102
+ ? `Argument \`${argumentName}\` must return ${returnType}.`
103
+ : `Field \`${fieldNode.name.value}\` must contain an argument \`${argumentName}\`, that return ${returnType}.`,
104
+ });
105
+ }
106
+ }
107
+ if (includeBoth || args.first || args.after) {
108
+ checkField('Int', 'first');
109
+ checkField('String', 'after');
110
+ }
111
+ if (includeBoth || args.last || args.before) {
112
+ checkField('Int', 'last');
113
+ checkField('String', 'before');
114
+ }
115
+ },
116
+ };
117
+ },
118
+ };
@@ -0,0 +1,104 @@
1
+ import { Kind } from 'graphql';
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
+ export const NON_OBJECT_TYPES = [
9
+ Kind.SCALAR_TYPE_DEFINITION,
10
+ Kind.UNION_TYPE_DEFINITION,
11
+ Kind.UNION_TYPE_EXTENSION,
12
+ Kind.INPUT_OBJECT_TYPE_DEFINITION,
13
+ Kind.INPUT_OBJECT_TYPE_EXTENSION,
14
+ Kind.ENUM_TYPE_DEFINITION,
15
+ Kind.ENUM_TYPE_EXTENSION,
16
+ Kind.INTERFACE_TYPE_DEFINITION,
17
+ Kind.INTERFACE_TYPE_EXTENSION,
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
+ export const rule = {
23
+ meta: {
24
+ type: 'problem',
25
+ docs: {
26
+ category: 'Schema',
27
+ description: [
28
+ 'Set of rules to follow Relay specification for Connection types.',
29
+ '',
30
+ '- Any type whose name ends in "Connection" is considered by spec to be a `Connection type`',
31
+ '- Connection type must be an Object type',
32
+ '- Connection type must contain a field `edges` that return a list type that wraps an edge type',
33
+ '- Connection type must contain a field `pageInfo` that return a non-null `PageInfo` Object type',
34
+ ].join('\n'),
35
+ url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/relay-connection-types.md',
36
+ isDisabledForAllConfig: true,
37
+ examples: [
38
+ {
39
+ title: 'Incorrect',
40
+ code: /* GraphQL */ `
41
+ type UserPayload { # should be an Object type with \`Connection\` suffix
42
+ edges: UserEdge! # should return a list type
43
+ pageInfo: PageInfo # should return a non-null \`PageInfo\` Object type
44
+ }
45
+ `,
46
+ },
47
+ {
48
+ title: 'Correct',
49
+ code: /* GraphQL */ `
50
+ type UserConnection {
51
+ edges: [UserEdge]
52
+ pageInfo: PageInfo!
53
+ }
54
+ `,
55
+ },
56
+ ],
57
+ },
58
+ messages: {
59
+ // Connection types
60
+ [MUST_BE_OBJECT_TYPE]: 'Connection type must be an Object type.',
61
+ [MUST_HAVE_CONNECTION_SUFFIX]: 'Connection type must have `Connection` suffix.',
62
+ [MUST_CONTAIN_FIELD_EDGES]: 'Connection type must contain a field `edges` that return a list type.',
63
+ [MUST_CONTAIN_FIELD_PAGE_INFO]: 'Connection type must contain a field `pageInfo` that return a non-null `PageInfo` Object type.',
64
+ [EDGES_FIELD_MUST_RETURN_LIST_TYPE]: '`edges` field must return a list type.',
65
+ [PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE]: '`pageInfo` field must return a non-null `PageInfo` Object type.',
66
+ },
67
+ schema: [],
68
+ },
69
+ create(context) {
70
+ return {
71
+ [notConnectionTypesSelector](node) {
72
+ context.report({ node, messageId: MUST_BE_OBJECT_TYPE });
73
+ },
74
+ ':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value!=/Connection$/]'(node) {
75
+ if (hasEdgesField(node) && hasPageInfoField(node)) {
76
+ context.report({ node: node.name, messageId: MUST_HAVE_CONNECTION_SUFFIX });
77
+ }
78
+ },
79
+ ':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/]'(node) {
80
+ if (!hasEdgesField(node)) {
81
+ context.report({ node: node.name, messageId: MUST_CONTAIN_FIELD_EDGES });
82
+ }
83
+ if (!hasPageInfoField(node)) {
84
+ context.report({ node: node.name, messageId: MUST_CONTAIN_FIELD_PAGE_INFO });
85
+ }
86
+ },
87
+ ':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=edges] > .gqlType'(node) {
88
+ const isListType = node.kind === Kind.LIST_TYPE ||
89
+ (node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.LIST_TYPE);
90
+ if (!isListType) {
91
+ context.report({ node, messageId: EDGES_FIELD_MUST_RETURN_LIST_TYPE });
92
+ }
93
+ },
94
+ ':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=pageInfo] > .gqlType'(node) {
95
+ const isNonNullPageInfoType = node.kind === Kind.NON_NULL_TYPE &&
96
+ node.gqlType.kind === Kind.NAMED_TYPE &&
97
+ node.gqlType.name.value === 'PageInfo';
98
+ if (!isNonNullPageInfoType) {
99
+ context.report({ node, messageId: PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE });
100
+ }
101
+ },
102
+ };
103
+ },
104
+ };
@@ -0,0 +1,186 @@
1
+ import { visit, isObjectType, Kind, isScalarType, } from 'graphql';
2
+ import { getDocumentNodeFromSchema } from '@graphql-tools/utils';
3
+ import { getTypeName, requireGraphQLSchemaFromContext } from '../utils.js';
4
+ const RULE_ID = 'relay-edge-types';
5
+ const MESSAGE_MUST_BE_OBJECT_TYPE = 'MESSAGE_MUST_BE_OBJECT_TYPE';
6
+ const MESSAGE_MISSING_EDGE_SUFFIX = 'MESSAGE_MISSING_EDGE_SUFFIX';
7
+ const MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE = 'MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE';
8
+ const MESSAGE_SHOULD_IMPLEMENTS_NODE = 'MESSAGE_SHOULD_IMPLEMENTS_NODE';
9
+ let edgeTypesCache;
10
+ function getEdgeTypes(schema) {
11
+ // We don't want cache edgeTypes on test environment
12
+ // Otherwise edgeTypes will be same for all tests
13
+ if (process.env.NODE_ENV !== 'test' && edgeTypesCache) {
14
+ return edgeTypesCache;
15
+ }
16
+ const edgeTypes = new Set();
17
+ const visitor = {
18
+ ObjectTypeDefinition(node) {
19
+ const typeName = node.name.value;
20
+ const hasConnectionSuffix = typeName.endsWith('Connection');
21
+ if (!hasConnectionSuffix) {
22
+ return;
23
+ }
24
+ const edges = node.fields.find(field => field.name.value === 'edges');
25
+ if (edges) {
26
+ const edgesTypeName = getTypeName(edges);
27
+ const edgesType = schema.getType(edgesTypeName);
28
+ if (isObjectType(edgesType)) {
29
+ edgeTypes.add(edgesTypeName);
30
+ }
31
+ }
32
+ },
33
+ };
34
+ const astNode = getDocumentNodeFromSchema(schema); // Transforms the schema into ASTNode
35
+ visit(astNode, visitor);
36
+ edgeTypesCache = edgeTypes;
37
+ return edgeTypesCache;
38
+ }
39
+ const schema = {
40
+ type: 'array',
41
+ maxItems: 1,
42
+ items: {
43
+ type: 'object',
44
+ additionalProperties: false,
45
+ minProperties: 1,
46
+ properties: {
47
+ withEdgeSuffix: {
48
+ type: 'boolean',
49
+ default: true,
50
+ description: 'Edge type name must end in "Edge".',
51
+ },
52
+ shouldImplementNode: {
53
+ type: 'boolean',
54
+ default: true,
55
+ description: "Edge type's field `node` must implement `Node` interface.",
56
+ },
57
+ listTypeCanWrapOnlyEdgeType: {
58
+ type: 'boolean',
59
+ default: true,
60
+ description: 'A list type should only wrap an edge type.',
61
+ },
62
+ },
63
+ },
64
+ };
65
+ export const rule = {
66
+ meta: {
67
+ type: 'problem',
68
+ docs: {
69
+ category: 'Schema',
70
+ description: [
71
+ 'Set of rules to follow Relay specification for Edge types.',
72
+ '',
73
+ "- A type that is returned in list form by a connection type's `edges` field is considered by this spec to be an Edge type",
74
+ '- Edge type must be an Object type',
75
+ '- Edge type must contain a field `node` that return either Scalar, Enum, Object, Interface, Union, or a non-null wrapper around one of those types. Notably, this field cannot return a list',
76
+ '- Edge type must contain a field `cursor` that return either String, Scalar, or a non-null wrapper around one of those types',
77
+ '- Edge type name must end in "Edge" _(optional)_',
78
+ "- Edge type's field `node` must implement `Node` interface _(optional)_",
79
+ '- A list type should only wrap an edge type _(optional)_',
80
+ ].join('\n'),
81
+ url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
82
+ isDisabledForAllConfig: true,
83
+ requiresSchema: true,
84
+ examples: [
85
+ {
86
+ title: 'Correct',
87
+ code: /* GraphQL */ `
88
+ type UserConnection {
89
+ edges: [UserEdge]
90
+ pageInfo: PageInfo!
91
+ }
92
+ `,
93
+ },
94
+ ],
95
+ },
96
+ messages: {
97
+ [MESSAGE_MUST_BE_OBJECT_TYPE]: 'Edge type must be an Object type.',
98
+ [MESSAGE_MISSING_EDGE_SUFFIX]: 'Edge type must have "Edge" suffix.',
99
+ [MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE]: 'A list type should only wrap an edge type.',
100
+ [MESSAGE_SHOULD_IMPLEMENTS_NODE]: "Edge type's field `node` must implement `Node` interface.",
101
+ },
102
+ schema,
103
+ },
104
+ create(context) {
105
+ const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
106
+ const edgeTypes = getEdgeTypes(schema);
107
+ const options = {
108
+ withEdgeSuffix: true,
109
+ shouldImplementNode: true,
110
+ listTypeCanWrapOnlyEdgeType: true,
111
+ ...context.options[0],
112
+ };
113
+ const isNamedOrNonNullNamed = (node) => node.kind === Kind.NAMED_TYPE ||
114
+ (node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.NAMED_TYPE);
115
+ const checkNodeField = (node) => {
116
+ const nodeField = node.fields.find(field => field.name.value === 'node');
117
+ const message = 'return either a Scalar, Enum, Object, Interface, Union, or a non-null wrapper around one of those types.';
118
+ if (!nodeField) {
119
+ context.report({
120
+ node: node.name,
121
+ message: `Edge type must contain a field \`node\` that ${message}`,
122
+ });
123
+ }
124
+ else if (!isNamedOrNonNullNamed(nodeField.gqlType)) {
125
+ context.report({ node: nodeField.name, message: `Field \`node\` must ${message}` });
126
+ }
127
+ else if (options.shouldImplementNode) {
128
+ const nodeReturnTypeName = getTypeName(nodeField.gqlType.rawNode());
129
+ const type = schema.getType(nodeReturnTypeName);
130
+ if (!isObjectType(type)) {
131
+ return;
132
+ }
133
+ const implementsNode = type.astNode.interfaces.some(n => n.name.value === 'Node');
134
+ if (!implementsNode) {
135
+ context.report({ node: node.name, messageId: MESSAGE_SHOULD_IMPLEMENTS_NODE });
136
+ }
137
+ }
138
+ };
139
+ const checkCursorField = (node) => {
140
+ const cursorField = node.fields.find(field => field.name.value === 'cursor');
141
+ const message = 'return either a String, Scalar, or a non-null wrapper wrapper around one of those types.';
142
+ if (!cursorField) {
143
+ context.report({
144
+ node: node.name,
145
+ message: `Edge type must contain a field \`cursor\` that ${message}`,
146
+ });
147
+ return;
148
+ }
149
+ const typeName = getTypeName(cursorField.rawNode());
150
+ if (!isNamedOrNonNullNamed(cursorField.gqlType) ||
151
+ (typeName !== 'String' && !isScalarType(schema.getType(typeName)))) {
152
+ context.report({ node: cursorField.name, message: `Field \`cursor\` must ${message}` });
153
+ }
154
+ };
155
+ const listeners = {
156
+ ':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=edges] > .gqlType Name'(node) {
157
+ const type = schema.getType(node.value);
158
+ if (!isObjectType(type)) {
159
+ context.report({ node, messageId: MESSAGE_MUST_BE_OBJECT_TYPE });
160
+ }
161
+ },
162
+ ':matches(ObjectTypeDefinition, ObjectTypeExtension)'(node) {
163
+ const typeName = node.name.value;
164
+ if (edgeTypes.has(typeName)) {
165
+ checkNodeField(node);
166
+ checkCursorField(node);
167
+ if (options.withEdgeSuffix && !typeName.endsWith('Edge')) {
168
+ context.report({ node: node.name, messageId: MESSAGE_MISSING_EDGE_SUFFIX });
169
+ }
170
+ }
171
+ },
172
+ };
173
+ if (options.listTypeCanWrapOnlyEdgeType) {
174
+ listeners['FieldDefinition > .gqlType'] = (node) => {
175
+ if (node.kind === Kind.LIST_TYPE ||
176
+ (node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.LIST_TYPE)) {
177
+ const typeName = getTypeName(node.rawNode());
178
+ if (!edgeTypes.has(typeName)) {
179
+ context.report({ node, messageId: MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE });
180
+ }
181
+ }
182
+ };
183
+ }
184
+ return listeners;
185
+ },
186
+ };
@@ -0,0 +1,97 @@
1
+ import { isScalarType, Kind } from 'graphql';
2
+ import { NON_OBJECT_TYPES } from './relay-connection-types.js';
3
+ import { REPORT_ON_FIRST_CHARACTER, requireGraphQLSchemaFromContext } from '../utils.js';
4
+ const RULE_ID = 'relay-page-info';
5
+ const MESSAGE_MUST_EXIST = 'MESSAGE_MUST_EXIST';
6
+ const MESSAGE_MUST_BE_OBJECT_TYPE = 'MESSAGE_MUST_BE_OBJECT_TYPE';
7
+ const notPageInfoTypesSelector = `:matches(${NON_OBJECT_TYPES})[name.value=PageInfo] > .name`;
8
+ let hasPageInfoChecked = false;
9
+ export const rule = {
10
+ meta: {
11
+ type: 'problem',
12
+ docs: {
13
+ category: 'Schema',
14
+ description: [
15
+ 'Set of rules to follow Relay specification for `PageInfo` object.',
16
+ '',
17
+ '- `PageInfo` must be an Object type',
18
+ '- `PageInfo` must contain fields `hasPreviousPage` and `hasNextPage`, that return non-null Boolean',
19
+ '- `PageInfo` must contain fields `startCursor` and `endCursor`, that return either String or Scalar, which can be null if there are no results',
20
+ ].join('\n'),
21
+ url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
22
+ examples: [
23
+ {
24
+ title: 'Correct',
25
+ code: /* GraphQL */ `
26
+ type PageInfo {
27
+ hasPreviousPage: Boolean!
28
+ hasNextPage: Boolean!
29
+ startCursor: String
30
+ endCursor: String
31
+ }
32
+ `,
33
+ },
34
+ ],
35
+ isDisabledForAllConfig: true,
36
+ requiresSchema: true,
37
+ },
38
+ messages: {
39
+ [MESSAGE_MUST_EXIST]: 'The server must provide a `PageInfo` object.',
40
+ [MESSAGE_MUST_BE_OBJECT_TYPE]: '`PageInfo` must be an Object type.',
41
+ },
42
+ schema: [],
43
+ },
44
+ create(context) {
45
+ const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
46
+ if (process.env.NODE_ENV === 'test' || !hasPageInfoChecked) {
47
+ const pageInfoType = schema.getType('PageInfo');
48
+ if (!pageInfoType) {
49
+ context.report({
50
+ loc: REPORT_ON_FIRST_CHARACTER,
51
+ messageId: MESSAGE_MUST_EXIST,
52
+ });
53
+ }
54
+ hasPageInfoChecked = true;
55
+ }
56
+ return {
57
+ [notPageInfoTypesSelector](node) {
58
+ context.report({ node, messageId: MESSAGE_MUST_BE_OBJECT_TYPE });
59
+ },
60
+ 'ObjectTypeDefinition[name.value=PageInfo]'(node) {
61
+ const fieldMap = Object.fromEntries(node.fields.map(field => [field.name.value, field]));
62
+ const checkField = (fieldName, typeName) => {
63
+ const field = fieldMap[fieldName];
64
+ let isAllowedType = false;
65
+ if (field) {
66
+ const type = field.gqlType;
67
+ if (typeName === 'Boolean') {
68
+ isAllowedType =
69
+ type.kind === Kind.NON_NULL_TYPE &&
70
+ type.gqlType.kind === Kind.NAMED_TYPE &&
71
+ type.gqlType.name.value === 'Boolean';
72
+ }
73
+ else if (type.kind === Kind.NAMED_TYPE) {
74
+ isAllowedType =
75
+ type.name.value === 'String' || isScalarType(schema.getType(type.name.value));
76
+ }
77
+ }
78
+ if (!isAllowedType) {
79
+ const returnType = typeName === 'Boolean'
80
+ ? 'non-null Boolean'
81
+ : 'either String or Scalar, which can be null if there are no results';
82
+ context.report({
83
+ node: field ? field.name : node.name,
84
+ message: field
85
+ ? `Field \`${fieldName}\` must return ${returnType}.`
86
+ : `\`PageInfo\` must contain a field \`${fieldName}\`, that return ${returnType}.`,
87
+ });
88
+ }
89
+ };
90
+ checkField('hasPreviousPage', 'Boolean');
91
+ checkField('hasNextPage', 'Boolean');
92
+ checkField('startCursor', 'String');
93
+ checkField('endCursor', 'String');
94
+ },
95
+ };
96
+ },
97
+ };
@@ -0,0 +1,120 @@
1
+ import { valueFromNode } from '../estree-converter/index.js';
2
+ // eslint-disable-next-line unicorn/better-regex
3
+ const DATE_REGEX = /^\d{2}\/\d{2}\/\d{4}$/;
4
+ const MESSAGE_REQUIRE_DATE = 'MESSAGE_REQUIRE_DATE';
5
+ const MESSAGE_INVALID_FORMAT = 'MESSAGE_INVALID_FORMAT';
6
+ const MESSAGE_INVALID_DATE = 'MESSAGE_INVALID_DATE';
7
+ const MESSAGE_CAN_BE_REMOVED = 'MESSAGE_CAN_BE_REMOVED';
8
+ const schema = {
9
+ type: 'array',
10
+ maxItems: 1,
11
+ items: {
12
+ type: 'object',
13
+ additionalProperties: false,
14
+ properties: {
15
+ argumentName: {
16
+ type: 'string',
17
+ },
18
+ },
19
+ },
20
+ };
21
+ export const rule = {
22
+ meta: {
23
+ type: 'suggestion',
24
+ hasSuggestions: true,
25
+ docs: {
26
+ category: 'Schema',
27
+ description: 'Require deletion date on `@deprecated` directive. Suggest removing deprecated things after deprecated date.',
28
+ url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/require-deprecation-date.md',
29
+ examples: [
30
+ {
31
+ title: 'Incorrect',
32
+ code: /* GraphQL */ `
33
+ type User {
34
+ firstname: String @deprecated
35
+ firstName: String
36
+ }
37
+ `,
38
+ },
39
+ {
40
+ title: 'Incorrect',
41
+ code: /* GraphQL */ `
42
+ type User {
43
+ firstname: String @deprecated(reason: "Use 'firstName' instead")
44
+ firstName: String
45
+ }
46
+ `,
47
+ },
48
+ {
49
+ title: 'Correct',
50
+ code: /* GraphQL */ `
51
+ type User {
52
+ firstname: String
53
+ @deprecated(reason: "Use 'firstName' instead", deletionDate: "25/12/2022")
54
+ firstName: String
55
+ }
56
+ `,
57
+ },
58
+ ],
59
+ },
60
+ messages: {
61
+ [MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date',
62
+ [MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY"',
63
+ [MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date',
64
+ [MESSAGE_CAN_BE_REMOVED]: '"{{ nodeName }}" сan be removed',
65
+ },
66
+ schema,
67
+ },
68
+ create(context) {
69
+ return {
70
+ 'Directive[name.value=deprecated]'(node) {
71
+ var _a;
72
+ const argName = ((_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.argumentName) || 'deletionDate';
73
+ const deletionDateNode = node.arguments.find(arg => arg.name.value === argName);
74
+ if (!deletionDateNode) {
75
+ context.report({
76
+ node: node.name,
77
+ messageId: MESSAGE_REQUIRE_DATE,
78
+ });
79
+ return;
80
+ }
81
+ const deletionDate = valueFromNode(deletionDateNode.value);
82
+ const isValidDate = DATE_REGEX.test(deletionDate);
83
+ if (!isValidDate) {
84
+ context.report({ node: deletionDateNode.value, messageId: MESSAGE_INVALID_FORMAT });
85
+ return;
86
+ }
87
+ let [day, month, year] = deletionDate.split('/');
88
+ day = day.padStart(2, '0');
89
+ month = month.padStart(2, '0');
90
+ const deletionDateInMS = Date.parse(`${year}-${month}-${day}`);
91
+ if (Number.isNaN(deletionDateInMS)) {
92
+ context.report({
93
+ node: deletionDateNode.value,
94
+ messageId: MESSAGE_INVALID_DATE,
95
+ data: {
96
+ deletionDate,
97
+ },
98
+ });
99
+ return;
100
+ }
101
+ const canRemove = Date.now() > deletionDateInMS;
102
+ if (canRemove) {
103
+ const { parent } = node;
104
+ const nodeName = parent.name.value;
105
+ context.report({
106
+ node: parent.name,
107
+ messageId: MESSAGE_CAN_BE_REMOVED,
108
+ data: { nodeName },
109
+ suggest: [
110
+ {
111
+ desc: `Remove \`${nodeName}\``,
112
+ fix: fixer => fixer.remove(parent),
113
+ },
114
+ ],
115
+ });
116
+ }
117
+ },
118
+ };
119
+ },
120
+ };
@@ -0,0 +1,53 @@
1
+ import { valueFromNode } from '../estree-converter/index.js';
2
+ export const rule = {
3
+ meta: {
4
+ docs: {
5
+ description: 'Require all deprecation directives to specify a reason.',
6
+ category: 'Schema',
7
+ url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/require-deprecation-reason.md',
8
+ recommended: true,
9
+ examples: [
10
+ {
11
+ title: 'Incorrect',
12
+ code: /* GraphQL */ `
13
+ type MyType {
14
+ name: String @deprecated
15
+ }
16
+ `,
17
+ },
18
+ {
19
+ title: 'Incorrect',
20
+ code: /* GraphQL */ `
21
+ type MyType {
22
+ name: String @deprecated(reason: "")
23
+ }
24
+ `,
25
+ },
26
+ {
27
+ title: 'Correct',
28
+ code: /* GraphQL */ `
29
+ type MyType {
30
+ name: String @deprecated(reason: "no longer relevant, please use fullName field")
31
+ }
32
+ `,
33
+ },
34
+ ],
35
+ },
36
+ type: 'suggestion',
37
+ schema: [],
38
+ },
39
+ create(context) {
40
+ return {
41
+ 'Directive[name.value=deprecated]'(node) {
42
+ const reasonArgument = node.arguments.find(arg => arg.name.value === 'reason');
43
+ const value = reasonArgument && String(valueFromNode(reasonArgument.value)).trim();
44
+ if (!value) {
45
+ context.report({
46
+ node: node.name,
47
+ message: 'Directive "@deprecated" must have a reason!',
48
+ });
49
+ }
50
+ },
51
+ };
52
+ },
53
+ };