@graphql-eslint/eslint-plugin 3.2.0-alpha-4ca7218.0 → 3.3.0-alpha-b07557f.0
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.
- package/docs/rules/require-id-when-available.md +16 -1
- package/graphql-config.d.ts +2 -1
- package/index.js +246 -176
- package/index.mjs +246 -176
- package/package.json +3 -2
- package/rules/index.d.ts +2 -129
- package/rules/match-document-filename.d.ts +3 -3
- package/rules/naming-convention.d.ts +1 -1
- package/types.d.ts +1 -0
- package/utils.d.ts +2 -7
package/index.js
CHANGED
@@ -10,10 +10,10 @@ const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
11
11
|
const utils = require('@graphql-tools/utils');
|
12
12
|
const lowerCase = _interopDefault(require('lodash.lowercase'));
|
13
|
+
const graphqlConfig = require('graphql-config');
|
14
|
+
const codeFileLoader = require('@graphql-tools/code-file-loader');
|
13
15
|
const depthLimit = _interopDefault(require('graphql-depth-limit'));
|
14
16
|
const graphqlTagPluck = require('@graphql-tools/graphql-tag-pluck');
|
15
|
-
const graphqlConfig$1 = require('graphql-config');
|
16
|
-
const codeFileLoader = require('@graphql-tools/code-file-loader');
|
17
17
|
const eslint = require('eslint');
|
18
18
|
const codeFrame = require('@babel/code-frame');
|
19
19
|
|
@@ -174,6 +174,47 @@ const configs = {
|
|
174
174
|
'operations-all': operationsAllConfig,
|
175
175
|
};
|
176
176
|
|
177
|
+
let graphQLConfig;
|
178
|
+
function loadCachedGraphQLConfig(options) {
|
179
|
+
// We don't want cache config on test environment
|
180
|
+
// Otherwise schema and documents will be same for all tests
|
181
|
+
if (process.env.NODE_ENV !== 'test' && graphQLConfig) {
|
182
|
+
return graphQLConfig;
|
183
|
+
}
|
184
|
+
graphQLConfig = loadGraphQLConfig(options);
|
185
|
+
return graphQLConfig;
|
186
|
+
}
|
187
|
+
function loadGraphQLConfig(options) {
|
188
|
+
const onDiskConfig = options.skipGraphQLConfig
|
189
|
+
? null
|
190
|
+
: graphqlConfig.loadConfigSync({
|
191
|
+
throwOnEmpty: false,
|
192
|
+
throwOnMissing: false,
|
193
|
+
extensions: [addCodeFileLoaderExtension],
|
194
|
+
});
|
195
|
+
const configOptions = options.projects
|
196
|
+
? { projects: options.projects }
|
197
|
+
: {
|
198
|
+
schema: (options.schema || ''),
|
199
|
+
documents: options.documents || options.operations,
|
200
|
+
extensions: options.extensions,
|
201
|
+
include: options.include,
|
202
|
+
exclude: options.exclude,
|
203
|
+
};
|
204
|
+
graphQLConfig =
|
205
|
+
onDiskConfig ||
|
206
|
+
new graphqlConfig.GraphQLConfig({
|
207
|
+
config: configOptions,
|
208
|
+
filepath: 'virtual-config',
|
209
|
+
}, [addCodeFileLoaderExtension]);
|
210
|
+
return graphQLConfig;
|
211
|
+
}
|
212
|
+
const addCodeFileLoaderExtension = api => {
|
213
|
+
api.loaders.schema.register(new codeFileLoader.CodeFileLoader());
|
214
|
+
api.loaders.documents.register(new codeFileLoader.CodeFileLoader());
|
215
|
+
return { name: 'graphql-eslint-loaders' };
|
216
|
+
};
|
217
|
+
|
177
218
|
function requireSiblingsOperations(ruleName, context) {
|
178
219
|
if (!context.parserServices) {
|
179
220
|
throw new Error(`Rule '${ruleName}' requires 'parserOptions.operations' to be set and loaded. See http://bit.ly/graphql-eslint-operations for more info`);
|
@@ -192,6 +233,32 @@ function requireGraphQLSchemaFromContext(ruleName, context) {
|
|
192
233
|
}
|
193
234
|
return context.parserServices.schema;
|
194
235
|
}
|
236
|
+
const schemaToExtendCache = new Map();
|
237
|
+
function getGraphQLSchemaToExtend(context) {
|
238
|
+
// If parserOptions.schema not set or not loaded, there is no reason to make partial schema aka schemaToExtend
|
239
|
+
if (!context.parserServices.hasTypeInfo) {
|
240
|
+
return null;
|
241
|
+
}
|
242
|
+
const filename = context.getPhysicalFilename();
|
243
|
+
if (!schemaToExtendCache.has(filename)) {
|
244
|
+
const { schema, schemaOptions } = context.parserOptions;
|
245
|
+
const gqlConfig = loadGraphQLConfig({ schema });
|
246
|
+
const projectForFile = gqlConfig.getProjectForFile(filename);
|
247
|
+
let schemaToExtend;
|
248
|
+
try {
|
249
|
+
schemaToExtend = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', {
|
250
|
+
...schemaOptions,
|
251
|
+
ignore: filename,
|
252
|
+
});
|
253
|
+
}
|
254
|
+
catch (_a) {
|
255
|
+
// If error throws just ignore it because maybe schema is located in 1 file
|
256
|
+
schemaToExtend = null;
|
257
|
+
}
|
258
|
+
schemaToExtendCache.set(filename, schemaToExtend);
|
259
|
+
}
|
260
|
+
return schemaToExtendCache.get(filename);
|
261
|
+
}
|
195
262
|
function requireReachableTypesFromContext(ruleName, context) {
|
196
263
|
const schema = requireGraphQLSchemaFromContext(ruleName, context);
|
197
264
|
return context.parserServices.reachableTypes(schema);
|
@@ -285,14 +352,6 @@ const TYPES_KINDS = [
|
|
285
352
|
graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
286
353
|
graphql.Kind.UNION_TYPE_DEFINITION,
|
287
354
|
];
|
288
|
-
var CaseStyle;
|
289
|
-
(function (CaseStyle) {
|
290
|
-
CaseStyle["camelCase"] = "camelCase";
|
291
|
-
CaseStyle["pascalCase"] = "PascalCase";
|
292
|
-
CaseStyle["snakeCase"] = "snake_case";
|
293
|
-
CaseStyle["upperCase"] = "UPPER_CASE";
|
294
|
-
CaseStyle["kebabCase"] = "kebab-case";
|
295
|
-
})(CaseStyle || (CaseStyle = {}));
|
296
355
|
const pascalCase = (str) => lowerCase(str)
|
297
356
|
.split(' ')
|
298
357
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
@@ -303,15 +362,15 @@ const camelCase = (str) => {
|
|
303
362
|
};
|
304
363
|
const convertCase = (style, str) => {
|
305
364
|
switch (style) {
|
306
|
-
case
|
365
|
+
case 'camelCase':
|
307
366
|
return camelCase(str);
|
308
|
-
case
|
367
|
+
case 'PascalCase':
|
309
368
|
return pascalCase(str);
|
310
|
-
case
|
369
|
+
case 'snake_case':
|
311
370
|
return lowerCase(str).replace(/ /g, '_');
|
312
|
-
case
|
371
|
+
case 'UPPER_CASE':
|
313
372
|
return lowerCase(str).replace(/ /g, '_').toUpperCase();
|
314
|
-
case
|
373
|
+
case 'kebab-case':
|
315
374
|
return lowerCase(str).replace(/ /g, '-');
|
316
375
|
}
|
317
376
|
};
|
@@ -334,17 +393,32 @@ function getLocation(loc, fieldName = '', offset) {
|
|
334
393
|
};
|
335
394
|
}
|
336
395
|
|
337
|
-
function validateDocument(sourceNode, context, schema, documentNode, rule) {
|
396
|
+
function validateDocument(sourceNode, context, schema = null, documentNode, rule, isSchemaToExtend = false) {
|
338
397
|
if (documentNode.definitions.length === 0) {
|
339
398
|
return;
|
340
399
|
}
|
341
400
|
try {
|
342
|
-
const validationErrors = schema
|
401
|
+
const validationErrors = schema && !isSchemaToExtend
|
343
402
|
? graphql.validate(schema, documentNode, [rule])
|
344
|
-
: validate.validateSDL(documentNode,
|
403
|
+
: validate.validateSDL(documentNode, schema, [rule]);
|
345
404
|
for (const error of validationErrors) {
|
405
|
+
/*
|
406
|
+
* TODO: Fix ESTree-AST converter because currently it's incorrectly convert loc.end
|
407
|
+
* Example: loc.end always equal loc.start
|
408
|
+
* {
|
409
|
+
* token: {
|
410
|
+
* type: 'Name',
|
411
|
+
* loc: { start: { line: 4, column: 13 }, end: { line: 4, column: 13 } },
|
412
|
+
* value: 'veryBad',
|
413
|
+
* range: [ 40, 47 ]
|
414
|
+
* }
|
415
|
+
* }
|
416
|
+
*/
|
417
|
+
const { line, column } = error.locations[0];
|
418
|
+
const ancestors = context.getAncestors();
|
419
|
+
const token = ancestors[0].tokens.find(token => token.loc.start.line === line && token.loc.start.column === column);
|
346
420
|
context.report({
|
347
|
-
loc: getLocation({ start: error.locations[0] }),
|
421
|
+
loc: getLocation({ start: error.locations[0] }, token === null || token === void 0 ? void 0 : token.value),
|
348
422
|
message: error.message,
|
349
423
|
});
|
350
424
|
}
|
@@ -433,18 +507,24 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
|
|
433
507
|
},
|
434
508
|
},
|
435
509
|
create(context) {
|
510
|
+
if (!ruleFn) {
|
511
|
+
// eslint-disable-next-line no-console
|
512
|
+
console.warn(`You rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql-js" version you are using. Skipping...`);
|
513
|
+
return {};
|
514
|
+
}
|
515
|
+
let schema;
|
516
|
+
if (docs.requiresSchemaToExtend) {
|
517
|
+
schema = getGraphQLSchemaToExtend(context);
|
518
|
+
}
|
519
|
+
if (docs.requiresSchema) {
|
520
|
+
schema = requireGraphQLSchemaFromContext(ruleId, context);
|
521
|
+
}
|
436
522
|
return {
|
437
523
|
Document(node) {
|
438
|
-
if (!ruleFn) {
|
439
|
-
// eslint-disable-next-line no-console
|
440
|
-
console.warn(`You rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql-js" version you are using. Skipping...`);
|
441
|
-
return;
|
442
|
-
}
|
443
|
-
const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
|
444
524
|
const documentNode = getDocumentNode
|
445
525
|
? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
|
446
526
|
: node.rawNode();
|
447
|
-
validateDocument(node, context, schema, documentNode, ruleFn);
|
527
|
+
validateDocument(node, context, schema, documentNode, ruleFn, docs.requiresSchemaToExtend);
|
448
528
|
},
|
449
529
|
};
|
450
530
|
},
|
@@ -594,7 +674,8 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
|
|
594
674
|
}), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
|
595
675
|
category: 'Schema',
|
596
676
|
description: `A type extension is only valid if the type is defined and has the same kind.`,
|
597
|
-
recommended: false,
|
677
|
+
recommended: false,
|
678
|
+
requiresSchemaToExtend: true,
|
598
679
|
}), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
|
599
680
|
category: ['Schema', 'Operations'],
|
600
681
|
description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
|
@@ -1083,13 +1164,7 @@ const rule$2 = {
|
|
1083
1164
|
const MATCH_EXTENSION = 'MATCH_EXTENSION';
|
1084
1165
|
const MATCH_STYLE = 'MATCH_STYLE';
|
1085
1166
|
const ACCEPTED_EXTENSIONS = ['.gql', '.graphql'];
|
1086
|
-
const CASE_STYLES = [
|
1087
|
-
CaseStyle.camelCase,
|
1088
|
-
CaseStyle.pascalCase,
|
1089
|
-
CaseStyle.snakeCase,
|
1090
|
-
CaseStyle.upperCase,
|
1091
|
-
CaseStyle.kebabCase,
|
1092
|
-
];
|
1167
|
+
const CASE_STYLES = ['camelCase', 'PascalCase', 'snake_case', 'UPPER_CASE', 'kebab-case'];
|
1093
1168
|
const schemaOption = {
|
1094
1169
|
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
|
1095
1170
|
};
|
@@ -1113,7 +1188,7 @@ const rule$3 = {
|
|
1113
1188
|
},
|
1114
1189
|
{
|
1115
1190
|
title: 'Correct',
|
1116
|
-
usage: [{ query:
|
1191
|
+
usage: [{ query: 'snake_case' }],
|
1117
1192
|
code: /* GraphQL */ `
|
1118
1193
|
# user_by_id.gql
|
1119
1194
|
query UserById {
|
@@ -1127,7 +1202,7 @@ const rule$3 = {
|
|
1127
1202
|
},
|
1128
1203
|
{
|
1129
1204
|
title: 'Correct',
|
1130
|
-
usage: [{ fragment: { style:
|
1205
|
+
usage: [{ fragment: { style: 'kebab-case', suffix: '.fragment' } }],
|
1131
1206
|
code: /* GraphQL */ `
|
1132
1207
|
# user-fields.fragment.gql
|
1133
1208
|
fragment user_fields on User {
|
@@ -1138,7 +1213,7 @@ const rule$3 = {
|
|
1138
1213
|
},
|
1139
1214
|
{
|
1140
1215
|
title: 'Correct',
|
1141
|
-
usage: [{ mutation: { style:
|
1216
|
+
usage: [{ mutation: { style: 'PascalCase', suffix: 'Mutation' } }],
|
1142
1217
|
code: /* GraphQL */ `
|
1143
1218
|
# DeleteUserMutation.gql
|
1144
1219
|
mutation DELETE_USER {
|
@@ -1158,7 +1233,7 @@ const rule$3 = {
|
|
1158
1233
|
},
|
1159
1234
|
{
|
1160
1235
|
title: 'Incorrect',
|
1161
|
-
usage: [{ query:
|
1236
|
+
usage: [{ query: 'PascalCase' }],
|
1162
1237
|
code: /* GraphQL */ `
|
1163
1238
|
# user-by-id.gql
|
1164
1239
|
query UserById {
|
@@ -1173,10 +1248,10 @@ const rule$3 = {
|
|
1173
1248
|
],
|
1174
1249
|
configOptions: [
|
1175
1250
|
{
|
1176
|
-
query:
|
1177
|
-
mutation:
|
1178
|
-
subscription:
|
1179
|
-
fragment:
|
1251
|
+
query: 'kebab-case',
|
1252
|
+
mutation: 'kebab-case',
|
1253
|
+
subscription: 'kebab-case',
|
1254
|
+
fragment: 'kebab-case',
|
1180
1255
|
},
|
1181
1256
|
],
|
1182
1257
|
},
|
@@ -1193,25 +1268,22 @@ const rule$3 = {
|
|
1193
1268
|
asObject: {
|
1194
1269
|
type: 'object',
|
1195
1270
|
additionalProperties: false,
|
1271
|
+
minProperties: 1,
|
1196
1272
|
properties: {
|
1197
|
-
style: {
|
1198
|
-
|
1199
|
-
},
|
1200
|
-
suffix: {
|
1201
|
-
type: 'string',
|
1202
|
-
},
|
1273
|
+
style: { enum: CASE_STYLES },
|
1274
|
+
suffix: { type: 'string' },
|
1203
1275
|
},
|
1204
1276
|
},
|
1205
1277
|
},
|
1206
1278
|
type: 'array',
|
1279
|
+
minItems: 1,
|
1207
1280
|
maxItems: 1,
|
1208
1281
|
items: {
|
1209
1282
|
type: 'object',
|
1210
1283
|
additionalProperties: false,
|
1284
|
+
minProperties: 1,
|
1211
1285
|
properties: {
|
1212
|
-
fileExtension: {
|
1213
|
-
enum: ACCEPTED_EXTENSIONS,
|
1214
|
-
},
|
1286
|
+
fileExtension: { enum: ACCEPTED_EXTENSIONS },
|
1215
1287
|
query: schemaOption,
|
1216
1288
|
mutation: schemaOption,
|
1217
1289
|
subscription: schemaOption,
|
@@ -1266,7 +1338,7 @@ const rule$3 = {
|
|
1266
1338
|
option = { style: option };
|
1267
1339
|
}
|
1268
1340
|
const expectedExtension = options.fileExtension || fileExtension;
|
1269
|
-
const expectedFilename = convertCase(option.style, docName) + (option.suffix || '') + expectedExtension;
|
1341
|
+
const expectedFilename = (option.style ? convertCase(option.style, docName) : filename) + (option.suffix || '') + expectedExtension;
|
1270
1342
|
const filenameWithExtension = filename + expectedExtension;
|
1271
1343
|
if (expectedFilename !== filenameWithExtension) {
|
1272
1344
|
context.report({
|
@@ -1418,6 +1490,7 @@ const rule$4 = {
|
|
1418
1490
|
],
|
1419
1491
|
},
|
1420
1492
|
},
|
1493
|
+
hasSuggestions: true,
|
1421
1494
|
schema: {
|
1422
1495
|
definitions: {
|
1423
1496
|
asString: {
|
@@ -1492,65 +1565,90 @@ const rule$4 = {
|
|
1492
1565
|
const style = restOptions[kind] || types;
|
1493
1566
|
return typeof style === 'object' ? style : { style };
|
1494
1567
|
}
|
1495
|
-
const checkNode = (selector) => (
|
1496
|
-
const { name } =
|
1497
|
-
if (!
|
1568
|
+
const checkNode = (selector) => (n) => {
|
1569
|
+
const { name: node } = n.kind === graphql.Kind.VARIABLE_DEFINITION ? n.variable : n;
|
1570
|
+
if (!node) {
|
1498
1571
|
return;
|
1499
1572
|
}
|
1500
1573
|
const { prefix, suffix, forbiddenPrefixes, forbiddenSuffixes, style } = normalisePropertyOption(selector);
|
1501
|
-
const nodeType = KindToDisplayName[
|
1502
|
-
const nodeName =
|
1503
|
-
const
|
1504
|
-
if (
|
1574
|
+
const nodeType = KindToDisplayName[n.kind] || n.kind;
|
1575
|
+
const nodeName = node.value;
|
1576
|
+
const error = getError();
|
1577
|
+
if (error) {
|
1578
|
+
const { errorMessage, renameToName } = error;
|
1579
|
+
const [leadingUnderscore] = nodeName.match(/^_*/);
|
1580
|
+
const [trailingUnderscore] = nodeName.match(/_*$/);
|
1581
|
+
const suggestedName = leadingUnderscore + renameToName + trailingUnderscore;
|
1505
1582
|
context.report({
|
1506
|
-
loc: getLocation(
|
1583
|
+
loc: getLocation(node.loc, node.value),
|
1507
1584
|
message: `${nodeType} "${nodeName}" should ${errorMessage}`,
|
1585
|
+
suggest: [
|
1586
|
+
{
|
1587
|
+
desc: `Rename to "${suggestedName}"`,
|
1588
|
+
fix: fixer => fixer.replaceText(node, suggestedName),
|
1589
|
+
},
|
1590
|
+
],
|
1508
1591
|
});
|
1509
1592
|
}
|
1510
|
-
function
|
1511
|
-
|
1512
|
-
if (allowLeadingUnderscore) {
|
1513
|
-
name = name.replace(/^_*/, '');
|
1514
|
-
}
|
1515
|
-
if (allowTrailingUnderscore) {
|
1516
|
-
name = name.replace(/_*$/, '');
|
1517
|
-
}
|
1593
|
+
function getError() {
|
1594
|
+
const name = nodeName.replace(/(^_+)|(_+$)/g, '');
|
1518
1595
|
if (prefix && !name.startsWith(prefix)) {
|
1519
|
-
return
|
1596
|
+
return {
|
1597
|
+
errorMessage: `have "${prefix}" prefix`,
|
1598
|
+
renameToName: prefix + name,
|
1599
|
+
};
|
1520
1600
|
}
|
1521
1601
|
if (suffix && !name.endsWith(suffix)) {
|
1522
|
-
return
|
1602
|
+
return {
|
1603
|
+
errorMessage: `have "${suffix}" suffix`,
|
1604
|
+
renameToName: name + suffix,
|
1605
|
+
};
|
1523
1606
|
}
|
1524
1607
|
const forbiddenPrefix = forbiddenPrefixes === null || forbiddenPrefixes === void 0 ? void 0 : forbiddenPrefixes.find(prefix => name.startsWith(prefix));
|
1525
1608
|
if (forbiddenPrefix) {
|
1526
|
-
return
|
1609
|
+
return {
|
1610
|
+
errorMessage: `not have "${forbiddenPrefix}" prefix`,
|
1611
|
+
renameToName: name.replace(new RegExp(`^${forbiddenPrefix}`), ''),
|
1612
|
+
};
|
1527
1613
|
}
|
1528
1614
|
const forbiddenSuffix = forbiddenSuffixes === null || forbiddenSuffixes === void 0 ? void 0 : forbiddenSuffixes.find(suffix => name.endsWith(suffix));
|
1529
1615
|
if (forbiddenSuffix) {
|
1530
|
-
return
|
1531
|
-
|
1532
|
-
|
1533
|
-
|
1616
|
+
return {
|
1617
|
+
errorMessage: `not have "${forbiddenSuffix}" suffix`,
|
1618
|
+
renameToName: name.replace(new RegExp(`${forbiddenSuffix}$`), ''),
|
1619
|
+
};
|
1534
1620
|
}
|
1535
1621
|
const caseRegex = StyleToRegex[style];
|
1536
1622
|
if (caseRegex && !caseRegex.test(name)) {
|
1537
|
-
return
|
1623
|
+
return {
|
1624
|
+
errorMessage: `be in ${style} format`,
|
1625
|
+
renameToName: convertCase(style, name),
|
1626
|
+
};
|
1538
1627
|
}
|
1539
1628
|
}
|
1540
1629
|
};
|
1541
|
-
const checkUnderscore = (node) => {
|
1630
|
+
const checkUnderscore = (isLeading) => (node) => {
|
1542
1631
|
const name = node.value;
|
1632
|
+
const renameToName = name.replace(new RegExp(isLeading ? '^_+' : '_+$'), '');
|
1543
1633
|
context.report({
|
1544
1634
|
loc: getLocation(node.loc, name),
|
1545
|
-
message: `${
|
1635
|
+
message: `${isLeading ? 'Leading' : 'Trailing'} underscores are not allowed`,
|
1636
|
+
suggest: [
|
1637
|
+
{
|
1638
|
+
desc: `Rename to "${renameToName}"`,
|
1639
|
+
fix: fixer => fixer.replaceText(node, renameToName),
|
1640
|
+
},
|
1641
|
+
],
|
1546
1642
|
});
|
1547
1643
|
};
|
1548
1644
|
const listeners = {};
|
1549
1645
|
if (!allowLeadingUnderscore) {
|
1550
|
-
listeners['Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
|
1646
|
+
listeners['Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
|
1647
|
+
checkUnderscore(true);
|
1551
1648
|
}
|
1552
1649
|
if (!allowTrailingUnderscore) {
|
1553
|
-
listeners['Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
|
1650
|
+
listeners['Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
|
1651
|
+
checkUnderscore(false);
|
1554
1652
|
}
|
1555
1653
|
const selectors = new Set([types && TYPES_KINDS, Object.keys(restOptions)].flat().filter(Boolean));
|
1556
1654
|
for (const selector of selectors) {
|
@@ -2893,9 +2991,22 @@ const rule$j = {
|
|
2893
2991
|
recommended: true,
|
2894
2992
|
},
|
2895
2993
|
messages: {
|
2896
|
-
[REQUIRE_ID_WHEN_AVAILABLE]:
|
2994
|
+
[REQUIRE_ID_WHEN_AVAILABLE]: [
|
2995
|
+
`Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!`,
|
2996
|
+
`If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.`,
|
2997
|
+
].join('\n'),
|
2897
2998
|
},
|
2898
2999
|
schema: {
|
3000
|
+
definitions: {
|
3001
|
+
asString: {
|
3002
|
+
type: 'string',
|
3003
|
+
},
|
3004
|
+
asArray: {
|
3005
|
+
type: 'array',
|
3006
|
+
minItems: 1,
|
3007
|
+
uniqueItems: true,
|
3008
|
+
},
|
3009
|
+
},
|
2899
3010
|
type: 'array',
|
2900
3011
|
maxItems: 1,
|
2901
3012
|
items: {
|
@@ -2903,7 +3014,7 @@ const rule$j = {
|
|
2903
3014
|
additionalProperties: false,
|
2904
3015
|
properties: {
|
2905
3016
|
fieldName: {
|
2906
|
-
|
3017
|
+
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asArray' }],
|
2907
3018
|
default: DEFAULT_ID_FIELD_NAME,
|
2908
3019
|
},
|
2909
3020
|
},
|
@@ -2911,69 +3022,64 @@ const rule$j = {
|
|
2911
3022
|
},
|
2912
3023
|
},
|
2913
3024
|
create(context) {
|
3025
|
+
requireGraphQLSchemaFromContext('require-id-when-available', context);
|
3026
|
+
const siblings = requireSiblingsOperations('require-id-when-available', context);
|
3027
|
+
const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
|
3028
|
+
const idNames = Array.isArray(fieldName) ? fieldName : [fieldName];
|
3029
|
+
const isFound = (s) => s.kind === graphql.Kind.FIELD && idNames.includes(s.name.value);
|
2914
3030
|
return {
|
2915
3031
|
SelectionSet(node) {
|
2916
3032
|
var _a, _b;
|
2917
|
-
|
2918
|
-
|
2919
|
-
const fieldName = (context.options[0] || {}).fieldName || DEFAULT_ID_FIELD_NAME;
|
2920
|
-
if (!node.selections || node.selections.length === 0) {
|
3033
|
+
const typeInfo = node.typeInfo();
|
3034
|
+
if (!typeInfo.gqlType) {
|
2921
3035
|
return;
|
2922
3036
|
}
|
2923
|
-
const
|
2924
|
-
|
2925
|
-
|
2926
|
-
|
2927
|
-
|
2928
|
-
|
2929
|
-
|
2930
|
-
|
2931
|
-
|
2932
|
-
|
2933
|
-
|
2934
|
-
|
2935
|
-
|
2936
|
-
|
2937
|
-
|
2938
|
-
|
2939
|
-
|
2940
|
-
|
2941
|
-
|
2942
|
-
|
2943
|
-
|
2944
|
-
|
2945
|
-
|
2946
|
-
|
2947
|
-
|
2948
|
-
}
|
2949
|
-
}
|
2950
|
-
const { parent } = node;
|
2951
|
-
const hasIdFieldInInterfaceSelectionSet = parent &&
|
2952
|
-
parent.kind === 'InlineFragment' &&
|
2953
|
-
parent.parent &&
|
2954
|
-
parent.parent.kind === 'SelectionSet' &&
|
2955
|
-
parent.parent.selections.some(s => s.kind === 'Field' && s.name.value === fieldName);
|
2956
|
-
if (!found && !hasIdFieldInInterfaceSelectionSet) {
|
2957
|
-
context.report({
|
2958
|
-
loc: {
|
2959
|
-
start: {
|
2960
|
-
line: node.loc.start.line,
|
2961
|
-
column: node.loc.start.column - 1,
|
2962
|
-
},
|
2963
|
-
end: {
|
2964
|
-
line: node.loc.end.line,
|
2965
|
-
column: node.loc.end.column - 1,
|
2966
|
-
},
|
2967
|
-
},
|
2968
|
-
messageId: REQUIRE_ID_WHEN_AVAILABLE,
|
2969
|
-
data: {
|
2970
|
-
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${Array.from(checkedFragmentSpreads).join(', ')})`,
|
2971
|
-
fieldName,
|
2972
|
-
},
|
2973
|
-
});
|
2974
|
-
}
|
3037
|
+
const rawType = getBaseType(typeInfo.gqlType);
|
3038
|
+
const isObjectType = rawType instanceof graphql.GraphQLObjectType;
|
3039
|
+
const isInterfaceType = rawType instanceof graphql.GraphQLInterfaceType;
|
3040
|
+
if (!isObjectType && !isInterfaceType) {
|
3041
|
+
return;
|
3042
|
+
}
|
3043
|
+
const fields = rawType.getFields();
|
3044
|
+
const hasIdFieldInType = idNames.some(name => fields[name]);
|
3045
|
+
if (!hasIdFieldInType) {
|
3046
|
+
return;
|
3047
|
+
}
|
3048
|
+
const checkedFragmentSpreads = new Set();
|
3049
|
+
let found = false;
|
3050
|
+
for (const selection of node.selections) {
|
3051
|
+
if (isFound(selection)) {
|
3052
|
+
found = true;
|
3053
|
+
}
|
3054
|
+
else if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
|
3055
|
+
found = (_a = selection.selectionSet) === null || _a === void 0 ? void 0 : _a.selections.some(s => isFound(s));
|
3056
|
+
}
|
3057
|
+
else if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
|
3058
|
+
const [foundSpread] = siblings.getFragment(selection.name.value);
|
3059
|
+
if (foundSpread) {
|
3060
|
+
checkedFragmentSpreads.add(foundSpread.document.name.value);
|
3061
|
+
found = (_b = foundSpread.document.selectionSet) === null || _b === void 0 ? void 0 : _b.selections.some(s => isFound(s));
|
2975
3062
|
}
|
2976
3063
|
}
|
3064
|
+
if (found) {
|
3065
|
+
break;
|
3066
|
+
}
|
3067
|
+
}
|
3068
|
+
const { parent } = node;
|
3069
|
+
const hasIdFieldInInterfaceSelectionSet = parent &&
|
3070
|
+
parent.kind === graphql.Kind.INLINE_FRAGMENT &&
|
3071
|
+
parent.parent &&
|
3072
|
+
parent.parent.kind === graphql.Kind.SELECTION_SET &&
|
3073
|
+
parent.parent.selections.some(s => isFound(s));
|
3074
|
+
if (!found && !hasIdFieldInInterfaceSelectionSet) {
|
3075
|
+
context.report({
|
3076
|
+
loc: getLocation(node.loc),
|
3077
|
+
messageId: REQUIRE_ID_WHEN_AVAILABLE,
|
3078
|
+
data: {
|
3079
|
+
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
|
3080
|
+
fieldName: idNames.map(name => `"${name}"`).join(' or '),
|
3081
|
+
},
|
3082
|
+
});
|
2977
3083
|
}
|
2978
3084
|
},
|
2979
3085
|
};
|
@@ -3065,7 +3171,7 @@ const rule$k = {
|
|
3065
3171
|
// eslint-disable-next-line no-console
|
3066
3172
|
console.warn(`Rule "selection-set-depth" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
|
3067
3173
|
}
|
3068
|
-
const maxDepth = context.options[0]
|
3174
|
+
const { maxDepth } = context.options[0];
|
3069
3175
|
const ignore = context.options[0].ignore || [];
|
3070
3176
|
const checkFn = depthLimit(maxDepth, { ignore });
|
3071
3177
|
return {
|
@@ -3669,42 +3775,6 @@ function getSiblingOperations(options, gqlConfig) {
|
|
3669
3775
|
return siblingOperations;
|
3670
3776
|
}
|
3671
3777
|
|
3672
|
-
let graphqlConfig;
|
3673
|
-
function loadGraphqlConfig(options) {
|
3674
|
-
// We don't want cache config on test environment
|
3675
|
-
// Otherwise schema and documents will be same for all tests
|
3676
|
-
if (process.env.NODE_ENV !== 'test' && graphqlConfig) {
|
3677
|
-
return graphqlConfig;
|
3678
|
-
}
|
3679
|
-
const onDiskConfig = options.skipGraphQLConfig
|
3680
|
-
? null
|
3681
|
-
: graphqlConfig$1.loadConfigSync({
|
3682
|
-
throwOnEmpty: false,
|
3683
|
-
throwOnMissing: false,
|
3684
|
-
extensions: [addCodeFileLoaderExtension],
|
3685
|
-
});
|
3686
|
-
graphqlConfig =
|
3687
|
-
onDiskConfig ||
|
3688
|
-
new graphqlConfig$1.GraphQLConfig({
|
3689
|
-
config: options.projects
|
3690
|
-
? { projects: options.projects }
|
3691
|
-
: {
|
3692
|
-
schema: (options.schema || ''),
|
3693
|
-
documents: options.documents || options.operations,
|
3694
|
-
extensions: options.extensions,
|
3695
|
-
include: options.include,
|
3696
|
-
exclude: options.exclude,
|
3697
|
-
},
|
3698
|
-
filepath: 'virtual-config',
|
3699
|
-
}, [addCodeFileLoaderExtension]);
|
3700
|
-
return graphqlConfig;
|
3701
|
-
}
|
3702
|
-
const addCodeFileLoaderExtension = api => {
|
3703
|
-
api.loaders.schema.register(new codeFileLoader.CodeFileLoader());
|
3704
|
-
api.loaders.documents.register(new codeFileLoader.CodeFileLoader());
|
3705
|
-
return { name: 'graphql-eslint-loaders' };
|
3706
|
-
};
|
3707
|
-
|
3708
3778
|
let reachableTypesCache;
|
3709
3779
|
function getReachableTypes(schema) {
|
3710
3780
|
// We don't want cache reachableTypes on test environment
|
@@ -3787,7 +3857,7 @@ function parse(code, options) {
|
|
3787
3857
|
return parseForESLint(code, options).ast;
|
3788
3858
|
}
|
3789
3859
|
function parseForESLint(code, options = {}) {
|
3790
|
-
const gqlConfig =
|
3860
|
+
const gqlConfig = loadCachedGraphQLConfig(options);
|
3791
3861
|
const schema = getSchema(options, gqlConfig);
|
3792
3862
|
const parserServices = {
|
3793
3863
|
hasTypeInfo: schema !== null,
|