import { didYouMean } from '../jsutils/didYouMean.ts';
import { inspect } from '../jsutils/inspect.ts';
import { isIterableObject } from '../jsutils/isIterableObject.ts';
import { isObjectLike } from '../jsutils/isObjectLike.ts';
import { keyMap } from '../jsutils/keyMap.ts';
import type { Maybe } from '../jsutils/Maybe.ts';
import type { Path } from '../jsutils/Path.ts';
import { addPath, pathToArray } from '../jsutils/Path.ts';
import { suggestionList } from '../jsutils/suggestionList.ts';
import { ensureGraphQLError } from '../error/ensureGraphQLError.ts';
import { GraphQLError } from '../error/GraphQLError.ts';
import type { ASTNode, ValueNode, VariableNode } from '../language/ast.ts';
import { Kind } from '../language/kinds.ts';
import { print } from '../language/printer.ts';
import type { GraphQLInputType } from '../type/definition.ts';
import {
assertLeafType,
isInputObjectType,
isListType,
isNonNullType,
isRequiredInputField,
} from '../type/definition.ts';
import type { FragmentVariableValues } from '../execution/collectFields.ts';
import type { VariableValues } from '../execution/values.ts';
import { replaceVariables } from './replaceVariables.ts';
export function validateInputValue(
inputValue: unknown,
type: GraphQLInputType,
onError: (error: GraphQLError, path: ReadonlyArray<string | number>) => void,
hideSuggestions?: Maybe<boolean>,
): void {
return validateInputValueImpl(
inputValue,
type,
onError,
hideSuggestions,
undefined,
);
}
function validateInputValueImpl(
inputValue: unknown,
type: GraphQLInputType,
onError: (error: GraphQLError, path: ReadonlyArray<string | number>) => void,
hideSuggestions: Maybe<boolean>,
path: Path | undefined,
): void {
if (isNonNullType(type)) {
if (inputValue === undefined) {
reportInvalidValue(
onError,
`Expected a value of non-null type "${type}" to be provided.`,
path,
);
return;
}
if (inputValue === null) {
reportInvalidValue(
onError,
`Expected value of non-null type "${type}" not to be null.`,
path,
);
return;
}
return validateInputValueImpl(
inputValue,
type.ofType,
onError,
hideSuggestions,
path,
);
}
if (inputValue == null) {
return;
}
if (isListType(type)) {
if (!isIterableObject(inputValue)) {
validateInputValueImpl(
inputValue,
type.ofType,
onError,
hideSuggestions,
path,
);
} else {
let index = 0;
for (const itemValue of inputValue) {
validateInputValueImpl(
itemValue,
type.ofType,
onError,
hideSuggestions,
addPath(path, index++, undefined),
);
}
}
} else if (isInputObjectType(type)) {
if (!isObjectLike(inputValue) || Array.isArray(inputValue)) {
reportInvalidValue(
onError,
`Expected value of type "${type}" to be an object, found: ${inspect(
inputValue,
)}.`,
path,
);
return;
}
const fieldDefs = type.getFields();
for (const field of Object.values(fieldDefs)) {
const fieldValue = inputValue[field.name];
if (fieldValue === undefined) {
if (isRequiredInputField(field)) {
reportInvalidValue(
onError,
`Expected value of type "${type}" to include required field "${
field.name
}", found: ${inspect(inputValue)}.`,
path,
);
}
} else {
validateInputValueImpl(
fieldValue,
field.type,
onError,
hideSuggestions,
addPath(path, field.name, type.name),
);
}
}
const fields: Array<string> = [];
for (const fieldName of Object.keys(inputValue)) {
if (inputValue[fieldName] === undefined) {
continue;
}
if (!Object.hasOwn(fieldDefs, fieldName)) {
const suggestion = hideSuggestions
? ''
: didYouMean(suggestionList(fieldName, Object.keys(fieldDefs)));
reportInvalidValue(
onError,
`Expected value of type "${type}" not to include unknown field "${fieldName}"${
suggestion ? `.${suggestion} Found` : ', found'
}: ${inspect(inputValue)}.`,
path,
);
continue;
}
fields.push(fieldName);
}
if (type.isOneOf) {
if (fields.length !== 1) {
reportInvalidValue(
onError,
getOneOfInputObjectErrorMessage(type),
path,
);
}
const field = fields[0];
const value = inputValue[field];
if (value === null) {
reportInvalidValue(
onError,
getOneOfInputObjectErrorMessage(type),
addPath(path, field, type.name),
);
}
}
} else {
assertLeafType(type);
let result;
let caughtError: unknown;
try {
result = type.coerceInputValue(inputValue, hideSuggestions);
} catch (error) {
if (error instanceof GraphQLError) {
onError(error, pathToArray(path));
return;
}
caughtError = error;
}
if (result === undefined) {
reportInvalidValue(
onError,
`Expected value of type "${type}"${
caughtError != null
? `, but encountered error "${getCaughtErrorMessage(caughtError)}"; found`
: ', found'
}: ${inspect(inputValue)}.`,
path,
ensureGraphQLError(caughtError),
);
}
}
}
function reportInvalidValue(
onError: (error: GraphQLError, path: ReadonlyArray<string | number>) => void,
message: string,
path: Path | undefined,
originalError?: GraphQLError,
): void {
onError(new GraphQLError(message, { originalError }), pathToArray(path));
}
export function validateInputLiteral(
valueNode: ValueNode,
type: GraphQLInputType,
onError: (error: GraphQLError, path: ReadonlyArray<string | number>) => void,
variables?: Maybe<VariableValues>,
fragmentVariableValues?: Maybe<FragmentVariableValues>,
hideSuggestions?: Maybe<boolean>,
): void {
const context: ValidationContext = {
static: !variables && !fragmentVariableValues,
onError,
variables,
fragmentVariableValues,
};
return validateInputLiteralImpl(
context,
valueNode,
type,
hideSuggestions,
undefined,
);
}
interface ValidationContext {
static: boolean;
onError: (error: GraphQLError, path: ReadonlyArray<string | number>) => void;
variables?: Maybe<VariableValues>;
fragmentVariableValues?: Maybe<FragmentVariableValues>;
}
function validateInputLiteralImpl(
context: ValidationContext,
valueNode: ValueNode,
type: GraphQLInputType,
hideSuggestions: Maybe<boolean>,
path: Path | undefined,
): void {
if (valueNode.kind === Kind.VARIABLE) {
if (context.static) {
return;
}
const scopedVariableValues = getScopedVariableValues(context, valueNode);
const value = scopedVariableValues?.coerced[valueNode.name.value];
if (isNonNullType(type)) {
if (value === undefined) {
reportInvalidLiteral(
context.onError,
`Expected variable "$${valueNode.name.value}" provided to type "${type}" to provide a runtime value.`,
valueNode,
path,
);
} else if (value === null) {
reportInvalidLiteral(
context.onError,
`Expected variable "$${valueNode.name.value}" provided to non-null type "${type}" not to be null.`,
valueNode,
path,
);
}
}
return;
}
if (isNonNullType(type)) {
if (valueNode.kind === Kind.NULL) {
reportInvalidLiteral(
context.onError,
`Expected value of non-null type "${type}" not to be null.`,
valueNode,
path,
);
return;
}
return validateInputLiteralImpl(
context,
valueNode,
type.ofType,
hideSuggestions,
path,
);
}
if (valueNode.kind === Kind.NULL) {
return;
}
if (isListType(type)) {
if (valueNode.kind !== Kind.LIST) {
validateInputLiteralImpl(
context,
valueNode,
type.ofType,
hideSuggestions,
path,
);
} else {
let index = 0;
for (const itemNode of valueNode.values) {
validateInputLiteralImpl(
context,
itemNode,
type.ofType,
hideSuggestions,
addPath(path, index++, undefined),
);
}
}
} else if (isInputObjectType(type)) {
if (valueNode.kind !== Kind.OBJECT) {
reportInvalidLiteral(
context.onError,
`Expected value of type "${type}" to be an object, found: ${print(
valueNode,
)}.`,
valueNode,
path,
);
return;
}
const fieldDefs = type.getFields();
const fieldNodes = keyMap(valueNode.fields, (field) => field.name.value);
for (const field of Object.values(fieldDefs)) {
const fieldNode = fieldNodes[field.name];
if (fieldNode === undefined) {
if (isRequiredInputField(field)) {
reportInvalidLiteral(
context.onError,
`Expected value of type "${type}" to include required field "${
field.name
}", found: ${print(valueNode)}.`,
valueNode,
path,
);
}
} else {
const fieldValueNode = fieldNode.value;
if (fieldValueNode.kind === Kind.VARIABLE && !context.static) {
const scopedVariableValues = getScopedVariableValues(
context,
fieldValueNode,
);
const variableName = fieldValueNode.name.value;
const value = scopedVariableValues?.coerced[variableName];
if (type.isOneOf) {
if (value === undefined) {
reportInvalidLiteral(
context.onError,
`Expected variable "$${variableName}" provided to field "${field.name}" for OneOf Input Object type "${type}" to provide a runtime value.`,
valueNode,
path,
);
} else if (value === null) {
reportInvalidLiteral(
context.onError,
`Expected variable "$${variableName}" provided to field "${field.name}" for OneOf Input Object type "${type}" not to be null.`,
valueNode,
path,
);
}
} else if (value === undefined && !isRequiredInputField(field)) {
continue;
}
}
validateInputLiteralImpl(
context,
fieldValueNode,
field.type,
hideSuggestions,
addPath(path, field.name, type.name),
);
}
}
const fields = valueNode.fields;
const knownFields: Array<(typeof fields)[number]> = [];
for (const fieldNode of fields) {
const fieldName = fieldNode.name.value;
if (!Object.hasOwn(fieldDefs, fieldName)) {
const suggestion = hideSuggestions
? ''
: didYouMean(suggestionList(fieldName, Object.keys(fieldDefs)));
reportInvalidLiteral(
context.onError,
`Expected value of type "${type}" not to include unknown field "${fieldName}"${
suggestion ? `.${suggestion} Found` : ', found'
}: ${print(valueNode)}.`,
fieldNode,
path,
);
} else {
knownFields.push(fieldNode);
}
}
if (type.isOneOf) {
const isNotExactlyOneField = knownFields.length !== 1;
if (isNotExactlyOneField) {
reportInvalidLiteral(
context.onError,
getOneOfInputObjectErrorMessage(type),
valueNode,
path,
);
return;
}
const fieldValueNode = knownFields[0].value;
if (fieldValueNode.kind === Kind.NULL) {
const fieldName = knownFields[0].name.value;
reportInvalidLiteral(
context.onError,
getOneOfInputObjectErrorMessage(type),
valueNode,
addPath(path, fieldName, undefined),
);
}
}
} else {
assertLeafType(type);
let result;
let caughtError: unknown;
try {
result = type.coerceInputLiteral
? type.coerceInputLiteral(
replaceVariables(
valueNode,
context.variables,
context.fragmentVariableValues,
),
hideSuggestions,
)
: type.parseLiteral(valueNode, undefined, hideSuggestions);
} catch (error) {
if (error instanceof GraphQLError) {
context.onError(error, pathToArray(path));
return;
}
caughtError = error;
}
if (result === undefined) {
reportInvalidLiteral(
context.onError,
`Expected value of type "${type}"${
caughtError != null
? `, but encountered error "${getCaughtErrorMessage(caughtError)}"; found`
: ', found'
}: ${print(valueNode)}.`,
valueNode,
path,
ensureGraphQLError(caughtError),
);
}
}
}
function getScopedVariableValues(
context: ValidationContext,
valueNode: VariableNode,
): Maybe<VariableValues> {
const variableName = valueNode.name.value;
const { fragmentVariableValues, variables } = context;
return fragmentVariableValues?.sources[variableName]
? fragmentVariableValues
: variables;
}
function reportInvalidLiteral(
onError: (error: GraphQLError, path: ReadonlyArray<string | number>) => void,
message: string,
valueNode: ASTNode,
path: Path | undefined,
originalError?: GraphQLError,
): void {
onError(
new GraphQLError(message, {
nodes: valueNode,
originalError,
}),
pathToArray(path),
);
}
function getCaughtErrorMessage(caughtError: unknown): string {
if (isObjectLike(caughtError)) {
const message = caughtError.message;
if (typeof message === 'string' && message !== '') {
return message;
}
}
return String(caughtError);
}
function getOneOfInputObjectErrorMessage(type: GraphQLInputType): string {
return `Within OneOf Input Object type "${type}", exactly one field must be specified, and the value for that field must be non-null.`;
}