1
- import { getLocation , requireGraphQLSchemaFromContext , requireSiblingsOperations } from '../utils' ;
1
+ import { requireGraphQLSchemaFromContext , requireSiblingsOperations } from '../utils' ;
2
2
import { GraphQLESLintRule } from '../types' ;
3
- import { GraphQLInterfaceType , GraphQLObjectType , Kind , SelectionNode , SelectionSetNode } from 'graphql' ;
3
+ import { GraphQLInterfaceType , GraphQLObjectType , Kind , SelectionSetNode } from 'graphql' ;
4
4
import { asArray } from '@graphql-tools/utils' ;
5
5
import { getBaseType , GraphQLESTreeNode } from '../estree-parser' ;
6
6
7
7
export type RequireIdWhenAvailableRuleConfig = { fieldName : string | string [ ] } ;
8
8
9
9
const RULE_ID = 'require-id-when-available' ;
10
- const MESSAGE_ID = 'REQUIRE_ID_WHEN_AVAILABLE' ;
11
10
const DEFAULT_ID_FIELD_NAME = 'id' ;
12
11
12
+ declare namespace Intl {
13
+ class ListFormat {
14
+ constructor ( locales : string , options : any ) ;
15
+
16
+ public format : ( items : [ string ] ) => string ;
17
+ }
18
+ }
19
+
20
+ const englishJoinWords = words => new Intl . ListFormat ( 'en-US' , { type : 'disjunction' } ) . format ( words ) ;
21
+
13
22
const rule : GraphQLESLintRule < [ RequireIdWhenAvailableRuleConfig ] , true > = {
14
23
meta : {
15
24
type : 'suggestion' ,
@@ -59,10 +68,7 @@ const rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true> = {
59
68
recommended : true ,
60
69
} ,
61
70
messages : {
62
- [ MESSAGE_ID ] : [
63
- `Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!` ,
64
- `If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.` ,
65
- ] . join ( '\n' ) ,
71
+ [ RULE_ID ] : `Field{{ pluralSuffix }} {{ fieldName }} must be selected when it's available on a type.\nInclude it in your selection set{{ addition }}.` ,
66
72
} ,
67
73
schema : {
68
74
definitions : {
@@ -95,11 +101,9 @@ const rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true> = {
95
101
const { fieldName = DEFAULT_ID_FIELD_NAME } = context . options [ 0 ] || { } ;
96
102
const idNames = asArray ( fieldName ) ;
97
103
98
- const isFound = ( s : GraphQLESTreeNode < SelectionNode > | SelectionNode ) =>
99
- s . kind === Kind . FIELD && idNames . includes ( s . name . value ) ;
100
-
101
- // Skip check selections in FragmentDefinition
102
- const selector = 'OperationDefinition SelectionSet[parent.kind!=OperationDefinition]' ;
104
+ // Check selections only in OperationDefinition,
105
+ // skip selections of OperationDefinition and InlineFragment
106
+ const selector = 'OperationDefinition SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]' ;
103
107
104
108
return {
105
109
[ selector ] ( node : GraphQLESTreeNode < SelectionSetNode , true > ) {
@@ -121,39 +125,56 @@ const rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true> = {
121
125
}
122
126
const checkedFragmentSpreads = new Set < string > ( ) ;
123
127
124
- for ( const selection of node . selections ) {
125
- if ( isFound ( selection ) ) {
126
- return ;
127
- }
128
- if ( selection . kind === Kind . INLINE_FRAGMENT && selection . selectionSet . selections . some ( isFound ) ) {
129
- return ;
130
- }
131
- if ( selection . kind === Kind . FRAGMENT_SPREAD ) {
132
- const [ foundSpread ] = siblings . getFragment ( selection . name . value ) ;
133
- if ( foundSpread ) {
134
- checkedFragmentSpreads . add ( foundSpread . document . name . value ) ;
135
- if ( foundSpread . document . selectionSet . selections . some ( isFound ) ) {
136
- return ;
128
+ function checkSelections ( selections ) : boolean {
129
+ let hasIdField = false ;
130
+ for ( const selection of selections ) {
131
+ if ( hasIdField ) {
132
+ return true ;
133
+ }
134
+
135
+ if ( selection . kind === Kind . FIELD ) {
136
+ hasIdField = idNames . includes ( selection . name . value ) ;
137
+ continue ;
138
+ }
139
+
140
+ if ( selection . kind === Kind . FRAGMENT_SPREAD ) {
141
+ const [ foundSpread ] = siblings . getFragment ( selection . name . value ) ;
142
+ if ( foundSpread ) {
143
+ const fragmentSpread = foundSpread . document ;
144
+ checkedFragmentSpreads . add ( fragmentSpread . name . value ) ;
145
+ hasIdField = checkSelections ( fragmentSpread . selectionSet . selections ) ;
137
146
}
147
+ continue ;
148
+ }
149
+
150
+ if ( selection . kind === Kind . INLINE_FRAGMENT ) {
151
+ hasIdField = checkSelections ( selection . selectionSet . selections ) ;
138
152
}
139
153
}
154
+ return hasIdField ;
140
155
}
141
156
142
- const { parent } = node as any ;
143
- const hasIdFieldInInterfaceSelectionSet =
144
- parent ?. kind === Kind . INLINE_FRAGMENT &&
145
- parent . parent ?. kind === Kind . SELECTION_SET &&
146
- parent . parent . selections . some ( isFound ) ;
147
- if ( hasIdFieldInInterfaceSelectionSet ) {
157
+ const idFound = checkSelections ( node . selections ) ;
158
+ if ( idFound ) {
148
159
return ;
149
160
}
150
161
162
+ const pluralSuffix = idNames . length > 1 ? 's' : '' ;
163
+ const fieldName = englishJoinWords ( idNames . map ( name => `\`${ name } \`` ) ) ;
164
+ const addition =
165
+ checkedFragmentSpreads . size === 0
166
+ ? ''
167
+ : ` or add to used fragment${ checkedFragmentSpreads . size > 1 ? 's' : '' } ${ englishJoinWords (
168
+ [ ...checkedFragmentSpreads ] . map ( name => `\`${ name } \`` )
169
+ ) } `;
170
+
151
171
context . report ( {
152
- loc : getLocation ( node . loc ) ,
153
- messageId : MESSAGE_ID ,
172
+ loc : node . loc . start ,
173
+ messageId : RULE_ID ,
154
174
data : {
155
- checkedFragments : checkedFragmentSpreads . size === 0 ? '' : `(${ [ ...checkedFragmentSpreads ] . join ( ', ' ) } ) ` ,
156
- fieldName : idNames . map ( name => `"${ name } "` ) . join ( ' or ' ) ,
175
+ pluralSuffix,
176
+ fieldName,
177
+ addition,
157
178
} ,
158
179
} ) ;
159
180
} ,
0 commit comments