@graphql-eslint/eslint-plugin
Version:
GraphQL plugin for ESLint
201 lines (183 loc) • 7.47 kB
JavaScript
;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", DEFAULT_ID_FIELD_NAME = "id", schema = {
definitions: {
asString: {
type: "string"
},
asArray: _utilsjs.ARRAY_DEFAULT_OPTIONS
},
type: "array",
maxItems: 1,
items: {
type: "object",
additionalProperties: !1,
properties: {
fieldName: {
oneOf: [{ $ref: "#/definitions/asString" }, { $ref: "#/definitions/asArray" }],
default: DEFAULT_ID_FIELD_NAME
}
}
}
}, rule = exports.rule = {
meta: {
type: "suggestion",
hasSuggestions: !0,
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: !0,
requiresSiblings: !0,
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: !0,
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.
Include it in your selection set{{ addition }}.`
},
schema
},
create(context) {
const schema2 = _utilsjs.requireGraphQLSchema.call(void 0, RULE_ID, context), siblings = _utilsjs.requireGraphQLOperations.call(void 0, RULE_ID, context), { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {}, idNames = _utils.asArray.call(void 0, fieldName), selector = "SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]", 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(), visitor = _graphql.visitWithTypeInfo.call(void 0, typeInfo, {
SelectionSet(node2, key, _parent) {
const parent = _parent;
parent.kind === _graphql.Kind.FRAGMENT_DEFINITION ? checkedFragmentSpreads.add(parent.name.value) : 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);
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, 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();
if (!idNames.some((name) => fields[name]))
return;
function hasIdField({ selections }) {
return selections.some((selection) => {
if (selection.kind === _graphql.Kind.FIELD)
return selection.alias && idNames.includes(selection.alias.value) ? !0 : idNames.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;
return checkedFragmentSpreads.add(fragmentSpread.name.value), hasIdField(fragmentSpread.selectionSet);
}
}
return !1;
});
}
const hasId = hasIdField(node);
if (checkFragments(node), hasId)
return;
const pluralSuffix = idNames.length > 1 ? "s" : "", fieldName2 = _utilsjs.englishJoinWords.call(void 0,
idNames.map((name) => `\`${(parent.alias || parent.name).value}.${name}\``)
), addition = checkedFragmentSpreads.size === 0 ? "" : ` or add to used fragment${checkedFragmentSpreads.size > 1 ? "s" : ""} ${_utilsjs.englishJoinWords.call(void 0, [...checkedFragmentSpreads].map((name) => `\`${name}\``))}`, problem = {
loc,
messageId: RULE_ID,
data: {
pluralSuffix,
fieldName: fieldName2,
addition
}
};
"type" in node && (problem.suggest = idNames.map((idName) => ({
desc: `Add \`${idName}\` selection`,
fix: (fixer) => {
let insertNode = node.selections[0];
return insertNode = insertNode.kind === _graphql.Kind.INLINE_FRAGMENT ? insertNode.selectionSet.selections[0] : insertNode, fixer.insertTextBefore(insertNode, `${idName} `);
}
}))), context.report(problem);
}
}
return {
[selector](node) {
const typeInfo2 = node.typeInfo();
typeInfo2.gqlType && checkSelections(node, typeInfo2.gqlType, node.loc.start, node.parent);
}
};
}
};
exports.rule = rule;