UNPKG

@graphql-eslint/eslint-plugin

Version:
252 lines (234 loc) • 8.43 kB
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); var _graphql = require('graphql'); var _utils = require('@graphql-tools/utils'); var _indexjs = require('../../estree-converter/index.js'); var _utilsjs = require('../../utils.js'); const RULE_ID = "require-selections"; const DEFAULT_ID_FIELD_NAME = "id"; const schema = { definitions: { asString: { type: "string" }, asArray: _utilsjs.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 }, requireAllFields: { type: "boolean", description: "Whether all fields of `fieldName` option must be included." } } } }; const rule = { meta: { type: "suggestion", hasSuggestions: true, docs: { category: "Operations", description: "Enforce selecting specific fields when they are available on the GraphQL type.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID}`, 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, whenNotToUseIt: "Relay Compiler automatically adds an `id` field to any type that has an `id` field, even if it hasn't been explicitly requested. Requesting a field that is not used directly in the code can conflict with another Relay rule: `relay/unused-fields`." }, 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 schema2 = _utilsjs.requireGraphQLSchema.call(void 0, RULE_ID, context); const siblings = _utilsjs.requireGraphQLOperations.call(void 0, RULE_ID, context); const { fieldName = DEFAULT_ID_FIELD_NAME, requireAllFields } = context.options[0] || {}; const idNames = _utils.asArray.call(void 0, fieldName); const selector = "SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]"; const typeInfo = new (0, _graphql.TypeInfo)(schema2); function checkFragments(node) { for (const selection of node.selections) { if (selection.kind !== _graphql.Kind.FRAGMENT_SPREAD) { continue; } const [foundSpread] = siblings.getFragment(selection.name.value); if (!foundSpread) { continue; } const checkedFragmentSpreads = /* @__PURE__ */ new Set(); const visitor = _graphql.visitWithTypeInfo.call(void 0, typeInfo, { SelectionSet(node2, key, _parent) { const parent = _parent; if (parent.kind === _graphql.Kind.FRAGMENT_DEFINITION) { checkedFragmentSpreads.add(parent.name.value); } else if (parent.kind !== _graphql.Kind.INLINE_FRAGMENT) { checkSelections( node2, typeInfo.getType(), selection.loc.start, parent, checkedFragmentSpreads ); } } }); _graphql.visit.call(void 0, foundSpread.document, visitor); } } function checkSelections(node, type, loc, parent, checkedFragmentSpreads = /* @__PURE__ */ new Set()) { const rawType = _indexjs.getBaseType.call(void 0, type); if (rawType instanceof _graphql.GraphQLObjectType || rawType instanceof _graphql.GraphQLInterfaceType) { checkFields(rawType); } else if (rawType instanceof _graphql.GraphQLUnionType) { for (const selection of node.selections) { const types = rawType.getTypes(); if (selection.kind === _graphql.Kind.INLINE_FRAGMENT) { const t = types.find((t2) => t2.name === selection.typeCondition.name.value); if (t) { checkFields(t); } } else if (selection.kind === _graphql.Kind.FRAGMENT_SPREAD) { const [foundSpread] = siblings.getFragment(selection.name.value); if (!foundSpread) return; const fragmentSpread = foundSpread.document; const t = fragmentSpread.typeCondition.name.value === rawType.name ? rawType : types.find((t2) => t2.name === fragmentSpread.typeCondition.name.value); checkedFragmentSpreads.add(fragmentSpread.name.value); checkSelections(fragmentSpread.selectionSet, t, loc, parent, checkedFragmentSpreads); } } } function checkFields(rawType2) { const fields = rawType2.getFields(); const hasIdFieldInType = idNames.some((name) => fields[name]); if (!hasIdFieldInType) { return; } checkFragments(node); if (requireAllFields) { for (const idName of idNames) { report([idName]); } } else { report(idNames); } } function report(idNames2) { function hasIdField({ selections }) { return selections.some((selection) => { if (selection.kind === _graphql.Kind.FIELD) { if (selection.alias && idNames2.includes(selection.alias.value)) { return true; } return idNames2.includes(selection.name.value); } if (selection.kind === _graphql.Kind.INLINE_FRAGMENT) { return hasIdField(selection.selectionSet); } if (selection.kind === _graphql.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); if (hasId) { return; } const fieldName2 = _utilsjs.englishJoinWords.call(void 0, idNames2.map((name) => `\`${(parent.alias || parent.name).value}.${name}\``) ); const pluralSuffix = idNames2.length > 1 ? "s" : ""; const addition = checkedFragmentSpreads.size === 0 ? "" : ` or add to used fragment${checkedFragmentSpreads.size > 1 ? "s" : ""} ${_utilsjs.englishJoinWords.call(void 0, [...checkedFragmentSpreads].map((name) => `\`${name}\``))}`; const problem = { loc, messageId: RULE_ID, data: { pluralSuffix, fieldName: fieldName2, addition } }; if ("type" in node) { problem.suggest = idNames2.map((idName) => ({ desc: `Add \`${idName}\` selection`, fix: (fixer) => { let insertNode = node.selections[0]; insertNode = insertNode.kind === _graphql.Kind.INLINE_FRAGMENT ? insertNode.selectionSet.selections[0] : insertNode; return fixer.insertTextBefore(insertNode, `${idName} `); } })); } context.report(problem); } } return { [selector](node) { const typeInfo2 = node.typeInfo(); if (typeInfo2.gqlType) { checkSelections(node, typeInfo2.gqlType, node.loc.start, node.parent); } } }; } }; exports.rule = rule;