@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.mjs CHANGED
@@ -1,8 +1,7 @@
1
- import { Kind, validate, TokenKind, isScalarType, isNonNullType, isListType, isObjectType as isObjectType$1, visit, visitWithTypeInfo, GraphQLObjectType, GraphQLInterfaceType, TypeInfo, isInterfaceType, Source, GraphQLError } from 'graphql';
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, join, extname, basename, relative, resolve } from 'path';
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 extractRuleName(stack) {
325
- const match = (stack || '').match(/validation[/\\]rules[/\\](.*?)\.js:/) || [];
326
- return match[1] || null;
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
- function validateDoc(sourceNode, context, schema, documentNode, rules, ruleName = null) {
329
- var _a;
330
- if (((_a = documentNode === null || documentNode === void 0 ? void 0 : documentNode.definitions) === null || _a === void 0 ? void 0 : _a.length) > 0) {
331
- try {
332
- const validationErrors = schema ? validate(schema, documentNode, rules) : validateSDL(documentNode, null, rules);
333
- for (const error of validationErrors) {
334
- const validateRuleName = ruleName || `[${extractRuleName(error.stack)}]`;
335
- context.report({
336
- loc: getLocation({ start: error.locations[0] }),
337
- message: ruleName ? error.message : `${validateRuleName} ${error.message}`,
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
- catch (e) {
342
- context.report({
343
- node: sourceNode,
344
- message: e.message,
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 = (name, ruleName, docs, getDocumentNode) => {
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 (e) {
417
+ catch (_a) {
360
418
  try {
361
419
  ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
362
420
  }
363
- catch (e) {
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
- [name]: {
426
+ [ruleId]: {
370
427
  meta: {
371
428
  docs: {
372
429
  recommended: true,
373
430
  ...docs,
374
431
  graphQLJSRuleName: ruleName,
375
- requiresSchema,
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 "${name}" depends on a GraphQL validation rule ("${ruleName}") but it's not available in the "graphql-js" version you are using. Skipping...`);
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(name, context) : null;
389
- let documentNode;
390
- const isRealFile = existsSync(context.getFilename());
391
- if (isRealFile && getDocumentNode) {
392
- documentNode = getDocumentNode(context);
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 (fragment not defined in the document)',
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 (existing import to UserFields fragment)',
509
+ title: 'Correct (`UserFields` fragment located in a separate file)',
457
510
  code: /* GraphQL */ `
458
- #import '../UserFields.gql'
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
- # Will give false positive error 'Unknown fragment "UserFields"'
487
- \${USER_FIELDS}
488
- \``,
519
+ # user-fields.gql
520
+ fragment UserFields on User {
521
+ id
522
+ }
523
+ `,
489
524
  },
490
525
  ],
491
- }, importFiles), validationToRule('known-type-names', 'KnownTypeNames', {
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
- }, importFiles), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
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('no-unused-fragments', context);
513
- const documents = [...siblings.getOperations(), ...siblings.getFragments()]
514
- .filter(({ document }) => isGraphQLImportFile(document.loc.source.body))
515
- .map(({ filePath, document }) => ({
516
- filePath,
517
- code: document.loc.source.body,
518
- }));
519
- const getParentNode = (filePath) => {
520
- for (const { filePath: docFilePath, code } of documents) {
521
- const isFileImported = code
522
- .split('\n')
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
- return null;
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
- }, importFiles), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
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 REQUIRE_ID_WHEN_AVAILABLE = 'REQUIRE_ID_WHEN_AVAILABLE';
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: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/require-id-when-available.md',
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 user {
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 user {
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
- [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 }}".`,
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
- type: 'string',
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
- SelectionSet(node) {
2883
- var _a, _b;
2884
- requireGraphQLSchemaFromContext('require-id-when-available', context);
2885
- const siblings = requireSiblingsOperations('require-id-when-available', context);
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 typeInfo = node.typeInfo();
2891
- if (typeInfo && typeInfo.gqlType) {
2892
- const rawType = getBaseType(typeInfo.gqlType);
2893
- if (rawType instanceof GraphQLObjectType || rawType instanceof GraphQLInterfaceType) {
2894
- const fields = rawType.getFields();
2895
- const hasIdFieldInType = !!fields[fieldName];
2896
- const checkedFragmentSpreads = new Set();
2897
- if (hasIdFieldInType) {
2898
- let found = false;
2899
- for (const selection of node.selections) {
2900
- if (selection.kind === 'Field' && selection.name.value === fieldName) {
2901
- found = true;
2902
- }
2903
- else if (selection.kind === 'InlineFragment') {
2904
- found = (((_a = selection.selectionSet) === null || _a === void 0 ? void 0 : _a.selections) || []).some(s => s.kind === 'Field' && s.name.value === fieldName);
2905
- }
2906
- else if (selection.kind === 'FragmentSpread') {
2907
- const foundSpread = siblings.getFragment(selection.name.value);
2908
- if (foundSpread[0]) {
2909
- checkedFragmentSpreads.add(foundSpread[0].document.name.value);
2910
- found = (((_b = foundSpread[0].document.selectionSet) === null || _b === void 0 ? void 0 : _b.selections) || []).some(s => s.kind === 'Field' && s.name.value === fieldName);
2911
- }
2912
- }
2913
- if (found) {
2914
- break;
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].maxDepth;
3094
+ const { maxDepth } = context.options[0];
3036
3095
  const ignore = context.options[0].ignore || [];
3037
3096
  const checkFn = depthLimit(maxDepth, { ignore });
3038
3097
  return {