@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/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 extractRuleName(stack) {
331
- const match = (stack || '').match(/validation[/\\]rules[/\\](.*?)\.js:/) || [];
332
- return match[1] || null;
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
- function validateDoc(sourceNode, context, schema, documentNode, rules, ruleName = null) {
335
- var _a;
336
- if (((_a = documentNode === null || documentNode === void 0 ? void 0 : documentNode.definitions) === null || _a === void 0 ? void 0 : _a.length) > 0) {
337
- try {
338
- const validationErrors = schema ? graphql.validate(schema, documentNode, rules) : validate.validateSDL(documentNode, null, rules);
339
- for (const error of validationErrors) {
340
- const validateRuleName = ruleName || `[${extractRuleName(error.stack)}]`;
341
- context.report({
342
- loc: getLocation({ start: error.locations[0] }),
343
- message: ruleName ? error.message : `${validateRuleName} ${error.message}`,
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
- catch (e) {
348
- context.report({
349
- node: sourceNode,
350
- message: e.message,
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 = (name, ruleName, docs, getDocumentNode) => {
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 (e) {
423
+ catch (_a) {
366
424
  try {
367
425
  ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
368
426
  }
369
- catch (e) {
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
- [name]: {
432
+ [ruleId]: {
376
433
  meta: {
377
434
  docs: {
378
435
  recommended: true,
379
436
  ...docs,
380
437
  graphQLJSRuleName: ruleName,
381
- requiresSchema,
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 "${name}" depends on a GraphQL validation rule ("${ruleName}") but it's not available in the "graphql-js" version you are using. Skipping...`);
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(name, context) : null;
395
- let documentNode;
396
- const isRealFile = fs.existsSync(context.getFilename());
397
- if (isRealFile && getDocumentNode) {
398
- documentNode = getDocumentNode(context);
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 (fragment not defined in the document)',
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 (existing import to UserFields fragment)',
515
+ title: 'Correct (`UserFields` fragment located in a separate file)',
463
516
  code: /* GraphQL */ `
464
- #import '../UserFields.gql'
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
- # Will give false positive error 'Unknown fragment "UserFields"'
493
- \${USER_FIELDS}
494
- \``,
525
+ # user-fields.gql
526
+ fragment UserFields on User {
527
+ id
528
+ }
529
+ `,
495
530
  },
496
531
  ],
497
- }, importFiles), validationToRule('known-type-names', 'KnownTypeNames', {
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
- }, importFiles), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
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('no-unused-fragments', context);
519
- const documents = [...siblings.getOperations(), ...siblings.getFragments()]
520
- .filter(({ document }) => isGraphQLImportFile(document.loc.source.body))
521
- .map(({ filePath, document }) => ({
522
- filePath,
523
- code: document.loc.source.body,
524
- }));
525
- const getParentNode = (filePath) => {
526
- for (const { filePath: docFilePath, code } of documents) {
527
- const isFileImported = code
528
- .split('\n')
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
- return null;
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
- }, importFiles), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
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';
@@ -2815,7 +2866,8 @@ const convertNode = (typeInfo) => (node, key, parent) => {
2815
2866
  }
2816
2867
  };
2817
2868
 
2818
- const REQUIRE_ID_WHEN_AVAILABLE = 'REQUIRE_ID_WHEN_AVAILABLE';
2869
+ const RULE_ID$2 = 'require-id-when-available';
2870
+ const MESSAGE_ID = 'REQUIRE_ID_WHEN_AVAILABLE';
2819
2871
  const DEFAULT_ID_FIELD_NAME = 'id';
2820
2872
  const rule$j = {
2821
2873
  meta: {
@@ -2823,7 +2875,7 @@ const rule$j = {
2823
2875
  docs: {
2824
2876
  category: 'Operations',
2825
2877
  description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
2826
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/require-id-when-available.md',
2878
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
2827
2879
  requiresSchema: true,
2828
2880
  requiresSiblings: true,
2829
2881
  examples: [
@@ -2837,7 +2889,7 @@ const rule$j = {
2837
2889
  }
2838
2890
 
2839
2891
  # Query
2840
- query user {
2892
+ query {
2841
2893
  user {
2842
2894
  name
2843
2895
  }
@@ -2854,7 +2906,7 @@ const rule$j = {
2854
2906
  }
2855
2907
 
2856
2908
  # Query
2857
- query user {
2909
+ query {
2858
2910
  user {
2859
2911
  id
2860
2912
  name
@@ -2866,9 +2918,22 @@ const rule$j = {
2866
2918
  recommended: true,
2867
2919
  },
2868
2920
  messages: {
2869
- [REQUIRE_ID_WHEN_AVAILABLE]: `Field "{{ fieldName }}" must be selected when it's available on a type. Please make sure to include it in your selection set!\nIf you are using fragments, make sure that all used fragments {{ checkedFragments }} specifies the field "{{ fieldName }}".`,
2921
+ [MESSAGE_ID]: [
2922
+ `Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!`,
2923
+ `If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.`,
2924
+ ].join('\n'),
2870
2925
  },
2871
2926
  schema: {
2927
+ definitions: {
2928
+ asString: {
2929
+ type: 'string',
2930
+ },
2931
+ asArray: {
2932
+ type: 'array',
2933
+ minItems: 1,
2934
+ uniqueItems: true,
2935
+ },
2936
+ },
2872
2937
  type: 'array',
2873
2938
  maxItems: 1,
2874
2939
  items: {
@@ -2876,7 +2941,7 @@ const rule$j = {
2876
2941
  additionalProperties: false,
2877
2942
  properties: {
2878
2943
  fieldName: {
2879
- type: 'string',
2944
+ oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asArray' }],
2880
2945
  default: DEFAULT_ID_FIELD_NAME,
2881
2946
  },
2882
2947
  },
@@ -2884,70 +2949,64 @@ const rule$j = {
2884
2949
  },
2885
2950
  },
2886
2951
  create(context) {
2952
+ requireGraphQLSchemaFromContext(RULE_ID$2, context);
2953
+ const siblings = requireSiblingsOperations(RULE_ID$2, context);
2954
+ const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
2955
+ const idNames = utils.asArray(fieldName);
2956
+ const isFound = (s) => s.kind === graphql.Kind.FIELD && idNames.includes(s.name.value);
2957
+ // Skip check selections in FragmentDefinition
2958
+ const selector = 'OperationDefinition SelectionSet[parent.kind!=OperationDefinition]';
2887
2959
  return {
2888
- SelectionSet(node) {
2889
- var _a, _b;
2890
- requireGraphQLSchemaFromContext('require-id-when-available', context);
2891
- const siblings = requireSiblingsOperations('require-id-when-available', context);
2892
- const fieldName = (context.options[0] || {}).fieldName || DEFAULT_ID_FIELD_NAME;
2893
- if (!node.selections || node.selections.length === 0) {
2960
+ [selector](node) {
2961
+ var _a;
2962
+ const typeInfo = node.typeInfo();
2963
+ if (!typeInfo.gqlType) {
2894
2964
  return;
2895
2965
  }
2896
- const typeInfo = node.typeInfo();
2897
- if (typeInfo && typeInfo.gqlType) {
2898
- const rawType = getBaseType(typeInfo.gqlType);
2899
- if (rawType instanceof graphql.GraphQLObjectType || rawType instanceof graphql.GraphQLInterfaceType) {
2900
- const fields = rawType.getFields();
2901
- const hasIdFieldInType = !!fields[fieldName];
2902
- const checkedFragmentSpreads = new Set();
2903
- if (hasIdFieldInType) {
2904
- let found = false;
2905
- for (const selection of node.selections) {
2906
- if (selection.kind === 'Field' && selection.name.value === fieldName) {
2907
- found = true;
2908
- }
2909
- else if (selection.kind === 'InlineFragment') {
2910
- found = (((_a = selection.selectionSet) === null || _a === void 0 ? void 0 : _a.selections) || []).some(s => s.kind === 'Field' && s.name.value === fieldName);
2911
- }
2912
- else if (selection.kind === 'FragmentSpread') {
2913
- const foundSpread = siblings.getFragment(selection.name.value);
2914
- if (foundSpread[0]) {
2915
- checkedFragmentSpreads.add(foundSpread[0].document.name.value);
2916
- found = (((_b = foundSpread[0].document.selectionSet) === null || _b === void 0 ? void 0 : _b.selections) || []).some(s => s.kind === 'Field' && s.name.value === fieldName);
2917
- }
2918
- }
2919
- if (found) {
2920
- break;
2921
- }
2922
- }
2923
- const { parent } = node;
2924
- const hasIdFieldInInterfaceSelectionSet = parent &&
2925
- parent.kind === 'InlineFragment' &&
2926
- parent.parent &&
2927
- parent.parent.kind === 'SelectionSet' &&
2928
- parent.parent.selections.some(s => s.kind === 'Field' && s.name.value === fieldName);
2929
- if (!found && !hasIdFieldInInterfaceSelectionSet) {
2930
- context.report({
2931
- loc: {
2932
- start: {
2933
- line: node.loc.start.line,
2934
- column: node.loc.start.column - 1,
2935
- },
2936
- end: {
2937
- line: node.loc.end.line,
2938
- column: node.loc.end.column - 1,
2939
- },
2940
- },
2941
- messageId: REQUIRE_ID_WHEN_AVAILABLE,
2942
- data: {
2943
- checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${Array.from(checkedFragmentSpreads).join(', ')})`,
2944
- fieldName,
2945
- },
2946
- });
2966
+ const rawType = getBaseType(typeInfo.gqlType);
2967
+ const isObjectType = rawType instanceof graphql.GraphQLObjectType;
2968
+ const isInterfaceType = rawType instanceof graphql.GraphQLInterfaceType;
2969
+ if (!isObjectType && !isInterfaceType) {
2970
+ return;
2971
+ }
2972
+ const fields = rawType.getFields();
2973
+ const hasIdFieldInType = idNames.some(name => fields[name]);
2974
+ if (!hasIdFieldInType) {
2975
+ return;
2976
+ }
2977
+ const checkedFragmentSpreads = new Set();
2978
+ for (const selection of node.selections) {
2979
+ if (isFound(selection)) {
2980
+ return;
2981
+ }
2982
+ if (selection.kind === graphql.Kind.INLINE_FRAGMENT && selection.selectionSet.selections.some(isFound)) {
2983
+ return;
2984
+ }
2985
+ if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
2986
+ const [foundSpread] = siblings.getFragment(selection.name.value);
2987
+ if (foundSpread) {
2988
+ checkedFragmentSpreads.add(foundSpread.document.name.value);
2989
+ if (foundSpread.document.selectionSet.selections.some(isFound)) {
2990
+ return;
2947
2991
  }
2948
2992
  }
2949
2993
  }
2950
2994
  }
2995
+ const { parent } = node;
2996
+ const hasIdFieldInInterfaceSelectionSet = (parent === null || parent === void 0 ? void 0 : parent.kind) === graphql.Kind.INLINE_FRAGMENT &&
2997
+ ((_a = parent.parent) === null || _a === void 0 ? void 0 : _a.kind) === graphql.Kind.SELECTION_SET &&
2998
+ parent.parent.selections.some(isFound);
2999
+ if (hasIdFieldInInterfaceSelectionSet) {
3000
+ return;
3001
+ }
3002
+ context.report({
3003
+ loc: getLocation(node.loc),
3004
+ messageId: MESSAGE_ID,
3005
+ data: {
3006
+ checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
3007
+ fieldName: idNames.map(name => `"${name}"`).join(' or '),
3008
+ },
3009
+ });
2951
3010
  },
2952
3011
  };
2953
3012
  },
@@ -3038,7 +3097,7 @@ const rule$k = {
3038
3097
  // eslint-disable-next-line no-console
3039
3098
  console.warn(`Rule "selection-set-depth" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
3040
3099
  }
3041
- const maxDepth = context.options[0].maxDepth;
3100
+ const { maxDepth } = context.options[0];
3042
3101
  const ignore = context.options[0].ignore || [];
3043
3102
  const checkFn = depthLimit(maxDepth, { ignore });
3044
3103
  return {