From 14f260bb91fe58416e038fb444301adb405c24ae Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Wed, 7 Aug 2019 12:44:17 +0300 Subject: [PATCH] Limits errors in getVariableValues() (#2062) Based on #2037 --- src/execution/__tests__/variables-test.js | 53 +++- src/execution/execute.js | 1 + src/execution/values.js | 72 ++++-- src/index.js | 4 +- src/jsutils/Path.js | 2 +- src/jsutils/printPathArray.js | 14 + ...Value-test.js => coerceInputValue-test.js} | 158 ++++++++++-- src/utilities/coerceInputValue.js | 214 +++++++++++++++ src/utilities/coerceValue.js | 243 +++--------------- src/utilities/index.js | 5 +- src/utilities/isValidJSValue.js | 2 +- 11 files changed, 503 insertions(+), 265 deletions(-) create mode 100644 src/jsutils/printPathArray.js rename src/utilities/__tests__/{coerceValue-test.js => coerceInputValue-test.js} (66%) create mode 100644 src/utilities/coerceInputValue.js diff --git a/src/execution/__tests__/variables-test.js b/src/execution/__tests__/variables-test.js index 17a4fe6f9b..06ac502c99 100644 --- a/src/execution/__tests__/variables-test.js +++ b/src/execution/__tests__/variables-test.js @@ -4,7 +4,9 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import inspect from '../../jsutils/inspect'; +import invariant from '../../jsutils/invariant'; +import { Kind } from '../../language/kinds'; import { parse } from '../../language/parser'; import { GraphQLSchema } from '../../type/schema'; @@ -19,6 +21,7 @@ import { } from '../../type/definition'; import { execute } from '../execute'; +import { getVariableValues } from '../values'; const TestComplexScalar = new GraphQLScalarType({ name: 'ComplexScalar', @@ -369,7 +372,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" got invalid value { a: "foo", b: "bar", c: null }; Expected non-nullable type String! not to be null at value.c.', + 'Variable "$input" got invalid value null at "input.c"; Expected non-nullable type String! not to be null.', locations: [{ line: 2, column: 16 }], }, ], @@ -397,7 +400,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" got invalid value { a: "foo", b: "bar" }; Field of required type String! was not provided at value.c.', + 'Variable "$input" got invalid value { a: "foo", b: "bar" }; Field c of required type String! was not provided.', locations: [{ line: 2, column: 16 }], }, ], @@ -416,12 +419,12 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" got invalid value { na: { a: "foo" } }; Field of required type String! was not provided at value.na.c.', + 'Variable "$input" got invalid value { a: "foo" } at "input.na"; Field c of required type String! was not provided.', locations: [{ line: 2, column: 18 }], }, { message: - 'Variable "$input" got invalid value { na: { a: "foo" } }; Field of required type String! was not provided at value.nb.', + 'Variable "$input" got invalid value { na: { a: "foo" } }; Field nb of required type String! was not provided.', locations: [{ line: 2, column: 18 }], }, ], @@ -830,7 +833,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" got invalid value ["A", null, "B"]; Expected non-nullable type String! not to be null at value[1].', + 'Variable "$input" got invalid value null at "input[1]"; Expected non-nullable type String! not to be null.', locations: [{ line: 2, column: 16 }], }, ], @@ -879,7 +882,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" got invalid value ["A", null, "B"]; Expected non-nullable type String! not to be null at value[1].', + 'Variable "$input" got invalid value null at "input[1]"; Expected non-nullable type String! not to be null.', locations: [{ line: 2, column: 16 }], }, ], @@ -986,4 +989,42 @@ describe('Execute: Handles inputs', () => { }); }); }); + + describe('getVariableValues: limit maximum number of coercion errors', () => { + it('when values are invalid', () => { + const doc = parse(` + query ($input: [String!]) { + listNN(input: $input) + } + `); + const operation = doc.definitions[0]; + invariant(operation.kind === Kind.OPERATION_DEFINITION); + + const result = getVariableValues( + schema, + operation.variableDefinitions || [], + { input: [0, 1, 2] }, + { maxErrors: 2 }, + ); + + expect(result).to.deep.equal({ + errors: [ + { + message: + 'Variable "$input" got invalid value 0 at "input[0]"; Expected type String. String cannot represent a non string value: 0', + locations: [{ line: 2, column: 16 }], + }, + { + message: + 'Variable "$input" got invalid value 1 at "input[1]"; Expected type String. String cannot represent a non string value: 1', + locations: [{ line: 2, column: 16 }], + }, + { + message: + 'Too many errors processing variables, error limit reached. Execution aborted.', + }, + ], + }); + }); + }); }); diff --git a/src/execution/execute.js b/src/execution/execute.js index 93e9d76fc1..aa1e54776c 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -319,6 +319,7 @@ export function buildExecutionContext( schema, operation.variableDefinitions || [], rawVariableValues || {}, + { maxErrors: 50 }, ); if (coercedVariableValues.errors) { diff --git a/src/execution/values.js b/src/execution/values.js index d4dd60e9e5..945528191e 100644 --- a/src/execution/values.js +++ b/src/execution/values.js @@ -5,6 +5,7 @@ import find from '../polyfills/find'; import keyMap from '../jsutils/keyMap'; import inspect from '../jsutils/inspect'; import { type ObjMap } from '../jsutils/ObjMap'; +import printPathArray from '../jsutils/printPathArray'; import { GraphQLError } from '../error/GraphQLError'; @@ -24,9 +25,9 @@ import { isNonNullType, } from '../type/definition'; -import { coerceValue } from '../utilities/coerceValue'; import { typeFromAST } from '../utilities/typeFromAST'; import { valueFromAST } from '../utilities/valueFromAST'; +import { coerceInputValue } from '../utilities/coerceInputValue'; type CoercedVariableValues = | {| errors: $ReadOnlyArray |} @@ -45,8 +46,36 @@ export function getVariableValues( schema: GraphQLSchema, varDefNodes: $ReadOnlyArray, inputs: { +[variable: string]: mixed, ... }, + options?: {| maxErrors?: number |}, ): CoercedVariableValues { + const maxErrors = options && options.maxErrors; const errors = []; + try { + const coerced = coerceVariableValues(schema, varDefNodes, inputs, error => { + if (maxErrors != null && errors.length >= maxErrors) { + throw new GraphQLError( + 'Too many errors processing variables, error limit reached. Execution aborted.', + ); + } + errors.push(error); + }); + + if (errors.length === 0) { + return { coerced }; + } + } catch (error) { + errors.push(error); + } + + return { errors }; +} + +function coerceVariableValues( + schema: GraphQLSchema, + varDefNodes: $ReadOnlyArray, + inputs: { +[variable: string]: mixed, ... }, + onError: GraphQLError => void, +): { [variable: string]: mixed, ... } { const coercedValues = {}; for (const varDefNode of varDefNodes) { const varName = varDefNode.variable.name.value; @@ -55,7 +84,7 @@ export function getVariableValues( // Must use input types for variables. This should be caught during // validation, however is checked again here for safety. const varTypeStr = print(varDefNode.type); - errors.push( + onError( new GraphQLError( `Variable "$${varName}" expected value of type "${varTypeStr}" which cannot be used as an input type.`, varDefNode.type, @@ -71,7 +100,7 @@ export function getVariableValues( if (isNonNullType(varType)) { const varTypeStr = inspect(varType); - errors.push( + onError( new GraphQLError( `Variable "$${varName}" of required type "${varTypeStr}" was not provided.`, varDefNode, @@ -84,7 +113,7 @@ export function getVariableValues( const value = inputs[varName]; if (value === null && isNonNullType(varType)) { const varTypeStr = inspect(varType); - errors.push( + onError( new GraphQLError( `Variable "$${varName}" of non-null type "${varTypeStr}" must not be null.`, varDefNode, @@ -93,21 +122,30 @@ export function getVariableValues( continue; } - const coerced = coerceValue(value, varType, varDefNode); - if (coerced.errors) { - for (const error of coerced.errors) { - error.message = - `Variable "$${varName}" got invalid value ${inspect(value)}; ` + - error.message; - } - errors.push(...coerced.errors); - continue; - } - - coercedValues[varName] = coerced.value; + coercedValues[varName] = coerceInputValue( + value, + varType, + (path, invalidValue, error) => { + let prefix = + `Variable "$${varName}" got invalid value ` + inspect(invalidValue); + if (path.length > 0) { + prefix += ` at "${varName}${printPathArray(path)}"`; + } + onError( + new GraphQLError( + prefix + '; ' + error.message, + varDefNode, + undefined, + undefined, + undefined, + error.originalError, + ), + ); + }, + ); } - return errors.length === 0 ? { coerced: coercedValues } : { errors }; + return coercedValues; } /** diff --git a/src/index.js b/src/index.js index b56d72bf1c..614a048cef 100644 --- a/src/index.js +++ b/src/index.js @@ -386,8 +386,10 @@ export { // the GraphQL type system. TypeInfo, // Coerces a JavaScript value to a GraphQL type, or produces errors. + coerceInputValue, + // @deprecated use coerceInputValue - will be removed in v15 coerceValue, - // @deprecated use coerceValue - will be removed in v15 + // @deprecated use coerceInputValue - will be removed in v15 isValidJSValue, // @deprecated use validation - will be removed in v15 isValidLiteralValue, diff --git a/src/jsutils/Path.js b/src/jsutils/Path.js index 2dd1363cc9..17f78e6c77 100644 --- a/src/jsutils/Path.js +++ b/src/jsutils/Path.js @@ -15,7 +15,7 @@ export function addPath(prev: $ReadOnly | void, key: string | number) { /** * Given a Path, return an Array of the path keys. */ -export function pathToArray(path: $ReadOnly): Array { +export function pathToArray(path: ?$ReadOnly): Array { const flattened = []; let curr = path; while (curr) { diff --git a/src/jsutils/printPathArray.js b/src/jsutils/printPathArray.js new file mode 100644 index 0000000000..f850c5e124 --- /dev/null +++ b/src/jsutils/printPathArray.js @@ -0,0 +1,14 @@ +// @flow strict + +/** + * Build a string describing the path. + */ +export default function printPathArray( + path: $ReadOnlyArray, +): string { + return path + .map(key => + typeof key === 'number' ? '[' + key.toString() + ']' : '.' + key, + ) + .join(''); +} diff --git a/src/utilities/__tests__/coerceValue-test.js b/src/utilities/__tests__/coerceInputValue-test.js similarity index 66% rename from src/utilities/__tests__/coerceValue-test.js rename to src/utilities/__tests__/coerceInputValue-test.js index be6953299c..646e13bc2f 100644 --- a/src/utilities/__tests__/coerceValue-test.js +++ b/src/utilities/__tests__/coerceInputValue-test.js @@ -14,20 +14,29 @@ import { GraphQLInputObjectType, } from '../../type/definition'; -import { coerceValue } from '../coerceValue'; +import { coerceInputValue } from '../coerceInputValue'; function expectValue(result) { - expect(result.errors).to.equal(undefined); + expect(result.errors).to.deep.equal([]); return expect(result.value); } function expectErrors(result) { - expect(result.value).to.equal(undefined); - const messages = result.errors && result.errors.map(error => error.message); - return expect(messages); + return expect(result.errors); } -describe('coerceValue', () => { +describe('coerceInputValue', () => { + function coerceValue(inputValue, type) { + const errors = []; + + const value = coerceInputValue(inputValue, type, onError); + return { errors, value }; + + function onError(path, invalidValue, error) { + errors.push({ path, value: invalidValue, error: error.message }); + } + } + describe('for GraphQLNonNull', () => { const TestNonNull = new GraphQLNonNull(GraphQLInt); @@ -39,14 +48,22 @@ describe('coerceValue', () => { it('returns an error for undefined value', () => { const result = coerceValue(undefined, TestNonNull); expectErrors(result).to.deep.equal([ - 'Expected non-nullable type Int! not to be null.', + { + error: 'Expected non-nullable type Int! not to be null.', + path: [], + value: undefined, + }, ]); }); it('returns an error for null value', () => { const result = coerceValue(null, TestNonNull); expectErrors(result).to.deep.equal([ - 'Expected non-nullable type Int! not to be null.', + { + error: 'Expected non-nullable type Int! not to be null.', + path: [], + value: null, + }, ]); }); }); @@ -57,7 +74,7 @@ describe('coerceValue', () => { parseValue(input) { invariant(typeof input === 'object' && input !== null); if (input.error != null) { - throw input.error; + throw new Error(input.error); } return input.value; }, @@ -80,14 +97,24 @@ describe('coerceValue', () => { it('returns an error for undefined result', () => { const result = coerceValue({ value: undefined }, TestScalar); - expectErrors(result).to.deep.equal(['Expected type TestScalar.']); + expectErrors(result).to.deep.equal([ + { + error: 'Expected type TestScalar.', + path: [], + value: { value: undefined }, + }, + ]); }); it('returns an error for undefined result', () => { - const error = new Error('Some error message'); - const result = coerceValue({ error }, TestScalar); + const inputValue = { error: 'Some error message' }; + const result = coerceValue(inputValue, TestScalar); expectErrors(result).to.deep.equal([ - 'Expected type TestScalar. Some error message', + { + error: 'Expected type TestScalar. Some error message', + path: [], + value: { error: 'Some error message' }, + }, ]); }); }); @@ -112,16 +139,32 @@ describe('coerceValue', () => { it('returns an error for misspelled enum value', () => { const result = coerceValue('foo', TestEnum); expectErrors(result).to.deep.equal([ - 'Expected type TestEnum. Did you mean FOO?', + { + error: 'Expected type TestEnum. Did you mean FOO?', + path: [], + value: 'foo', + }, ]); }); it('returns an error for incorrect value type', () => { const result1 = coerceValue(123, TestEnum); - expectErrors(result1).to.deep.equal(['Expected type TestEnum.']); + expectErrors(result1).to.deep.equal([ + { + error: 'Expected type TestEnum.', + path: [], + value: 123, + }, + ]); const result2 = coerceValue({ field: 'value' }, TestEnum); - expectErrors(result2).to.deep.equal(['Expected type TestEnum.']); + expectErrors(result2).to.deep.equal([ + { + error: 'Expected type TestEnum.', + path: [], + value: { field: 'value' }, + }, + ]); }); }); @@ -142,29 +185,52 @@ describe('coerceValue', () => { it('returns an error for a non-object type', () => { const result = coerceValue(123, TestInputObject); expectErrors(result).to.deep.equal([ - 'Expected type TestInputObject to be an object.', + { + error: 'Expected type TestInputObject to be an object.', + path: [], + value: 123, + }, ]); }); it('returns an error for an invalid field', () => { const result = coerceValue({ foo: NaN }, TestInputObject); expectErrors(result).to.deep.equal([ - 'Expected type Int at value.foo. Int cannot represent non-integer value: NaN', + { + error: + 'Expected type Int. Int cannot represent non-integer value: NaN', + path: ['foo'], + value: NaN, + }, ]); }); it('returns multiple errors for multiple invalid fields', () => { const result = coerceValue({ foo: 'abc', bar: 'def' }, TestInputObject); expectErrors(result).to.deep.equal([ - 'Expected type Int at value.foo. Int cannot represent non-integer value: "abc"', - 'Expected type Int at value.bar. Int cannot represent non-integer value: "def"', + { + error: + 'Expected type Int. Int cannot represent non-integer value: "abc"', + path: ['foo'], + value: 'abc', + }, + { + error: + 'Expected type Int. Int cannot represent non-integer value: "def"', + path: ['bar'], + value: 'def', + }, ]); }); it('returns error for a missing required field', () => { const result = coerceValue({ bar: 123 }, TestInputObject); expectErrors(result).to.deep.equal([ - 'Field of required type Int! was not provided at value.foo.', + { + error: 'Field foo of required type Int! was not provided.', + path: [], + value: { bar: 123 }, + }, ]); }); @@ -174,14 +240,23 @@ describe('coerceValue', () => { TestInputObject, ); expectErrors(result).to.deep.equal([ - 'Field "unknownField" is not defined by type TestInputObject.', + { + error: 'Field "unknownField" is not defined by type TestInputObject.', + path: [], + value: { foo: 123, unknownField: 123 }, + }, ]); }); it('returns error for a misspelled field', () => { const result = coerceValue({ foo: 123, bart: 123 }, TestInputObject); expectErrors(result).to.deep.equal([ - 'Field "bart" is not defined by type TestInputObject. Did you mean bar?', + { + error: + 'Field "bart" is not defined by type TestInputObject. Did you mean bar?', + path: [], + value: { foo: 123, bart: 123 }, + }, ]); }); }); @@ -232,8 +307,18 @@ describe('coerceValue', () => { it('returns an error for an invalid input', () => { const result = coerceValue([1, 'b', true, 4], TestList); expectErrors(result).to.deep.equal([ - 'Expected type Int at value[1]. Int cannot represent non-integer value: "b"', - 'Expected type Int at value[2]. Int cannot represent non-integer value: true', + { + error: + 'Expected type Int. Int cannot represent non-integer value: "b"', + path: [1], + value: 'b', + }, + { + error: + 'Expected type Int. Int cannot represent non-integer value: true', + path: [2], + value: true, + }, ]); }); @@ -245,7 +330,12 @@ describe('coerceValue', () => { it('returns an error for a non-list invalid value', () => { const result = coerceValue('INVALID', TestList); expectErrors(result).to.deep.equal([ - 'Expected type Int. Int cannot represent non-integer value: "INVALID"', + { + error: + 'Expected type Int. Int cannot represent non-integer value: "INVALID"', + path: [], + value: 'INVALID', + }, ]); }); @@ -283,4 +373,20 @@ describe('coerceValue', () => { expectValue(result).to.deep.equal([[42], [null], null]); }); }); + + describe('with default onError', () => { + it('throw error without path', () => { + expect(() => coerceInputValue(null, GraphQLNonNull(GraphQLInt))).to.throw( + 'Invalid value null: Expected non-nullable type Int! not to be null.', + ); + }); + + it('throw error with path', () => { + expect(() => + coerceInputValue([null], GraphQLList(GraphQLNonNull(GraphQLInt))), + ).to.throw( + 'Invalid value null at "value[0]": : Expected non-nullable type Int! not to be null.', + ); + }); + }); }); diff --git a/src/utilities/coerceInputValue.js b/src/utilities/coerceInputValue.js new file mode 100644 index 0000000000..cd2fe88701 --- /dev/null +++ b/src/utilities/coerceInputValue.js @@ -0,0 +1,214 @@ +// @flow strict + +import { forEach, isCollection } from 'iterall'; + +import objectValues from '../polyfills/objectValues'; + +import inspect from '../jsutils/inspect'; +import invariant from '../jsutils/invariant'; +import didYouMean from '../jsutils/didYouMean'; +import isObjectLike from '../jsutils/isObjectLike'; +import suggestionList from '../jsutils/suggestionList'; +import printPathArray from '../jsutils/printPathArray'; +import { type Path, addPath, pathToArray } from '../jsutils/Path'; + +import { GraphQLError } from '../error/GraphQLError'; +import { + type GraphQLInputType, + isScalarType, + isEnumType, + isInputObjectType, + isListType, + isNonNullType, +} from '../type/definition'; + +type OnErrorCB = ( + path: $ReadOnlyArray, + invalidValue: mixed, + error: GraphQLError, +) => void; + +/** + * Coerces a JavaScript value given a GraphQL Input Type. + */ +export function coerceInputValue( + inputValue: mixed, + type: GraphQLInputType, + onError?: OnErrorCB = defaultOnError, +): mixed { + return coerceInputValueImpl(inputValue, type, onError); +} + +function defaultOnError( + path: $ReadOnlyArray, + invalidValue: mixed, + error: GraphQLError, +) { + let errorPrefix = 'Invalid value ' + inspect(invalidValue); + if (path.length > 0) { + errorPrefix += ` at "value${printPathArray(path)}": `; + } + error.message = errorPrefix + ': ' + error.message; + throw error; +} + +function coerceInputValueImpl( + inputValue: mixed, + type: GraphQLInputType, + onError: OnErrorCB, + path: Path | void, +): mixed { + if (isNonNullType(type)) { + if (inputValue != null) { + return coerceInputValueImpl(inputValue, type.ofType, onError, path); + } + onError( + pathToArray(path), + inputValue, + new GraphQLError( + `Expected non-nullable type ${inspect(type)} not to be null.`, + ), + ); + return; + } + + if (inputValue == null) { + // Explicitly return the value null. + return null; + } + + if (isListType(type)) { + const itemType = type.ofType; + if (isCollection(inputValue)) { + const coercedValue = []; + forEach((inputValue: any), (itemValue, index) => { + coercedValue.push( + coerceInputValueImpl( + itemValue, + itemType, + onError, + addPath(path, index), + ), + ); + }); + return coercedValue; + } + // Lists accept a non-list value as a list of one. + return [coerceInputValueImpl(inputValue, itemType, onError, path)]; + } + + if (isInputObjectType(type)) { + if (!isObjectLike(inputValue)) { + onError( + pathToArray(path), + inputValue, + new GraphQLError(`Expected type ${type.name} to be an object.`), + ); + return; + } + + const coercedValue = {}; + const fieldDefs = type.getFields(); + + for (const field of objectValues(fieldDefs)) { + const fieldValue = inputValue[field.name]; + + if (fieldValue === undefined) { + if (field.defaultValue !== undefined) { + coercedValue[field.name] = field.defaultValue; + } else if (isNonNullType(field.type)) { + const typeStr = inspect(field.type); + onError( + pathToArray(path), + inputValue, + new GraphQLError( + `Field ${field.name} of required type ${typeStr} was not provided.`, + ), + ); + } + continue; + } + + coercedValue[field.name] = coerceInputValueImpl( + fieldValue, + field.type, + onError, + addPath(path, field.name), + ); + } + + // Ensure every provided field is defined. + for (const fieldName of Object.keys(inputValue)) { + if (!fieldDefs[fieldName]) { + const suggestions = suggestionList( + fieldName, + Object.keys(type.getFields()), + ); + onError( + pathToArray(path), + inputValue, + new GraphQLError( + `Field "${fieldName}" is not defined by type ${type.name}.` + + didYouMean(suggestions), + ), + ); + } + } + return coercedValue; + } + + if (isScalarType(type)) { + let parseResult; + + // Scalars determine if a input value is valid via parseValue(), which can + // throw to indicate failure. If it throws, maintain a reference to + // the original error. + try { + parseResult = type.parseValue(inputValue); + } catch (error) { + onError( + pathToArray(path), + inputValue, + new GraphQLError( + `Expected type ${type.name}. ` + error.message, + undefined, + undefined, + undefined, + undefined, + error, + ), + ); + return; + } + if (parseResult === undefined) { + onError( + pathToArray(path), + inputValue, + new GraphQLError(`Expected type ${type.name}.`), + ); + } + return parseResult; + } + + if (isEnumType(type)) { + if (typeof inputValue === 'string') { + const enumValue = type.getValue(inputValue); + if (enumValue) { + return enumValue.value; + } + } + const suggestions = suggestionList( + String(inputValue), + type.getValues().map(enumValue => enumValue.name), + ); + onError( + pathToArray(path), + inputValue, + new GraphQLError(`Expected type ${type.name}.` + didYouMean(suggestions)), + ); + return; + } + + // Not reachable. All possible input types have been considered. + invariant(false, 'Unexpected input type: ' + inspect((type: empty))); +} diff --git a/src/utilities/coerceValue.js b/src/utilities/coerceValue.js index e2fecfd5dd..e0ba4a9862 100644 --- a/src/utilities/coerceValue.js +++ b/src/utilities/coerceValue.js @@ -1,26 +1,15 @@ // @flow strict -import { forEach, isCollection } from 'iterall'; - -import objectValues from '../polyfills/objectValues'; - +/* istanbul ignore file */ import inspect from '../jsutils/inspect'; -import invariant from '../jsutils/invariant'; -import didYouMean from '../jsutils/didYouMean'; -import isObjectLike from '../jsutils/isObjectLike'; -import suggestionList from '../jsutils/suggestionList'; -import { type Path, addPath, pathToArray } from '../jsutils/Path'; +import printPathArray from '../jsutils/printPathArray'; +import { type Path, pathToArray } from '../jsutils/Path'; import { GraphQLError } from '../error/GraphQLError'; import { type ASTNode } from '../language/ast'; -import { - type GraphQLInputType, - isScalarType, - isEnumType, - isInputObjectType, - isListType, - isNonNullType, -} from '../type/definition'; +import { type GraphQLInputType } from '../type/definition'; + +import { coerceInputValue } from './coerceInputValue'; type CoercedValue = {| +errors: $ReadOnlyArray | void, @@ -28,210 +17,40 @@ type CoercedValue = {| |}; /** - * Coerces a JavaScript value given a GraphQL Type. - * - * Returns either a value which is valid for the provided type or a list of - * encountered coercion errors. + * Deprecated. Use coerceInputValue() directly for richer information. * + * This function will be removed in v15 */ export function coerceValue( - value: mixed, + inputValue: mixed, type: GraphQLInputType, blameNode?: ASTNode, path?: Path, ): CoercedValue { - // A value must be provided if the type is non-null. - if (isNonNullType(type)) { - if (value == null) { - return ofErrors([ - coercionError( - `Expected non-nullable type ${inspect(type)} not to be null`, - blameNode, - path, - ), - ]); - } - return coerceValue(value, type.ofType, blameNode, path); - } - - if (value == null) { - // Explicitly return the value null. - return ofValue(null); - } - - if (isScalarType(type)) { - // Scalars determine if a value is valid via parseValue(), which can - // throw to indicate failure. If it throws, maintain a reference to - // the original error. - try { - const parseResult = type.parseValue(value); - if (parseResult === undefined) { - return ofErrors([ - coercionError(`Expected type ${type.name}`, blameNode, path), - ]); + const errors = []; + const value = coerceInputValue( + inputValue, + type, + (errorPath, invalidValue, error) => { + let errorPrefix = 'Invalid value ' + inspect(invalidValue); + const pathArray = [...pathToArray(path), ...errorPath]; + if (pathArray.length > 0) { + errorPrefix += ` at "value${printPathArray(pathArray)}"`; } - return ofValue(parseResult); - } catch (error) { - return ofErrors([ - coercionError( - `Expected type ${type.name}`, + errors.push( + new GraphQLError( + errorPrefix + ': ' + error.message, blameNode, - path, - ' ' + error.message, - error, + undefined, + undefined, + undefined, + error.originalError, ), - ]); - } - } - - if (isEnumType(type)) { - if (typeof value === 'string') { - const enumValue = type.getValue(value); - if (enumValue) { - return ofValue(enumValue.value); - } - } - const suggestions = suggestionList( - String(value), - type.getValues().map(enumValue => enumValue.name), - ); - return ofErrors([ - coercionError( - `Expected type ${type.name}`, - blameNode, - path, - didYouMean(suggestions), - ), - ]); - } - - if (isListType(type)) { - const itemType = type.ofType; - if (isCollection(value)) { - let errors; - const coercedValue = []; - forEach((value: any), (itemValue, index) => { - const coercedItem = coerceValue( - itemValue, - itemType, - blameNode, - addPath(path, index), - ); - if (coercedItem.errors) { - errors = add(errors, coercedItem.errors); - } else if (!errors) { - coercedValue.push(coercedItem.value); - } - }); - return errors ? ofErrors(errors) : ofValue(coercedValue); - } - // Lists accept a non-list value as a list of one. - const coercedItem = coerceValue(value, itemType, blameNode); - return coercedItem.errors ? coercedItem : ofValue([coercedItem.value]); - } - - if (isInputObjectType(type)) { - if (!isObjectLike(value)) { - return ofErrors([ - coercionError( - `Expected type ${type.name} to be an object`, - blameNode, - path, - ), - ]); - } - let errors; - const coercedValue = {}; - const fields = type.getFields(); - - // Ensure every defined field is valid. - for (const field of objectValues(fields)) { - const fieldPath = addPath(path, field.name); - const fieldValue = value[field.name]; - if (fieldValue === undefined) { - if (field.defaultValue !== undefined) { - coercedValue[field.name] = field.defaultValue; - } else if (isNonNullType(field.type)) { - errors = add( - errors, - coercionError( - `Field of required type ${inspect(field.type)} was not provided`, - blameNode, - fieldPath, - ), - ); - } - } else { - const coercedField = coerceValue( - fieldValue, - field.type, - blameNode, - fieldPath, - ); - if (coercedField.errors) { - errors = add(errors, coercedField.errors); - } else if (!errors) { - coercedValue[field.name] = coercedField.value; - } - } - } - - // Ensure every provided field is defined. - for (const fieldName of Object.keys(value)) { - if (!fields[fieldName]) { - const suggestions = suggestionList(fieldName, Object.keys(fields)); - errors = add( - errors, - coercionError( - `Field "${fieldName}" is not defined by type ${type.name}`, - blameNode, - path, - didYouMean(suggestions), - ), - ); - } - } - - return errors ? ofErrors(errors) : ofValue(coercedValue); - } - - // Not reachable. All possible input types have been considered. - invariant(false, 'Unexpected input type: ' + inspect((type: empty))); -} - -function ofValue(value) { - return { errors: undefined, value }; -} - -function ofErrors(errors) { - return { errors, value: undefined }; -} - -function add(errors, moreErrors) { - return (errors || []).concat(moreErrors); -} - -function coercionError(message, blameNode, path, subMessage, originalError) { - let fullMessage = message; - - // Build a string describing the path into the value where the error was found - if (path) { - fullMessage += ' at value'; - for (const key of pathToArray(path)) { - fullMessage += - typeof key === 'string' ? '.' + key : '[' + key.toString() + ']'; - } - } - - fullMessage += subMessage ? '.' + subMessage : '.'; - - // Return a GraphQLError instance - return new GraphQLError( - fullMessage, - blameNode, - undefined, - undefined, - undefined, - originalError, + ); + }, ); + + return errors.length > 0 + ? { errors, value: undefined } + : { errors: undefined, value }; } diff --git a/src/utilities/index.js b/src/utilities/index.js index 636c98dad1..50e7f94172 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -86,9 +86,12 @@ export { astFromValue } from './astFromValue'; export { TypeInfo } from './TypeInfo'; // Coerces a JavaScript value to a GraphQL type, or produces errors. +export { coerceInputValue } from './coerceInputValue'; + +// @deprecated use coerceInputValue - will be removed in v15. export { coerceValue } from './coerceValue'; -// @deprecated use coerceValue - will be removed in v15. +// @deprecated use coerceInputValue - will be removed in v15. export { isValidJSValue } from './isValidJSValue'; // @deprecated use validation - will be removed in v15 diff --git a/src/utilities/isValidJSValue.js b/src/utilities/isValidJSValue.js index fb172ad9d9..af2fe40d6e 100644 --- a/src/utilities/isValidJSValue.js +++ b/src/utilities/isValidJSValue.js @@ -6,7 +6,7 @@ import { type GraphQLInputType } from '../type/definition'; import { coerceValue } from './coerceValue'; /** - * Deprecated. Use coerceValue() directly for richer information. + * Deprecated. Use coerceInputValue() directly for richer information. * * This function will be removed in v15 */