|
1 |
| -import { requireGraphQLSchemaFromContext, requireSiblingsOperations } from '../utils'; |
| 1 | +import { getLocation, requireGraphQLSchemaFromContext, requireSiblingsOperations } from '../utils'; |
2 | 2 | import { GraphQLESLintRule } from '../types';
|
3 |
| -import { GraphQLInterfaceType, GraphQLObjectType } from 'graphql'; |
4 |
| -import { getBaseType } from '../estree-parser'; |
| 3 | +import { GraphQLInterfaceType, GraphQLObjectType, Kind, SelectionNode } from 'graphql'; |
| 4 | +import { getBaseType, GraphQLESTreeNode } from '../estree-parser'; |
5 | 5 |
|
6 | 6 | const REQUIRE_ID_WHEN_AVAILABLE = 'REQUIRE_ID_WHEN_AVAILABLE';
|
7 | 7 | const DEFAULT_ID_FIELD_NAME = 'id';
|
@@ -57,100 +57,104 @@ const rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true> = {
|
57 | 57 | recommended: true,
|
58 | 58 | },
|
59 | 59 | messages: {
|
60 |
| - [REQUIRE_ID_WHEN_AVAILABLE]: `Field "{{ fieldName }}" must be selected when it's available on a type. Please make sure to include it in your selection set!\nIf you are using fragments, make sure that all used fragments {{ checkedFragments }} specifies the field "{{ fieldName }}".`, |
| 60 | + [REQUIRE_ID_WHEN_AVAILABLE]: [ |
| 61 | + `Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!`, |
| 62 | + `If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.`, |
| 63 | + ].join('\n'), |
61 | 64 | },
|
62 | 65 | schema: {
|
| 66 | + definitions: { |
| 67 | + asString: { |
| 68 | + type: 'string', |
| 69 | + }, |
| 70 | + asArray: { |
| 71 | + type: 'array', |
| 72 | + minItems: 1, |
| 73 | + uniqueItems: true, |
| 74 | + }, |
| 75 | + }, |
63 | 76 | type: 'array',
|
64 | 77 | maxItems: 1,
|
65 | 78 | items: {
|
66 | 79 | type: 'object',
|
67 | 80 | additionalProperties: false,
|
68 | 81 | properties: {
|
69 | 82 | fieldName: {
|
70 |
| - type: 'string', |
| 83 | + oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asArray' }], |
71 | 84 | default: DEFAULT_ID_FIELD_NAME,
|
72 | 85 | },
|
73 | 86 | },
|
74 | 87 | },
|
75 | 88 | },
|
76 | 89 | },
|
77 | 90 | create(context) {
|
| 91 | + requireGraphQLSchemaFromContext('require-id-when-available', context); |
| 92 | + const siblings = requireSiblingsOperations('require-id-when-available', context); |
| 93 | + const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {}; |
| 94 | + const idNames = Array.isArray(fieldName) ? fieldName : [fieldName]; |
| 95 | + |
| 96 | + const isFound = (s: GraphQLESTreeNode<SelectionNode> | SelectionNode) => |
| 97 | + s.kind === Kind.FIELD && idNames.includes(s.name.value); |
| 98 | + |
78 | 99 | return {
|
79 | 100 | SelectionSet(node) {
|
80 |
| - requireGraphQLSchemaFromContext('require-id-when-available', context); |
81 |
| - const siblings = requireSiblingsOperations('require-id-when-available', context); |
| 101 | + const typeInfo = node.typeInfo(); |
| 102 | + if (!typeInfo.gqlType) { |
| 103 | + return; |
| 104 | + } |
82 | 105 |
|
83 |
| - const fieldName = (context.options[0] || {}).fieldName || DEFAULT_ID_FIELD_NAME; |
| 106 | + const rawType = getBaseType(typeInfo.gqlType); |
| 107 | + const isObjectType = rawType instanceof GraphQLObjectType; |
| 108 | + const isInterfaceType = rawType instanceof GraphQLInterfaceType; |
| 109 | + if (!isObjectType && !isInterfaceType) { |
| 110 | + return; |
| 111 | + } |
84 | 112 |
|
85 |
| - if (!node.selections || node.selections.length === 0) { |
| 113 | + const fields = rawType.getFields(); |
| 114 | + const hasIdFieldInType = idNames.some(name => fields[name]); |
| 115 | + if (!hasIdFieldInType) { |
86 | 116 | return;
|
87 | 117 | }
|
88 | 118 |
|
89 |
| - const typeInfo = node.typeInfo(); |
90 |
| - if (typeInfo && typeInfo.gqlType) { |
91 |
| - const rawType = getBaseType(typeInfo.gqlType); |
92 |
| - if (rawType instanceof GraphQLObjectType || rawType instanceof GraphQLInterfaceType) { |
93 |
| - const fields = rawType.getFields(); |
94 |
| - const hasIdFieldInType = !!fields[fieldName]; |
95 |
| - const checkedFragmentSpreads: Set<string> = new Set(); |
96 |
| - |
97 |
| - if (hasIdFieldInType) { |
98 |
| - let found = false; |
99 |
| - |
100 |
| - for (const selection of node.selections) { |
101 |
| - if (selection.kind === 'Field' && selection.name.value === fieldName) { |
102 |
| - found = true; |
103 |
| - } else if (selection.kind === 'InlineFragment') { |
104 |
| - found = (selection.selectionSet?.selections || []).some( |
105 |
| - s => s.kind === 'Field' && s.name.value === fieldName |
106 |
| - ); |
107 |
| - } else if (selection.kind === 'FragmentSpread') { |
108 |
| - const foundSpread = siblings.getFragment(selection.name.value); |
109 |
| - |
110 |
| - if (foundSpread[0]) { |
111 |
| - checkedFragmentSpreads.add(foundSpread[0].document.name.value); |
112 |
| - |
113 |
| - found = (foundSpread[0].document.selectionSet?.selections || []).some( |
114 |
| - s => s.kind === 'Field' && s.name.value === fieldName |
115 |
| - ); |
116 |
| - } |
117 |
| - } |
118 |
| - |
119 |
| - if (found) { |
120 |
| - break; |
121 |
| - } |
122 |
| - } |
| 119 | + const checkedFragmentSpreads = new Set<string>(); |
| 120 | + let found = false; |
123 | 121 |
|
124 |
| - const { parent } = node as any; |
125 |
| - const hasIdFieldInInterfaceSelectionSet = |
126 |
| - parent && |
127 |
| - parent.kind === 'InlineFragment' && |
128 |
| - parent.parent && |
129 |
| - parent.parent.kind === 'SelectionSet' && |
130 |
| - parent.parent.selections.some(s => s.kind === 'Field' && s.name.value === fieldName); |
131 |
| - |
132 |
| - if (!found && !hasIdFieldInInterfaceSelectionSet) { |
133 |
| - context.report({ |
134 |
| - loc: { |
135 |
| - start: { |
136 |
| - line: node.loc.start.line, |
137 |
| - column: node.loc.start.column - 1, |
138 |
| - }, |
139 |
| - end: { |
140 |
| - line: node.loc.end.line, |
141 |
| - column: node.loc.end.column - 1, |
142 |
| - }, |
143 |
| - }, |
144 |
| - messageId: REQUIRE_ID_WHEN_AVAILABLE, |
145 |
| - data: { |
146 |
| - checkedFragments: |
147 |
| - checkedFragmentSpreads.size === 0 ? '' : `(${Array.from(checkedFragmentSpreads).join(', ')})`, |
148 |
| - fieldName, |
149 |
| - }, |
150 |
| - }); |
151 |
| - } |
| 122 | + for (const selection of node.selections) { |
| 123 | + if (isFound(selection)) { |
| 124 | + found = true; |
| 125 | + } else if (selection.kind === Kind.INLINE_FRAGMENT) { |
| 126 | + found = selection.selectionSet?.selections.some(s => isFound(s)); |
| 127 | + } else if (selection.kind === Kind.FRAGMENT_SPREAD) { |
| 128 | + const [foundSpread] = siblings.getFragment(selection.name.value); |
| 129 | + |
| 130 | + if (foundSpread) { |
| 131 | + checkedFragmentSpreads.add(foundSpread.document.name.value); |
| 132 | + found = foundSpread.document.selectionSet?.selections.some(s => isFound(s)); |
152 | 133 | }
|
153 | 134 | }
|
| 135 | + |
| 136 | + if (found) { |
| 137 | + break; |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + const { parent } = node as any; |
| 142 | + const hasIdFieldInInterfaceSelectionSet = |
| 143 | + parent && |
| 144 | + parent.kind === Kind.INLINE_FRAGMENT && |
| 145 | + parent.parent && |
| 146 | + parent.parent.kind === Kind.SELECTION_SET && |
| 147 | + parent.parent.selections.some(s => isFound(s)); |
| 148 | + |
| 149 | + if (!found && !hasIdFieldInInterfaceSelectionSet) { |
| 150 | + context.report({ |
| 151 | + loc: getLocation(node.loc), |
| 152 | + messageId: REQUIRE_ID_WHEN_AVAILABLE, |
| 153 | + data: { |
| 154 | + checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `, |
| 155 | + fieldName: idNames.map(name => `"${name}"`).join(' or '), |
| 156 | + }, |
| 157 | + }); |
154 | 158 | }
|
155 | 159 | },
|
156 | 160 | };
|
|
0 commit comments