@graphql-eslint/eslint-plugin
Version:
GraphQL plugin for ESLint
252 lines (234 loc) • 8.43 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";
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;
;