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