@graphql-eslint/eslint-plugin 3.2.0-alpha-45f5fcb.0 → 3.3.0-alpha-db2c2cb.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/known-fragment-names.md +9 -30
- package/docs/rules/no-undefined-variables.md +1 -1
- package/docs/rules/no-unused-variables.md +1 -1
- package/docs/rules/require-id-when-available.md +16 -1
- package/index.js +237 -175
- package/index.mjs +239 -177
- package/package.json +8 -2
- package/rules/graphql-js-validation.d.ts +2 -5
- package/types.d.ts +2 -2
package/index.js
CHANGED
@@ -6,7 +6,6 @@ function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'defau
|
|
6
6
|
|
7
7
|
const graphql = require('graphql');
|
8
8
|
const validate = require('graphql/validation/validate');
|
9
|
-
const _import = require('@graphql-tools/import');
|
10
9
|
const fs = require('fs');
|
11
10
|
const path = require('path');
|
12
11
|
const utils = require('@graphql-tools/utils');
|
@@ -327,59 +326,116 @@ function getLocation(loc, fieldName = '', offset) {
|
|
327
326
|
};
|
328
327
|
}
|
329
328
|
|
330
|
-
function
|
331
|
-
|
332
|
-
|
329
|
+
function validateDocument(sourceNode, context, schema, documentNode, rule) {
|
330
|
+
if (documentNode.definitions.length === 0) {
|
331
|
+
return;
|
332
|
+
}
|
333
|
+
try {
|
334
|
+
const validationErrors = schema
|
335
|
+
? graphql.validate(schema, documentNode, [rule])
|
336
|
+
: validate.validateSDL(documentNode, null, [rule]);
|
337
|
+
for (const error of validationErrors) {
|
338
|
+
/*
|
339
|
+
* TODO: Fix ESTree-AST converter because currently it's incorrectly convert loc.end
|
340
|
+
* Example: loc.end always equal loc.start
|
341
|
+
* {
|
342
|
+
* token: {
|
343
|
+
* type: 'Name',
|
344
|
+
* loc: { start: { line: 4, column: 13 }, end: { line: 4, column: 13 } },
|
345
|
+
* value: 'veryBad',
|
346
|
+
* range: [ 40, 47 ]
|
347
|
+
* }
|
348
|
+
* }
|
349
|
+
*/
|
350
|
+
const { line, column } = error.locations[0];
|
351
|
+
const ancestors = context.getAncestors();
|
352
|
+
const token = ancestors[0].tokens.find(token => token.loc.start.line === line && token.loc.start.column === column);
|
353
|
+
context.report({
|
354
|
+
loc: getLocation({ start: error.locations[0] }, token === null || token === void 0 ? void 0 : token.value),
|
355
|
+
message: error.message,
|
356
|
+
});
|
357
|
+
}
|
358
|
+
}
|
359
|
+
catch (e) {
|
360
|
+
context.report({
|
361
|
+
node: sourceNode,
|
362
|
+
message: e.message,
|
363
|
+
});
|
364
|
+
}
|
333
365
|
}
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
366
|
+
const getFragmentDefsAndFragmentSpreads = (schema, node) => {
|
367
|
+
const typeInfo = new graphql.TypeInfo(schema);
|
368
|
+
const fragmentDefs = new Set();
|
369
|
+
const fragmentSpreads = new Set();
|
370
|
+
const visitor = graphql.visitWithTypeInfo(typeInfo, {
|
371
|
+
FragmentDefinition(node) {
|
372
|
+
fragmentDefs.add(`${node.name.value}:${node.typeCondition.name.value}`);
|
373
|
+
},
|
374
|
+
FragmentSpread(node) {
|
375
|
+
const parentType = typeInfo.getParentType();
|
376
|
+
if (parentType) {
|
377
|
+
fragmentSpreads.add(`${node.name.value}:${parentType.name}`);
|
378
|
+
}
|
379
|
+
},
|
380
|
+
});
|
381
|
+
graphql.visit(node, visitor);
|
382
|
+
return { fragmentDefs, fragmentSpreads };
|
383
|
+
};
|
384
|
+
const getMissingFragments = (schema, node) => {
|
385
|
+
const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(schema, node);
|
386
|
+
return [...fragmentSpreads].filter(name => !fragmentDefs.has(name));
|
387
|
+
};
|
388
|
+
const handleMissingFragments = ({ ruleId, context, schema, node }) => {
|
389
|
+
const missingFragments = getMissingFragments(schema, node);
|
390
|
+
if (missingFragments.length > 0) {
|
391
|
+
const siblings = requireSiblingsOperations(ruleId, context);
|
392
|
+
const fragmentsToAdd = [];
|
393
|
+
for (const missingFragment of missingFragments) {
|
394
|
+
const [fragmentName, fragmentTypeName] = missingFragment.split(':');
|
395
|
+
const [foundFragment] = siblings
|
396
|
+
.getFragment(fragmentName)
|
397
|
+
.map(source => source.document)
|
398
|
+
.filter(fragment => fragment.typeCondition.name.value === fragmentTypeName);
|
399
|
+
if (foundFragment) {
|
400
|
+
fragmentsToAdd.push(foundFragment);
|
345
401
|
}
|
346
402
|
}
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
403
|
+
if (fragmentsToAdd.length > 0) {
|
404
|
+
// recall fn to make sure to add fragments inside fragments
|
405
|
+
return handleMissingFragments({
|
406
|
+
ruleId,
|
407
|
+
context,
|
408
|
+
schema,
|
409
|
+
node: {
|
410
|
+
kind: graphql.Kind.DOCUMENT,
|
411
|
+
definitions: [...node.definitions, ...fragmentsToAdd],
|
412
|
+
},
|
351
413
|
});
|
352
414
|
}
|
353
415
|
}
|
354
|
-
|
355
|
-
const isGraphQLImportFile = rawSDL => {
|
356
|
-
const trimmedRawSDL = rawSDL.trimLeft();
|
357
|
-
return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
|
416
|
+
return node;
|
358
417
|
};
|
359
|
-
const validationToRule = (
|
360
|
-
var _a;
|
418
|
+
const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
|
361
419
|
let ruleFn = null;
|
362
420
|
try {
|
363
421
|
ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
|
364
422
|
}
|
365
|
-
catch (
|
423
|
+
catch (_a) {
|
366
424
|
try {
|
367
425
|
ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
|
368
426
|
}
|
369
|
-
catch (
|
427
|
+
catch (_b) {
|
370
428
|
ruleFn = require('graphql/validation')[`${ruleName}Rule`];
|
371
429
|
}
|
372
430
|
}
|
373
|
-
const requiresSchema = (_a = docs.requiresSchema) !== null && _a !== void 0 ? _a : true;
|
374
431
|
return {
|
375
|
-
[
|
432
|
+
[ruleId]: {
|
376
433
|
meta: {
|
377
434
|
docs: {
|
378
435
|
recommended: true,
|
379
436
|
...docs,
|
380
437
|
graphQLJSRuleName: ruleName,
|
381
|
-
|
382
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${name}.md`,
|
438
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
|
383
439
|
description: `${docs.description}\n\n> This rule is a wrapper around a \`graphql-js\` validation function. [You can find its source code here](https://github.com/graphql/graphql-js/blob/main/src/validation/rules/${ruleName}Rule.ts).`,
|
384
440
|
},
|
385
441
|
},
|
@@ -388,56 +444,53 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
|
|
388
444
|
Document(node) {
|
389
445
|
if (!ruleFn) {
|
390
446
|
// eslint-disable-next-line no-console
|
391
|
-
console.warn(`You rule "${
|
447
|
+
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...`);
|
392
448
|
return;
|
393
449
|
}
|
394
|
-
const schema = requiresSchema ? requireGraphQLSchemaFromContext(
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
}
|
400
|
-
validateDoc(node, context, schema, documentNode || node.rawNode(), [ruleFn], ruleName);
|
450
|
+
const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
|
451
|
+
const documentNode = getDocumentNode
|
452
|
+
? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
|
453
|
+
: node.rawNode();
|
454
|
+
validateDocument(node, context, schema, documentNode, ruleFn);
|
401
455
|
},
|
402
456
|
};
|
403
457
|
},
|
404
458
|
},
|
405
459
|
};
|
406
460
|
};
|
407
|
-
const importFiles = (context) => {
|
408
|
-
const code = context.getSourceCode().text;
|
409
|
-
if (!isGraphQLImportFile(code)) {
|
410
|
-
return null;
|
411
|
-
}
|
412
|
-
// Import documents because file contains '#import' comments
|
413
|
-
return _import.processImport(context.getFilename());
|
414
|
-
};
|
415
461
|
const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
|
416
462
|
category: 'Operations',
|
417
463
|
description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`,
|
464
|
+
requiresSchema: true,
|
418
465
|
}), validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', {
|
419
466
|
category: 'Operations',
|
420
467
|
description: 'A GraphQL document is only valid if all fields selected are defined by the parent type, or are an allowed meta field such as `__typename`.',
|
468
|
+
requiresSchema: true,
|
421
469
|
}), validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', {
|
422
470
|
category: 'Operations',
|
423
471
|
description: `Fragments use a type condition to determine if they apply, since fragments can only be spread into a composite type (object, interface, or union), the type condition must also be a composite type.`,
|
472
|
+
requiresSchema: true,
|
424
473
|
}), validationToRule('known-argument-names', 'KnownArgumentNames', {
|
425
474
|
category: ['Schema', 'Operations'],
|
426
475
|
description: `A GraphQL field is only valid if all supplied arguments are defined by that field.`,
|
476
|
+
requiresSchema: true,
|
427
477
|
}), validationToRule('known-directives', 'KnownDirectives', {
|
428
478
|
category: ['Schema', 'Operations'],
|
429
479
|
description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
|
480
|
+
requiresSchema: true,
|
430
481
|
}), validationToRule('known-fragment-names', 'KnownFragmentNames', {
|
431
482
|
category: 'Operations',
|
432
483
|
description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
|
484
|
+
requiresSchema: true,
|
485
|
+
requiresSiblings: true,
|
433
486
|
examples: [
|
434
487
|
{
|
435
|
-
title: 'Incorrect
|
488
|
+
title: 'Incorrect',
|
436
489
|
code: /* GraphQL */ `
|
437
490
|
query {
|
438
491
|
user {
|
439
492
|
id
|
440
|
-
...UserFields
|
493
|
+
...UserFields # fragment not defined in the document
|
441
494
|
}
|
442
495
|
}
|
443
496
|
`,
|
@@ -459,153 +512,151 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
|
|
459
512
|
`,
|
460
513
|
},
|
461
514
|
{
|
462
|
-
title: 'Correct (
|
515
|
+
title: 'Correct (`UserFields` fragment located in a separate file)',
|
463
516
|
code: /* GraphQL */ `
|
464
|
-
#
|
465
|
-
|
517
|
+
# user.gql
|
466
518
|
query {
|
467
519
|
user {
|
468
520
|
id
|
469
521
|
...UserFields
|
470
522
|
}
|
471
523
|
}
|
472
|
-
`,
|
473
|
-
},
|
474
|
-
{
|
475
|
-
title: "False positive case\n\nFor extracting documents from code under the hood we use [graphql-tag-pluck](https://graphql-tools.com/docs/graphql-tag-pluck) that [don't support string interpolation](https://stackoverflow.com/questions/62749847/graphql-codegen-dynamic-fields-with-interpolation/62751311#62751311) for this moment.",
|
476
|
-
code: `
|
477
|
-
const USER_FIELDS = gql\`
|
478
|
-
fragment UserFields on User {
|
479
|
-
id
|
480
|
-
}
|
481
|
-
\`
|
482
|
-
|
483
|
-
const GET_USER = /* GraphQL */ \`
|
484
|
-
# eslint @graphql-eslint/known-fragment-names: 'error'
|
485
|
-
|
486
|
-
query User {
|
487
|
-
user {
|
488
|
-
...UserFields
|
489
|
-
}
|
490
|
-
}
|
491
524
|
|
492
|
-
|
493
|
-
|
494
|
-
|
525
|
+
# user-fields.gql
|
526
|
+
fragment UserFields on User {
|
527
|
+
id
|
528
|
+
}
|
529
|
+
`,
|
495
530
|
},
|
496
531
|
],
|
497
|
-
},
|
532
|
+
}, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
|
498
533
|
category: ['Schema', 'Operations'],
|
499
534
|
description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
|
535
|
+
requiresSchema: true,
|
500
536
|
}), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
|
501
537
|
category: 'Operations',
|
502
538
|
description: `A GraphQL document is only valid if when it contains an anonymous operation (the query short-hand) that it contains only that one operation definition.`,
|
539
|
+
requiresSchema: true,
|
503
540
|
}), validationToRule('lone-schema-definition', 'LoneSchemaDefinition', {
|
504
541
|
category: 'Schema',
|
505
542
|
description: `A GraphQL document is only valid if it contains only one schema definition.`,
|
506
|
-
requiresSchema: false,
|
507
543
|
}), validationToRule('no-fragment-cycles', 'NoFragmentCycles', {
|
508
544
|
category: 'Operations',
|
509
545
|
description: `A GraphQL fragment is only valid when it does not have cycles in fragments usage.`,
|
546
|
+
requiresSchema: true,
|
510
547
|
}), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
|
511
548
|
category: 'Operations',
|
512
549
|
description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`,
|
513
|
-
|
550
|
+
requiresSchema: true,
|
551
|
+
requiresSiblings: true,
|
552
|
+
}, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
|
514
553
|
category: 'Operations',
|
515
554
|
description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
|
555
|
+
requiresSchema: true,
|
516
556
|
requiresSiblings: true,
|
517
|
-
}, context => {
|
518
|
-
const siblings = requireSiblingsOperations(
|
519
|
-
const
|
520
|
-
|
521
|
-
|
522
|
-
filePath
|
523
|
-
|
524
|
-
}));
|
525
|
-
const getParentNode = (
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
.filter(isGraphQLImportFile)
|
530
|
-
.map(line => _import.parseImportLine(line.replace('#', '')))
|
531
|
-
.some(o => filePath === path.join(path.dirname(docFilePath), o.from));
|
532
|
-
if (!isFileImported) {
|
533
|
-
continue;
|
534
|
-
}
|
535
|
-
// Import first file that import this file
|
536
|
-
const document = _import.processImport(docFilePath);
|
537
|
-
// Import most top file that import this file
|
538
|
-
return getParentNode(docFilePath) || document;
|
557
|
+
}, ({ ruleId, context, schema, node }) => {
|
558
|
+
const siblings = requireSiblingsOperations(ruleId, context);
|
559
|
+
const FilePathToDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce((map, { filePath, document }) => {
|
560
|
+
var _a;
|
561
|
+
(_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
|
562
|
+
map[filePath].push(document);
|
563
|
+
return map;
|
564
|
+
}, Object.create(null));
|
565
|
+
const getParentNode = (currentFilePath, node) => {
|
566
|
+
const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(schema, node);
|
567
|
+
if (fragmentDefs.size === 0) {
|
568
|
+
return node;
|
539
569
|
}
|
540
|
-
|
570
|
+
// skip iteration over documents for current filepath
|
571
|
+
delete FilePathToDocumentsMap[currentFilePath];
|
572
|
+
for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
|
573
|
+
const missingFragments = getMissingFragments(schema, {
|
574
|
+
kind: graphql.Kind.DOCUMENT,
|
575
|
+
definitions: documents,
|
576
|
+
});
|
577
|
+
const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
|
578
|
+
if (isCurrentFileImportFragment) {
|
579
|
+
return getParentNode(filePath, {
|
580
|
+
kind: graphql.Kind.DOCUMENT,
|
581
|
+
definitions: [...node.definitions, ...documents],
|
582
|
+
});
|
583
|
+
}
|
584
|
+
}
|
585
|
+
return node;
|
541
586
|
};
|
542
|
-
return getParentNode(context.getFilename());
|
587
|
+
return getParentNode(context.getFilename(), node);
|
543
588
|
}), validationToRule('no-unused-variables', 'NoUnusedVariables', {
|
544
589
|
category: 'Operations',
|
545
590
|
description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`,
|
546
|
-
|
591
|
+
requiresSchema: true,
|
592
|
+
requiresSiblings: true,
|
593
|
+
}, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
|
547
594
|
category: 'Operations',
|
548
595
|
description: `A selection set is only valid if all fields (including spreading any fragments) either correspond to distinct response names or can be merged without ambiguity.`,
|
596
|
+
requiresSchema: true,
|
549
597
|
}), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
|
550
598
|
category: 'Operations',
|
551
599
|
description: `A fragment spread is only valid if the type condition could ever possibly be true: if there is a non-empty intersection of the possible parent types, and possible types which pass the type condition.`,
|
600
|
+
requiresSchema: true,
|
552
601
|
}), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
|
553
602
|
category: 'Schema',
|
554
603
|
description: `A type extension is only valid if the type is defined and has the same kind.`,
|
555
|
-
requiresSchema: false,
|
556
604
|
recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed
|
557
605
|
}), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
|
558
606
|
category: ['Schema', 'Operations'],
|
559
607
|
description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
|
608
|
+
requiresSchema: true,
|
560
609
|
}), validationToRule('scalar-leafs', 'ScalarLeafs', {
|
561
610
|
category: 'Operations',
|
562
611
|
description: `A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.`,
|
612
|
+
requiresSchema: true,
|
563
613
|
}), validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', {
|
564
614
|
category: 'Operations',
|
565
615
|
description: `A GraphQL subscription is valid only if it contains a single root field.`,
|
616
|
+
requiresSchema: true,
|
566
617
|
}), validationToRule('unique-argument-names', 'UniqueArgumentNames', {
|
567
618
|
category: 'Operations',
|
568
619
|
description: `A GraphQL field or directive is only valid if all supplied arguments are uniquely named.`,
|
620
|
+
requiresSchema: true,
|
569
621
|
}), validationToRule('unique-directive-names', 'UniqueDirectiveNames', {
|
570
622
|
category: 'Schema',
|
571
623
|
description: `A GraphQL document is only valid if all defined directives have unique names.`,
|
572
|
-
requiresSchema: false,
|
573
624
|
}), validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', {
|
574
625
|
category: ['Schema', 'Operations'],
|
575
626
|
description: `A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.`,
|
627
|
+
requiresSchema: true,
|
576
628
|
}), validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', {
|
577
629
|
category: 'Schema',
|
578
630
|
description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
|
579
|
-
requiresSchema: false,
|
580
631
|
recommended: false,
|
581
632
|
}), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
|
582
633
|
category: 'Schema',
|
583
634
|
description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
|
584
|
-
requiresSchema: false,
|
585
635
|
}), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
|
586
636
|
category: 'Operations',
|
587
637
|
description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`,
|
588
|
-
requiresSchema: false,
|
589
638
|
}), validationToRule('unique-operation-types', 'UniqueOperationTypes', {
|
590
639
|
category: 'Schema',
|
591
640
|
description: `A GraphQL document is only valid if it has only one type per operation.`,
|
592
|
-
requiresSchema: false,
|
593
641
|
}), validationToRule('unique-type-names', 'UniqueTypeNames', {
|
594
642
|
category: 'Schema',
|
595
643
|
description: `A GraphQL document is only valid if all defined types have unique names.`,
|
596
|
-
requiresSchema: false,
|
597
644
|
}), validationToRule('unique-variable-names', 'UniqueVariableNames', {
|
598
645
|
category: 'Operations',
|
599
646
|
description: `A GraphQL operation is only valid if all its variables are uniquely named.`,
|
647
|
+
requiresSchema: true,
|
600
648
|
}), validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', {
|
601
649
|
category: 'Operations',
|
602
650
|
description: `A GraphQL document is only valid if all value literals are of the type expected at their position.`,
|
651
|
+
requiresSchema: true,
|
603
652
|
}), validationToRule('variables-are-input-types', 'VariablesAreInputTypes', {
|
604
653
|
category: 'Operations',
|
605
654
|
description: `A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).`,
|
655
|
+
requiresSchema: true,
|
606
656
|
}), validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', {
|
607
657
|
category: 'Operations',
|
608
658
|
description: `Variables passed to field arguments conform to type.`,
|
659
|
+
requiresSchema: true,
|
609
660
|
}));
|
610
661
|
|
611
662
|
const ALPHABETIZE = 'ALPHABETIZE';
|
@@ -1451,13 +1502,16 @@ const rule$4 = {
|
|
1451
1502
|
const error = getError();
|
1452
1503
|
if (error) {
|
1453
1504
|
const { errorMessage, renameToName } = error;
|
1505
|
+
const [leadingUnderscore] = nodeName.match(/^_*/);
|
1506
|
+
const [trailingUnderscore] = nodeName.match(/_*$/);
|
1507
|
+
const suggestedName = leadingUnderscore + renameToName + trailingUnderscore;
|
1454
1508
|
context.report({
|
1455
1509
|
loc: getLocation(node.loc, node.value),
|
1456
1510
|
message: `${nodeType} "${nodeName}" should ${errorMessage}`,
|
1457
1511
|
suggest: [
|
1458
1512
|
{
|
1459
|
-
desc: `Rename to "${
|
1460
|
-
fix: fixer => fixer.replaceText(node,
|
1513
|
+
desc: `Rename to "${suggestedName}"`,
|
1514
|
+
fix: fixer => fixer.replaceText(node, suggestedName),
|
1461
1515
|
},
|
1462
1516
|
],
|
1463
1517
|
});
|
@@ -1467,34 +1521,34 @@ const rule$4 = {
|
|
1467
1521
|
if (prefix && !name.startsWith(prefix)) {
|
1468
1522
|
return {
|
1469
1523
|
errorMessage: `have "${prefix}" prefix`,
|
1470
|
-
renameToName: prefix +
|
1524
|
+
renameToName: prefix + name,
|
1471
1525
|
};
|
1472
1526
|
}
|
1473
1527
|
if (suffix && !name.endsWith(suffix)) {
|
1474
1528
|
return {
|
1475
1529
|
errorMessage: `have "${suffix}" suffix`,
|
1476
|
-
renameToName:
|
1530
|
+
renameToName: name + suffix,
|
1477
1531
|
};
|
1478
1532
|
}
|
1479
1533
|
const forbiddenPrefix = forbiddenPrefixes === null || forbiddenPrefixes === void 0 ? void 0 : forbiddenPrefixes.find(prefix => name.startsWith(prefix));
|
1480
1534
|
if (forbiddenPrefix) {
|
1481
1535
|
return {
|
1482
1536
|
errorMessage: `not have "${forbiddenPrefix}" prefix`,
|
1483
|
-
renameToName:
|
1537
|
+
renameToName: name.replace(new RegExp(`^${forbiddenPrefix}`), ''),
|
1484
1538
|
};
|
1485
1539
|
}
|
1486
1540
|
const forbiddenSuffix = forbiddenSuffixes === null || forbiddenSuffixes === void 0 ? void 0 : forbiddenSuffixes.find(suffix => name.endsWith(suffix));
|
1487
1541
|
if (forbiddenSuffix) {
|
1488
1542
|
return {
|
1489
1543
|
errorMessage: `not have "${forbiddenSuffix}" suffix`,
|
1490
|
-
renameToName:
|
1544
|
+
renameToName: name.replace(new RegExp(`${forbiddenSuffix}$`), ''),
|
1491
1545
|
};
|
1492
1546
|
}
|
1493
1547
|
const caseRegex = StyleToRegex[style];
|
1494
1548
|
if (caseRegex && !caseRegex.test(name)) {
|
1495
1549
|
return {
|
1496
1550
|
errorMessage: `be in ${style} format`,
|
1497
|
-
renameToName: convertCase(style,
|
1551
|
+
renameToName: convertCase(style, name),
|
1498
1552
|
};
|
1499
1553
|
}
|
1500
1554
|
}
|
@@ -2863,9 +2917,22 @@ const rule$j = {
|
|
2863
2917
|
recommended: true,
|
2864
2918
|
},
|
2865
2919
|
messages: {
|
2866
|
-
[REQUIRE_ID_WHEN_AVAILABLE]:
|
2920
|
+
[REQUIRE_ID_WHEN_AVAILABLE]: [
|
2921
|
+
`Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!`,
|
2922
|
+
`If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.`,
|
2923
|
+
].join('\n'),
|
2867
2924
|
},
|
2868
2925
|
schema: {
|
2926
|
+
definitions: {
|
2927
|
+
asString: {
|
2928
|
+
type: 'string',
|
2929
|
+
},
|
2930
|
+
asArray: {
|
2931
|
+
type: 'array',
|
2932
|
+
minItems: 1,
|
2933
|
+
uniqueItems: true,
|
2934
|
+
},
|
2935
|
+
},
|
2869
2936
|
type: 'array',
|
2870
2937
|
maxItems: 1,
|
2871
2938
|
items: {
|
@@ -2873,7 +2940,7 @@ const rule$j = {
|
|
2873
2940
|
additionalProperties: false,
|
2874
2941
|
properties: {
|
2875
2942
|
fieldName: {
|
2876
|
-
|
2943
|
+
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asArray' }],
|
2877
2944
|
default: DEFAULT_ID_FIELD_NAME,
|
2878
2945
|
},
|
2879
2946
|
},
|
@@ -2881,69 +2948,64 @@ const rule$j = {
|
|
2881
2948
|
},
|
2882
2949
|
},
|
2883
2950
|
create(context) {
|
2951
|
+
requireGraphQLSchemaFromContext('require-id-when-available', context);
|
2952
|
+
const siblings = requireSiblingsOperations('require-id-when-available', context);
|
2953
|
+
const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
|
2954
|
+
const idNames = Array.isArray(fieldName) ? fieldName : [fieldName];
|
2955
|
+
const isFound = (s) => s.kind === graphql.Kind.FIELD && idNames.includes(s.name.value);
|
2884
2956
|
return {
|
2885
2957
|
SelectionSet(node) {
|
2886
2958
|
var _a, _b;
|
2887
|
-
|
2888
|
-
|
2889
|
-
const fieldName = (context.options[0] || {}).fieldName || DEFAULT_ID_FIELD_NAME;
|
2890
|
-
if (!node.selections || node.selections.length === 0) {
|
2959
|
+
const typeInfo = node.typeInfo();
|
2960
|
+
if (!typeInfo.gqlType) {
|
2891
2961
|
return;
|
2892
2962
|
}
|
2893
|
-
const
|
2894
|
-
|
2895
|
-
|
2896
|
-
|
2897
|
-
|
2898
|
-
|
2899
|
-
|
2900
|
-
|
2901
|
-
|
2902
|
-
|
2903
|
-
|
2904
|
-
|
2905
|
-
|
2906
|
-
|
2907
|
-
|
2908
|
-
|
2909
|
-
|
2910
|
-
|
2911
|
-
|
2912
|
-
|
2913
|
-
|
2914
|
-
|
2915
|
-
|
2916
|
-
|
2917
|
-
|
2918
|
-
}
|
2919
|
-
}
|
2920
|
-
const { parent } = node;
|
2921
|
-
const hasIdFieldInInterfaceSelectionSet = parent &&
|
2922
|
-
parent.kind === 'InlineFragment' &&
|
2923
|
-
parent.parent &&
|
2924
|
-
parent.parent.kind === 'SelectionSet' &&
|
2925
|
-
parent.parent.selections.some(s => s.kind === 'Field' && s.name.value === fieldName);
|
2926
|
-
if (!found && !hasIdFieldInInterfaceSelectionSet) {
|
2927
|
-
context.report({
|
2928
|
-
loc: {
|
2929
|
-
start: {
|
2930
|
-
line: node.loc.start.line,
|
2931
|
-
column: node.loc.start.column - 1,
|
2932
|
-
},
|
2933
|
-
end: {
|
2934
|
-
line: node.loc.end.line,
|
2935
|
-
column: node.loc.end.column - 1,
|
2936
|
-
},
|
2937
|
-
},
|
2938
|
-
messageId: REQUIRE_ID_WHEN_AVAILABLE,
|
2939
|
-
data: {
|
2940
|
-
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${Array.from(checkedFragmentSpreads).join(', ')})`,
|
2941
|
-
fieldName,
|
2942
|
-
},
|
2943
|
-
});
|
2944
|
-
}
|
2963
|
+
const rawType = getBaseType(typeInfo.gqlType);
|
2964
|
+
const isObjectType = rawType instanceof graphql.GraphQLObjectType;
|
2965
|
+
const isInterfaceType = rawType instanceof graphql.GraphQLInterfaceType;
|
2966
|
+
if (!isObjectType && !isInterfaceType) {
|
2967
|
+
return;
|
2968
|
+
}
|
2969
|
+
const fields = rawType.getFields();
|
2970
|
+
const hasIdFieldInType = idNames.some(name => fields[name]);
|
2971
|
+
if (!hasIdFieldInType) {
|
2972
|
+
return;
|
2973
|
+
}
|
2974
|
+
const checkedFragmentSpreads = new Set();
|
2975
|
+
let found = false;
|
2976
|
+
for (const selection of node.selections) {
|
2977
|
+
if (isFound(selection)) {
|
2978
|
+
found = true;
|
2979
|
+
}
|
2980
|
+
else if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
|
2981
|
+
found = (_a = selection.selectionSet) === null || _a === void 0 ? void 0 : _a.selections.some(s => isFound(s));
|
2982
|
+
}
|
2983
|
+
else if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
|
2984
|
+
const [foundSpread] = siblings.getFragment(selection.name.value);
|
2985
|
+
if (foundSpread) {
|
2986
|
+
checkedFragmentSpreads.add(foundSpread.document.name.value);
|
2987
|
+
found = (_b = foundSpread.document.selectionSet) === null || _b === void 0 ? void 0 : _b.selections.some(s => isFound(s));
|
2945
2988
|
}
|
2946
2989
|
}
|
2990
|
+
if (found) {
|
2991
|
+
break;
|
2992
|
+
}
|
2993
|
+
}
|
2994
|
+
const { parent } = node;
|
2995
|
+
const hasIdFieldInInterfaceSelectionSet = parent &&
|
2996
|
+
parent.kind === graphql.Kind.INLINE_FRAGMENT &&
|
2997
|
+
parent.parent &&
|
2998
|
+
parent.parent.kind === graphql.Kind.SELECTION_SET &&
|
2999
|
+
parent.parent.selections.some(s => isFound(s));
|
3000
|
+
if (!found && !hasIdFieldInInterfaceSelectionSet) {
|
3001
|
+
context.report({
|
3002
|
+
loc: getLocation(node.loc),
|
3003
|
+
messageId: REQUIRE_ID_WHEN_AVAILABLE,
|
3004
|
+
data: {
|
3005
|
+
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
|
3006
|
+
fieldName: idNames.map(name => `"${name}"`).join(' or '),
|
3007
|
+
},
|
3008
|
+
});
|
2947
3009
|
}
|
2948
3010
|
},
|
2949
3011
|
};
|
@@ -3035,7 +3097,7 @@ const rule$k = {
|
|
3035
3097
|
// eslint-disable-next-line no-console
|
3036
3098
|
console.warn(`Rule "selection-set-depth" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
|
3037
3099
|
}
|
3038
|
-
const maxDepth = context.options[0]
|
3100
|
+
const { maxDepth } = context.options[0];
|
3039
3101
|
const ignore = context.options[0].ignore || [];
|
3040
3102
|
const checkFn = depthLimit(maxDepth, { ignore });
|
3041
3103
|
return {
|