/** * Copyright (c) 2015-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import { isObjectType, isInterfaceType, isUnionType, isEnumType, isInputObjectType, isNonNullType, isNamedType, isInputType, isOutputType, } from './definition'; import type { GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, GraphQLEnumType, GraphQLInputObjectType, } from './definition'; import { isDirective } from './directives'; import type { GraphQLDirective } from './directives'; import { isIntrospectionType } from './introspection'; import { isSchema } from './schema'; import type { GraphQLSchema } from './schema'; import find from '../jsutils/find'; import invariant from '../jsutils/invariant'; import { GraphQLError } from '../error/GraphQLError'; import type { ASTNode, ObjectTypeDefinitionNode, ObjectTypeExtensionNode, InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode, FieldDefinitionNode, EnumValueDefinitionNode, InputValueDefinitionNode, NamedTypeNode, TypeNode, } from '../language/ast'; import { isValidNameError } from '../utilities/assertValidName'; import { isEqualType, isTypeSubTypeOf } from '../utilities/typeComparators'; /** * 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: GraphQLSchema, ): $ReadOnlyArray { // First check to ensure the provided value is in fact a GraphQLSchema. invariant( isSchema(schema), `Expected ${String(schema)} to be a GraphQL 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: GraphQLSchema): void { const errors = validateSchema(schema); if (errors.length !== 0) { throw new Error(errors.map(error => error.message).join('\n\n')); } } class SchemaValidationContext { +_errors: Array; +schema: GraphQLSchema; constructor(schema) { this._errors = []; this.schema = schema; } reportError( message: string, nodes?: $ReadOnlyArray | ?ASTNode, ): void { const _nodes = (Array.isArray(nodes) ? nodes : [nodes]).filter(Boolean); this.addError(new GraphQLError(message, _nodes)); } addError(error: GraphQLError): void { this._errors.push(error); } getErrors(): $ReadOnlyArray { 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)) { context.reportError( `Query root type must be Object type, it cannot be ${String(queryType)}.`, getOperationTypeNode(schema, queryType, 'query'), ); } const mutationType = schema.getMutationType(); if (mutationType && !isObjectType(mutationType)) { context.reportError( 'Mutation root type must be Object type if provided, it cannot be ' + `${String(mutationType)}.`, getOperationTypeNode(schema, mutationType, 'mutation'), ); } const subscriptionType = schema.getSubscriptionType(); if (subscriptionType && !isObjectType(subscriptionType)) { context.reportError( 'Subscription root type must be Object type if provided, it cannot be ' + `${String(subscriptionType)}.`, getOperationTypeNode(schema, subscriptionType, 'subscription'), ); } } function getOperationTypeNode( schema: GraphQLSchema, type: GraphQLObjectType, operation: string, ): ?ASTNode { const astNode = schema.astNode; const operationTypeNode = astNode && astNode.operationTypes.find( operationType => operationType.operation === operation, ); return operationTypeNode ? operationTypeNode.type : type && type.astNode; } function validateDirectives(context: SchemaValidationContext): void { const directives = context.schema.getDirectives(); directives.forEach(directive => { // Ensure all directives are in fact GraphQL directives. if (!isDirective(directive)) { context.reportError( `Expected directive but got: ${String(directive)}.`, directive && directive.astNode, ); return; } // Ensure they are named correctly. validateName(context, directive); // TODO: Ensure proper locations. // Ensure the arguments are valid. const argNames = Object.create(null); directive.args.forEach(arg => { const argName = arg.name; // Ensure they are named correctly. validateName(context, arg); // Ensure they are unique per directive. if (argNames[argName]) { context.reportError( `Argument @${directive.name}(${argName}:) can only be defined once.`, getAllDirectiveArgNodes(directive, argName), ); return; // continue loop } argNames[argName] = true; // Ensure the type is an input type. if (!isInputType(arg.type)) { context.reportError( `The type of @${directive.name}(${argName}:) must be Input Type ` + `but got: ${String(arg.type)}.`, getDirectiveArgTypeNode(directive, argName), ); } }); }); } function validateName( context: SchemaValidationContext, node: { +name: string, +astNode: ?ASTNode }, ): void { // Ensure names are valid, however introspection types opt out. const error = isValidNameError(node.name, node.astNode || undefined); if (error && !isIntrospectionType((node: any))) { context.addError(error); } } function validateTypes(context: SchemaValidationContext): void { const typeMap = context.schema.getTypeMap(); Object.keys(typeMap).forEach(typeName => { const type = typeMap[typeName]; // Ensure all provided types are in fact GraphQL type. if (!isNamedType(type)) { context.reportError( `Expected GraphQL named type but got: ${String(type)}.`, type && type.astNode, ); return; } // Ensure they are named correctly. validateName(context, type); if (isObjectType(type)) { // Ensure fields are valid validateFields(context, type); // Ensure objects implement the interfaces they claim to. validateObjectInterfaces(context, type); } else if (isInterfaceType(type)) { // Ensure fields are valid. validateFields(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); } }); } function validateFields( context: SchemaValidationContext, type: GraphQLObjectType | GraphQLInterfaceType, ): void { const fieldMap = type.getFields(); const fieldNames = Object.keys(fieldMap); // Objects and Interfaces both must define one or more fields. if (fieldNames.length === 0) { context.reportError( `Type ${type.name} must define one or more fields.`, getAllObjectOrInterfaceNodes(type), ); } fieldNames.forEach(fieldName => { const field = fieldMap[fieldName]; // Ensure they are named correctly. validateName(context, field); // Ensure they were defined at most once. const fieldNodes = getAllFieldNodes(type, fieldName); if (fieldNodes.length > 1) { context.reportError( `Field ${type.name}.${fieldName} can only be defined once.`, fieldNodes, ); return; // continue loop } // Ensure the type is an output type if (!isOutputType(field.type)) { context.reportError( `The type of ${type.name}.${fieldName} must be Output Type ` + `but got: ${String(field.type)}.`, getFieldTypeNode(type, fieldName), ); } // Ensure the arguments are valid const argNames = Object.create(null); field.args.forEach(arg => { const argName = arg.name; // Ensure they are named correctly. validateName(context, arg); // Ensure they are unique per field. if (argNames[argName]) { context.reportError( `Field argument ${type.name}.${fieldName}(${argName}:) can only ` + 'be defined once.', getAllFieldArgNodes(type, fieldName, argName), ); } argNames[argName] = true; // Ensure the type is an input type if (!isInputType(arg.type)) { context.reportError( `The type of ${type.name}.${fieldName}(${argName}:) must be Input ` + `Type but got: ${String(arg.type)}.`, getFieldArgTypeNode(type, fieldName, argName), ); } }); }); } function validateObjectInterfaces( context: SchemaValidationContext, object: GraphQLObjectType, ): void { const implementedTypeNames = Object.create(null); object.getInterfaces().forEach(iface => { if (implementedTypeNames[iface.name]) { context.reportError( `Type ${object.name} can only implement ${iface.name} once.`, getAllImplementsInterfaceNodes(object, iface), ); return; // continue loop } implementedTypeNames[iface.name] = true; validateObjectImplementsInterface(context, object, iface); }); } function validateObjectImplementsInterface( context: SchemaValidationContext, object: GraphQLObjectType, iface: GraphQLInterfaceType, ): void { if (!isInterfaceType(iface)) { context.reportError( `Type ${String(object)} must only implement Interface types, ` + `it cannot implement ${String(iface)}.`, getImplementsInterfaceNode(object, iface), ); return; } const objectFieldMap = object.getFields(); const ifaceFieldMap = iface.getFields(); // Assert each interface field is implemented. Object.keys(ifaceFieldMap).forEach(fieldName => { const objectField = objectFieldMap[fieldName]; const ifaceField = ifaceFieldMap[fieldName]; // Assert interface field exists on object. if (!objectField) { context.reportError( `Interface field ${iface.name}.${fieldName} expected but ` + `${object.name} does not provide it.`, [getFieldNode(iface, fieldName), object.astNode], ); // Continue loop over fields. return; } // Assert interface field type is satisfied by object field type, by being // a valid subtype. (covariant) if (!isTypeSubTypeOf(context.schema, objectField.type, ifaceField.type)) { context.reportError( `Interface field ${iface.name}.${fieldName} expects type ` + `${String(ifaceField.type)} but ${object.name}.${fieldName} ` + `is type ${String(objectField.type)}.`, [ getFieldTypeNode(iface, fieldName), getFieldTypeNode(object, fieldName), ], ); } // Assert each interface field arg is implemented. ifaceField.args.forEach(ifaceArg => { const argName = ifaceArg.name; const objectArg = find(objectField.args, arg => arg.name === argName); // Assert interface field arg exists on object field. if (!objectArg) { context.reportError( `Interface field argument ${iface.name}.${fieldName}(${argName}:) ` + `expected but ${object.name}.${fieldName} does not provide it.`, [ getFieldArgNode(iface, fieldName, argName), getFieldNode(object, fieldName), ], ); // Continue loop over arguments. return; } // Assert interface field arg type matches object field arg type. // (invariant) // TODO: change to contravariant? if (!isEqualType(ifaceArg.type, objectArg.type)) { context.reportError( `Interface field argument ${iface.name}.${fieldName}(${argName}:) ` + `expects type ${String(ifaceArg.type)} but ` + `${object.name}.${fieldName}(${argName}:) is type ` + `${String(objectArg.type)}.`, [ getFieldArgTypeNode(iface, fieldName, argName), getFieldArgTypeNode(object, fieldName, argName), ], ); } // TODO: validate default values? }); // Assert additional arguments must not be required. objectField.args.forEach(objectArg => { const argName = objectArg.name; const ifaceArg = find(ifaceField.args, arg => arg.name === argName); if (!ifaceArg && isNonNullType(objectArg.type)) { context.reportError( `Object field argument ${object.name}.${fieldName}(${argName}:) ` + `is of required type ${String(objectArg.type)} but is not also ` + `provided by the Interface field ${iface.name}.${fieldName}.`, [ getFieldArgTypeNode(object, fieldName, argName), getFieldNode(iface, fieldName), ], ); } }); }); } function validateUnionMembers( context: SchemaValidationContext, union: GraphQLUnionType, ): void { const memberTypes = union.getTypes(); if (memberTypes.length === 0) { context.reportError( `Union type ${union.name} must define one or more member types.`, union.astNode, ); } const includedTypeNames = Object.create(null); memberTypes.forEach(memberType => { if (includedTypeNames[memberType.name]) { context.reportError( `Union type ${union.name} can only include type ` + `${memberType.name} once.`, getUnionMemberTypeNodes(union, memberType.name), ); return; // continue loop } includedTypeNames[memberType.name] = true; if (!isObjectType(memberType)) { context.reportError( `Union type ${union.name} can only include Object types, ` + `it cannot include ${String(memberType)}.`, getUnionMemberTypeNodes(union, String(memberType)), ); } }); } function validateEnumValues( context: SchemaValidationContext, enumType: GraphQLEnumType, ): void { const enumValues = enumType.getValues(); if (enumValues.length === 0) { context.reportError( `Enum type ${enumType.name} must define one or more values.`, enumType.astNode, ); } enumValues.forEach(enumValue => { const valueName = enumValue.name; // Ensure no duplicates. const allNodes = getEnumValueNodes(enumType, valueName); if (allNodes && allNodes.length > 1) { context.reportError( `Enum type ${enumType.name} can include value ${valueName} only once.`, allNodes, ); } // Ensure valid name. validateName(context, enumValue); if (valueName === 'true' || valueName === 'false' || valueName === 'null') { context.reportError( `Enum type ${enumType.name} cannot include value: ${valueName}.`, enumValue.astNode, ); } }); } function validateInputFields( context: SchemaValidationContext, inputObj: GraphQLInputObjectType, ): void { const fieldMap = inputObj.getFields(); const fieldNames = Object.keys(fieldMap); if (fieldNames.length === 0) { context.reportError( `Input Object type ${inputObj.name} must define one or more fields.`, inputObj.astNode, ); } // Ensure the arguments are valid fieldNames.forEach(fieldName => { const field = fieldMap[fieldName]; // Ensure they are named correctly. validateName(context, field); // TODO: Ensure they are unique per field. // Ensure the type is an input type if (!isInputType(field.type)) { context.reportError( `The type of ${inputObj.name}.${fieldName} must be Input Type ` + `but got: ${String(field.type)}.`, field.astNode && field.astNode.type, ); } }); } function getAllObjectNodes( type: GraphQLObjectType, ): $ReadOnlyArray { return type.astNode ? type.extensionASTNodes ? [type.astNode].concat(type.extensionASTNodes) : [type.astNode] : type.extensionASTNodes || []; } function getAllObjectOrInterfaceNodes( type: GraphQLObjectType | GraphQLInterfaceType, ): $ReadOnlyArray< | ObjectTypeDefinitionNode | ObjectTypeExtensionNode | InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode, > { return type.astNode ? type.extensionASTNodes ? [type.astNode].concat(type.extensionASTNodes) : [type.astNode] : type.extensionASTNodes || []; } function getImplementsInterfaceNode( type: GraphQLObjectType, iface: GraphQLInterfaceType, ): ?NamedTypeNode { return getAllImplementsInterfaceNodes(type, iface)[0]; } function getAllImplementsInterfaceNodes( type: GraphQLObjectType, iface: GraphQLInterfaceType, ): $ReadOnlyArray { const implementsNodes = []; const astNodes = getAllObjectNodes(type); for (let i = 0; i < astNodes.length; i++) { const astNode = astNodes[i]; if (astNode && astNode.interfaces) { astNode.interfaces.forEach(node => { if (node.name.value === iface.name) { implementsNodes.push(node); } }); } } return implementsNodes; } function getFieldNode( type: GraphQLObjectType | GraphQLInterfaceType, fieldName: string, ): ?FieldDefinitionNode { return getAllFieldNodes(type, fieldName)[0]; } function getAllFieldNodes( type: GraphQLObjectType | GraphQLInterfaceType, fieldName: string, ): $ReadOnlyArray { const fieldNodes = []; const astNodes = getAllObjectOrInterfaceNodes(type); for (let i = 0; i < astNodes.length; i++) { const astNode = astNodes[i]; if (astNode && astNode.fields) { astNode.fields.forEach(node => { if (node.name.value === fieldName) { fieldNodes.push(node); } }); } } return fieldNodes; } function getFieldTypeNode( type: GraphQLObjectType | GraphQLInterfaceType, fieldName: string, ): ?TypeNode { const fieldNode = getFieldNode(type, fieldName); return fieldNode && fieldNode.type; } function getFieldArgNode( type: GraphQLObjectType | GraphQLInterfaceType, fieldName: string, argName: string, ): ?InputValueDefinitionNode { return getAllFieldArgNodes(type, fieldName, argName)[0]; } function getAllFieldArgNodes( type: GraphQLObjectType | GraphQLInterfaceType, fieldName: string, argName: string, ): $ReadOnlyArray { const argNodes = []; const fieldNode = getFieldNode(type, fieldName); if (fieldNode && fieldNode.arguments) { fieldNode.arguments.forEach(node => { if (node.name.value === argName) { argNodes.push(node); } }); } return argNodes; } function getFieldArgTypeNode( type: GraphQLObjectType | GraphQLInterfaceType, fieldName: string, argName: string, ): ?TypeNode { const fieldArgNode = getFieldArgNode(type, fieldName, argName); return fieldArgNode && fieldArgNode.type; } function getAllDirectiveArgNodes( directive: GraphQLDirective, argName: string, ): $ReadOnlyArray { const argNodes = []; const directiveNode = directive.astNode; if (directiveNode && directiveNode.arguments) { directiveNode.arguments.forEach(node => { if (node.name.value === argName) { argNodes.push(node); } }); } return argNodes; } function getDirectiveArgTypeNode( directive: GraphQLDirective, argName: string, ): ?TypeNode { const argNode = getAllDirectiveArgNodes(directive, argName)[0]; return argNode && argNode.type; } function getUnionMemberTypeNodes( union: GraphQLUnionType, typeName: string, ): ?$ReadOnlyArray { return ( union.astNode && union.astNode.types && union.astNode.types.filter(type => type.name.value === typeName) ); } function getEnumValueNodes( enumType: GraphQLEnumType, valueName: string, ): ?$ReadOnlyArray { return ( enumType.astNode && enumType.astNode.values && enumType.astNode.values.filter(value => value.name.value === valueName) ); }