@graphql-eslint/eslint-plugin 3.2.0-alpha-001cd75.0 → 3.2.0-alpha-4ca7218.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/index.js +211 -184
- package/index.mjs +213 -186
- package/package.json +8 -2
- package/rules/graphql-js-validation.d.ts +2 -5
- package/rules/index.d.ts +129 -2
- package/rules/match-document-filename.d.ts +3 -3
- package/rules/naming-convention.d.ts +1 -1
- package/types.d.ts +2 -2
- package/utils.d.ts +7 -1
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');
|
@@ -286,6 +285,14 @@ const TYPES_KINDS = [
|
|
286
285
|
graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
287
286
|
graphql.Kind.UNION_TYPE_DEFINITION,
|
288
287
|
];
|
288
|
+
var CaseStyle;
|
289
|
+
(function (CaseStyle) {
|
290
|
+
CaseStyle["camelCase"] = "camelCase";
|
291
|
+
CaseStyle["pascalCase"] = "PascalCase";
|
292
|
+
CaseStyle["snakeCase"] = "snake_case";
|
293
|
+
CaseStyle["upperCase"] = "UPPER_CASE";
|
294
|
+
CaseStyle["kebabCase"] = "kebab-case";
|
295
|
+
})(CaseStyle || (CaseStyle = {}));
|
289
296
|
const pascalCase = (str) => lowerCase(str)
|
290
297
|
.split(' ')
|
291
298
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
@@ -296,15 +303,15 @@ const camelCase = (str) => {
|
|
296
303
|
};
|
297
304
|
const convertCase = (style, str) => {
|
298
305
|
switch (style) {
|
299
|
-
case
|
306
|
+
case CaseStyle.camelCase:
|
300
307
|
return camelCase(str);
|
301
|
-
case
|
308
|
+
case CaseStyle.pascalCase:
|
302
309
|
return pascalCase(str);
|
303
|
-
case
|
310
|
+
case CaseStyle.snakeCase:
|
304
311
|
return lowerCase(str).replace(/ /g, '_');
|
305
|
-
case
|
312
|
+
case CaseStyle.upperCase:
|
306
313
|
return lowerCase(str).replace(/ /g, '_').toUpperCase();
|
307
|
-
case
|
314
|
+
case CaseStyle.kebabCase:
|
308
315
|
return lowerCase(str).replace(/ /g, '-');
|
309
316
|
}
|
310
317
|
};
|
@@ -327,59 +334,101 @@ function getLocation(loc, fieldName = '', offset) {
|
|
327
334
|
};
|
328
335
|
}
|
329
336
|
|
330
|
-
function
|
331
|
-
|
332
|
-
|
337
|
+
function validateDocument(sourceNode, context, schema, documentNode, rule) {
|
338
|
+
if (documentNode.definitions.length === 0) {
|
339
|
+
return;
|
340
|
+
}
|
341
|
+
try {
|
342
|
+
const validationErrors = schema
|
343
|
+
? graphql.validate(schema, documentNode, [rule])
|
344
|
+
: validate.validateSDL(documentNode, null, [rule]);
|
345
|
+
for (const error of validationErrors) {
|
346
|
+
context.report({
|
347
|
+
loc: getLocation({ start: error.locations[0] }),
|
348
|
+
message: error.message,
|
349
|
+
});
|
350
|
+
}
|
351
|
+
}
|
352
|
+
catch (e) {
|
353
|
+
context.report({
|
354
|
+
node: sourceNode,
|
355
|
+
message: e.message,
|
356
|
+
});
|
357
|
+
}
|
333
358
|
}
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
359
|
+
const getFragmentDefsAndFragmentSpreads = (schema, node) => {
|
360
|
+
const typeInfo = new graphql.TypeInfo(schema);
|
361
|
+
const fragmentDefs = new Set();
|
362
|
+
const fragmentSpreads = new Set();
|
363
|
+
const visitor = graphql.visitWithTypeInfo(typeInfo, {
|
364
|
+
FragmentDefinition(node) {
|
365
|
+
fragmentDefs.add(`${node.name.value}:${node.typeCondition.name.value}`);
|
366
|
+
},
|
367
|
+
FragmentSpread(node) {
|
368
|
+
const parentType = typeInfo.getParentType();
|
369
|
+
if (parentType) {
|
370
|
+
fragmentSpreads.add(`${node.name.value}:${parentType.name}`);
|
371
|
+
}
|
372
|
+
},
|
373
|
+
});
|
374
|
+
graphql.visit(node, visitor);
|
375
|
+
return { fragmentDefs, fragmentSpreads };
|
376
|
+
};
|
377
|
+
const getMissingFragments = (schema, node) => {
|
378
|
+
const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(schema, node);
|
379
|
+
return [...fragmentSpreads].filter(name => !fragmentDefs.has(name));
|
380
|
+
};
|
381
|
+
const handleMissingFragments = ({ ruleId, context, schema, node }) => {
|
382
|
+
const missingFragments = getMissingFragments(schema, node);
|
383
|
+
if (missingFragments.length > 0) {
|
384
|
+
const siblings = requireSiblingsOperations(ruleId, context);
|
385
|
+
const fragmentsToAdd = [];
|
386
|
+
for (const missingFragment of missingFragments) {
|
387
|
+
const [fragmentName, fragmentTypeName] = missingFragment.split(':');
|
388
|
+
const [foundFragment] = siblings
|
389
|
+
.getFragment(fragmentName)
|
390
|
+
.map(source => source.document)
|
391
|
+
.filter(fragment => fragment.typeCondition.name.value === fragmentTypeName);
|
392
|
+
if (foundFragment) {
|
393
|
+
fragmentsToAdd.push(foundFragment);
|
345
394
|
}
|
346
395
|
}
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
396
|
+
if (fragmentsToAdd.length > 0) {
|
397
|
+
// recall fn to make sure to add fragments inside fragments
|
398
|
+
return handleMissingFragments({
|
399
|
+
ruleId,
|
400
|
+
context,
|
401
|
+
schema,
|
402
|
+
node: {
|
403
|
+
kind: graphql.Kind.DOCUMENT,
|
404
|
+
definitions: [...node.definitions, ...fragmentsToAdd],
|
405
|
+
},
|
351
406
|
});
|
352
407
|
}
|
353
408
|
}
|
354
|
-
|
355
|
-
const isGraphQLImportFile = rawSDL => {
|
356
|
-
const trimmedRawSDL = rawSDL.trimLeft();
|
357
|
-
return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
|
409
|
+
return node;
|
358
410
|
};
|
359
|
-
const validationToRule = (
|
360
|
-
var _a;
|
411
|
+
const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
|
361
412
|
let ruleFn = null;
|
362
413
|
try {
|
363
414
|
ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
|
364
415
|
}
|
365
|
-
catch (
|
416
|
+
catch (_a) {
|
366
417
|
try {
|
367
418
|
ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
|
368
419
|
}
|
369
|
-
catch (
|
420
|
+
catch (_b) {
|
370
421
|
ruleFn = require('graphql/validation')[`${ruleName}Rule`];
|
371
422
|
}
|
372
423
|
}
|
373
|
-
const requiresSchema = (_a = docs.requiresSchema) !== null && _a !== void 0 ? _a : true;
|
374
424
|
return {
|
375
|
-
[
|
425
|
+
[ruleId]: {
|
376
426
|
meta: {
|
377
427
|
docs: {
|
378
428
|
recommended: true,
|
379
429
|
...docs,
|
380
430
|
graphQLJSRuleName: ruleName,
|
381
|
-
|
382
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${name}.md`,
|
431
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
|
383
432
|
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
433
|
},
|
385
434
|
},
|
@@ -388,56 +437,53 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
|
|
388
437
|
Document(node) {
|
389
438
|
if (!ruleFn) {
|
390
439
|
// eslint-disable-next-line no-console
|
391
|
-
console.warn(`You rule "${
|
440
|
+
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
441
|
return;
|
393
442
|
}
|
394
|
-
const schema = requiresSchema ? requireGraphQLSchemaFromContext(
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
}
|
400
|
-
validateDoc(node, context, schema, documentNode || node.rawNode(), [ruleFn], ruleName);
|
443
|
+
const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
|
444
|
+
const documentNode = getDocumentNode
|
445
|
+
? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
|
446
|
+
: node.rawNode();
|
447
|
+
validateDocument(node, context, schema, documentNode, ruleFn);
|
401
448
|
},
|
402
449
|
};
|
403
450
|
},
|
404
451
|
},
|
405
452
|
};
|
406
453
|
};
|
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
454
|
const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
|
416
455
|
category: 'Operations',
|
417
456
|
description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`,
|
457
|
+
requiresSchema: true,
|
418
458
|
}), validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', {
|
419
459
|
category: 'Operations',
|
420
460
|
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`.',
|
461
|
+
requiresSchema: true,
|
421
462
|
}), validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', {
|
422
463
|
category: 'Operations',
|
423
464
|
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.`,
|
465
|
+
requiresSchema: true,
|
424
466
|
}), validationToRule('known-argument-names', 'KnownArgumentNames', {
|
425
467
|
category: ['Schema', 'Operations'],
|
426
468
|
description: `A GraphQL field is only valid if all supplied arguments are defined by that field.`,
|
469
|
+
requiresSchema: true,
|
427
470
|
}), validationToRule('known-directives', 'KnownDirectives', {
|
428
471
|
category: ['Schema', 'Operations'],
|
429
472
|
description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
|
473
|
+
requiresSchema: true,
|
430
474
|
}), validationToRule('known-fragment-names', 'KnownFragmentNames', {
|
431
475
|
category: 'Operations',
|
432
476
|
description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
|
477
|
+
requiresSchema: true,
|
478
|
+
requiresSiblings: true,
|
433
479
|
examples: [
|
434
480
|
{
|
435
|
-
title: 'Incorrect
|
481
|
+
title: 'Incorrect',
|
436
482
|
code: /* GraphQL */ `
|
437
483
|
query {
|
438
484
|
user {
|
439
485
|
id
|
440
|
-
...UserFields
|
486
|
+
...UserFields # fragment not defined in the document
|
441
487
|
}
|
442
488
|
}
|
443
489
|
`,
|
@@ -459,153 +505,151 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
|
|
459
505
|
`,
|
460
506
|
},
|
461
507
|
{
|
462
|
-
title: 'Correct (
|
508
|
+
title: 'Correct (`UserFields` fragment located in a separate file)',
|
463
509
|
code: /* GraphQL */ `
|
464
|
-
#
|
465
|
-
|
510
|
+
# user.gql
|
466
511
|
query {
|
467
512
|
user {
|
468
513
|
id
|
469
514
|
...UserFields
|
470
515
|
}
|
471
516
|
}
|
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
517
|
|
492
|
-
|
493
|
-
|
494
|
-
|
518
|
+
# user-fields.gql
|
519
|
+
fragment UserFields on User {
|
520
|
+
id
|
521
|
+
}
|
522
|
+
`,
|
495
523
|
},
|
496
524
|
],
|
497
|
-
},
|
525
|
+
}, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
|
498
526
|
category: ['Schema', 'Operations'],
|
499
527
|
description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
|
528
|
+
requiresSchema: true,
|
500
529
|
}), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
|
501
530
|
category: 'Operations',
|
502
531
|
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.`,
|
532
|
+
requiresSchema: true,
|
503
533
|
}), validationToRule('lone-schema-definition', 'LoneSchemaDefinition', {
|
504
534
|
category: 'Schema',
|
505
535
|
description: `A GraphQL document is only valid if it contains only one schema definition.`,
|
506
|
-
requiresSchema: false,
|
507
536
|
}), validationToRule('no-fragment-cycles', 'NoFragmentCycles', {
|
508
537
|
category: 'Operations',
|
509
538
|
description: `A GraphQL fragment is only valid when it does not have cycles in fragments usage.`,
|
539
|
+
requiresSchema: true,
|
510
540
|
}), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
|
511
541
|
category: 'Operations',
|
512
542
|
description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`,
|
513
|
-
|
543
|
+
requiresSchema: true,
|
544
|
+
requiresSiblings: true,
|
545
|
+
}, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
|
514
546
|
category: 'Operations',
|
515
547
|
description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
|
548
|
+
requiresSchema: true,
|
516
549
|
requiresSiblings: true,
|
517
|
-
}, context => {
|
518
|
-
const siblings = requireSiblingsOperations(
|
519
|
-
const
|
520
|
-
|
521
|
-
|
522
|
-
filePath
|
523
|
-
|
524
|
-
}));
|
525
|
-
const getParentNode = (
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
.filter(isGraphQLImportFile)
|
530
|
-
.map(line => _import.parseImportLine(line.replace('#', '')))
|
531
|
-
.some(o => filePath === path.join(path.dirname(docFilePath), o.from));
|
532
|
-
if (!isFileImported) {
|
533
|
-
continue;
|
534
|
-
}
|
535
|
-
// Import first file that import this file
|
536
|
-
const document = _import.processImport(docFilePath);
|
537
|
-
// Import most top file that import this file
|
538
|
-
return getParentNode(docFilePath) || document;
|
550
|
+
}, ({ ruleId, context, schema, node }) => {
|
551
|
+
const siblings = requireSiblingsOperations(ruleId, context);
|
552
|
+
const FilePathToDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce((map, { filePath, document }) => {
|
553
|
+
var _a;
|
554
|
+
(_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
|
555
|
+
map[filePath].push(document);
|
556
|
+
return map;
|
557
|
+
}, Object.create(null));
|
558
|
+
const getParentNode = (currentFilePath, node) => {
|
559
|
+
const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(schema, node);
|
560
|
+
if (fragmentDefs.size === 0) {
|
561
|
+
return node;
|
539
562
|
}
|
540
|
-
|
563
|
+
// skip iteration over documents for current filepath
|
564
|
+
delete FilePathToDocumentsMap[currentFilePath];
|
565
|
+
for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
|
566
|
+
const missingFragments = getMissingFragments(schema, {
|
567
|
+
kind: graphql.Kind.DOCUMENT,
|
568
|
+
definitions: documents,
|
569
|
+
});
|
570
|
+
const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
|
571
|
+
if (isCurrentFileImportFragment) {
|
572
|
+
return getParentNode(filePath, {
|
573
|
+
kind: graphql.Kind.DOCUMENT,
|
574
|
+
definitions: [...node.definitions, ...documents],
|
575
|
+
});
|
576
|
+
}
|
577
|
+
}
|
578
|
+
return node;
|
541
579
|
};
|
542
|
-
return getParentNode(context.getFilename());
|
580
|
+
return getParentNode(context.getFilename(), node);
|
543
581
|
}), validationToRule('no-unused-variables', 'NoUnusedVariables', {
|
544
582
|
category: 'Operations',
|
545
583
|
description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`,
|
546
|
-
|
584
|
+
requiresSchema: true,
|
585
|
+
requiresSiblings: true,
|
586
|
+
}, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
|
547
587
|
category: 'Operations',
|
548
588
|
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.`,
|
589
|
+
requiresSchema: true,
|
549
590
|
}), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
|
550
591
|
category: 'Operations',
|
551
592
|
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.`,
|
593
|
+
requiresSchema: true,
|
552
594
|
}), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
|
553
595
|
category: 'Schema',
|
554
596
|
description: `A type extension is only valid if the type is defined and has the same kind.`,
|
555
|
-
requiresSchema: false,
|
556
597
|
recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed
|
557
598
|
}), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
|
558
599
|
category: ['Schema', 'Operations'],
|
559
600
|
description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
|
601
|
+
requiresSchema: true,
|
560
602
|
}), validationToRule('scalar-leafs', 'ScalarLeafs', {
|
561
603
|
category: 'Operations',
|
562
604
|
description: `A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.`,
|
605
|
+
requiresSchema: true,
|
563
606
|
}), validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', {
|
564
607
|
category: 'Operations',
|
565
608
|
description: `A GraphQL subscription is valid only if it contains a single root field.`,
|
609
|
+
requiresSchema: true,
|
566
610
|
}), validationToRule('unique-argument-names', 'UniqueArgumentNames', {
|
567
611
|
category: 'Operations',
|
568
612
|
description: `A GraphQL field or directive is only valid if all supplied arguments are uniquely named.`,
|
613
|
+
requiresSchema: true,
|
569
614
|
}), validationToRule('unique-directive-names', 'UniqueDirectiveNames', {
|
570
615
|
category: 'Schema',
|
571
616
|
description: `A GraphQL document is only valid if all defined directives have unique names.`,
|
572
|
-
requiresSchema: false,
|
573
617
|
}), validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', {
|
574
618
|
category: ['Schema', 'Operations'],
|
575
619
|
description: `A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.`,
|
620
|
+
requiresSchema: true,
|
576
621
|
}), validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', {
|
577
622
|
category: 'Schema',
|
578
623
|
description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
|
579
|
-
requiresSchema: false,
|
580
624
|
recommended: false,
|
581
625
|
}), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
|
582
626
|
category: 'Schema',
|
583
627
|
description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
|
584
|
-
requiresSchema: false,
|
585
628
|
}), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
|
586
629
|
category: 'Operations',
|
587
630
|
description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`,
|
588
|
-
requiresSchema: false,
|
589
631
|
}), validationToRule('unique-operation-types', 'UniqueOperationTypes', {
|
590
632
|
category: 'Schema',
|
591
633
|
description: `A GraphQL document is only valid if it has only one type per operation.`,
|
592
|
-
requiresSchema: false,
|
593
634
|
}), validationToRule('unique-type-names', 'UniqueTypeNames', {
|
594
635
|
category: 'Schema',
|
595
636
|
description: `A GraphQL document is only valid if all defined types have unique names.`,
|
596
|
-
requiresSchema: false,
|
597
637
|
}), validationToRule('unique-variable-names', 'UniqueVariableNames', {
|
598
638
|
category: 'Operations',
|
599
639
|
description: `A GraphQL operation is only valid if all its variables are uniquely named.`,
|
640
|
+
requiresSchema: true,
|
600
641
|
}), validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', {
|
601
642
|
category: 'Operations',
|
602
643
|
description: `A GraphQL document is only valid if all value literals are of the type expected at their position.`,
|
644
|
+
requiresSchema: true,
|
603
645
|
}), validationToRule('variables-are-input-types', 'VariablesAreInputTypes', {
|
604
646
|
category: 'Operations',
|
605
647
|
description: `A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).`,
|
648
|
+
requiresSchema: true,
|
606
649
|
}), validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', {
|
607
650
|
category: 'Operations',
|
608
651
|
description: `Variables passed to field arguments conform to type.`,
|
652
|
+
requiresSchema: true,
|
609
653
|
}));
|
610
654
|
|
611
655
|
const ALPHABETIZE = 'ALPHABETIZE';
|
@@ -1039,7 +1083,13 @@ const rule$2 = {
|
|
1039
1083
|
const MATCH_EXTENSION = 'MATCH_EXTENSION';
|
1040
1084
|
const MATCH_STYLE = 'MATCH_STYLE';
|
1041
1085
|
const ACCEPTED_EXTENSIONS = ['.gql', '.graphql'];
|
1042
|
-
const CASE_STYLES = [
|
1086
|
+
const CASE_STYLES = [
|
1087
|
+
CaseStyle.camelCase,
|
1088
|
+
CaseStyle.pascalCase,
|
1089
|
+
CaseStyle.snakeCase,
|
1090
|
+
CaseStyle.upperCase,
|
1091
|
+
CaseStyle.kebabCase,
|
1092
|
+
];
|
1043
1093
|
const schemaOption = {
|
1044
1094
|
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
|
1045
1095
|
};
|
@@ -1063,7 +1113,7 @@ const rule$3 = {
|
|
1063
1113
|
},
|
1064
1114
|
{
|
1065
1115
|
title: 'Correct',
|
1066
|
-
usage: [{ query:
|
1116
|
+
usage: [{ query: CaseStyle.snakeCase }],
|
1067
1117
|
code: /* GraphQL */ `
|
1068
1118
|
# user_by_id.gql
|
1069
1119
|
query UserById {
|
@@ -1077,7 +1127,7 @@ const rule$3 = {
|
|
1077
1127
|
},
|
1078
1128
|
{
|
1079
1129
|
title: 'Correct',
|
1080
|
-
usage: [{ fragment: { style:
|
1130
|
+
usage: [{ fragment: { style: CaseStyle.kebabCase, suffix: '.fragment' } }],
|
1081
1131
|
code: /* GraphQL */ `
|
1082
1132
|
# user-fields.fragment.gql
|
1083
1133
|
fragment user_fields on User {
|
@@ -1088,7 +1138,7 @@ const rule$3 = {
|
|
1088
1138
|
},
|
1089
1139
|
{
|
1090
1140
|
title: 'Correct',
|
1091
|
-
usage: [{ mutation: { style:
|
1141
|
+
usage: [{ mutation: { style: CaseStyle.pascalCase, suffix: 'Mutation' } }],
|
1092
1142
|
code: /* GraphQL */ `
|
1093
1143
|
# DeleteUserMutation.gql
|
1094
1144
|
mutation DELETE_USER {
|
@@ -1108,7 +1158,7 @@ const rule$3 = {
|
|
1108
1158
|
},
|
1109
1159
|
{
|
1110
1160
|
title: 'Incorrect',
|
1111
|
-
usage: [{ query:
|
1161
|
+
usage: [{ query: CaseStyle.pascalCase }],
|
1112
1162
|
code: /* GraphQL */ `
|
1113
1163
|
# user-by-id.gql
|
1114
1164
|
query UserById {
|
@@ -1123,10 +1173,10 @@ const rule$3 = {
|
|
1123
1173
|
],
|
1124
1174
|
configOptions: [
|
1125
1175
|
{
|
1126
|
-
query:
|
1127
|
-
mutation:
|
1128
|
-
subscription:
|
1129
|
-
fragment:
|
1176
|
+
query: CaseStyle.kebabCase,
|
1177
|
+
mutation: CaseStyle.kebabCase,
|
1178
|
+
subscription: CaseStyle.kebabCase,
|
1179
|
+
fragment: CaseStyle.kebabCase,
|
1130
1180
|
},
|
1131
1181
|
],
|
1132
1182
|
},
|
@@ -1143,22 +1193,25 @@ const rule$3 = {
|
|
1143
1193
|
asObject: {
|
1144
1194
|
type: 'object',
|
1145
1195
|
additionalProperties: false,
|
1146
|
-
minProperties: 1,
|
1147
1196
|
properties: {
|
1148
|
-
style: {
|
1149
|
-
|
1197
|
+
style: {
|
1198
|
+
enum: CASE_STYLES,
|
1199
|
+
},
|
1200
|
+
suffix: {
|
1201
|
+
type: 'string',
|
1202
|
+
},
|
1150
1203
|
},
|
1151
1204
|
},
|
1152
1205
|
},
|
1153
1206
|
type: 'array',
|
1154
|
-
minItems: 1,
|
1155
1207
|
maxItems: 1,
|
1156
1208
|
items: {
|
1157
1209
|
type: 'object',
|
1158
1210
|
additionalProperties: false,
|
1159
|
-
minProperties: 1,
|
1160
1211
|
properties: {
|
1161
|
-
fileExtension: {
|
1212
|
+
fileExtension: {
|
1213
|
+
enum: ACCEPTED_EXTENSIONS,
|
1214
|
+
},
|
1162
1215
|
query: schemaOption,
|
1163
1216
|
mutation: schemaOption,
|
1164
1217
|
subscription: schemaOption,
|
@@ -1213,7 +1266,7 @@ const rule$3 = {
|
|
1213
1266
|
option = { style: option };
|
1214
1267
|
}
|
1215
1268
|
const expectedExtension = options.fileExtension || fileExtension;
|
1216
|
-
const expectedFilename =
|
1269
|
+
const expectedFilename = convertCase(option.style, docName) + (option.suffix || '') + expectedExtension;
|
1217
1270
|
const filenameWithExtension = filename + expectedExtension;
|
1218
1271
|
if (expectedFilename !== filenameWithExtension) {
|
1219
1272
|
context.report({
|
@@ -1365,7 +1418,6 @@ const rule$4 = {
|
|
1365
1418
|
],
|
1366
1419
|
},
|
1367
1420
|
},
|
1368
|
-
hasSuggestions: true,
|
1369
1421
|
schema: {
|
1370
1422
|
definitions: {
|
1371
1423
|
asString: {
|
@@ -1440,90 +1492,65 @@ const rule$4 = {
|
|
1440
1492
|
const style = restOptions[kind] || types;
|
1441
1493
|
return typeof style === 'object' ? style : { style };
|
1442
1494
|
}
|
1443
|
-
const checkNode = (selector) => (
|
1444
|
-
const { name
|
1445
|
-
if (!
|
1495
|
+
const checkNode = (selector) => (node) => {
|
1496
|
+
const { name } = node.kind === graphql.Kind.VARIABLE_DEFINITION ? node.variable : node;
|
1497
|
+
if (!name) {
|
1446
1498
|
return;
|
1447
1499
|
}
|
1448
1500
|
const { prefix, suffix, forbiddenPrefixes, forbiddenSuffixes, style } = normalisePropertyOption(selector);
|
1449
|
-
const nodeType = KindToDisplayName[
|
1450
|
-
const nodeName =
|
1451
|
-
const
|
1452
|
-
if (
|
1453
|
-
const { errorMessage, renameToName } = error;
|
1454
|
-
const [leadingUnderscore] = nodeName.match(/^_*/);
|
1455
|
-
const [trailingUnderscore] = nodeName.match(/_*$/);
|
1456
|
-
const suggestedName = leadingUnderscore + renameToName + trailingUnderscore;
|
1501
|
+
const nodeType = KindToDisplayName[node.kind] || node.kind;
|
1502
|
+
const nodeName = name.value;
|
1503
|
+
const errorMessage = getErrorMessage();
|
1504
|
+
if (errorMessage) {
|
1457
1505
|
context.report({
|
1458
|
-
loc: getLocation(
|
1506
|
+
loc: getLocation(name.loc, name.value),
|
1459
1507
|
message: `${nodeType} "${nodeName}" should ${errorMessage}`,
|
1460
|
-
suggest: [
|
1461
|
-
{
|
1462
|
-
desc: `Rename to "${suggestedName}"`,
|
1463
|
-
fix: fixer => fixer.replaceText(node, suggestedName),
|
1464
|
-
},
|
1465
|
-
],
|
1466
1508
|
});
|
1467
1509
|
}
|
1468
|
-
function
|
1469
|
-
|
1510
|
+
function getErrorMessage() {
|
1511
|
+
let name = nodeName;
|
1512
|
+
if (allowLeadingUnderscore) {
|
1513
|
+
name = name.replace(/^_*/, '');
|
1514
|
+
}
|
1515
|
+
if (allowTrailingUnderscore) {
|
1516
|
+
name = name.replace(/_*$/, '');
|
1517
|
+
}
|
1470
1518
|
if (prefix && !name.startsWith(prefix)) {
|
1471
|
-
return {
|
1472
|
-
errorMessage: `have "${prefix}" prefix`,
|
1473
|
-
renameToName: prefix + name,
|
1474
|
-
};
|
1519
|
+
return `have "${prefix}" prefix`;
|
1475
1520
|
}
|
1476
1521
|
if (suffix && !name.endsWith(suffix)) {
|
1477
|
-
return {
|
1478
|
-
errorMessage: `have "${suffix}" suffix`,
|
1479
|
-
renameToName: name + suffix,
|
1480
|
-
};
|
1522
|
+
return `have "${suffix}" suffix`;
|
1481
1523
|
}
|
1482
1524
|
const forbiddenPrefix = forbiddenPrefixes === null || forbiddenPrefixes === void 0 ? void 0 : forbiddenPrefixes.find(prefix => name.startsWith(prefix));
|
1483
1525
|
if (forbiddenPrefix) {
|
1484
|
-
return {
|
1485
|
-
errorMessage: `not have "${forbiddenPrefix}" prefix`,
|
1486
|
-
renameToName: name.replace(new RegExp(`^${forbiddenPrefix}`), ''),
|
1487
|
-
};
|
1526
|
+
return `not have "${forbiddenPrefix}" prefix`;
|
1488
1527
|
}
|
1489
1528
|
const forbiddenSuffix = forbiddenSuffixes === null || forbiddenSuffixes === void 0 ? void 0 : forbiddenSuffixes.find(suffix => name.endsWith(suffix));
|
1490
1529
|
if (forbiddenSuffix) {
|
1491
|
-
return {
|
1492
|
-
|
1493
|
-
|
1494
|
-
}
|
1530
|
+
return `not have "${forbiddenSuffix}" suffix`;
|
1531
|
+
}
|
1532
|
+
if (style && !ALLOWED_STYLES.includes(style)) {
|
1533
|
+
return `be in one of the following options: ${ALLOWED_STYLES.join(', ')}`;
|
1495
1534
|
}
|
1496
1535
|
const caseRegex = StyleToRegex[style];
|
1497
1536
|
if (caseRegex && !caseRegex.test(name)) {
|
1498
|
-
return {
|
1499
|
-
errorMessage: `be in ${style} format`,
|
1500
|
-
renameToName: convertCase(style, name),
|
1501
|
-
};
|
1537
|
+
return `be in ${style} format`;
|
1502
1538
|
}
|
1503
1539
|
}
|
1504
1540
|
};
|
1505
|
-
const checkUnderscore = (
|
1541
|
+
const checkUnderscore = (node) => {
|
1506
1542
|
const name = node.value;
|
1507
|
-
const renameToName = name.replace(new RegExp(isLeading ? '^_+' : '_+$'), '');
|
1508
1543
|
context.report({
|
1509
1544
|
loc: getLocation(node.loc, name),
|
1510
|
-
message: `${
|
1511
|
-
suggest: [
|
1512
|
-
{
|
1513
|
-
desc: `Rename to "${renameToName}"`,
|
1514
|
-
fix: fixer => fixer.replaceText(node, renameToName),
|
1515
|
-
},
|
1516
|
-
],
|
1545
|
+
message: `${name.startsWith('_') ? 'Leading' : 'Trailing'} underscores are not allowed`,
|
1517
1546
|
});
|
1518
1547
|
};
|
1519
1548
|
const listeners = {};
|
1520
1549
|
if (!allowLeadingUnderscore) {
|
1521
|
-
listeners['Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
|
1522
|
-
checkUnderscore(true);
|
1550
|
+
listeners['Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] = checkUnderscore;
|
1523
1551
|
}
|
1524
1552
|
if (!allowTrailingUnderscore) {
|
1525
|
-
listeners['Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
|
1526
|
-
checkUnderscore(false);
|
1553
|
+
listeners['Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] = checkUnderscore;
|
1527
1554
|
}
|
1528
1555
|
const selectors = new Set([types && TYPES_KINDS, Object.keys(restOptions)].flat().filter(Boolean));
|
1529
1556
|
for (const selector of selectors) {
|