UNPKG

@graphql-eslint/eslint-plugin

Version:
148 lines (147 loc) • 6.65 kB
import { isObjectType, isScalarType, Kind, visit } from "graphql"; import { getDocumentNodeFromSchema } from "@graphql-tools/utils"; import { getTypeName, requireGraphQLSchema } from "../../utils.js"; const RULE_ID = "relay-edge-types", MESSAGE_MUST_BE_OBJECT_TYPE = "MESSAGE_MUST_BE_OBJECT_TYPE", MESSAGE_MISSING_EDGE_SUFFIX = "MESSAGE_MISSING_EDGE_SUFFIX", MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE = "MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE", MESSAGE_SHOULD_IMPLEMENTS_NODE = "MESSAGE_SHOULD_IMPLEMENTS_NODE"; let edgeTypesCache; function getEdgeTypes(schema2) { if (process.env.NODE_ENV !== "test" && edgeTypesCache) return edgeTypesCache; const edgeTypes = /* @__PURE__ */ new Set(), visitor = { ObjectTypeDefinition(node) { if (!node.name.value.endsWith("Connection")) return; const edges = node.fields?.find((field) => field.name.value === "edges"); if (edges) { const edgesTypeName = getTypeName(edges), edgesType = schema2.getType(edgesTypeName); isObjectType(edgesType) && edgeTypes.add(edgesTypeName); } } }, astNode = getDocumentNodeFromSchema(schema2); return visit(astNode, visitor), edgeTypesCache = edgeTypes, edgeTypesCache; } const schema = { type: "array", maxItems: 1, items: { type: "object", additionalProperties: !1, minProperties: 1, properties: { withEdgeSuffix: { type: "boolean", default: !0, description: 'Edge type name must end in "Edge".' }, shouldImplementNode: { type: "boolean", default: !0, description: "Edge type's field `node` must implement `Node` interface." }, listTypeCanWrapOnlyEdgeType: { type: "boolean", default: !0, description: "A list type should only wrap an edge type." } } } }, rule = { meta: { type: "problem", docs: { category: "Schema", description: [ "Set of rules to follow Relay specification for Edge types.", "", "- A type that is returned in list form by a connection type's `edges` field is considered by this spec to be an Edge type", "- Edge type must be an Object type", "- Edge type must contain a field `node` that return either Scalar, Enum, Object, Interface, Union, or a non-null wrapper around one of those types. Notably, this field cannot return a list", "- Edge type must contain a field `cursor` that return either String, Scalar, or a non-null wrapper around one of those types", '- Edge type name must end in "Edge" _(optional)_', "- Edge type's field `node` must implement `Node` interface _(optional)_", "- A list type should only wrap an edge type _(optional)_" ].join(` `), url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID}`, isDisabledForAllConfig: !0, requiresSchema: !0, examples: [ { title: "Correct", code: ( /* GraphQL */ ` type UserConnection { edges: [UserEdge] pageInfo: PageInfo! } ` ) } ] }, messages: { [MESSAGE_MUST_BE_OBJECT_TYPE]: "Edge type must be an Object type.", [MESSAGE_MISSING_EDGE_SUFFIX]: 'Edge type must have "Edge" suffix.', [MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE]: "A list type should only wrap an edge type.", [MESSAGE_SHOULD_IMPLEMENTS_NODE]: "Edge type's field `node` must implement `Node` interface." }, schema }, create(context) { const schema2 = requireGraphQLSchema(RULE_ID, context), edgeTypes = getEdgeTypes(schema2), options = { withEdgeSuffix: !0, shouldImplementNode: !0, listTypeCanWrapOnlyEdgeType: !0, ...context.options[0] }, isNamedOrNonNullNamed = (node) => node.kind === Kind.NAMED_TYPE || node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.NAMED_TYPE, checkNodeField = (node) => { const nodeField = node.fields?.find((field) => field.name.value === "node"), message = "return either a Scalar, Enum, Object, Interface, Union, or a non-null wrapper around one of those types."; if (!nodeField) context.report({ node: node.name, message: `Edge type must contain a field \`node\` that ${message}` }); else if (!isNamedOrNonNullNamed(nodeField.gqlType)) context.report({ node: nodeField.name, message: `Field \`node\` must ${message}` }); else if (options.shouldImplementNode) { const nodeReturnTypeName = getTypeName(nodeField.gqlType.rawNode()), type = schema2.getType(nodeReturnTypeName); if (!isObjectType(type)) return; type.astNode.interfaces?.some((n) => n.name.value === "Node") || context.report({ node: node.name, messageId: MESSAGE_SHOULD_IMPLEMENTS_NODE }); } }, checkCursorField = (node) => { const cursorField = node.fields?.find((field) => field.name.value === "cursor"), message = "return either a String, Scalar, or a non-null wrapper wrapper around one of those types."; if (!cursorField) { context.report({ node: node.name, message: `Edge type must contain a field \`cursor\` that ${message}` }); return; } const typeName = getTypeName(cursorField.rawNode()); (!isNamedOrNonNullNamed(cursorField.gqlType) || typeName !== "String" && !isScalarType(schema2.getType(typeName))) && context.report({ node: cursorField.name, message: `Field \`cursor\` must ${message}` }); }, listeners = { ":matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=edges] > .gqlType Name"(node) { const type = schema2.getType(node.value); isObjectType(type) || context.report({ node, messageId: MESSAGE_MUST_BE_OBJECT_TYPE }); }, ":matches(ObjectTypeDefinition, ObjectTypeExtension)"(node) { const typeName = node.name.value; edgeTypes.has(typeName) && (checkNodeField(node), checkCursorField(node), options.withEdgeSuffix && !typeName.endsWith("Edge") && context.report({ node: node.name, messageId: MESSAGE_MISSING_EDGE_SUFFIX })); } }; return options.listTypeCanWrapOnlyEdgeType && (listeners["FieldDefinition > .gqlType"] = (node) => { if (node.kind === Kind.LIST_TYPE || node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.LIST_TYPE) { const typeName = getTypeName(node.rawNode()); edgeTypes.has(typeName) || context.report({ node, messageId: MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE }); } }), listeners; } }; export { rule };