@graphql-eslint/eslint-plugin 3.2.0-alpha-001cd75.0 → 3.3.0-alpha-0df1b98.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 +18 -3
- package/index.js +232 -173
- package/index.mjs +234 -175
- package/package.json +10 -3
- package/rules/graphql-js-validation.d.ts +2 -5
- package/rules/index.d.ts +1 -3
- package/rules/require-id-when-available.d.ts +2 -2
- 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';
|
@@ -2809,7 +2860,8 @@ const convertNode = (typeInfo) => (node, key, parent) => {
|
|
2809
2860
|
}
|
2810
2861
|
};
|
2811
2862
|
|
2812
|
-
const
|
2863
|
+
const RULE_ID$2 = 'require-id-when-available';
|
2864
|
+
const MESSAGE_ID = 'REQUIRE_ID_WHEN_AVAILABLE';
|
2813
2865
|
const DEFAULT_ID_FIELD_NAME = 'id';
|
2814
2866
|
const rule$j = {
|
2815
2867
|
meta: {
|
@@ -2817,7 +2869,7 @@ const rule$j = {
|
|
2817
2869
|
docs: {
|
2818
2870
|
category: 'Operations',
|
2819
2871
|
description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
|
2820
|
-
url:
|
2872
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
|
2821
2873
|
requiresSchema: true,
|
2822
2874
|
requiresSiblings: true,
|
2823
2875
|
examples: [
|
@@ -2831,7 +2883,7 @@ const rule$j = {
|
|
2831
2883
|
}
|
2832
2884
|
|
2833
2885
|
# Query
|
2834
|
-
query
|
2886
|
+
query {
|
2835
2887
|
user {
|
2836
2888
|
name
|
2837
2889
|
}
|
@@ -2848,7 +2900,7 @@ const rule$j = {
|
|
2848
2900
|
}
|
2849
2901
|
|
2850
2902
|
# Query
|
2851
|
-
query
|
2903
|
+
query {
|
2852
2904
|
user {
|
2853
2905
|
id
|
2854
2906
|
name
|
@@ -2860,9 +2912,22 @@ const rule$j = {
|
|
2860
2912
|
recommended: true,
|
2861
2913
|
},
|
2862
2914
|
messages: {
|
2863
|
-
[
|
2915
|
+
[MESSAGE_ID]: [
|
2916
|
+
`Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!`,
|
2917
|
+
`If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.`,
|
2918
|
+
].join('\n'),
|
2864
2919
|
},
|
2865
2920
|
schema: {
|
2921
|
+
definitions: {
|
2922
|
+
asString: {
|
2923
|
+
type: 'string',
|
2924
|
+
},
|
2925
|
+
asArray: {
|
2926
|
+
type: 'array',
|
2927
|
+
minItems: 1,
|
2928
|
+
uniqueItems: true,
|
2929
|
+
},
|
2930
|
+
},
|
2866
2931
|
type: 'array',
|
2867
2932
|
maxItems: 1,
|
2868
2933
|
items: {
|
@@ -2870,7 +2935,7 @@ const rule$j = {
|
|
2870
2935
|
additionalProperties: false,
|
2871
2936
|
properties: {
|
2872
2937
|
fieldName: {
|
2873
|
-
|
2938
|
+
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asArray' }],
|
2874
2939
|
default: DEFAULT_ID_FIELD_NAME,
|
2875
2940
|
},
|
2876
2941
|
},
|
@@ -2878,70 +2943,64 @@ const rule$j = {
|
|
2878
2943
|
},
|
2879
2944
|
},
|
2880
2945
|
create(context) {
|
2946
|
+
requireGraphQLSchemaFromContext(RULE_ID$2, context);
|
2947
|
+
const siblings = requireSiblingsOperations(RULE_ID$2, context);
|
2948
|
+
const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
|
2949
|
+
const idNames = asArray(fieldName);
|
2950
|
+
const isFound = (s) => s.kind === Kind.FIELD && idNames.includes(s.name.value);
|
2951
|
+
// Skip check selections in FragmentDefinition
|
2952
|
+
const selector = 'OperationDefinition SelectionSet[parent.kind!=OperationDefinition]';
|
2881
2953
|
return {
|
2882
|
-
|
2883
|
-
var _a
|
2884
|
-
|
2885
|
-
|
2886
|
-
const fieldName = (context.options[0] || {}).fieldName || DEFAULT_ID_FIELD_NAME;
|
2887
|
-
if (!node.selections || node.selections.length === 0) {
|
2954
|
+
[selector](node) {
|
2955
|
+
var _a;
|
2956
|
+
const typeInfo = node.typeInfo();
|
2957
|
+
if (!typeInfo.gqlType) {
|
2888
2958
|
return;
|
2889
2959
|
}
|
2890
|
-
const
|
2891
|
-
|
2892
|
-
|
2893
|
-
|
2894
|
-
|
2895
|
-
|
2896
|
-
|
2897
|
-
|
2898
|
-
|
2899
|
-
|
2900
|
-
|
2901
|
-
|
2902
|
-
|
2903
|
-
|
2904
|
-
|
2905
|
-
|
2906
|
-
|
2907
|
-
|
2908
|
-
|
2909
|
-
|
2910
|
-
|
2911
|
-
|
2912
|
-
|
2913
|
-
|
2914
|
-
|
2915
|
-
}
|
2916
|
-
}
|
2917
|
-
const { parent } = node;
|
2918
|
-
const hasIdFieldInInterfaceSelectionSet = parent &&
|
2919
|
-
parent.kind === 'InlineFragment' &&
|
2920
|
-
parent.parent &&
|
2921
|
-
parent.parent.kind === 'SelectionSet' &&
|
2922
|
-
parent.parent.selections.some(s => s.kind === 'Field' && s.name.value === fieldName);
|
2923
|
-
if (!found && !hasIdFieldInInterfaceSelectionSet) {
|
2924
|
-
context.report({
|
2925
|
-
loc: {
|
2926
|
-
start: {
|
2927
|
-
line: node.loc.start.line,
|
2928
|
-
column: node.loc.start.column - 1,
|
2929
|
-
},
|
2930
|
-
end: {
|
2931
|
-
line: node.loc.end.line,
|
2932
|
-
column: node.loc.end.column - 1,
|
2933
|
-
},
|
2934
|
-
},
|
2935
|
-
messageId: REQUIRE_ID_WHEN_AVAILABLE,
|
2936
|
-
data: {
|
2937
|
-
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${Array.from(checkedFragmentSpreads).join(', ')})`,
|
2938
|
-
fieldName,
|
2939
|
-
},
|
2940
|
-
});
|
2960
|
+
const rawType = getBaseType(typeInfo.gqlType);
|
2961
|
+
const isObjectType = rawType instanceof GraphQLObjectType;
|
2962
|
+
const isInterfaceType = rawType instanceof GraphQLInterfaceType;
|
2963
|
+
if (!isObjectType && !isInterfaceType) {
|
2964
|
+
return;
|
2965
|
+
}
|
2966
|
+
const fields = rawType.getFields();
|
2967
|
+
const hasIdFieldInType = idNames.some(name => fields[name]);
|
2968
|
+
if (!hasIdFieldInType) {
|
2969
|
+
return;
|
2970
|
+
}
|
2971
|
+
const checkedFragmentSpreads = new Set();
|
2972
|
+
for (const selection of node.selections) {
|
2973
|
+
if (isFound(selection)) {
|
2974
|
+
return;
|
2975
|
+
}
|
2976
|
+
if (selection.kind === Kind.INLINE_FRAGMENT && selection.selectionSet.selections.some(isFound)) {
|
2977
|
+
return;
|
2978
|
+
}
|
2979
|
+
if (selection.kind === Kind.FRAGMENT_SPREAD) {
|
2980
|
+
const [foundSpread] = siblings.getFragment(selection.name.value);
|
2981
|
+
if (foundSpread) {
|
2982
|
+
checkedFragmentSpreads.add(foundSpread.document.name.value);
|
2983
|
+
if (foundSpread.document.selectionSet.selections.some(isFound)) {
|
2984
|
+
return;
|
2941
2985
|
}
|
2942
2986
|
}
|
2943
2987
|
}
|
2944
2988
|
}
|
2989
|
+
const { parent } = node;
|
2990
|
+
const hasIdFieldInInterfaceSelectionSet = (parent === null || parent === void 0 ? void 0 : parent.kind) === Kind.INLINE_FRAGMENT &&
|
2991
|
+
((_a = parent.parent) === null || _a === void 0 ? void 0 : _a.kind) === Kind.SELECTION_SET &&
|
2992
|
+
parent.parent.selections.some(isFound);
|
2993
|
+
if (hasIdFieldInInterfaceSelectionSet) {
|
2994
|
+
return;
|
2995
|
+
}
|
2996
|
+
context.report({
|
2997
|
+
loc: getLocation(node.loc),
|
2998
|
+
messageId: MESSAGE_ID,
|
2999
|
+
data: {
|
3000
|
+
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
|
3001
|
+
fieldName: idNames.map(name => `"${name}"`).join(' or '),
|
3002
|
+
},
|
3003
|
+
});
|
2945
3004
|
},
|
2946
3005
|
};
|
2947
3006
|
},
|
@@ -3032,7 +3091,7 @@ const rule$k = {
|
|
3032
3091
|
// eslint-disable-next-line no-console
|
3033
3092
|
console.warn(`Rule "selection-set-depth" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
|
3034
3093
|
}
|
3035
|
-
const maxDepth = context.options[0]
|
3094
|
+
const { maxDepth } = context.options[0];
|
3036
3095
|
const ignore = context.options[0].ignore || [];
|
3037
3096
|
const checkFn = depthLimit(maxDepth, { ignore });
|
3038
3097
|
return {
|