UNPKG

graphql

Version:

A Query Language and Runtime which can target any service.

588 lines 25.5 kB
import { AccumulatorMap } from "../jsutils/AccumulatorMap.mjs"; import { capitalize } from "../jsutils/capitalize.mjs"; import { andList } from "../jsutils/formatList.mjs"; import { inspect } from "../jsutils/inspect.mjs"; import { invariant } from "../jsutils/invariant.mjs"; import { isIterableObject } from "../jsutils/isIterableObject.mjs"; import { isObjectLike } from "../jsutils/isObjectLike.mjs"; import { keyMap } from "../jsutils/keyMap.mjs"; import { mapValue } from "../jsutils/mapValue.mjs"; import { printPathArray } from "../jsutils/printPathArray.mjs"; import { GraphQLError } from "../error/GraphQLError.mjs"; import { OperationTypeNode } from "../language/ast.mjs"; import { Kind } from "../language/kinds.mjs"; import { isEqualType, isTypeSubTypeOf } from "../utilities/typeComparators.mjs"; import { validateInputLiteral, validateInputValue, } from "../utilities/validateInputValue.mjs"; import { assertLeafType, getNamedType, isEnumType, isInputObjectType, isInputType, isInterfaceType, isListType, 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"; export function validateSchema(schema) { assertSchema(schema); if (schema.__validationErrors) { return schema.__validationErrors; } const context = new SchemaValidationContext(schema); validateRootTypes(context); validateDirectives(context); validateTypes(context); const errors = context.getErrors(); schema.__validationErrors = errors; return errors; } 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; if (schema.getQueryType() == null) { context.reportError('Query root type must be provided.', schema.astNode); } const rootTypesMap = new AccumulatorMap(); for (const operationType of Object.values(OperationTypeNode)) { const rootType = schema.getRootType(operationType); if (rootType != null) { if (!isObjectType(rootType)) { const operationTypeStr = capitalize(operationType); const rootTypeStr = inspect(rootType); context.reportError(operationType === OperationTypeNode.QUERY ? `${operationTypeStr} root type must be Object type, it cannot be ${rootTypeStr}.` : `${operationTypeStr} root type must be Object type if provided, it cannot be ${rootTypeStr}.`, getOperationTypeNode(schema, operationType) ?? rootType.astNode); } else { rootTypesMap.add(rootType, operationType); } } } for (const [rootType, operationTypes] of rootTypesMap) { if (operationTypes.length > 1) { const operationList = andList(operationTypes); context.reportError(`All root types must be different, "${rootType}" type is used as ${operationList} root types.`, operationTypes.map((operationType) => getOperationTypeNode(schema, operationType))); } } } function getOperationTypeNode(schema, operation) { return [schema.astNode, ...schema.extensionASTNodes] .flatMap((schemaNode) => schemaNode?.operationTypes ?? []) .find((operationNode) => operationNode.operation === operation)?.type; } function validateDirectives(context) { for (const directive of context.schema.getDirectives()) { if (!isDirective(directive)) { context.reportError(`Expected directive but got: ${inspect(directive)}.`, directive?.astNode); continue; } validateName(context, directive); if (directive.locations.length === 0) { context.reportError(`Directive ${directive} must include 1 or more locations.`, directive.astNode); } for (const arg of directive.args) { validateName(context, arg); if (!isInputType(arg.type)) { context.reportError(`The type of ${arg} must be Input Type ` + `but got: ${inspect(arg.type)}.`, arg.astNode); } if (isRequiredArgument(arg) && arg.deprecationReason != null) { context.reportError(`Required argument ${arg} cannot be deprecated.`, [ getDeprecatedDirectiveNode(arg.astNode), arg.astNode?.type, ]); } validateDefaultValue(context, arg); } } } function validateDefaultValue(context, inputValue) { const defaultInput = inputValue.default; if (!defaultInput) { return; } const errors = []; validateDefaultInput(defaultInput, inputValue.type, (error, path) => { errors.push([error, path]); }); if (errors.length === 0) { return; } if (!defaultInput.literal) { try { const uncoercedValue = uncoerceDefaultValue(defaultInput.value, inputValue.type); const uncoercedErrors = []; validateInputValue(uncoercedValue, inputValue.type, (error, path) => { uncoercedErrors.push([error, path]); }); if (uncoercedErrors.length === 0) { context.reportError(`${inputValue} has invalid default value: ${inspect(defaultInput.value)}. Did you mean: ${inspect(uncoercedValue)}?`, inputValue.astNode?.defaultValue); return; } } catch (_error) { } } for (const [error, path] of errors) { context.reportError(`${inputValue} has invalid default value${printPathArray(path)}: ${error.message}`, error.nodes ?? inputValue.astNode?.defaultValue); } } export function validateDefaultInput(defaultInput, inputType, onError, hideSuggestions) { if (defaultInput.literal) { validateInputLiteral(defaultInput.literal, inputType, onError, undefined, undefined, hideSuggestions); return; } validateInputValue(defaultInput.value, inputType, onError, hideSuggestions); } function uncoerceDefaultValue(value, type) { if (isNonNullType(type)) { return uncoerceDefaultValue(value, type.ofType); } if (value === null) { return null; } if (isListType(type)) { if (isIterableObject(value)) { return Array.from(value, (itemValue) => uncoerceDefaultValue(itemValue, type.ofType)); } return [uncoerceDefaultValue(value, type.ofType)]; } if (isInputObjectType(type)) { if (!(isObjectLike(value))) invariant(false); const fieldDefs = type.getFields(); return mapValue(value, (fieldValue, fieldName) => { if (!(fieldName in fieldDefs)) invariant(false); return uncoerceDefaultValue(fieldValue, fieldDefs[fieldName].type); }); } assertLeafType(type); return type.coerceOutputValue(value); } function validateName(context, node) { 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 validateInputObjectDefaultValueCircularRefs = createInputObjectDefaultValueCircularRefsValidator(context); const typeMap = context.schema.getTypeMap(); const finiteValueStates = new Map(); for (const type of Object.values(typeMap)) { if (!isNamedType(type)) { context.reportError(`Expected GraphQL named type but got: ${inspect(type)}.`, type.astNode); continue; } if (!isIntrospectionType(type)) { validateName(context, type); } if (isObjectType(type)) { validateFields(context, type); validateInterfaces(context, type); } else if (isInterfaceType(type)) { validateFields(context, type); validateInterfaces(context, type); } else if (isUnionType(type)) { validateUnionMembers(context, type); } else if (isEnumType(type)) { validateEnumValues(context, type); } else if (isInputObjectType(type)) { validateInputFields(context, type); initializeInputObjectFiniteValueState(type); validateInputObjectDefaultValueCircularRefs(type); } } detectInputObjectNonFiniteValues(context, finiteValueStates); function initializeInputObjectFiniteValueState(inputObj) { finiteValueStates.set(inputObj, { inputObj, targets: [], dependents: [], unresolvedTargetCount: 0, hasFiniteValue: false, }); } } function validateFields(context, type) { const fields = Object.values(type.getFields()); if (fields.length === 0) { context.reportError(`Type ${type} must define one or more fields.`, [ type.astNode, ...type.extensionASTNodes, ]); } for (const field of fields) { validateName(context, field); if (!isOutputType(field.type)) { context.reportError(`The type of ${field} must be Output Type ` + `but got: ${inspect(field.type)}.`, field.astNode?.type); } for (const arg of field.args) { validateName(context, arg); if (!isInputType(arg.type)) { context.reportError(`The type of ${arg} must be Input Type but got: ${inspect(arg.type)}.`, arg.astNode?.type); } if (isRequiredArgument(arg) && arg.deprecationReason != null) { context.reportError(`Required argument ${arg} cannot be deprecated.`, [ getDeprecatedDirectiveNode(arg.astNode), arg.astNode?.type, ]); } validateDefaultValue(context, arg); } } } function validateInterfaces(context, type) { const ifaceTypeNames = new Set(); for (const iface of type.getInterfaces()) { if (!isInterfaceType(iface)) { context.reportError(`Type ${type} must only implement Interface types, ` + `it cannot implement ${inspect(iface)}.`, getAllImplementsInterfaceNodes(type, iface)); continue; } if (type === iface) { context.reportError(`Type ${type} cannot implement itself because it would create a circular reference.`, getAllImplementsInterfaceNodes(type, iface)); continue; } if (ifaceTypeNames.has(iface.name)) { context.reportError(`Type ${type} can only implement ${iface} once.`, getAllImplementsInterfaceNodes(type, iface)); continue; } ifaceTypeNames.add(iface.name); validateTypeImplementsAncestors(context, type, iface); validateTypeImplementsInterface(context, type, iface); } } function validateTypeImplementsInterface(context, type, iface) { const typeFieldMap = type.getFields(); for (const ifaceField of Object.values(iface.getFields())) { const typeField = typeFieldMap[ifaceField.name]; if (typeField == null) { context.reportError(`Interface field ${ifaceField} expected but ${type} does not provide it.`, [ifaceField.astNode, type.astNode, ...type.extensionASTNodes]); continue; } if (!isTypeSubTypeOf(context.schema, typeField.type, ifaceField.type)) { context.reportError(`Interface field ${ifaceField} expects type ${ifaceField.type} ` + `but ${typeField} is type ${typeField.type}.`, [ifaceField.astNode?.type, typeField.astNode?.type]); } for (const ifaceArg of ifaceField.args) { const typeArg = typeField.args.find((arg) => arg.name === ifaceArg.name); if (!typeArg) { context.reportError(`Interface field argument ${ifaceArg} expected but ${typeField} does not provide it.`, [ifaceArg.astNode, typeField.astNode]); continue; } if (!isEqualType(ifaceArg.type, typeArg.type)) { context.reportError(`Interface field argument ${ifaceArg} expects type ${ifaceArg.type} ` + `but ${typeArg} is type ${typeArg.type}.`, [ifaceArg.astNode?.type, typeArg.astNode?.type]); } } for (const typeArg of typeField.args) { if (isRequiredArgument(typeArg)) { const ifaceArg = ifaceField.args.find((arg) => arg.name === typeArg.name); if (!ifaceArg) { context.reportError(`Argument "${typeArg}" must not be required type "${typeArg.type}" ` + `if not provided by the Interface field "${ifaceField}".`, [typeArg.astNode, ifaceField.astNode]); } } } if (typeField.deprecationReason != null && ifaceField.deprecationReason == null) { context.reportError(`Interface field ${iface.name}.${ifaceField.name} is not deprecated, so ` + `implementation field ${type.name}.${typeField.name} must not be deprecated.`, [ getDeprecatedDirectiveNode(typeField.astNode), typeField.astNode?.type, ]); } } } 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} cannot implement ${iface} because it would create a circular reference.` : `Type ${type} must implement ${transitive} because it is implemented by ${iface}.`, [ ...getAllImplementsInterfaceNodes(iface, transitive), ...getAllImplementsInterfaceNodes(type, iface), ]); } } } function validateUnionMembers(context, union) { const memberTypes = union.getTypes(); if (memberTypes.length === 0) { context.reportError(`Union type ${union} must define one or more member types.`, [union.astNode, ...union.extensionASTNodes]); } const includedTypeNames = new Set(); for (const memberType of memberTypes) { if (includedTypeNames.has(memberType.name)) { context.reportError(`Union type ${union} can only include type ${memberType} once.`, getUnionMemberTypeNodes(union, memberType.name)); continue; } includedTypeNames.add(memberType.name); if (!isObjectType(memberType)) { context.reportError(`Union type ${union} 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} must define one or more values.`, [enumType.astNode, ...enumType.extensionASTNodes]); } for (const enumValue of enumValues) { validateName(context, enumValue); } } function validateInputFields(context, inputObj) { const fields = Object.values(inputObj.getFields()); if (fields.length === 0) { context.reportError(`Input Object type ${inputObj} must define one or more fields.`, [inputObj.astNode, ...inputObj.extensionASTNodes]); } for (const field of fields) { validateName(context, field); if (!isInputType(field.type)) { context.reportError(`The type of ${field} must be Input Type ` + `but got: ${inspect(field.type)}.`, field.astNode?.type); } if (isRequiredInputField(field) && field.deprecationReason != null) { context.reportError(`Required input field ${field} cannot be deprecated.`, [getDeprecatedDirectiveNode(field.astNode), field.astNode?.type]); } validateDefaultValue(context, field); if (inputObj.isOneOf) { validateOneOfInputObjectField(inputObj, field, context); } } } function validateOneOfInputObjectField(type, field, context) { if (isNonNullType(field.type)) { context.reportError(`OneOf input field ${type}.${field.name} must be nullable.`, field.astNode?.type); } if (field.default !== undefined || field.defaultValue !== undefined) { context.reportError(`OneOf input field ${type}.${field.name} cannot have a default value.`, field.astNode); } } function detectInputObjectNonFiniteValues(context, finiteValueStates) { const inputObjectsWithFiniteValues = []; for (const state of finiteValueStates.values()) { const inputObj = state.inputObj; const fields = Object.values(inputObj.getFields()); for (const field of fields) { const target = getFiniteValueTarget(inputObj, field.type); if (target === undefined) { continue; } state.targets.push({ field, target }); const targetState = finiteValueStates.get(target); if (targetState !== undefined) { targetState.dependents.push(state); } } if (inputObj.isOneOf) { if (fields.length === 0 || state.targets.length < fields.length) { markInputObjectHasFiniteValue(state); } } else { state.unresolvedTargetCount = state.targets.length; if (state.targets.length === 0) { markInputObjectHasFiniteValue(state); } } } let nextFiniteValueState; while ((nextFiniteValueState = inputObjectsWithFiniteValues.pop()) !== undefined) { for (const dependentState of nextFiniteValueState.dependents) { if (dependentState.hasFiniteValue) { continue; } if (dependentState.inputObj.isOneOf) { markInputObjectHasFiniteValue(dependentState); continue; } --dependentState.unresolvedTargetCount; if (dependentState.unresolvedTargetCount === 0) { markInputObjectHasFiniteValue(dependentState); } } } const visitedTypes = new Set(); const fieldPath = []; const fieldPathIndexByType = new Map(); for (const state of finiteValueStates.values()) { if (!state.hasFiniteValue) { reportCycleRecursive(state); } } function markInputObjectHasFiniteValue(finiteValueState) { if (!finiteValueState.hasFiniteValue) { finiteValueState.hasFiniteValue = true; inputObjectsWithFiniteValues.push(finiteValueState); } } function reportCycleRecursive(state) { const inputObj = state.inputObj; if (visitedTypes.has(inputObj)) { return; } visitedTypes.add(inputObj); fieldPathIndexByType.set(inputObj, fieldPath.length); for (const { field, target } of state.targets) { const targetState = finiteValueStates.get(target); if (targetState?.hasFiniteValue !== false) { continue; } const cycleIndex = fieldPathIndexByType.get(target); fieldPath.push({ fieldStr: `${inputObj}.${field.name}`, astNode: field.astNode, }); if (cycleIndex === undefined) { reportCycleRecursive(targetState); } else { const cyclePath = fieldPath.slice(cycleIndex); const pathStr = cyclePath.map((p) => p.fieldStr).join(', '); context.reportError(`Input Object ${target} cannot be provided a finite value because it references itself through fields: ${pathStr}.`, cyclePath.map((p) => p.astNode)); } fieldPath.pop(); } fieldPathIndexByType.delete(inputObj); } } function getFiniteValueTarget(inputObj, fieldType) { if (inputObj.isOneOf) { if (isInputObjectType(fieldType)) { return fieldType; } return; } if (isNonNullType(fieldType) && isInputObjectType(fieldType.ofType)) { return fieldType.ofType; } } function createInputObjectDefaultValueCircularRefsValidator(context) { const visitedFields = Object.create(null); const fieldPath = []; const fieldPathIndex = Object.create(null); return function validateInputObjectDefaultValueCircularRefs(inputObj) { return detectValueDefaultValueCycle(inputObj, Object.create(null)); }; function detectValueDefaultValueCycle(inputObj, defaultValue) { if (isIterableObject(defaultValue)) { for (const itemValue of defaultValue) { detectValueDefaultValueCycle(inputObj, itemValue); } return; } else if (!isObjectLike(defaultValue)) { return; } for (const field of Object.values(inputObj.getFields())) { const namedFieldType = getNamedType(field.type); if (!isInputObjectType(namedFieldType)) { continue; } if (Object.hasOwn(defaultValue, field.name)) { detectValueDefaultValueCycle(namedFieldType, defaultValue[field.name]); } else { detectFieldDefaultValueCycle(field, namedFieldType, `${inputObj}.${field.name}`); } } } function detectLiteralDefaultValueCycle(inputObj, defaultValue) { if (defaultValue.kind === Kind.LIST) { for (const itemLiteral of defaultValue.values) { detectLiteralDefaultValueCycle(inputObj, itemLiteral); } return; } else if (defaultValue.kind !== Kind.OBJECT) { return; } const fieldNodes = keyMap(defaultValue.fields, (field) => field.name.value); for (const field of Object.values(inputObj.getFields())) { const namedFieldType = getNamedType(field.type); if (!isInputObjectType(namedFieldType)) { continue; } if (Object.hasOwn(fieldNodes, field.name)) { detectLiteralDefaultValueCycle(namedFieldType, fieldNodes[field.name].value); } else { detectFieldDefaultValueCycle(field, namedFieldType, `${inputObj}.${field.name}`); } } } function detectFieldDefaultValueCycle(field, fieldType, fieldStr) { const defaultInput = field.default; if (defaultInput === undefined) { return; } const cycleIndex = fieldPathIndex[fieldStr]; if (cycleIndex !== undefined) { context.reportError(`Invalid circular reference. The default value of Input Object field ${fieldStr} references itself${cycleIndex < fieldPath.length ? ` via the default values of: ${fieldPath .slice(cycleIndex) .map(([stringForMessage]) => stringForMessage) .join(', ')}` : ''}.`, fieldPath.slice(cycleIndex - 1).map(([, node]) => node)); return; } if (visitedFields[fieldStr] === undefined) { visitedFields[fieldStr] = true; fieldPathIndex[fieldStr] = fieldPath.push([ fieldStr, field.astNode?.defaultValue, ]); if (defaultInput.literal) { detectLiteralDefaultValueCycle(fieldType, defaultInput.literal); } else { detectValueDefaultValueCycle(fieldType, defaultInput.value); } fieldPath.pop(); fieldPathIndex[fieldStr] = undefined; } } } function getAllImplementsInterfaceNodes(type, iface) { const { astNode, extensionASTNodes } = type; const nodes = astNode != null ? [astNode, ...extensionASTNodes] : extensionASTNodes; return nodes .flatMap((typeNode) => typeNode.interfaces ?? []) .filter((ifaceNode) => ifaceNode.name.value === iface.name); } function getUnionMemberTypeNodes(union, typeName) { const { astNode, extensionASTNodes } = union; const nodes = astNode != null ? [astNode, ...extensionASTNodes] : extensionASTNodes; return nodes .flatMap((unionNode) => unionNode.types ?? []) .filter((typeNode) => typeNode.name.value === typeName); } function getDeprecatedDirectiveNode(definitionNode) { return definitionNode?.directives?.find((node) => node.name.value === GraphQLDeprecatedDirective.name); } //# sourceMappingURL=validate.js.map