@graphql-eslint/eslint-plugin
Version:
GraphQL plugin for ESLint
242 lines (238 loc) • 8.3 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var require_id_when_available_exports = {};
__export(require_id_when_available_exports, {
rule: () => rule
});
module.exports = __toCommonJS(require_id_when_available_exports);
var import_utils = require("@graphql-tools/utils");
var import_graphql = require("graphql");
var import_estree_converter = require("../estree-converter/index.js");
var import_utils2 = require("../utils.js");
const RULE_ID = "require-id-when-available";
const DEFAULT_ID_FIELD_NAME = "id";
const schema = {
definitions: {
asString: {
type: "string"
},
asArray: import_utils2.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
}
}
}
};
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
},
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 = (0, import_utils2.requireGraphQLSchemaFromContext)(RULE_ID, context);
const siblings = (0, import_utils2.requireSiblingsOperations)(RULE_ID, context);
const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
const idNames = (0, import_utils.asArray)(fieldName);
const selector = "OperationDefinition SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]";
const typeInfo = new import_graphql.TypeInfo(schema2);
function checkFragments(node) {
for (const selection of node.selections) {
if (selection.kind !== import_graphql.Kind.FRAGMENT_SPREAD) {
continue;
}
const [foundSpread] = siblings.getFragment(selection.name.value);
if (!foundSpread) {
continue;
}
const checkedFragmentSpreads = /* @__PURE__ */ new Set();
const visitor = (0, import_graphql.visitWithTypeInfo)(typeInfo, {
SelectionSet(node2, key, _parent) {
const parent = _parent;
if (parent.kind === import_graphql.Kind.FRAGMENT_DEFINITION) {
checkedFragmentSpreads.add(parent.name.value);
} else if (parent.kind !== import_graphql.Kind.INLINE_FRAGMENT) {
checkSelections(
node2,
typeInfo.getType(),
selection.loc.start,
parent,
checkedFragmentSpreads
);
}
}
});
(0, import_graphql.visit)(foundSpread.document, visitor);
}
}
function checkSelections(node, type, loc, parent, checkedFragmentSpreads = /* @__PURE__ */ new Set()) {
const rawType = (0, import_estree_converter.getBaseType)(type);
if (rawType instanceof import_graphql.GraphQLObjectType || rawType instanceof import_graphql.GraphQLInterfaceType) {
checkFields(rawType);
} else if (rawType instanceof import_graphql.GraphQLUnionType) {
for (const selection of node.selections) {
if (selection.kind === import_graphql.Kind.INLINE_FRAGMENT) {
const types = rawType.getTypes();
const t = types.find((t2) => t2.name === selection.typeCondition.name.value);
if (t) {
checkFields(t);
}
}
}
}
function checkFields(rawType2) {
const fields = rawType2.getFields();
const hasIdFieldInType = idNames.some((name) => fields[name]);
if (!hasIdFieldInType) {
return;
}
function hasIdField({ selections }) {
return selections.some((selection) => {
if (selection.kind === import_graphql.Kind.FIELD) {
if (selection.alias && idNames.includes(selection.alias.value)) {
return true;
}
return idNames.includes(selection.name.value);
}
if (selection.kind === import_graphql.Kind.INLINE_FRAGMENT) {
return hasIdField(selection.selectionSet);
}
if (selection.kind === import_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);
checkFragments(node);
if (hasId) {
return;
}
const pluralSuffix = idNames.length > 1 ? "s" : "";
const fieldName2 = (0, import_utils2.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, import_utils2.englishJoinWords)([...checkedFragmentSpreads].map((name) => `\`${name}\``))}`;
const problem = {
loc,
messageId: RULE_ID,
data: {
pluralSuffix,
fieldName: fieldName2,
addition
}
};
if ("type" in node) {
problem.suggest = idNames.map((idName) => ({
desc: `Add \`${idName}\` selection`,
fix: (fixer) => {
let insertNode = node.selections[0];
insertNode = insertNode.kind === import_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);
}
}
};
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
rule
});