@graphql-tools/executor
Version:
Fork of GraphQL.js' execute function
434 lines (433 loc) • 18.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateInputValue = validateInputValue;
exports.validateInputLiteral = validateInputLiteral;
exports.keyMap = keyMap;
const graphql_1 = require("graphql");
const utils_1 = require("@graphql-tools/utils");
const didYouMean_js_1 = require("./didYouMean.js");
const suggestionList_js_1 = require("./suggestionList.js");
/**
* Validate that the provided input value is allowed for this type, collecting
* all errors via a callback function.
* @param inputValue - JavaScript value to validate.
* @param type - GraphQL input type to validate the value against.
* @param onError - Callback invoked for each validation error and path.
* @param hideSuggestions - Whether suggestion text should be omitted from errors.
* @returns Nothing.
* @example
* ```ts
* // Collect validation errors with their input paths.
* import {
* GraphQLInputObjectType,
* GraphQLInt,
* GraphQLNonNull,
* } from 'graphql/type';
* import { validateInputValue } from 'graphql/utilities';
*
* const ReviewInput = new GraphQLInputObjectType({
* name: 'ReviewInput',
* fields: {
* stars: { type: new GraphQLNonNull(GraphQLInt) },
* },
* });
* const errors = [];
*
* validateInputValue({ stars: 'bad' }, ReviewInput, (path, _invalidValue, error) => {
* errors.push({ message: error.message, path });
* });
*
* errors; // => [ { message: 'Expected value of type "Int", found: "bad".', path: ['stars'] } ]
* ```
* @example
* ```ts
* // This variant hides suggestion text for unknown input fields.
* import { GraphQLInputObjectType, GraphQLString } from 'graphql/type';
* import { validateInputValue } from 'graphql/utilities';
*
* const ReviewInput = new GraphQLInputObjectType({
* name: 'ReviewInput',
* fields: {
* comment: { type: GraphQLString },
* },
* });
* const errors = [];
*
* validateInputValue(
* { rating: 'extra field' },
* ReviewInput,
* (_path, _invalidValue, error) => {
* errors.push(error.message);
* },
* true,
* );
*
* errors; // => ['Expected value of type "ReviewInput" not to include unknown field "rating", found: { rating: "extra field" }.']
* ```
*/
function validateInputValue(inputValue, type, onError, hideSuggestions) {
return validateInputValueImpl(inputValue, type, onError, hideSuggestions, undefined);
}
function validateInputValueImpl(inputValue, type, onError, hideSuggestions, path) {
if ((0, graphql_1.isNonNullType)(type)) {
if (inputValue === undefined) {
reportInvalidValue(onError, `Expected a value of non-null type "${type}" to be provided.`, path, inputValue);
return;
}
if (inputValue === null) {
reportInvalidValue(onError, `Expected value of non-null type "${type}" not to be null.`, path, inputValue);
return;
}
return validateInputValueImpl(inputValue, type.ofType, onError, hideSuggestions, path);
}
if (inputValue == null) {
return;
}
if ((0, graphql_1.isListType)(type)) {
if (!(0, utils_1.isIterableObject)(inputValue)) {
// Lists accept a non-list value as a list of one.
validateInputValueImpl(inputValue, type.ofType, onError, hideSuggestions, path);
}
else {
let index = 0;
for (const itemValue of inputValue) {
validateInputValueImpl(itemValue, type.ofType, onError, hideSuggestions, (0, utils_1.addPath)(path, index++, undefined));
}
}
}
else if ((0, graphql_1.isInputObjectType)(type)) {
if (!(0, utils_1.isObjectLike)(inputValue) || Array.isArray(inputValue)) {
reportInvalidValue(onError, `Expected value of type "${type}" to be an object, found: ${(0, utils_1.inspect)(inputValue)}.`, path, inputValue);
return;
}
const fieldDefs = type.getFields();
for (const field of Object.values(fieldDefs)) {
const fieldValue = inputValue[field.name];
if (fieldValue === undefined) {
if ((0, graphql_1.isRequiredInputField)(field)) {
reportInvalidValue(onError, `Expected value of type "${type}" to include required field "${field.name}", found: ${(0, utils_1.inspect)(inputValue)}.`, path, inputValue);
}
}
else {
validateInputValueImpl(fieldValue, field.type, onError, hideSuggestions, (0, utils_1.addPath)(path, field.name, type.name));
}
}
const fields = [];
// Ensure every provided field is defined.
for (const fieldName of Object.keys(inputValue)) {
if (inputValue[fieldName] === undefined) {
continue;
}
if (!Object.hasOwn(fieldDefs, fieldName)) {
const suggestion = hideSuggestions
? ''
: (0, didYouMean_js_1.didYouMean)((0, suggestionList_js_1.suggestionList)(fieldName, Object.keys(fieldDefs)));
reportInvalidValue(onError, `Expected value of type "${type}" not to include unknown field "${fieldName}"${suggestion ? `.${suggestion} Found` : ', found'}: ${(0, utils_1.inspect)(inputValue)}.`, path, inputValue);
continue;
}
fields.push(fieldName);
}
if (type.isOneOf) {
if (fields.length !== 1) {
reportInvalidValue(onError, getOneOfInputObjectErrorMessage(type), path, inputValue);
return;
}
const field = fields[0];
const value = inputValue[field];
if (value === null) {
reportInvalidValue(onError, getOneOfInputObjectErrorMessage(type), (0, utils_1.addPath)(path, field, type.name), inputValue);
}
}
}
else {
(0, graphql_1.assertLeafType)(type);
let result;
let caughtError;
try {
const typeAny = type;
const methodName = graphql_1.versionInfo.major >= 17 ? 'coerceInputValue' : 'parseValue';
result = typeAny[methodName](inputValue);
}
catch (error) {
if (error instanceof graphql_1.GraphQLError) {
onError((0, utils_1.pathToArray)(path), inputValue, error);
return;
}
caughtError = error;
}
if (result === undefined) {
reportInvalidValue(onError, `Expected value of type "${type}"${caughtError != null
? `, but encountered error "${getCaughtErrorMessage(caughtError)}"; found`
: ', found'}: ${(0, utils_1.inspect)(inputValue)}.`, path, inputValue, caughtError);
}
}
}
function reportInvalidValue(onError, message, path, invalidValue, originalError) {
onError((0, utils_1.pathToArray)(path), invalidValue, (0, utils_1.createGraphQLError)(message, { originalError }));
}
/**
* Validate that the provided input literal is allowed for this type, collecting
* all errors via a callback function.
*
* If variable values are not provided, the literal is validated statically
* (not assuming that those variables are missing runtime values).
* @param valueNode - GraphQL value AST node to validate.
* @param type - GraphQL input type to validate the literal against.
* @param onError - Callback invoked for each validation error and path.
* @param variables - Operation variable values returned by getVariableValues.
* @param fragmentVariableValues - Fragment variable values for the current fragment scope.
* @param hideSuggestions - Whether suggestion text should be omitted from errors.
* @returns Nothing.
* @example
* ```ts
* // Validate literal input values and collect literal paths.
* import { parseValue } from 'graphql/language';
* import {
* GraphQLInputObjectType,
* GraphQLInt,
* GraphQLNonNull,
* } from 'graphql/type';
* import { validateInputLiteral } from 'graphql/utilities';
*
* const ReviewInput = new GraphQLInputObjectType({
* name: 'ReviewInput',
* fields: {
* stars: { type: new GraphQLNonNull(GraphQLInt) },
* },
* });
* const errors = [];
*
* validateInputLiteral(
* parseValue('{ stars: "bad" }'),
* ReviewInput,
* (error, path) => {
* errors.push({ message: error.message, path });
* },
* );
*
* errors; // => [ { message: 'Expected value of type "Int", found: "bad".', path: ['stars'] } ]
* ```
* @example
* ```ts
* // This variant resolves variable references using VariableValues from getVariableValues().
* import assert from 'node:assert';
* import { parse, parseValue } from 'graphql/language';
* import { GraphQLInt } from 'graphql/type';
* import { getVariableValues } from 'graphql/execution';
* import { buildSchema, validateInputLiteral } from 'graphql/utilities';
*
* const schema = buildSchema(`
* type Query {
* review(stars: Int): String
* }
* `);
* const document = parse('query ($stars: Int = 5) { review(stars: $stars) }');
* const operation = document.definitions[0];
* const result = getVariableValues(schema, operation.variableDefinitions, {
* stars: '4',
* });
*
* assert('variableValues' in result);
*
* const errors = [];
* validateInputLiteral(
* parseValue('$stars'),
* GraphQLInt,
* (error) => errors.push(error.message),
* result.variableValues,
* undefined,
* true,
* );
*
* errors; // => []
* ```
*/
function validateInputLiteral(valueNode, type, onError, variables, fragmentVariableValues, hideSuggestions) {
const context = {
static: !variables && !fragmentVariableValues,
onError,
variables,
fragmentVariableValues,
};
return validateInputLiteralImpl(context, valueNode, type, hideSuggestions, undefined);
}
function validateInputLiteralImpl(context, valueNode, type, hideSuggestions, path) {
if (valueNode.kind === graphql_1.Kind.VARIABLE) {
if (context.static) {
// If no variable values are provided, this is being validated statically,
// and cannot yet produce any validation errors for variables.
return;
}
const scopedVariableValues = getScopedVariableValues(context, valueNode);
const value = scopedVariableValues?.coerced[valueNode.name.value];
if ((0, graphql_1.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);
}
}
// Note: This does no further checking that this variable is correct.
// This assumes this variable usage has already been validated.
return;
}
if ((0, graphql_1.isNonNullType)(type)) {
if (valueNode.kind === graphql_1.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 === graphql_1.Kind.NULL) {
return;
}
if ((0, graphql_1.isListType)(type)) {
if (valueNode.kind !== graphql_1.Kind.LIST) {
// Lists accept a non-list value as a list of one.
validateInputLiteralImpl(context, valueNode, type.ofType, hideSuggestions, path);
}
else {
let index = 0;
for (const itemNode of valueNode.values) {
validateInputLiteralImpl(context, itemNode, type.ofType, hideSuggestions, (0, utils_1.addPath)(path, index++, undefined));
}
}
}
else if ((0, graphql_1.isInputObjectType)(type)) {
if (valueNode.kind !== graphql_1.Kind.OBJECT) {
reportInvalidLiteral(context.onError, `Expected value of type "${type}" to be an object, found: ${(0, graphql_1.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 ((0, graphql_1.isRequiredInputField)(field)) {
reportInvalidLiteral(context.onError, `Expected value of type "${type}" to include required field "${field.name}", found: ${(0, graphql_1.print)(valueNode)}.`, valueNode, path);
}
}
else {
const fieldValueNode = fieldNode.value;
if (fieldValueNode.kind === graphql_1.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 && !(0, graphql_1.isRequiredInputField)(field)) {
continue;
}
}
validateInputLiteralImpl(context, fieldValueNode, field.type, hideSuggestions, (0, utils_1.addPath)(path, field.name, type.name));
}
}
const fields = valueNode.fields;
const knownFields = [];
// Ensure every provided field is defined.
for (const fieldNode of fields) {
const fieldName = fieldNode.name.value;
if (!Object.hasOwn(fieldDefs, fieldName)) {
const suggestion = hideSuggestions
? ''
: (0, didYouMean_js_1.didYouMean)((0, suggestionList_js_1.suggestionList)(fieldName, Object.keys(fieldDefs)));
reportInvalidLiteral(context.onError, `Expected value of type "${type}" not to include unknown field "${fieldName}"${suggestion ? `.${suggestion} Found` : ', found'}: ${(0, graphql_1.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 === graphql_1.Kind.NULL) {
const fieldName = knownFields[0].name.value;
reportInvalidLiteral(context.onError, getOneOfInputObjectErrorMessage(type), valueNode, (0, utils_1.addPath)(path, fieldName, undefined));
}
}
}
else {
(0, graphql_1.assertLeafType)(type);
let result;
let caughtError;
try {
const typeAny = type;
result = typeAny.coerceInputLiteral
? typeAny.coerceInputLiteral(valueNode, context.variables, hideSuggestions)
: type.parseLiteral(valueNode, context.variables?.coerced);
}
catch (error) {
if (error instanceof graphql_1.GraphQLError) {
context.onError(error, (0, utils_1.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'}: ${(0, graphql_1.print)(valueNode)}.`, valueNode, path, caughtError);
}
}
}
function getScopedVariableValues(context, valueNode) {
const variableName = valueNode.name.value;
const { fragmentVariableValues, variables } = context;
return fragmentVariableValues?.sources?.[variableName] ? fragmentVariableValues : variables;
}
function reportInvalidLiteral(onError, message, valueNode, path, originalError) {
onError((0, utils_1.createGraphQLError)(message, {
nodes: valueNode,
originalError,
}), (0, utils_1.pathToArray)(path));
}
function getCaughtErrorMessage(caughtError) {
if ((0, utils_1.isObjectLike)(caughtError)) {
const message = caughtError['message'];
if (typeof message === 'string' && message !== '') {
return message;
}
}
return String(caughtError);
}
function getOneOfInputObjectErrorMessage(type) {
return `Within OneOf Input Object type "${type}", exactly one field must be specified, and the value for that field must be non-null.`;
}
/**
* Creates a keyed JS object from an array, given a function to produce the keys
* for each value in the array.
*
* This provides a convenient lookup for the array items if the key function
* produces unique results.
* @internal
* @example
* ```ts
* const phoneBook = [
* { name: 'Jon', num: '555-1234' },
* { name: 'Jenny', num: '867-5309' },
* ];
*
* const entriesByName = keyMap(phoneBook, (entry) => entry.name);
*
* Object.keys(entriesByName); // => ['Jon', 'Jenny']
* entriesByName['Jenny']; // => { name: 'Jenny', num: '867-5309' }
* ```
*/
function keyMap(list, keyFn) {
const result = Object.create(null);
for (const item of list) {
result[keyFn(item)] = item;
}
return result;
}