UNPKG

@graphql-tools/executor

Version:

Fork of GraphQL.js' execute function

434 lines (433 loc) • 18.4 kB
"use strict"; 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; }