1- import { getLocation , requireGraphQLSchemaFromContext , requireSiblingsOperations } from '../utils' ;
1+ import { requireGraphQLSchemaFromContext , requireSiblingsOperations } from '../utils' ;
22import { GraphQLESLintRule } from '../types' ;
3- import { GraphQLInterfaceType , GraphQLObjectType , Kind , SelectionNode , SelectionSetNode } from 'graphql' ;
3+ import { GraphQLInterfaceType , GraphQLObjectType , Kind , SelectionSetNode } from 'graphql' ;
44import { asArray } from '@graphql-tools/utils' ;
55import { getBaseType , GraphQLESTreeNode } from '../estree-parser' ;
66
77export type RequireIdWhenAvailableRuleConfig = { fieldName : string | string [ ] } ;
88
99const RULE_ID = 'require-id-when-available' ;
10- const MESSAGE_ID = 'REQUIRE_ID_WHEN_AVAILABLE' ;
1110const DEFAULT_ID_FIELD_NAME = 'id' ;
1211
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+
1322const rule : GraphQLESLintRule < [ RequireIdWhenAvailableRuleConfig ] , true > = {
1423 meta : {
1524 type : 'suggestion' ,
@@ -59,10 +68,7 @@ const rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true> = {
5968 recommended : true ,
6069 } ,
6170 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 }}.` ,
6672 } ,
6773 schema : {
6874 definitions : {
@@ -95,11 +101,9 @@ const rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true> = {
95101 const { fieldName = DEFAULT_ID_FIELD_NAME } = context . options [ 0 ] || { } ;
96102 const idNames = asArray ( fieldName ) ;
97103
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)$/]' ;
103107
104108 return {
105109 [ selector ] ( node : GraphQLESTreeNode < SelectionSetNode , true > ) {
@@ -121,39 +125,56 @@ const rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true> = {
121125 }
122126 const checkedFragmentSpreads = new Set < string > ( ) ;
123127
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 ) ;
137146 }
147+ continue ;
148+ }
149+
150+ if ( selection . kind === Kind . INLINE_FRAGMENT ) {
151+ hasIdField = checkSelections ( selection . selectionSet . selections ) ;
138152 }
139153 }
154+ return hasIdField ;
140155 }
141156
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 ) {
148159 return ;
149160 }
150161
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+
151171 context . report ( {
152- loc : getLocation ( node . loc ) ,
153- messageId : MESSAGE_ID ,
172+ loc : node . loc . start ,
173+ messageId : RULE_ID ,
154174 data : {
155- checkedFragments : checkedFragmentSpreads . size === 0 ? '' : `(${ [ ...checkedFragmentSpreads ] . join ( ', ' ) } ) ` ,
156- fieldName : idNames . map ( name => `"${ name } "` ) . join ( ' or ' ) ,
175+ pluralSuffix,
176+ fieldName,
177+ addition,
157178 } ,
158179 } ) ;
159180 } ,
0 commit comments