UNPKG

@graphql-eslint/eslint-plugin

Version:
201 lines (197 loc) • 7.83 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.rule = void 0; const graphql_1 = require("graphql"); const utils_1 = require("@graphql-tools/utils"); const utils_js_1 = require("../utils.js"); const index_js_1 = require("../estree-converter/index.js"); const RULE_ID = 'require-id-when-available'; const DEFAULT_ID_FIELD_NAME = 'id'; const schema = { definitions: { asString: { type: 'string', }, asArray: utils_js_1.ARRAY_DEFAULT_OPTIONS, }, type: 'array', maxItems: 1, items: { type: 'object', additionalProperties: false, properties: { fieldName: { oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asArray' }], default: DEFAULT_ID_FIELD_NAME, }, }, }, }; exports.rule = { meta: { type: 'suggestion', hasSuggestions: true, docs: { category: 'Operations', description: 'Enforce selecting specific fields when they are available on the GraphQL type.', url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`, requiresSchema: true, requiresSiblings: true, examples: [ { title: 'Incorrect', code: /* GraphQL */ ` # In your schema type User { id: ID! name: String! } # Query query { user { name } } `, }, { title: 'Correct', code: /* GraphQL */ ` # In your schema type User { id: ID! name: String! } # Query query { user { id name } } # Selecting \`id\` with an alias is also valid query { user { id: name } } `, }, ], recommended: true, }, messages: { [RULE_ID]: "Field{{ pluralSuffix }} {{ fieldName }} must be selected when it's available on a type.\nInclude it in your selection set{{ addition }}.", }, schema, }, create(context) { const schema = (0, utils_js_1.requireGraphQLSchemaFromContext)(RULE_ID, context); const siblings = (0, utils_js_1.requireSiblingsOperations)(RULE_ID, context); const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {}; const idNames = (0, utils_1.asArray)(fieldName); // Check selections only in OperationDefinition, // skip selections of OperationDefinition and InlineFragment const selector = 'OperationDefinition SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]'; const typeInfo = new graphql_1.TypeInfo(schema); function checkFragments(node) { for (const selection of node.selections) { if (selection.kind !== graphql_1.Kind.FRAGMENT_SPREAD) { continue; } const [foundSpread] = siblings.getFragment(selection.name.value); if (!foundSpread) { continue; } const checkedFragmentSpreads = new Set(); const visitor = (0, graphql_1.visitWithTypeInfo)(typeInfo, { SelectionSet(node, key, _parent) { const parent = _parent; if (parent.kind === graphql_1.Kind.FRAGMENT_DEFINITION) { checkedFragmentSpreads.add(parent.name.value); } else if (parent.kind !== graphql_1.Kind.INLINE_FRAGMENT) { checkSelections(node, typeInfo.getType(), selection.loc.start, parent, checkedFragmentSpreads); } }, }); (0, graphql_1.visit)(foundSpread.document, visitor); } } function checkSelections(node, type, // Fragment can be placed in separate file // Provide actual fragment spread location instead of location in fragment loc, // Can't access to node.parent in GraphQL AST.Node, so pass as argument parent, checkedFragmentSpreads = new Set()) { const rawType = (0, index_js_1.getBaseType)(type); const isObjectType = rawType instanceof graphql_1.GraphQLObjectType; const isInterfaceType = rawType instanceof graphql_1.GraphQLInterfaceType; if (!isObjectType && !isInterfaceType) { return; } const fields = rawType.getFields(); const hasIdFieldInType = idNames.some(name => fields[name]); if (!hasIdFieldInType) { return; } function hasIdField({ selections }) { return selections.some(selection => { if (selection.kind === graphql_1.Kind.FIELD) { if (selection.alias && idNames.includes(selection.alias.value)) { return true; } return idNames.includes(selection.name.value); } if (selection.kind === graphql_1.Kind.INLINE_FRAGMENT) { return hasIdField(selection.selectionSet); } if (selection.kind === graphql_1.Kind.FRAGMENT_SPREAD) { const [foundSpread] = siblings.getFragment(selection.name.value); if (foundSpread) { const fragmentSpread = foundSpread.document; checkedFragmentSpreads.add(fragmentSpread.name.value); return hasIdField(fragmentSpread.selectionSet); } } return false; }); } const hasId = hasIdField(node); checkFragments(node); if (hasId) { return; } const pluralSuffix = idNames.length > 1 ? 's' : ''; const fieldName = (0, utils_js_1.englishJoinWords)(idNames.map(name => `\`${(parent.alias || parent.name).value}.${name}\``)); const addition = checkedFragmentSpreads.size === 0 ? '' : ` or add to used fragment${checkedFragmentSpreads.size > 1 ? 's' : ''} ${(0, utils_js_1.englishJoinWords)([...checkedFragmentSpreads].map(name => `\`${name}\``))}`; const problem = { loc, messageId: RULE_ID, data: { pluralSuffix, fieldName, addition, }, }; // Don't provide suggestions for selections in fragments as fragment can be in a separate file if ('type' in node) { problem.suggest = idNames.map(idName => ({ desc: `Add \`${idName}\` selection`, fix: fixer => fixer.insertTextBefore(node.selections[0], `${idName} `), })); } context.report(problem); } return { [selector](node) { const typeInfo = node.typeInfo(); if (typeInfo.gqlType) { checkSelections(node, typeInfo.gqlType, node.loc.start, node.parent); } }, }; }, };