| 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, |
| ], |
| ); |
| } |
|
|
| if (inputObj.isOneOf) { |
| validateOneOfInputObjectField(inputObj, field, context); |
| } |
| } |
| } |
|
|
| function validateOneOfInputObjectField(type, field, context) { |
| if (isNonNullType(field.type)) { |
| var _field$astNode4; |
|
|
| context.reportError( |
| `OneOf input field ${type.name}.${field.name} must be nullable.`, |
| (_field$astNode4 = field.astNode) === null || _field$astNode4 === void 0 |
| ? void 0 |
| : _field$astNode4.type, |
| ); |
| } |
|
|
| if (field.defaultValue !== undefined) { |
| context.reportError( |
| `OneOf input field ${type.name}.${field.name} cannot have a default value.`, |
| field.astNode, |
| ); |
| } |
| } |
|
|
| 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, |
| ); |
| } |