UNPKG

@graphql-eslint/eslint-plugin

Version:
191 lines (184 loc) • 8.38 kB
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _graphql = require('graphql'); var _utils = require('@graphql-tools/utils'); var _utilsjs = require('../../utils.js'); const RULE_ID = "relay-edge-types"; const MESSAGE_MUST_BE_OBJECT_TYPE = "MESSAGE_MUST_BE_OBJECT_TYPE"; const MESSAGE_MISSING_EDGE_SUFFIX = "MESSAGE_MISSING_EDGE_SUFFIX"; const MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE = "MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE"; const 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(); const visitor = { ObjectTypeDefinition(node) { const typeName = node.name.value; const hasConnectionSuffix = typeName.endsWith("Connection"); if (!hasConnectionSuffix) { return; } const edges = _optionalChain([node, 'access', _ => _.fields, 'optionalAccess', _2 => _2.find, 'call', _3 => _3((field) => field.name.value === "edges")]); if (edges) { const edgesTypeName = _utilsjs.getTypeName.call(void 0, edges); const edgesType = schema2.getType(edgesTypeName); if (_graphql.isObjectType.call(void 0, edgesType)) { edgeTypes.add(edgesTypeName); } } } }; const astNode = _utils.getDocumentNodeFromSchema.call(void 0, schema2); _graphql.visit.call(void 0, astNode, visitor); edgeTypesCache = edgeTypes; return edgeTypesCache; } const schema = { type: "array", maxItems: 1, items: { type: "object", additionalProperties: false, minProperties: 1, properties: { withEdgeSuffix: { type: "boolean", default: true, description: 'Edge type name must end in "Edge".' }, shouldImplementNode: { type: "boolean", default: true, description: "Edge type's field `node` must implement `Node` interface." }, listTypeCanWrapOnlyEdgeType: { type: "boolean", default: true, description: "A list type should only wrap an edge type." } } } }; const 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("\n"), url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID}`, isDisabledForAllConfig: true, requiresSchema: true, 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 = _utilsjs.requireGraphQLSchema.call(void 0, RULE_ID, context); const edgeTypes = getEdgeTypes(schema2); const options = { withEdgeSuffix: true, shouldImplementNode: true, listTypeCanWrapOnlyEdgeType: true, ...context.options[0] }; const isNamedOrNonNullNamed = (node) => node.kind === _graphql.Kind.NAMED_TYPE || node.kind === _graphql.Kind.NON_NULL_TYPE && node.gqlType.kind === _graphql.Kind.NAMED_TYPE; const checkNodeField = (node) => { const nodeField = _optionalChain([node, 'access', _4 => _4.fields, 'optionalAccess', _5 => _5.find, 'call', _6 => _6((field) => field.name.value === "node")]); const 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 = _utilsjs.getTypeName.call(void 0, nodeField.gqlType.rawNode()); const type = schema2.getType(nodeReturnTypeName); if (!_graphql.isObjectType.call(void 0, type)) { return; } const implementsNode = _optionalChain([type, 'access', _7 => _7.astNode, 'access', _8 => _8.interfaces, 'optionalAccess', _9 => _9.some, 'call', _10 => _10((n) => n.name.value === "Node")]); if (!implementsNode) { context.report({ node: node.name, messageId: MESSAGE_SHOULD_IMPLEMENTS_NODE }); } } }; const checkCursorField = (node) => { const cursorField = _optionalChain([node, 'access', _11 => _11.fields, 'optionalAccess', _12 => _12.find, 'call', _13 => _13((field) => field.name.value === "cursor")]); const 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 = _utilsjs.getTypeName.call(void 0, cursorField.rawNode()); if (!isNamedOrNonNullNamed(cursorField.gqlType) || typeName !== "String" && !_graphql.isScalarType.call(void 0, schema2.getType(typeName))) { context.report({ node: cursorField.name, message: `Field \`cursor\` must ${message}` }); } }; const listeners = { ":matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=edges] > .gqlType Name"(node) { const type = schema2.getType(node.value); if (!_graphql.isObjectType.call(void 0, type)) { context.report({ node, messageId: MESSAGE_MUST_BE_OBJECT_TYPE }); } }, ":matches(ObjectTypeDefinition, ObjectTypeExtension)"(node) { const typeName = node.name.value; if (edgeTypes.has(typeName)) { checkNodeField(node); checkCursorField(node); if (options.withEdgeSuffix && !typeName.endsWith("Edge")) { context.report({ node: node.name, messageId: MESSAGE_MISSING_EDGE_SUFFIX }); } } } }; if (options.listTypeCanWrapOnlyEdgeType) { listeners["FieldDefinition > .gqlType"] = (node) => { if (node.kind === _graphql.Kind.LIST_TYPE || node.kind === _graphql.Kind.NON_NULL_TYPE && node.gqlType.kind === _graphql.Kind.LIST_TYPE) { const typeName = _utilsjs.getTypeName.call(void 0, node.rawNode()); if (!edgeTypes.has(typeName)) { context.report({ node, messageId: MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE }); } } }; } return listeners; } }; exports.rule = rule;