import { inspect } from '../jsutils/inspect.mjs'; import { GraphQLError } from '../error/GraphQLError.mjs'; import { OperationTypeNode } from '../language/ast.mjs'; import { isEqualType, isTypeSubTypeOf } from '../utilities/typeComparators.mjs'; import { isEnumType, isInputObjectType, isInputType, isInterfaceType, isNamedType, isNonNullType, isObjectType, isOutputType, isRequiredArgument, isRequiredInputField, isUnionType, } from './definition.mjs'; import { GraphQLDeprecatedDirective, isDirective } from './directives.mjs'; import { isIntrospectionType } from './introspection.mjs'; import { assertSchema } from './schema.mjs'; /** * Implements the "Type Validation" sub-sections of the specification's * "Type System" section. * * Validation runs synchronously, returning an array of encountered errors, or * an empty array if no errors were encountered and the Schema is valid. */ export function validateSchema(schema) { // First check to ensure the provided value is in fact a GraphQLSchema. assertSchema(schema); // If this Schema has already been validated, return the previous results. if (schema.__validationErrors) { return schema.__validationErrors; } // Validate the schema, producing a list of errors. const context = new SchemaValidationContext(schema); validateRootTypes(context); validateDirectives(context); validateTypes(context); // Persist the results of validation before returning to ensure validation // does not run multiple times for this schema. const errors = context.getErrors(); schema.__validationErrors = errors; return errors; } /** * Utility function which asserts a schema is valid by throwing an error if * it is invalid. */ export function assertValidSchema(schema) { const errors = validateSchema(schema); if (errors.length !== 0) { throw new Error(errors.map((error) => error.message).join('\n\n')); } } class SchemaValidationContext { constructor(schema) { this._errors = []; this.schema = schema; } reportError(message, nodes) { const _nodes = Array.isArray(nodes) ? nodes.filter(Boolean) : nodes; this._errors.push( new GraphQLError(message, { nodes: _nodes, }), ); } getErrors() { return this._errors; } } function validateRootTypes(context) { const schema = context.schema; const queryType = schema.getQueryType(); if (!queryType) { context.reportError('Query root type must be provided.', schema.astNode); } else if (!isObjectType(queryType)) { var _getOperationTypeNode; context.reportError( `Query root type must be Object type, it cannot be ${inspect( queryType, )}.`, (_getOperationTypeNode = getOperationTypeNode( schema, OperationTypeNode.QUERY, )) !== null && _getOperationTypeNode !== void 0 ? _getOperationTypeNode : queryType.astNode, ); } const mutationType = schema.getMutationType(); if (mutationType && !isObjectType(mutationType)) { var _getOperationTypeNode2; context.reportError( 'Mutation root type must be Object type if provided, it cannot be ' + `${inspect(mutationType)}.`, (_getOperationTypeNode2 = getOperationTypeNode( schema, OperationTypeNode.MUTATION, )) !== null && _getOperationTypeNode2 !== void 0 ? _getOperationTypeNode2 : mutationType.astNode, ); } const subscriptionType = schema.getSubscriptionType(); if (subscriptionType && !isObjectType(subscriptionType)) { var _getOperationTypeNode3; context.reportError( 'Subscription root type must be Object type if provided, it cannot be ' + `${inspect(subscriptionType)}.`, (_getOperationTypeNode3 = getOperationTypeNode( schema, OperationTypeNode.SUBSCRIPTION, )) !== null && _getOperationTypeNode3 !== void 0 ? _getOperationTypeNode3 : subscriptionType.astNode, ); } } function getOperationTypeNode(schema, operation) { var _flatMap$find; return (_flatMap$find = [schema.astNode, ...schema.extensionASTNodes] .flatMap( // FIXME: https://github.com/graphql/graphql-js/issues/2203 (schemaNode) => { var _schemaNode$operation; return ( /* c8 ignore next */ (_schemaNode$operation = schemaNode === null || schemaNode === void 0 ? void 0 : schemaNode.operationTypes) !== null && _schemaNode$operation !== void 0 ? _schemaNode$operation : [] ); }, ) .find((operationNode) => operationNode.operation === operation)) === null || _flatMap$find === void 0 ? void 0 : _flatMap$find.type; } function validateDirectives(context) { for (const directive of context.schema.getDirectives()) { // Ensure all directives are in fact GraphQL directives. if (!isDirective(directive)) { context.reportError( `Expected directive but got: ${inspect(directive)}.`, directive === null || directive === void 0 ? void 0 : directive.astNode, ); continue; } // Ensure they are named correctly. validateName(context, directive); // TODO: Ensure proper locations. // Ensure the arguments are valid. for (const arg of directive.args) { // Ensure they are named correctly. validateName(context, arg); // Ensure the type is an input type. if (!isInputType(arg.type)) { context.reportError( `The type of @${directive.name}(${arg.name}:) must be Input Type ` + `but got: ${inspect(arg.type)}.`, arg.astNode, ); } if (isRequiredArgument(arg) && arg.deprecationReason != null) { var _arg$astNode; context.reportError( `Required argument @${directive.name}(${arg.name}:) cannot be deprecated.`, [ getDeprecatedDirectiveNode(arg.astNode), (_arg$astNode = arg.astNode) === null || _arg$astNode === void 0 ? void 0 : _arg$astNode.type, ], ); } } } } function validateName(context, node) { // Ensure names are valid, however introspection types opt out. if (node.name.startsWith('__')) { context.reportError( `Name "${node.name}" must not begin with "__", which is reserved by GraphQL introspection.`, node.astNode, ); } } function validateTypes(context) { const validateInputObjectCircularRefs = createInputObjectCircularRefsValidator(context); const typeMap = context.schema.getTypeMap(); for (const type of Object.values(typeMap)) { // Ensure all provided types are in fact GraphQL type. if (!isNamedType(type)) { context.reportError( `Expected GraphQL named type but got: ${inspect(type)}.`, type.astNode, ); continue; } // Ensure it is named correctly (excluding introspection types). if (!isIntrospectionType(type)) { validateName(context, type); } if (isObjectType(type)) { // Ensure fields are valid validateFields(context, type); // Ensure objects implement the interfaces they claim to. validateInterfaces(context, type); } else if (isInterfaceType(type)) { // Ensure fields are valid. validateFields(context, type); // Ensure interfaces implement the interfaces they claim to. validateInterfaces(context, type); } else if (isUnionType(type)) { // Ensure Unions include valid member types. validateUnionMembers(context, type); } else if (isEnumType(type)) { // Ensure Enums have valid values. validateEnumValues(context, type); } else if (isInputObjectType(type)) { // Ensure Input Object fields are valid. validateInputFields(context, type); // Ensure Input Objects do not contain non-nullable circular references validateInputObjectCircularRefs(type); } } } function validateFields(context, type) { const fields = Object.values(type.getFields()); // Objects and Interfaces both must define one or more fields. if (fields.length === 0) { context.reportError(`Type ${type.name} must define one or more fields.`, [ type.astNode, ...type.extensionASTNodes, ]); } for (const field of fields) { // Ensure they are named correctly. validateName(context, field); // Ensure the type is an output type if (!isOutputType(field.type)) { var _field$astNode; context.reportError( `The type of ${type.name}.${field.name} must be Output Type ` + `but got: ${inspect(field.type)}.`, (_field$astNode = field.astNode) === null || _field$astNode === void 0 ? void 0 : _field$astNode.type, ); } // Ensure the arguments are valid for (const arg of field.args) { const argName = arg.name; // Ensure they are named correctly. validateName(context, arg); // Ensure the type is an input type if (!isInputType(arg.type)) { var _arg$astNode2; context.reportError( `The type of ${type.name}.${field.name}(${argName}:) must be Input ` + `Type but got: ${inspect(arg.type)}.`, (_arg$astNode2 = arg.astNode) === null || _arg$astNode2 === void 0 ? void 0 : _arg$astNode2.type, ); } if (isRequiredArgument(arg) && arg.deprecationReason != null) { var _arg$astNode3; context.reportError( `Required argument ${type.name}.${field.name}(${argName}:) cannot be deprecated.`, [ getDeprecatedDirectiveNode(arg.astNode), (_arg$astNode3 = arg.astNode) === null || _arg$astNode3 === void 0 ? void 0 : _arg$astNode3.type, ], ); } } } } function validateInterfaces(context, type) { const ifaceTypeNames = Object.create(null); for (const iface of type.getInterfaces()) { if (!isInterfaceType(iface)) { context.reportError( `Type ${inspect(type)} must only implement Interface types, ` + `it cannot implement ${inspect(iface)}.`, getAllImplementsInterfaceNodes(type, iface), ); continue; } if (type === iface) { context.reportError( `Type ${type.name} cannot implement itself because it would create a circular reference.`, getAllImplementsInterfaceNodes(type, iface), ); continue; } if (ifaceTypeNames[iface.name]) { context.reportError( `Type ${type.name} can only implement ${iface.name} once.`, getAllImplementsInterfaceNodes(type, iface), ); continue; } ifaceTypeNames[iface.name] = true; validateTypeImplementsAncestors(context, type, iface); validateTypeImplementsInterface(context, type, iface); } } function validateTypeImplementsInterface(context, type, iface) { const typeFieldMap = type.getFields(); // Assert each interface field is implemented. for (const ifaceField of Object.values(iface.getFields())) { const fieldName = ifaceField.name; const typeField = typeFieldMap[fieldName]; // Assert interface field exists on type. if (!typeField) { context.reportError( `Interface field ${iface.name}.${fieldName} expected but ${type.name} does not provide it.`, [ifaceField.astNode, type.astNode, ...type.extensionASTNodes], ); continue; } // Assert interface field type is satisfied by type field type, by being // a valid subtype. (covariant) if (!isTypeSubTypeOf(context.schema, typeField.type, ifaceField.type)) { var _ifaceField$astNode, _typeField$astNode; context.reportError( `Interface field ${iface.name}.${fieldName} expects type ` + `${inspect(ifaceField.type)} but ${type.name}.${fieldName} ` + `is type ${inspect(typeField.type)}.`, [ (_ifaceField$astNode = ifaceField.astNode) === null || _ifaceField$astNode === void 0 ? void 0 : _ifaceField$astNode.type, (_typeField$astNode = typeField.astNode) === null || _typeField$astNode === void 0 ? void 0 : _typeField$astNode.type, ], ); } // Assert each interface field arg is implemented. for (const ifaceArg of ifaceField.args) { const argName = ifaceArg.name; const typeArg = typeField.args.find((arg) => arg.name === argName); // Assert interface field arg exists on object field. if (!typeArg) { context.reportError( `Interface field argument ${iface.name}.${fieldName}(${argName}:) expected but ${type.name}.${fieldName} does not provide it.`, [ifaceArg.astNode, typeField.astNode], ); continue; } // Assert interface field arg type matches object field arg type. // (invariant) // TODO: change to contravariant? if (!isEqualType(ifaceArg.type, typeArg.type)) { var _ifaceArg$astNode, _typeArg$astNode; context.reportError( `Interface field argument ${iface.name}.${fieldName}(${argName}:) ` + `expects type ${inspect(ifaceArg.type)} but ` + `${type.name}.${fieldName}(${argName}:) is type ` + `${inspect(typeArg.type)}.`, [ (_ifaceArg$astNode = ifaceArg.astNode) === null || _ifaceArg$astNode === void 0 ? void 0 : _ifaceArg$astNode.type, (_typeArg$astNode = typeArg.astNode) === null || _typeArg$astNode === void 0 ? void 0 : _typeArg$astNode.type, ], ); } // TODO: validate default values? } // Assert additional arguments must not be required. for (const typeArg of typeField.args) { const argName = typeArg.name; const ifaceArg = ifaceField.args.find((arg) => arg.name === argName); if (!ifaceArg && isRequiredArgument(typeArg)) { context.reportError( `Object field ${type.name}.${fieldName} includes required argument ${argName} that is missing from the Interface field ${iface.name}.${fieldName}.`, [typeArg.astNode, ifaceField.astNode], ); } } } } function validateTypeImplementsAncestors(context, type, iface) { const ifaceInterfaces = type.getInterfaces(); for (const transitive of iface.getInterfaces()) { if (!ifaceInterfaces.includes(transitive)) { context.reportError( transitive === type ? `Type ${type.name} cannot implement ${iface.name} because it would create a circular reference.` : `Type ${type.name} must implement ${transitive.name} because it is implemented by ${iface.name}.`, [ ...getAllImplementsInterfaceNodes(iface, transitive), ...getAllImplementsInterfaceNodes(type, iface), ], ); } } } function validateUnionMembers(context, union) { const memberTypes = union.getTypes(); if (memberTypes.length === 0) { context.reportError( `Union type ${union.name} must define one or more member types.`, [union.astNode, ...union.extensionASTNodes], ); } const includedTypeNames = Object.create(null); for (const memberType of memberTypes) { if (includedTypeNames[memberType.name]) { context.reportError( `Union type ${union.name} can only include type ${memberType.name} once.`, getUnionMemberTypeNodes(union, memberType.name), ); continue; } includedTypeNames[memberType.name] = true; if (!isObjectType(memberType)) { context.reportError( `Union type ${union.name} can only include Object types, ` + `it cannot include ${inspect(memberType)}.`, getUnionMemberTypeNodes(union, String(memberType)), ); } } } function validateEnumValues(context, enumType) { const enumValues = enumType.getValues(); if (enumValues.length === 0) { context.reportError( `Enum type ${enumType.name} must define one or more values.`, [enumType.astNode, ...enumType.extensionASTNodes], ); } for (const enumValue of enumValues) { // Ensure valid name. validateName(context, enumValue); } } function validateInputFields(context, inputObj) { const fields = Object.values(inputObj.getFields()); if (fields.length === 0) { context.reportError( `Input Object type ${inputObj.name} must define one or more fields.`, [inputObj.astNode, ...inputObj.extensionASTNodes], ); } // Ensure the arguments are valid for (const field of fields) { // Ensure they are named correctly. validateName(context, field); // Ensure the type is an input type if (!isInputType(field.type)) { var _field$astNode2; context.reportError( `The type of ${inputObj.name}.${field.name} must be Input Type ` + `but got: ${inspect(field.type)}.`, (_field$astNode2 = field.astNode) === null || _field$astNode2 === void 0 ? void 0 : _field$astNode2.type, ); } if (isRequiredInputField(field) && field.deprecationReason != null) { var _field$astNode3; context.reportError( `Required input field ${inputObj.name}.${field.name} cannot be deprecated.`, [ getDeprecatedDirectiveNode(field.astNode), (_field$astNode3 = field.astNode) === null || _field$astNode3 === void 0 ? void 0 : _field$astNode3.type, ], ); } } } function createInputObjectCircularRefsValidator(context) { // Modified copy of algorithm from 'src/validation/rules/NoFragmentCycles.js'. // Tracks already visited types to maintain O(N) and to ensure that cycles // are not redundantly reported. const visitedTypes = Object.create(null); // Array of types nodes used to produce meaningful errors const fieldPath = []; // Position in the type path const fieldPathIndexByTypeName = Object.create(null); return detectCycleRecursive; // This does a straight-forward DFS to find cycles. // It does not terminate when a cycle was found but continues to explore // the graph to find all possible cycles. function detectCycleRecursive(inputObj) { if (visitedTypes[inputObj.name]) { return; } visitedTypes[inputObj.name] = true; fieldPathIndexByTypeName[inputObj.name] = fieldPath.length; const fields = Object.values(inputObj.getFields()); for (const field of fields) { if (isNonNullType(field.type) && isInputObjectType(field.type.ofType)) { const fieldType = field.type.ofType; const cycleIndex = fieldPathIndexByTypeName[fieldType.name]; fieldPath.push(field); if (cycleIndex === undefined) { detectCycleRecursive(fieldType); } else { const cyclePath = fieldPath.slice(cycleIndex); const pathStr = cyclePath.map((fieldObj) => fieldObj.name).join('.'); context.reportError( `Cannot reference Input Object "${fieldType.name}" within itself through a series of non-null fields: "${pathStr}".`, cyclePath.map((fieldObj) => fieldObj.astNode), ); } fieldPath.pop(); } } fieldPathIndexByTypeName[inputObj.name] = undefined; } } function getAllImplementsInterfaceNodes(type, iface) { const { astNode, extensionASTNodes } = type; const nodes = astNode != null ? [astNode, ...extensionASTNodes] : extensionASTNodes; // FIXME: https://github.com/graphql/graphql-js/issues/2203 return nodes .flatMap((typeNode) => { var _typeNode$interfaces; return ( /* c8 ignore next */ (_typeNode$interfaces = typeNode.interfaces) !== null && _typeNode$interfaces !== void 0 ? _typeNode$interfaces : [] ); }) .filter((ifaceNode) => ifaceNode.name.value === iface.name); } function getUnionMemberTypeNodes(union, typeName) { const { astNode, extensionASTNodes } = union; const nodes = astNode != null ? [astNode, ...extensionASTNodes] : extensionASTNodes; // FIXME: https://github.com/graphql/graphql-js/issues/2203 return nodes .flatMap((unionNode) => { var _unionNode$types; return ( /* c8 ignore next */ (_unionNode$types = unionNode.types) !== null && _unionNode$types !== void 0 ? _unionNode$types : [] ); }) .filter((typeNode) => typeNode.name.value === typeName); } function getDeprecatedDirectiveNode(definitionNode) { var _definitionNode$direc; return definitionNode === null || definitionNode === void 0 ? void 0 : (_definitionNode$direc = definitionNode.directives) === null || _definitionNode$direc === void 0 ? void 0 : _definitionNode$direc.find( (node) => node.name.value === GraphQLDeprecatedDirective.name, ); }