/**
* 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 strict
*/
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 objectValues from '../jsutils/objectValues';
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<GraphQLError> {
// 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<GraphQLError>;
+schema: GraphQLSchema;
constructor(schema) {
this._errors = [];
this.schema = schema;
}
reportError(
message: string,
nodes?: $ReadOnlyArray<?ASTNode> | ?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<GraphQLError> {
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 {
// If a schema explicitly allows some legacy name which is no longer valid,
// allow it to be assumed valid.
if (
context.schema.__allowedLegacyNames &&
context.schema.__allowedLegacyNames.indexOf(node.name) !== -1
) {
return;
}
// Ensure names are valid, however introspection types opt out.
const error = isValidNameError(node.name, node.astNode || undefined);
if (error) {
context.addError(error);
}
}
function validateTypes(context: SchemaValidationContext): void {
const typeMap = context.schema.getTypeMap();
objectValues(typeMap).forEach(type => {
// 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 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.
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 fields = objectValues(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.`,
getAllObjectOrInterfaceNodes(type),
);
}
fields.forEach(field => {
// Ensure they are named correctly.
validateName(context, field);
// Ensure they were defined at most once.
const fieldNodes = getAllFieldNodes(type, field.name);
if (fieldNodes.length > 1) {
context.reportError(
`Field ${type.name}.${field.name} 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}.${field.name} must be Output Type ` +
`but got: ${String(field.type)}.`,
getFieldTypeNode(type, field.name),
);
}
// 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}.${field.name}(${argName}:) can only ` +
'be defined once.',
getAllFieldArgNodes(type, field.name, argName),
);
}
argNames[argName] = true;
// Ensure the type is an input type
if (!isInputType(arg.type)) {
context.reportError(
`The type of ${type.name}.${field.name}(${argName}:) must be Input ` +
`Type but got: ${String(arg.type)}.`,
getFieldArgTypeNode(type, field.name, argName),
);
}
});
});
}
function validateObjectInterfaces(
context: SchemaValidationContext,
object: GraphQLObjectType,
): void {
const implementedTypeNames = Object.create(null);
object.getInterfaces().forEach(iface => {
if (!isInterfaceType(iface)) {
context.reportError(
`Type ${String(object)} must only implement Interface types, ` +
`it cannot implement ${String(iface)}.`,
getImplementsInterfaceNode(object, iface),
);
return;
}
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 {
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 fields = objectValues(inputObj.getFields());
if (fields.length === 0) {
context.reportError(
`Input Object type ${inputObj.name} must define one or more fields.`,
inputObj.astNode,
);
}
// Ensure the arguments are valid
fields.forEach(field => {
// 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}.${field.name} must be Input Type ` +
`but got: ${String(field.type)}.`,
field.astNode && field.astNode.type,
);
}
});
}
function getAllObjectNodes(
type: GraphQLObjectType,
): $ReadOnlyArray<ObjectTypeDefinitionNode | ObjectTypeExtensionNode> {
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<NamedTypeNode> {
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<FieldDefinitionNode> {
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<InputValueDefinitionNode> {
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<InputValueDefinitionNode> {
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<NamedTypeNode> {
return (
union.astNode &&
union.astNode.types &&
union.astNode.types.filter(type => type.name.value === typeName)
);
}
function getEnumValueNodes(
enumType: GraphQLEnumType,
valueName: string,
): ?$ReadOnlyArray<EnumValueDefinitionNode> {
return (
enumType.astNode &&
enumType.astNode.values &&
enumType.astNode.values.filter(value => value.name.value === valueName)
);
}