From 855af69d874d3f8e8b359272a7d5e9ae39e5abf2 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Thu, 13 May 2021 10:30:16 -0700 Subject: [PATCH] Input Value Validation Depends on #3065 Factors out input validation to reusable functions: * Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`. * Introduces `validateInputValue` by extracting this behavior from `coerceInputValue` * Simplifies `coerceInputValue` to return early on validation error * Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved. These two parallel functions will be used to validate default values in #3049 --- src/execution/__tests__/nonnull-test.js | 6 +- src/execution/__tests__/variables-test.js | 35 +- src/execution/values.js | 146 ++--- src/index.d.ts | 4 + src/index.js | 4 + src/jsutils/printPathArray.js | 11 +- src/subscription/__tests__/subscribe-test.js | 2 +- src/type/__tests__/enumType-test.js | 2 +- .../__tests__/coerceInputValue-test.js | 312 +++-------- .../__tests__/validateInputLiteral-test.js | 498 ++++++++++++++++++ .../__tests__/validateInputValue-test.js | 346 ++++++++++++ src/utilities/coerceInputValue.d.ts | 8 - src/utilities/coerceInputValue.js | 175 ++---- src/utilities/index.d.ts | 6 + src/utilities/index.js | 6 + src/utilities/validateInputLiteral.d.ts | 23 + src/utilities/validateInputLiteral.js | 243 +++++++++ src/utilities/validateInputValue.d.ts | 13 + src/utilities/validateInputValue.js | 162 ++++++ .../__tests__/ValuesOfCorrectTypeRule-test.js | 18 +- .../rules/ValuesOfCorrectTypeRule.js | 153 +----- 21 files changed, 1516 insertions(+), 657 deletions(-) create mode 100644 src/utilities/__tests__/validateInputLiteral-test.js create mode 100644 src/utilities/__tests__/validateInputValue-test.js create mode 100644 src/utilities/validateInputLiteral.d.ts create mode 100644 src/utilities/validateInputLiteral.js create mode 100644 src/utilities/validateInputValue.d.ts create mode 100644 src/utilities/validateInputValue.js diff --git a/src/execution/__tests__/nonnull-test.js b/src/execution/__tests__/nonnull-test.js index 4eb38f12b6e..f6b2a67f762 100644 --- a/src/execution/__tests__/nonnull-test.js +++ b/src/execution/__tests__/nonnull-test.js @@ -643,7 +643,7 @@ describe('Execute: handles non-nullable types', () => { errors: [ { message: - 'Argument "cannotBeNull" of non-null type "String!" must not be null.', + 'Argument "cannotBeNull" has invalid value: Expected value of non-null type "String!" not to be null.', locations: [{ line: 3, column: 42 }], path: ['withNonNullArg'], }, @@ -673,7 +673,7 @@ describe('Execute: handles non-nullable types', () => { errors: [ { message: - 'Argument "cannotBeNull" of required type "String!" was provided the variable "$testVar" which was not provided a runtime value.', + 'Argument "cannotBeNull" has invalid value: Expected variable "$testVar" provided to type "String!" to provide a runtime value.', locations: [{ line: 3, column: 42 }], path: ['withNonNullArg'], }, @@ -701,7 +701,7 @@ describe('Execute: handles non-nullable types', () => { errors: [ { message: - 'Argument "cannotBeNull" of non-null type "String!" must not be null.', + 'Argument "cannotBeNull" has invalid value: Expected variable "$testVar" provided to non-null type "String!" not to be null.', locations: [{ line: 3, column: 43 }], path: ['withNonNullArg'], }, diff --git a/src/execution/__tests__/variables-test.js b/src/execution/__tests__/variables-test.js index 1c45e2bc9da..d83938a20c5 100644 --- a/src/execution/__tests__/variables-test.js +++ b/src/execution/__tests__/variables-test.js @@ -198,7 +198,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Argument "input" has invalid value ["foo", "bar", "baz"].', + 'Argument "input" has invalid value: Expected value of type "TestInputObject" to be an object, found ["foo", "bar", "baz"].', path: ['fieldWithObjectInput'], locations: [{ line: 3, column: 41 }], }, @@ -368,7 +368,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" got invalid value null at "input.c"; Expected non-nullable type "String!" not to be null.', + 'Variable "$input" has invalid value at .c: Expected value of non-null type "String!" not to be null.', locations: [{ line: 2, column: 16 }], }, ], @@ -382,7 +382,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" got invalid value "foo bar"; Expected type "TestInputObject" to be an object.', + 'Variable "$input" has invalid value: Expected value of type "TestInputObject" to be an object, found "foo bar".', locations: [{ line: 2, column: 16 }], }, ], @@ -396,7 +396,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" got invalid value { a: "foo", b: "bar" }; Field "c" of required type "String!" was not provided.', + 'Variable "$input" has invalid value: Expected value of type "TestInputObject" to include required field "c", found { a: "foo", b: "bar" }', locations: [{ line: 2, column: 16 }], }, ], @@ -415,12 +415,12 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" got invalid value { a: "foo" } at "input.na"; Field "c" of required type "String!" was not provided.', + 'Variable "$input" has invalid value at .na: Expected value of type "TestInputObject" to include required field "c", found { a: "foo" }', locations: [{ line: 2, column: 18 }], }, { message: - 'Variable "$input" got invalid value { na: { a: "foo" } }; Field "nb" of required type "String!" was not provided.', + 'Variable "$input" has invalid value: Expected value of type "TestNestedInputObject" to include required field "nb", found { na: { a: "foo" } }', locations: [{ line: 2, column: 18 }], }, ], @@ -437,7 +437,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" got invalid value { a: "foo", b: "bar", c: "baz", extra: "dog" }; Field "extra" is not defined by type "TestInputObject".', + 'Variable "$input" has invalid value: Expected value of type "TestInputObject" not to include unknown field "extra", found { a: "foo", b: "bar", c: "baz", extra: "dog" }', locations: [{ line: 2, column: 16 }], }, ], @@ -612,7 +612,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$value" of required type "String!" was not provided.', + 'Variable "$value" has invalid value: Expected a value of non-null type "String!" to be provided.', locations: [{ line: 2, column: 16 }], }, ], @@ -631,7 +631,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$value" of non-null type "String!" must not be null.', + 'Variable "$value" has invalid value: Expected value of non-null type "String!" not to be null.', locations: [{ line: 2, column: 16 }], }, ], @@ -697,7 +697,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$value" got invalid value [1, 2, 3]; String cannot represent a non string value: [1, 2, 3]', + 'Variable "$value" has invalid value: String cannot represent a non string value: [1, 2, 3]', locations: [{ line: 2, column: 16 }], }, ], @@ -725,7 +725,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Argument "input" of required type "String!" was provided the variable "$foo" which was not provided a runtime value.', + 'Argument "input" has invalid value: Expected variable "$foo" provided to type "String!" to provide a runtime value.', locations: [{ line: 3, column: 50 }], path: ['fieldWithNonNullableStringInput'], }, @@ -780,7 +780,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" of non-null type "[String]!" must not be null.', + 'Variable "$input" has invalid value: Expected value of non-null type "[String]!" not to be null.', locations: [{ line: 2, column: 16 }], }, ], @@ -843,7 +843,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" got invalid value null at "input[1]"; Expected non-nullable type "String!" not to be null.', + 'Variable "$input" has invalid value at [1]: Expected value of non-null type "String!" not to be null.', locations: [{ line: 2, column: 16 }], }, ], @@ -862,7 +862,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" of non-null type "[String!]!" must not be null.', + 'Variable "$input" has invalid value: Expected value of non-null type "[String!]!" not to be null.', locations: [{ line: 2, column: 16 }], }, ], @@ -892,7 +892,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" got invalid value null at "input[1]"; Expected non-nullable type "String!" not to be null.', + 'Variable "$input" has invalid value at [1]: Expected value of non-null type "String!" not to be null.', locations: [{ line: 2, column: 16 }], }, ], @@ -976,7 +976,8 @@ describe('Execute: Handles inputs', () => { }, errors: [ { - message: 'Argument "input" has invalid value WRONG_TYPE.', + message: + 'Argument "input" has invalid value: String cannot represent a non string value: WRONG_TYPE', locations: [{ line: 3, column: 48 }], path: ['fieldWithDefaultArgumentValue'], }, @@ -1016,7 +1017,7 @@ describe('Execute: Handles inputs', () => { function invalidValueError(value: number, index: number) { return { - message: `Variable "$input" got invalid value ${value} at "input[${index}]"; String cannot represent a non string value: ${value}`, + message: `Variable "$input" has invalid value at [${index}]: String cannot represent a non string value: ${value}`, locations: [{ line: 2, column: 14 }], }; } diff --git a/src/execution/values.js b/src/execution/values.js index e04139247d8..a02e606a375 100644 --- a/src/execution/values.js +++ b/src/execution/values.js @@ -1,5 +1,6 @@ import type { ReadOnlyObjMap, ReadOnlyObjMapLike } from '../jsutils/ObjMap'; -import { inspect } from '../jsutils/inspect'; +import { hasOwnProperty } from '../jsutils/hasOwnProperty'; +import { invariant } from '../jsutils/invariant'; import { keyMap } from '../jsutils/keyMap'; import { printPathArray } from '../jsutils/printPathArray'; @@ -16,12 +17,18 @@ import { print } from '../language/printer'; import type { GraphQLSchema } from '../type/schema'; import type { GraphQLInputType, GraphQLField } from '../type/definition'; import type { GraphQLDirective } from '../type/directives'; -import { isInputType, isNonNullType } from '../type/definition'; +import { + isInputType, + isNonNullType, + isRequiredInput, +} from '../type/definition'; import { getCoercedDefaultValue } from '../type/defaultValues'; import { typeFromAST } from '../utilities/typeFromAST'; import { valueFromAST } from '../utilities/valueFromAST'; import { coerceInputValue } from '../utilities/coerceInputValue'; +import { validateInputValue } from '../utilities/validateInputValue'; +import { validateInputLiteral } from '../utilities/validateInputLiteral'; export type VariableValues = {| +sources: ReadOnlyObjMap<{| @@ -104,51 +111,29 @@ function coerceVariableValues( continue; } - if (!hasOwnProperty(inputs, varName)) { + const value = hasOwnProperty(inputs, varName) ? inputs[varName] : undefined; + sources[varName] = { variable: varDefNode, type: varType, value }; + + if (value === undefined) { if (varDefNode.defaultValue) { - sources[varName] = { - variable: varDefNode, - type: varType, - value: undefined, - }; coerced[varName] = valueFromAST(varDefNode.defaultValue, varType); - } else if (isNonNullType(varType)) { - const varTypeStr = inspect(varType); - onError( - new GraphQLError( - `Variable "$${varName}" of required type "${varTypeStr}" was not provided.`, - varDefNode, - ), - ); + continue; + } else if (!isNonNullType(varType)) { + // Non-provided values for nullable variables are omitted. + continue; } - continue; } - const value = inputs[varName]; - if (value === null && isNonNullType(varType)) { - const varTypeStr = inspect(varType); - onError( - new GraphQLError( - `Variable "$${varName}" of non-null type "${varTypeStr}" must not be null.`, - varDefNode, - ), - ); - continue; - } - - sources[varName] = { variable: varDefNode, type: varType, value }; - coerced[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)}"`; - } + const coercedValue = coerceInputValue(value, varType); + if (coercedValue !== undefined) { + coerced[varName] = coercedValue; + } else { + validateInputValue(value, varType, (error, path) => { onError( new GraphQLError( - prefix + '; ' + error.message, + `Variable "$${varName}" has invalid value${printPathArray(path)}: ${ + error.message + }`, varDefNode, undefined, undefined, @@ -156,8 +141,8 @@ function coerceVariableValues( error.originalError, ), ); - }, - ); + }); + } } return { sources, coerced }; @@ -189,65 +174,54 @@ export function getArgumentValues( const argType = argDef.type; const argumentNode = argNodeMap[name]; - if (!argumentNode) { + if (!argumentNode && isRequiredInput(argDef)) { + // Note: ProvidedRequiredArgumentsRule validation should catch this before + // execution. This is a runtime check to ensure execution does not + // continue with an invalid argument value. + throw new GraphQLError( + `Argument "${name}" of required type "${String( + argType, + )}" was not provided.`, + node, + ); + } + + // Variables without a value are treated as if no argument was provided if + // the argument is not required. + if ( + !argumentNode || + (argumentNode.value.kind === Kind.VARIABLE && + variableValues?.coerced[argumentNode.value.name.value] === undefined && + !isRequiredInput(argDef)) + ) { if (argDef.defaultValue) { coercedValues[name] = getCoercedDefaultValue( argDef.defaultValue, argDef.type, ); - } else if (isNonNullType(argType)) { - throw new GraphQLError( - `Argument "${name}" of required type "${inspect(argType)}" ` + - 'was not provided.', - node, - ); } continue; } const valueNode = argumentNode.value; - let isNull = valueNode.kind === Kind.NULL; - - if (valueNode.kind === Kind.VARIABLE) { - const variableName = valueNode.name.value; - if ( - variableValues == null || - variableValues.coerced[variableName] === undefined - ) { - if (argDef.defaultValue) { - coercedValues[name] = getCoercedDefaultValue( - argDef.defaultValue, - argDef.type, - ); - } else if (isNonNullType(argType)) { - throw new GraphQLError( - `Argument "${name}" of required type "${inspect(argType)}" ` + - `was provided the variable "$${variableName}" which was not provided a runtime value.`, - valueNode, - ); - } - continue; - } - isNull = variableValues.coerced[variableName] == null; - } - - if (isNull && isNonNullType(argType)) { - throw new GraphQLError( - `Argument "${name}" of non-null type "${inspect(argType)}" ` + - 'must not be null.', - valueNode, - ); - } - const coercedValue = valueFromAST(valueNode, argType, variableValues); if (coercedValue === undefined) { // Note: ValuesOfCorrectTypeRule validation should catch this before // execution. This is a runtime check to ensure execution does not // continue with an invalid argument value. - throw new GraphQLError( - `Argument "${name}" has invalid value ${print(valueNode)}.`, + validateInputLiteral( valueNode, + argType, + variableValues, + (error, path) => { + error.message = `Argument "${name}" has invalid value${printPathArray( + path, + )}: ${error.message}`; + throw error; + }, ); + // istanbul ignore next (validateInputLiteral should throw) + invariant(false, 'Invalid argument'); } coercedValues[name] = coercedValue; } @@ -279,7 +253,3 @@ export function getDirectiveValues( return getArgumentValues(directiveDef, directiveNode, variableValues); } } - -function hasOwnProperty(obj: mixed, prop: string): boolean { - return Object.prototype.hasOwnProperty.call(obj, prop); -} diff --git a/src/index.d.ts b/src/index.d.ts index df23aa53be7..142a8fee7fa 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -423,6 +423,10 @@ export { valueToLiteral, // Coerces a JavaScript value to a GraphQL type, or produces errors. coerceInputValue, + // Validate a JavaScript value with a GraphQL type, collecting all errors. + validateInputValue, + // Validate a GraphQL Literal AST with a GraphQL type, collecting all errors. + validateInputLiteral, // Concatenates multiple AST together. concatAST, // Separates an AST into an AST per Operation. diff --git a/src/index.js b/src/index.js index 0fc9009f501..dd4976c724f 100644 --- a/src/index.js +++ b/src/index.js @@ -412,6 +412,10 @@ export { valueToLiteral, // Coerces a JavaScript value to a GraphQL type, or produces errors. coerceInputValue, + // Validate a JavaScript value with a GraphQL type, collecting all errors. + validateInputValue, + // Validate a GraphQL Literal AST with a GraphQL type, collecting all errors. + validateInputLiteral, // Concatenates multiple AST together. concatAST, // Separates an AST into an AST per Operation. diff --git a/src/jsutils/printPathArray.js b/src/jsutils/printPathArray.js index 4e9e773afa1..f8f38c7081d 100644 --- a/src/jsutils/printPathArray.js +++ b/src/jsutils/printPathArray.js @@ -2,9 +2,10 @@ * Build a string describing the path. */ export function printPathArray(path: $ReadOnlyArray): string { - return path - .map((key) => - typeof key === 'number' ? '[' + key.toString() + ']' : '.' + key, - ) - .join(''); + if (path.length === 0) { + return ''; + } + return ` at ${path + .map((key) => (typeof key === 'number' ? `[${key}]` : `.${key}`)) + .join('')}`; } diff --git a/src/subscription/__tests__/subscribe-test.js b/src/subscription/__tests__/subscribe-test.js index e9e242fe65d..056ef8cd2e2 100644 --- a/src/subscription/__tests__/subscribe-test.js +++ b/src/subscription/__tests__/subscribe-test.js @@ -472,7 +472,7 @@ describe('Subscription Initialization Phase', () => { errors: [ { message: - 'Variable "$arg" got invalid value "meow"; Int cannot represent non-integer value: "meow"', + 'Variable "$arg" has invalid value: Int cannot represent non-integer value: "meow"', locations: [{ line: 2, column: 21 }], }, ], diff --git a/src/type/__tests__/enumType-test.js b/src/type/__tests__/enumType-test.js index 0145657733f..9736631b88c 100644 --- a/src/type/__tests__/enumType-test.js +++ b/src/type/__tests__/enumType-test.js @@ -263,7 +263,7 @@ describe('Type System: Enum Values', () => { errors: [ { message: - 'Variable "$color" got invalid value 2; Enum "Color" cannot represent non-string value: 2.', + 'Variable "$color" has invalid value: Enum "Color" cannot represent non-string value: 2.', locations: [{ line: 1, column: 8 }], }, ], diff --git a/src/utilities/__tests__/coerceInputValue-test.js b/src/utilities/__tests__/coerceInputValue-test.js index a5aa72096c8..4b095a26aff 100644 --- a/src/utilities/__tests__/coerceInputValue-test.js +++ b/src/utilities/__tests__/coerceInputValue-test.js @@ -15,66 +15,24 @@ import { import { coerceInputValue } from '../coerceInputValue'; -type CoerceResult = {| - value: mixed, - errors: $ReadOnlyArray<{| - path: $ReadOnlyArray, - value: mixed, - error: string, - |}>, -|}; - -function coerceValue(inputValue: mixed, type: GraphQLInputType): CoerceResult { - const errors = []; - const value = coerceInputValue( - inputValue, - type, - (path, invalidValue, error) => { - errors.push({ path, value: invalidValue, error: error.message }); - }, - ); - - return { errors, value }; -} - -function expectValue(result: CoerceResult) { - expect(result.errors).to.deep.equal([]); - return expect(result.value); -} - -function expectErrors(result: CoerceResult) { - return expect(result.errors); -} - describe('coerceInputValue', () => { + function test(inputValue: mixed, type: GraphQLInputType, expected: mixed) { + expect(coerceInputValue(inputValue, type)).to.deep.equal(expected); + } + describe('for GraphQLNonNull', () => { const TestNonNull = new GraphQLNonNull(GraphQLInt); - it('returns no error for non-null value', () => { - const result = coerceValue(1, TestNonNull); - expectValue(result).to.equal(1); + it('returns for a non-null value', () => { + test(1, TestNonNull, 1); }); - it('returns an error for undefined value', () => { - const result = coerceValue(undefined, TestNonNull); - expectErrors(result).to.deep.equal([ - { - error: 'Expected non-nullable type "Int!" not to be null.', - path: [], - value: undefined, - }, - ]); + it('invalid for undefined value', () => { + test(undefined, TestNonNull, undefined); }); - it('returns an error for null value', () => { - const result = coerceValue(null, TestNonNull); - expectErrors(result).to.deep.equal([ - { - error: 'Expected non-nullable type "Int!" not to be null.', - path: [], - value: null, - }, - ]); + it('invalid for null value', () => { + test(null, TestNonNull, undefined); }); }); @@ -90,42 +48,27 @@ describe('coerceInputValue', () => { }, }); - it('returns no error for valid input', () => { - const result = coerceValue({ value: 1 }, TestScalar); - expectValue(result).to.equal(1); + it('returns for valid input', () => { + test({ value: 1 }, TestScalar, 1); }); - it('returns no error for null result', () => { - const result = coerceValue({ value: null }, TestScalar); - expectValue(result).to.equal(null); + it('returns for null result', () => { + test({ value: null }, TestScalar, null); }); - it('returns no error for NaN result', () => { - const result = coerceValue({ value: NaN }, TestScalar); - expectValue(result).to.satisfy(Number.isNaN); + it('returns for NaN result', () => { + expect(coerceInputValue({ value: NaN }, TestScalar)).to.satisfy( + Number.isNaN, + ); }); - it('returns an error for undefined result', () => { - const result = coerceValue({ value: undefined }, TestScalar); - expectErrors(result).to.deep.equal([ - { - error: 'Expected type "TestScalar".', - path: [], - value: { value: undefined }, - }, - ]); + it('invalid for undefined result', () => { + test({ value: undefined }, TestScalar, undefined); }); - it('returns an error for undefined result', () => { + it('invalid for undefined result', () => { const inputValue = { error: 'Some error message' }; - const result = coerceValue(inputValue, TestScalar); - expectErrors(result).to.deep.equal([ - { - error: 'Expected type "TestScalar". Some error message', - path: [], - value: { error: 'Some error message' }, - }, - ]); + test(inputValue, TestScalar, undefined); }); }); @@ -139,44 +82,18 @@ describe('coerceInputValue', () => { }); it('returns no error for a known enum name', () => { - const fooResult = coerceValue('FOO', TestEnum); - expectValue(fooResult).to.equal('InternalFoo'); + test('FOO', TestEnum, 'InternalFoo'); - const barResult = coerceValue('BAR', TestEnum); - expectValue(barResult).to.equal(123456789); + test('BAR', TestEnum, 123456789); }); - it('returns an error for misspelled enum value', () => { - const result = coerceValue('foo', TestEnum); - expectErrors(result).to.deep.equal([ - { - error: - 'Value "foo" does not exist in "TestEnum" enum. Did you mean the enum value "FOO"?', - path: [], - value: 'foo', - }, - ]); + it('invalid for misspelled enum value', () => { + test('foo', TestEnum, undefined); }); - it('returns an error for incorrect value type', () => { - const result1 = coerceValue(123, TestEnum); - expectErrors(result1).to.deep.equal([ - { - error: 'Enum "TestEnum" cannot represent non-string value: 123.', - path: [], - value: 123, - }, - ]); - - const result2 = coerceValue({ field: 'value' }, TestEnum); - expectErrors(result2).to.deep.equal([ - { - error: - 'Enum "TestEnum" cannot represent non-string value: { field: "value" }.', - path: [], - value: { field: 'value' }, - }, - ]); + it('invalid for incorrect value type', () => { + test(123, TestEnum, undefined); + test({ field: 'value' }, TestEnum, undefined); }); }); @@ -190,84 +107,27 @@ describe('coerceInputValue', () => { }); it('returns no error for a valid input', () => { - const result = coerceValue({ foo: 123 }, TestInputObject); - expectValue(result).to.deep.equal({ foo: 123 }); + test({ foo: 123 }, TestInputObject, { foo: 123 }); }); - it('returns an error for a non-object type', () => { - const result = coerceValue(123, TestInputObject); - expectErrors(result).to.deep.equal([ - { - error: 'Expected type "TestInputObject" to be an object.', - path: [], - value: 123, - }, - ]); + it('invalid for a non-object type', () => { + test(123, TestInputObject, undefined); }); - it('returns an error for an invalid field', () => { - const result = coerceValue({ foo: NaN }, TestInputObject); - expectErrors(result).to.deep.equal([ - { - error: '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([ - { - error: 'Int cannot represent non-integer value: "abc"', - path: ['foo'], - value: 'abc', - }, - { - error: 'Int cannot represent non-integer value: "def"', - path: ['bar'], - value: 'def', - }, - ]); + it('invalid for an invalid field', () => { + test({ foo: NaN }, TestInputObject, undefined); }); - it('returns error for a missing required field', () => { - const result = coerceValue({ bar: 123 }, TestInputObject); - expectErrors(result).to.deep.equal([ - { - error: 'Field "foo" of required type "Int!" was not provided.', - path: [], - value: { bar: 123 }, - }, - ]); + it('invalid for multiple invalid fields', () => { + test({ foo: 'abc', bar: 'def' }, TestInputObject, undefined); }); - it('returns error for an unknown field', () => { - const result = coerceValue( - { foo: 123, unknownField: 123 }, - TestInputObject, - ); - expectErrors(result).to.deep.equal([ - { - error: - 'Field "unknownField" is not defined by type "TestInputObject".', - path: [], - value: { foo: 123, unknownField: 123 }, - }, - ]); + it('invalid for a missing required field', () => { + test({ bar: 123 }, TestInputObject, undefined); }); - it('returns error for a misspelled field', () => { - const result = coerceValue({ foo: 123, bart: 123 }, TestInputObject); - expectErrors(result).to.deep.equal([ - { - error: - 'Field "bart" is not defined by type "TestInputObject". Did you mean "bar"?', - path: [], - value: { foo: 123, bart: 123 }, - }, - ]); + it('invalid for an unknown field', () => { + test({ foo: 123, unknownField: 123 }, TestInputObject, undefined); }); }); @@ -284,23 +144,21 @@ describe('coerceInputValue', () => { }); it('returns no errors for valid input value', () => { - const result = coerceValue({ foo: 5 }, makeTestInputObject(7)); - expectValue(result).to.deep.equal({ foo: 5 }); + test({ foo: 5 }, makeTestInputObject(7), { foo: 5 }); }); it('returns object with default value', () => { - const result = coerceValue({}, makeTestInputObject(7)); - expectValue(result).to.deep.equal({ foo: 7 }); + test({}, makeTestInputObject(7), { foo: 7 }); }); it('returns null as value', () => { - const result = coerceValue({}, makeTestInputObject(null)); - expectValue(result).to.deep.equal({ foo: null }); + test({}, makeTestInputObject(null), { foo: null }); }); it('returns NaN as value', () => { - const result = coerceValue({}, makeTestInputObject(NaN)); - expectValue(result).to.have.property('foo').that.satisfy(Number.isNaN); + expect(coerceInputValue({}, makeTestInputObject(NaN))) + .to.have.property('foo') + .that.satisfy(Number.isNaN); }); }); @@ -308,8 +166,7 @@ describe('coerceInputValue', () => { const TestList = new GraphQLList(GraphQLInt); it('returns no error for a valid input', () => { - const result = coerceValue([1, 2, 3], TestList); - expectValue(result).to.deep.equal([1, 2, 3]); + test([1, 2, 3], TestList, [1, 2, 3]); }); it('returns no error for a valid iterable input', () => { @@ -319,29 +176,15 @@ describe('coerceInputValue', () => { yield 3; } - const result = coerceValue(listGenerator(), TestList); - expectValue(result).to.deep.equal([1, 2, 3]); + test(listGenerator(), TestList, [1, 2, 3]); }); - it('returns an error for an invalid input', () => { - const result = coerceValue([1, 'b', true, 4], TestList); - expectErrors(result).to.deep.equal([ - { - error: 'Int cannot represent non-integer value: "b"', - path: [1], - value: 'b', - }, - { - error: 'Int cannot represent non-integer value: true', - path: [2], - value: true, - }, - ]); + it('invalid for an invalid input', () => { + test([1, 'b', true, 4], TestList, undefined); }); it('returns a list for a non-list value', () => { - const result = coerceValue(42, TestList); - expectValue(result).to.deep.equal([42]); + test(42, TestList, [42]); }); it('returns a list for a non-list object value', () => { @@ -354,24 +197,15 @@ describe('coerceInputValue', () => { }), ); - const result = coerceValue({ length: 100500 }, TestListOfObjects); - expectValue(result).to.deep.equal([{ length: 100500 }]); + test({ length: 100500 }, TestListOfObjects, [{ length: 100500 }]); }); - it('returns an error for a non-list invalid value', () => { - const result = coerceValue('INVALID', TestList); - expectErrors(result).to.deep.equal([ - { - error: 'Int cannot represent non-integer value: "INVALID"', - path: [], - value: 'INVALID', - }, - ]); + it('invalid for a non-list invalid value', () => { + test('INVALID', TestList, undefined); }); it('returns null for a null value', () => { - const result = coerceValue(null, TestList); - expectValue(result).to.deep.equal(null); + test(null, TestList, null); }); }); @@ -379,49 +213,23 @@ describe('coerceInputValue', () => { const TestNestedList = new GraphQLList(new GraphQLList(GraphQLInt)); it('returns no error for a valid input', () => { - const result = coerceValue([[1], [2, 3]], TestNestedList); - expectValue(result).to.deep.equal([[1], [2, 3]]); + test([[1], [2, 3]], TestNestedList, [[1], [2, 3]]); }); it('returns a list for a non-list value', () => { - const result = coerceValue(42, TestNestedList); - expectValue(result).to.deep.equal([[42]]); + test(42, TestNestedList, [[42]]); }); it('returns null for a null value', () => { - const result = coerceValue(null, TestNestedList); - expectValue(result).to.deep.equal(null); + test(null, TestNestedList, null); }); it('returns nested lists for nested non-list values', () => { - const result = coerceValue([1, 2, 3], TestNestedList); - expectValue(result).to.deep.equal([[1], [2], [3]]); + test([1, 2, 3], TestNestedList, [[1], [2], [3]]); }); it('returns nested null for nested null values', () => { - const result = coerceValue([42, [null], null], TestNestedList); - expectValue(result).to.deep.equal([[42], [null], null]); - }); - }); - - describe('with default onError', () => { - it('throw error without path', () => { - expect(() => - coerceInputValue(null, new GraphQLNonNull(GraphQLInt)), - ).to.throw( - 'Invalid value null: Expected non-nullable type "Int!" not to be null.', - ); - }); - - it('throw error with path', () => { - expect(() => - coerceInputValue( - [null], - new GraphQLList(new GraphQLNonNull(GraphQLInt)), - ), - ).to.throw( - 'Invalid value null at "value[0]": Expected non-nullable type "Int!" not to be null.', - ); + test([42, [null], null], TestNestedList, [[42], [null], null]); }); }); }); diff --git a/src/utilities/__tests__/validateInputLiteral-test.js b/src/utilities/__tests__/validateInputLiteral-test.js new file mode 100644 index 00000000000..8a28cf35961 --- /dev/null +++ b/src/utilities/__tests__/validateInputLiteral-test.js @@ -0,0 +1,498 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import type { ReadOnlyObjMap } from '../../jsutils/ObjMap'; +import { invariant } from '../../jsutils/invariant'; + +import { parseValue, Parser } from '../../language/parser'; + +import type { GraphQLInputType } from '../../type/definition'; +import { GraphQLInt } from '../../type/scalars'; +import { + GraphQLList, + GraphQLNonNull, + GraphQLScalarType, + GraphQLEnumType, + GraphQLInputObjectType, +} from '../../type/definition'; +import { GraphQLSchema } from '../../type/schema'; + +import type { VariableValues } from '../../execution/values'; +import { getVariableValues } from '../../execution/values'; + +import { validateInputLiteral } from '../validateInputLiteral'; + +describe('validateInputLiteral', () => { + function test( + inputValue: string, + type: GraphQLInputType, + expected: mixed, + variableValues?: VariableValues, + ) { + const errors = []; + validateInputLiteral( + parseValue(inputValue), + type, + variableValues, + (error, path) => { + errors.push({ error: error.message, path }); + }, + ); + expect(errors).to.deep.equal(expected); + } + + function testWithVariables( + variableDefs: string, + values: ReadOnlyObjMap, + inputValue: string, + type: GraphQLInputType, + expected: mixed, + ) { + const parser = new Parser(variableDefs); + parser.expectToken(''); + const coercedVariables = getVariableValues( + new GraphQLSchema({ types: [GraphQLInt] }), + parser.parseVariableDefinitions(), + values, + ); + invariant(coercedVariables.coerced); + test(inputValue, type, expected, coercedVariables.coerced); + } + + it('ignores variables statically', () => { + const TestNonNull = new GraphQLNonNull(GraphQLInt); + test('$var', TestNonNull, []); + }); + + it('returns an error for missing variables', () => { + testWithVariables('($var: Int)', {}, '$var', GraphQLInt, [ + { + error: + 'Expected variable "$var" provided to type "Int" to provide a runtime value.', + path: [], + }, + ]); + }); + + it('returns an error for null variables for nullable types', () => { + const TestNonNull = new GraphQLNonNull(GraphQLInt); + testWithVariables('($var: Int)', { var: null }, '$var', TestNonNull, [ + { + error: + 'Expected variable "$var" provided to non-null type "Int!" not to be null.', + path: [], + }, + ]); + }); + + describe('for GraphQLNonNull', () => { + const TestNonNull = new GraphQLNonNull(GraphQLInt); + + it('returns no error for non-null value', () => { + test('1', TestNonNull, []); + }); + + it('returns an error for null value', () => { + test('null', TestNonNull, [ + { + error: 'Expected value of non-null type "Int!" not to be null.', + path: [], + }, + ]); + }); + }); + + describe('for GraphQLScalar', () => { + const TestScalar = new GraphQLScalarType({ + name: 'TestScalar', + parseValue(input) { + invariant(typeof input === 'object' && input !== null); + if (input.error != null) { + throw new Error(input.error); + } + return input.value; + }, + }); + + it('returns no error for valid input', () => { + test('{ value: 1 }', TestScalar, []); + }); + + it('returns no error for null result', () => { + test('{ value: null }', TestScalar, []); + }); + + it('returns no error for NaN result', () => { + test('{ value: NaN }', TestScalar, []); + }); + + it('returns an error for undefined result', () => { + test('{}', TestScalar, [ + { + error: 'Expected value of type "TestScalar", found {}.', + path: [], + }, + ]); + }); + + it('returns an error for undefined result', () => { + const inputValue = '{ error: "Some error message" }'; + test(inputValue, TestScalar, [ + { + error: + 'Expected value of type "TestScalar", found {error: "Some error message"}; Some error message', + path: [], + }, + ]); + }); + + it('reports thrown non-error', () => { + const TestThrowScalar = new GraphQLScalarType({ + name: 'TestScalar', + parseValue() { + // eslint-disable-next-line no-throw-literal + throw 'Not an error object'; + }, + }); + + test('{}', TestThrowScalar, [ + { + error: + 'Expected value of type "TestScalar", found {}; Not an error object', + path: [], + }, + ]); + }); + }); + + describe('for GraphQLEnum', () => { + const TestEnum = new GraphQLEnumType({ + name: 'TestEnum', + values: { + FOO: { value: 'InternalFoo' }, + BAR: { value: 123456789 }, + }, + }); + + it('returns no error for a known enum name', () => { + test('FOO', TestEnum, []); + + test('BAR', TestEnum, []); + }); + + it('returns an error for unknown enum value', () => { + test('UNKNOWN', TestEnum, [ + { + error: 'Value "UNKNOWN" does not exist in "TestEnum" enum.', + path: [], + }, + ]); + }); + + it('returns an error for misspelled enum value', () => { + test('foo', TestEnum, [ + { + error: + 'Value "foo" does not exist in "TestEnum" enum. Did you mean the enum value "FOO"?', + path: [], + }, + ]); + }); + + it('returns an error for incorrect value type', () => { + test('"FOO"', TestEnum, [ + { + error: + 'Enum "TestEnum" cannot represent non-enum value: "FOO". Did you mean the enum value "FOO"?', + path: [], + }, + ]); + + test('"UNKNOWN"', TestEnum, [ + { + error: 'Enum "TestEnum" cannot represent non-enum value: "UNKNOWN".', + path: [], + }, + ]); + + test('123', TestEnum, [ + { + error: 'Enum "TestEnum" cannot represent non-enum value: 123.', + path: [], + }, + ]); + + test('{ field: "value" }', TestEnum, [ + { + error: + 'Enum "TestEnum" cannot represent non-enum value: {field: "value"}.', + path: [], + }, + ]); + }); + }); + + describe('for GraphQLInputObject', () => { + const TestInputObject = new GraphQLInputObjectType({ + name: 'TestInputObject', + fields: { + foo: { type: new GraphQLNonNull(GraphQLInt) }, + bar: { type: GraphQLInt }, + optional: { type: new GraphQLNonNull(GraphQLInt), defaultValue: 42 }, + }, + }); + + it('returns no error for a valid input', () => { + test('{ foo: 123 }', TestInputObject, []); + }); + + it('returns an error for a non-object type', () => { + test('123', TestInputObject, [ + { + error: + 'Expected value of type "TestInputObject" to be an object, found 123.', + path: [], + }, + ]); + }); + + it('returns an error for an invalid field', () => { + test('{ foo: 1.5 }', TestInputObject, [ + { + error: 'Int cannot represent non-integer value: 1.5', + path: ['foo'], + }, + ]); + }); + + it('returns multiple errors for multiple invalid fields', () => { + test('{ foo: "abc", bar: "def" }', TestInputObject, [ + { + error: 'Int cannot represent non-integer value: "abc"', + path: ['foo'], + }, + { + error: 'Int cannot represent non-integer value: "def"', + path: ['bar'], + }, + ]); + }); + + it('returns error for a missing required field', () => { + test('{ bar: 123 }', TestInputObject, [ + { + error: + 'Expected value of type "TestInputObject" to include required field "foo", found {bar: 123}', + path: [], + }, + ]); + }); + + it('returns error for an unknown field', () => { + test('{ foo: 123, unknownField: 123 }', TestInputObject, [ + { + error: + 'Expected value of type "TestInputObject" not to include unknown field "unknownField", found {foo: 123, unknownField: 123}', + path: [], + }, + ]); + }); + + it('returns error for a misspelled field', () => { + test('{ foo: 123, bart: 123 }', TestInputObject, [ + { + error: + 'Expected value of type "TestInputObject" not to include unknown field "bart". Did you mean "bar"? Found {foo: 123, bart: 123}', + path: [], + }, + ]); + }); + + it('allows variables in an object, statically', () => { + test('{ foo: $var }', TestInputObject, []); + }); + + it('allows correct use of variables', () => { + testWithVariables( + '($var: Int)', + { var: 123 }, + '{ foo: $var }', + TestInputObject, + [], + ); + }); + + it('allows missing variables in an nullable field', () => { + testWithVariables( + '($var: Int)', + {}, + '{ foo: 123, bar: $var }', + TestInputObject, + [], + ); + }); + + it('allows missing variables in an optional field', () => { + testWithVariables( + '($var: Int)', + {}, + '{ foo: 123, optional: $var }', + TestInputObject, + [], + ); + }); + + it('errors on missing variable in an required field', () => { + testWithVariables('($var: Int)', {}, '{ foo: $var }', TestInputObject, [ + { + error: + 'Expected variable "$var" provided to type "Int!" to provide a runtime value.', + path: ['foo'], + }, + ]); + }); + + it('errors on null variable in an non-null field', () => { + testWithVariables( + '($var: Int)', + { var: null }, + '{ foo: 123, optional: $var }', + TestInputObject, + [ + { + error: + 'Expected variable "$var" provided to non-null type "Int!" not to be null.', + path: ['optional'], + }, + ], + ); + }); + }); + + describe('for GraphQLInputObject with default value', () => { + const makeTestInputObject = (defaultValue) => + new GraphQLInputObjectType({ + name: 'TestInputObject', + fields: { + foo: { + type: new GraphQLScalarType({ name: 'TestScalar' }), + defaultValue, + }, + }, + }); + + it('no error for no errors for valid input value', () => { + test('{ foo: 5 }', makeTestInputObject(7), []); + }); + + it('no error for object with default value', () => { + test('{}', makeTestInputObject(7), []); + }); + + it('no error for null as value', () => { + test('{}', makeTestInputObject(null), []); + }); + + it('no error for NaN as value', () => { + test('{}', makeTestInputObject(NaN), []); + }); + }); + + describe('for GraphQLList', () => { + const TestList = new GraphQLList(GraphQLInt); + + it('returns no error for a valid input', () => { + test('[1, 2, 3]', TestList, []); + }); + + it('returns an error for an invalid input', () => { + test('[1, "b", true, 4]', TestList, [ + { + error: 'Int cannot represent non-integer value: "b"', + path: [1], + }, + { + error: 'Int cannot represent non-integer value: true', + path: [2], + }, + ]); + }); + + it('no error for a list for a non-list value', () => { + test('42', TestList, []); + }); + + it('returns an error for a non-list invalid value', () => { + test('"INVALID"', TestList, [ + { + error: 'Int cannot represent non-integer value: "INVALID"', + path: [], + }, + ]); + }); + + it('no error for null for a null value', () => { + test('null', TestList, []); + }); + + it('allows variables in a list, statically', () => { + test('[1, $var, 3]', TestList, []); + }); + + it('allows missing variables in a list (which coerce to null)', () => { + testWithVariables('($var: Int)', {}, '[1, $var, 3]', TestList, []); + }); + + it('errors on missing variables in a list of non-null', () => { + const TestListNonNull = new GraphQLList(new GraphQLNonNull(GraphQLInt)); + testWithVariables('($var: Int)', {}, '[1, $var, 3]', TestListNonNull, [ + { + error: + 'Expected variable "$var" provided to type "Int!" to provide a runtime value.', + path: [1], + }, + ]); + }); + + it('errors on null variables in a list of non-null', () => { + const TestListNonNull = new GraphQLList(new GraphQLNonNull(GraphQLInt)); + testWithVariables( + '($var: Int)', + { var: null }, + '[1, $var, 3]', + TestListNonNull, + [ + { + error: + 'Expected variable "$var" provided to non-null type "Int!" not to be null.', + path: [1], + }, + ], + ); + }); + }); + + describe('for nested GraphQLList', () => { + const TestNestedList = new GraphQLList(new GraphQLList(GraphQLInt)); + + it('no error for a valid input', () => { + test('[[1], [2, 3]]', TestNestedList, []); + }); + + it('no error for a list for a non-list value', () => { + test('42', TestNestedList, []); + }); + + it('no error for null for a null value', () => { + test('null', TestNestedList, []); + }); + + it('no error for nested lists for nested non-list values', () => { + test('[1, 2, 3]', TestNestedList, []); + }); + + it('no error for nested null for nested null values', () => { + test('[42, [null], null]', TestNestedList, []); + }); + }); +}); diff --git a/src/utilities/__tests__/validateInputValue-test.js b/src/utilities/__tests__/validateInputValue-test.js new file mode 100644 index 00000000000..ac3e62ff38f --- /dev/null +++ b/src/utilities/__tests__/validateInputValue-test.js @@ -0,0 +1,346 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { invariant } from '../../jsutils/invariant'; + +import type { GraphQLInputType } from '../../type/definition'; +import { GraphQLInt } from '../../type/scalars'; +import { + GraphQLList, + GraphQLNonNull, + GraphQLScalarType, + GraphQLEnumType, + GraphQLInputObjectType, +} from '../../type/definition'; + +import { validateInputValue } from '../validateInputValue'; + +describe('validateInputValue', () => { + function test(inputValue: mixed, type: GraphQLInputType, expected: mixed) { + const errors = []; + validateInputValue(inputValue, type, (error, path) => { + errors.push({ error: error.message, path }); + }); + expect(errors).to.deep.equal(expected); + } + + describe('for GraphQLNonNull', () => { + const TestNonNull = new GraphQLNonNull(GraphQLInt); + + it('returns no error for non-null value', () => { + test(1, TestNonNull, []); + }); + + it('returns an error for undefined value', () => { + test(undefined, TestNonNull, [ + { + error: 'Expected a value of non-null type "Int!" to be provided.', + path: [], + }, + ]); + }); + + it('returns an error for null value', () => { + test(null, TestNonNull, [ + { + error: 'Expected value of non-null type "Int!" not to be null.', + path: [], + }, + ]); + }); + }); + + describe('for GraphQLScalar', () => { + const TestScalar = new GraphQLScalarType({ + name: 'TestScalar', + parseValue(input) { + invariant(typeof input === 'object' && input !== null); + if (input.error != null) { + throw new Error(input.error); + } + return input.value; + }, + }); + + it('returns no error for valid input', () => { + test({ value: 1 }, TestScalar, []); + }); + + it('returns no error for null result', () => { + test({ value: null }, TestScalar, []); + }); + + it('returns no error for NaN result', () => { + test({ value: NaN }, TestScalar, []); + }); + + it('returns an error for undefined result', () => { + test({ value: undefined }, TestScalar, [ + { + error: + 'Expected value of type "TestScalar", found { value: undefined }.', + path: [], + }, + ]); + }); + + it('returns an error for undefined result', () => { + const inputValue = { error: 'Some error message' }; + test(inputValue, TestScalar, [ + { + error: + 'Expected value of type "TestScalar", found { error: "Some error message" }; Some error message', + path: [], + }, + ]); + }); + }); + + describe('for GraphQLEnum', () => { + const TestEnum = new GraphQLEnumType({ + name: 'TestEnum', + values: { + FOO: { value: 'InternalFoo' }, + BAR: { value: 123456789 }, + }, + }); + + it('returns no error for a known enum name', () => { + test('FOO', TestEnum, []); + + test('BAR', TestEnum, []); + }); + + it('returns an error for unknown enum value', () => { + test('UNKNOWN', TestEnum, [ + { + error: 'Value "UNKNOWN" does not exist in "TestEnum" enum.', + path: [], + }, + ]); + }); + + it('returns an error for misspelled enum value', () => { + test('foo', TestEnum, [ + { + error: + 'Value "foo" does not exist in "TestEnum" enum. Did you mean the enum value "FOO"?', + path: [], + }, + ]); + }); + + it('returns an error for incorrect value type', () => { + test(123, TestEnum, [ + { + error: 'Enum "TestEnum" cannot represent non-string value: 123.', + path: [], + }, + ]); + + test({ field: 'value' }, TestEnum, [ + { + error: + 'Enum "TestEnum" cannot represent non-string value: { field: "value" }.', + path: [], + }, + ]); + }); + + it('reports thrown non-error', () => { + const TestThrowScalar = new GraphQLScalarType({ + name: 'TestScalar', + parseValue() { + // eslint-disable-next-line no-throw-literal + throw 'Not an error object'; + }, + }); + + test({}, TestThrowScalar, [ + { + error: + 'Expected value of type "TestScalar", found {}; Not an error object', + path: [], + }, + ]); + }); + }); + + describe('for GraphQLInputObject', () => { + const TestInputObject = new GraphQLInputObjectType({ + name: 'TestInputObject', + fields: { + foo: { type: new GraphQLNonNull(GraphQLInt) }, + bar: { type: GraphQLInt }, + }, + }); + + it('returns no error for a valid input', () => { + test({ foo: 123 }, TestInputObject, []); + }); + + it('returns an error for a non-object type', () => { + test(123, TestInputObject, [ + { + error: + 'Expected value of type "TestInputObject" to be an object, found 123.', + path: [], + }, + ]); + }); + + it('returns an error for an invalid field', () => { + test({ foo: NaN }, TestInputObject, [ + { + error: 'Int cannot represent non-integer value: NaN', + path: ['foo'], + }, + ]); + }); + + it('returns multiple errors for multiple invalid fields', () => { + test({ foo: 'abc', bar: 'def' }, TestInputObject, [ + { + error: 'Int cannot represent non-integer value: "abc"', + path: ['foo'], + }, + { + error: 'Int cannot represent non-integer value: "def"', + path: ['bar'], + }, + ]); + }); + + it('returns error for a missing required field', () => { + test({ bar: 123 }, TestInputObject, [ + { + error: + 'Expected value of type "TestInputObject" to include required field "foo", found { bar: 123 }', + path: [], + }, + ]); + }); + + it('returns error for an unknown field', () => { + test({ foo: 123, unknownField: 123 }, TestInputObject, [ + { + error: + 'Expected value of type "TestInputObject" not to include unknown field "unknownField", found { foo: 123, unknownField: 123 }', + path: [], + }, + ]); + }); + + it('returns error for a misspelled field', () => { + test({ foo: 123, bart: 123 }, TestInputObject, [ + { + error: + 'Expected value of type "TestInputObject" not to include unknown field "bart". Did you mean "bar"? Found { foo: 123, bart: 123 }', + path: [], + }, + ]); + }); + }); + + describe('for GraphQLInputObject with default value', () => { + const makeTestInputObject = (defaultValue) => + new GraphQLInputObjectType({ + name: 'TestInputObject', + fields: { + foo: { + type: new GraphQLScalarType({ name: 'TestScalar' }), + defaultValue, + }, + }, + }); + + it('no error for no errors for valid input value', () => { + test({ foo: 5 }, makeTestInputObject(7), []); + }); + + it('no error for object with default value', () => { + test({}, makeTestInputObject(7), []); + }); + + it('no error for null as value', () => { + test({}, makeTestInputObject(null), []); + }); + + it('no error for NaN as value', () => { + test({}, makeTestInputObject(NaN), []); + }); + }); + + describe('for GraphQLList', () => { + const TestList = new GraphQLList(GraphQLInt); + + it('returns no error for a valid input', () => { + test([1, 2, 3], TestList, []); + }); + + it('returns no error for a valid iterable input', () => { + // TODO: put an error in this list and show it appears + function* listGenerator() { + yield 1; + yield 2; + yield 3; + } + + test(listGenerator(), TestList, []); + }); + + it('returns an error for an invalid input', () => { + test([1, 'b', true, 4], TestList, [ + { + error: 'Int cannot represent non-integer value: "b"', + path: [1], + }, + { + error: 'Int cannot represent non-integer value: true', + path: [2], + }, + ]); + }); + + it('no error for a list for a non-list value', () => { + test(42, TestList, []); + }); + + it('returns an error for a non-list invalid value', () => { + test('INVALID', TestList, [ + { + error: 'Int cannot represent non-integer value: "INVALID"', + path: [], + }, + ]); + }); + + it('no error for null for a null value', () => { + test(null, TestList, []); + }); + }); + + describe('for nested GraphQLList', () => { + const TestNestedList = new GraphQLList(new GraphQLList(GraphQLInt)); + + it('no error for a valid input', () => { + test([[1], [2, 3]], TestNestedList, []); + }); + + it('no error for a list for a non-list value', () => { + test(42, TestNestedList, []); + }); + + it('no error for null for a null value', () => { + test(null, TestNestedList, []); + }); + + it('no error for nested lists for nested non-list values', () => { + test([1, 2, 3], TestNestedList, []); + }); + + it('no error for nested null for nested null values', () => { + test([42, [null], null], TestNestedList, []); + }); + }); +}); diff --git a/src/utilities/coerceInputValue.d.ts b/src/utilities/coerceInputValue.d.ts index 78dafb257fc..d820fff7652 100644 --- a/src/utilities/coerceInputValue.d.ts +++ b/src/utilities/coerceInputValue.d.ts @@ -1,11 +1,4 @@ import { GraphQLInputType } from '../type/definition'; -import { GraphQLError } from '../error/GraphQLError'; - -type OnErrorCB = ( - path: ReadonlyArray, - invalidValue: unknown, - error: GraphQLError, -) => void; /** * Coerces a JavaScript value given a GraphQL Input Type. @@ -13,5 +6,4 @@ type OnErrorCB = ( export function coerceInputValue( inputValue: unknown, type: GraphQLInputType, - onError?: OnErrorCB, ): unknown; diff --git a/src/utilities/coerceInputValue.js b/src/utilities/coerceInputValue.js index 31896064f50..e3ad5edaa09 100644 --- a/src/utilities/coerceInputValue.js +++ b/src/utilities/coerceInputValue.js @@ -1,149 +1,85 @@ -import type { Path } from '../jsutils/Path'; +import { hasOwnProperty } from '../jsutils/hasOwnProperty'; 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 { addPath, pathToArray } from '../jsutils/Path'; import { isIterableObject } from '../jsutils/isIterableObject'; -import { GraphQLError } from '../error/GraphQLError'; - import type { GraphQLInputType } from '../type/definition'; import { isLeafType, isInputObjectType, isListType, isNonNullType, + isRequiredInput, } from '../type/definition'; import { getCoercedDefaultValue } from '../type/defaultValues'; -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, -): void { - 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); + if (inputValue == null) { + return; // Invalid: intentionally return no value. } - onError( - pathToArray(path), - inputValue, - new GraphQLError( - `Expected non-nullable type "${inspect(type)}" not to be null.`, - ), - ); - return; + return coerceInputValue(inputValue, type.ofType); } if (inputValue == null) { - // Explicitly return the value null. - return null; + return null; // Explicitly return the value null. } if (isListType(type)) { - const itemType = type.ofType; - if (isIterableObject(inputValue)) { - return Array.from(inputValue, (itemValue, index) => { - const itemPath = addPath(path, index, undefined); - return coerceInputValueImpl(itemValue, itemType, onError, itemPath); - }); + if (!isIterableObject(inputValue)) { + // Lists accept a non-list value as a list of one. + const coercedItem = coerceInputValue(inputValue, type.ofType); + if (coercedItem === undefined) { + return; // Invalid: intentionally return no value. + } + return [coercedItem]; } - // Lists accept a non-list value as a list of one. - return [coerceInputValueImpl(inputValue, itemType, onError, path)]; + const coercedValue = []; + for (const itemValue of inputValue) { + const coercedItem = coerceInputValue(itemValue, type.ofType); + if (coercedItem === undefined) { + return; // Invalid: intentionally return no value. + } + coercedValue.push(coercedItem); + } + return coercedValue; } if (isInputObjectType(type)) { if (!isObjectLike(inputValue)) { - onError( - pathToArray(path), - inputValue, - new GraphQLError(`Expected type "${type.name}" to be an object.`), - ); - return; + return; // Invalid: intentionally return no value. } const coercedValue = {}; const fieldDefs = type.getFields(); - + const hasUndefinedField = Object.keys(inputValue).some( + (name) => !hasOwnProperty(fieldDefs, name), + ); + if (hasUndefinedField) { + return; // Invalid: intentionally return no value. + } for (const field of Object.values(fieldDefs)) { const fieldValue = inputValue[field.name]; - if (fieldValue === undefined) { + if (isRequiredInput(field)) { + return; // Invalid: intentionally return no value. + } if (field.defaultValue) { coercedValue[field.name] = getCoercedDefaultValue( field.defaultValue, field.type, ); - } 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, type.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), - ), - ); + } else { + const coercedField = coerceInputValue(fieldValue, field.type); + if (coercedField === undefined) { + return; // Invalid: intentionally return no value. + } + coercedValue[field.name] = coercedField; } } return coercedValue; @@ -151,40 +87,11 @@ function coerceInputValueImpl( // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') if (isLeafType(type)) { - let parseResult; - - // Scalars and Enums 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) { - if (error instanceof GraphQLError) { - onError(pathToArray(path), inputValue, error); - } else { - 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 type.parseValue(inputValue); + } catch (_error) { + return; // Invalid: ignore error and intentionally return no value. } - return parseResult; } // istanbul ignore next (Not reachable. All possible input types have been considered) diff --git a/src/utilities/index.d.ts b/src/utilities/index.d.ts index 5f8a929fedb..b32bf8f00f5 100644 --- a/src/utilities/index.d.ts +++ b/src/utilities/index.d.ts @@ -83,6 +83,12 @@ export { valueToLiteral } from './valueToLiteral'; // Coerces a JavaScript value to a GraphQL type, or produces errors. export { coerceInputValue } from './coerceInputValue'; +// Validate a JavaScript value with a GraphQL type, collecting all errors. +export { validateInputValue } from './validateInputValue'; + +// Validate a GraphQL Literal AST with a GraphQL type, collecting all errors. +export { validateInputLiteral } from './validateInputLiteral'; + // Concatenates multiple AST together. export { concatAST } from './concatAST'; diff --git a/src/utilities/index.js b/src/utilities/index.js index 18280976182..d370afcc8fa 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -81,6 +81,12 @@ export { valueToLiteral } from './valueToLiteral'; // Coerces a JavaScript value to a GraphQL type, or produces errors. export { coerceInputValue } from './coerceInputValue'; +// Validate a JavaScript value with a GraphQL type, collecting all errors. +export { validateInputValue } from './validateInputValue'; + +// Validate a GraphQL Literal AST with a GraphQL type, collecting all errors. +export { validateInputLiteral } from './validateInputLiteral'; + // Concatenates multiple AST together. export { concatAST } from './concatAST'; diff --git a/src/utilities/validateInputLiteral.d.ts b/src/utilities/validateInputLiteral.d.ts new file mode 100644 index 00000000000..1af135ef0f2 --- /dev/null +++ b/src/utilities/validateInputLiteral.d.ts @@ -0,0 +1,23 @@ +import { Maybe } from '../jsutils/Maybe'; + +import { GraphQLError } from '../error/GraphQLError'; + +import { ValueNode } from '../language/ast'; + +import { GraphQLInputType } from '../type/definition'; + +import { VariableValues } from '../execution/values'; + +/** + * 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). + */ +export function validateInputLiteral( + valueNode: ValueNode, + type: GraphQLInputType, + variables: Maybe, + onError: (error: GraphQLError, path: ReadonlyArray) => void, +): void; diff --git a/src/utilities/validateInputLiteral.js b/src/utilities/validateInputLiteral.js new file mode 100644 index 00000000000..207d2813824 --- /dev/null +++ b/src/utilities/validateInputLiteral.js @@ -0,0 +1,243 @@ +import type { Path } from '../jsutils/Path'; +import { addPath, pathToArray } from '../jsutils/Path'; +import { didYouMean } from '../jsutils/didYouMean'; +import { hasOwnProperty } from '../jsutils/hasOwnProperty'; +import { inspect } from '../jsutils/inspect'; +import { invariant } from '../jsutils/invariant'; +import { keyMap } from '../jsutils/keyMap'; +import { suggestionList } from '../jsutils/suggestionList'; + +import { GraphQLError } from '../error/GraphQLError'; + +import type { ASTNode, ValueNode } from '../language/ast'; +import { Kind } from '../language/kinds'; +import { print } from '../language/printer'; + +import type { GraphQLInputType } from '../type/definition'; +import { + isInputObjectType, + isListType, + isNonNullType, + isLeafType, + isRequiredInput, +} from '../type/definition'; + +import type { VariableValues } from '../execution/values'; + +import { replaceVariables } from './replaceVariables'; + +/** + * 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). + */ +export function validateInputLiteral( + valueNode: ValueNode, + type: GraphQLInputType, + variables: ?VariableValues, + onError: (error: GraphQLError, path: $ReadOnlyArray) => void, + path?: Path, +): void { + if (valueNode.kind === Kind.VARIABLE) { + if (!variables) { + // If no variable values are provided, this is being validated statically, + // and cannot yet produce any validation errors for variables. + return; + } + if (isMissingVariable(valueNode, variables)) { + reportInvalidValue( + onError, + `Expected variable "$${ + valueNode.name.value + }" provided to type "${String(type)}" to provide a runtime value.`, + valueNode, + path, + ); + } else if ( + isNonNullType(type) && + variables.coerced[valueNode.name.value] === null + ) { + reportInvalidValue( + onError, + `Expected variable "$${ + valueNode.name.value + }" provided to non-null type "${String(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 (isNonNullType(type)) { + if (valueNode.kind === Kind.NULL) { + reportInvalidValue( + onError, + `Expected value of non-null type "${String(type)}" not to be null.`, + valueNode, + path, + ); + return; + } + return validateInputLiteral( + valueNode, + type.ofType, + variables, + onError, + path, + ); + } + + if (valueNode.kind === Kind.NULL) { + return; + } + + if (isListType(type)) { + if (valueNode.kind !== Kind.LIST) { + // Lists accept a non-list value as a list of one. + validateInputLiteral(valueNode, type.ofType, variables, onError, path); + } else { + let index = 0; + for (const itemNode of valueNode.values) { + // A variable may be missing if the item type is nullable. + if ( + variables && + isMissingVariable(itemNode, variables) && + !isNonNullType(type.ofType) + ) { + continue; + } + validateInputLiteral( + itemNode, + type.ofType, + variables, + onError, + addPath(path, index++, undefined), + ); + } + } + } else if (isInputObjectType(type)) { + if (valueNode.kind !== Kind.OBJECT) { + reportInvalidValue( + onError, + `Expected value of type "${type.name}" 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 (isRequiredInput(field)) { + reportInvalidValue( + onError, + `Expected value of type "${type.name}" to include required field "${ + field.name + }", found ${print(valueNode)}`, + valueNode, + path, + ); + } + } else { + // A variable may be missing if the input field is not required. + if ( + variables && + isMissingVariable(fieldNode.value, variables) && + !isRequiredInput(field) + ) { + continue; + } + validateInputLiteral( + fieldNode.value, + field.type, + variables, + onError, + addPath(path, field.name, type.name), + ); + } + } + + // Ensure every provided field is defined. + for (const fieldNode of valueNode.fields) { + const fieldName = fieldNode.name.value; + if (!hasOwnProperty(fieldDefs, fieldName)) { + const suggestions = suggestionList(fieldName, Object.keys(fieldDefs)); + reportInvalidValue( + onError, + `Expected value of type "${ + type.name + }" not to include unknown field "${fieldName}"${ + suggestions.length > 0 + ? `.${didYouMean(suggestions)} Found` + : ', found' + } ${print(valueNode)}`, + fieldNode, + path, + ); + } + } + } /* istanbul ignore else (Not reachable) */ else if (isLeafType(type)) { + const constValueNode = replaceVariables(valueNode); + + let result; + let caughtError; + try { + result = type.parseLiteral(constValueNode); + } catch (error) { + if (error instanceof GraphQLError) { + onError(error, pathToArray(path)); + return; + } + caughtError = error; + } + + if (result === undefined) { + reportInvalidValue( + onError, + `Expected value of type "${type.name}", found ${print(valueNode)}` + + (caughtError ? `; ${caughtError.message || caughtError}` : '.'), + valueNode, + path, + caughtError, + ); + } + } else { + invariant(false, 'Unexpected input type: ' + inspect((type: empty))); + } +} + +// Returns true if the provided valueNode is a variable which is not defined +// in the set of variables. +function isMissingVariable( + valueNode: ValueNode, + variables: VariableValues, +): boolean { + return ( + valueNode.kind === Kind.VARIABLE && + variables.coerced[valueNode.name.value] === undefined + ); +} + +function reportInvalidValue( + onError: (error: GraphQLError, path: $ReadOnlyArray) => void, + message: string, + valueNode: ASTNode, + path: Path | void, + originalError?: ?GraphQLError, +): void { + onError( + new GraphQLError(message, valueNode, null, null, null, originalError), + pathToArray(path), + ); +} diff --git a/src/utilities/validateInputValue.d.ts b/src/utilities/validateInputValue.d.ts new file mode 100644 index 00000000000..e3c22543bb6 --- /dev/null +++ b/src/utilities/validateInputValue.d.ts @@ -0,0 +1,13 @@ +import { GraphQLError } from '../error/GraphQLError'; + +import { GraphQLInputType } from '../type/definition'; + +/** + * Validate that the provided input value is allowed for this type, collecting + * all errors via a callback function. + */ +export function validateInputValue( + inputValue: unknown, + type: GraphQLInputType, + onError: (error: GraphQLError, path: ReadonlyArray) => void, +): void; diff --git a/src/utilities/validateInputValue.js b/src/utilities/validateInputValue.js new file mode 100644 index 00000000000..51661b25d9f --- /dev/null +++ b/src/utilities/validateInputValue.js @@ -0,0 +1,162 @@ +import type { Path } from '../jsutils/Path'; +import { addPath, pathToArray } from '../jsutils/Path'; +import { didYouMean } from '../jsutils/didYouMean'; +import { hasOwnProperty } from '../jsutils/hasOwnProperty'; +import { inspect } from '../jsutils/inspect'; +import { invariant } from '../jsutils/invariant'; +import { isIterableObject } from '../jsutils/isIterableObject'; +import { isObjectLike } from '../jsutils/isObjectLike'; +import { suggestionList } from '../jsutils/suggestionList'; + +import { GraphQLError } from '../error/GraphQLError'; + +import type { GraphQLInputType } from '../type/definition'; +import { + isInputObjectType, + isListType, + isNonNullType, + isLeafType, + isRequiredInputField, +} from '../type/definition'; + +/** + * Validate that the provided input value is allowed for this type, collecting + * all errors via a callback function. + */ +export function validateInputValue( + inputValue: mixed, + type: GraphQLInputType, + onError: (error: GraphQLError, path: $ReadOnlyArray) => void, + path?: Path, +): void { + if (isNonNullType(type)) { + if (inputValue === undefined) { + reportInvalidValue( + onError, + `Expected a value of non-null type "${String(type)}" to be provided.`, + path, + ); + return; + } + if (inputValue === null) { + reportInvalidValue( + onError, + `Expected value of non-null type "${String(type)}" not to be null.`, + path, + ); + return; + } + return validateInputValue(inputValue, type.ofType, onError, path); + } + + if (inputValue == null) { + return; + } + + if (isListType(type)) { + if (!isIterableObject(inputValue)) { + // Lists accept a non-list value as a list of one. + validateInputValue(inputValue, type.ofType, onError, path); + } else { + let index = 0; + for (const itemValue of inputValue) { + validateInputValue( + itemValue, + type.ofType, + onError, + addPath(path, index++, undefined), + ); + } + } + } else if (isInputObjectType(type)) { + if (!isObjectLike(inputValue)) { + reportInvalidValue( + onError, + `Expected value of type "${type.name}" 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.name}" to include required field "${ + field.name + }", found ${inspect(inputValue)}`, + path, + ); + } + } else { + validateInputValue( + fieldValue, + field.type, + onError, + addPath(path, field.name, type.name), + ); + } + } + + // Ensure every provided field is defined. + for (const fieldName of Object.keys(inputValue)) { + if (!hasOwnProperty(fieldDefs, fieldName)) { + const suggestions = suggestionList(fieldName, Object.keys(fieldDefs)); + reportInvalidValue( + onError, + `Expected value of type "${ + type.name + }" not to include unknown field "${fieldName}"${ + suggestions.length > 0 + ? `.${didYouMean(suggestions)} Found` + : ', found' + } ${inspect(inputValue)}`, + path, + ); + } + } + } /* istanbul ignore else (Not reachable) */ else if (isLeafType(type)) { + let result; + let caughtError; + + try { + result = type.parseValue(inputValue); + } catch (error) { + if (error instanceof GraphQLError) { + onError(error, pathToArray(path)); + return; + } + caughtError = error; + } + + if (result === undefined) { + reportInvalidValue( + onError, + `Expected value of type "${type.name}", found ${inspect(inputValue)}` + + (caughtError ? `; ${caughtError.message || caughtError}` : '.'), + path, + caughtError, + ); + } + } else { + invariant(false, 'Unexpected input type: ' + inspect((type: empty))); + } +} + +function reportInvalidValue( + onError: (error: GraphQLError, path: $ReadOnlyArray) => void, + message: string, + path: Path | void, + originalError?: ?GraphQLError, +): void { + onError( + new GraphQLError(message, null, null, null, null, originalError), + pathToArray(path), + ); +} diff --git a/src/validation/__tests__/ValuesOfCorrectTypeRule-test.js b/src/validation/__tests__/ValuesOfCorrectTypeRule-test.js index 1f3cbab8e01..07d397fa796 100644 --- a/src/validation/__tests__/ValuesOfCorrectTypeRule-test.js +++ b/src/validation/__tests__/ValuesOfCorrectTypeRule-test.js @@ -787,7 +787,7 @@ describe('Validate: Values of correct type', () => { } `).to.deep.equal([ { - message: 'Expected value of type "Int!", found null.', + message: 'Expected value of non-null type "Int!" not to be null.', locations: [{ line: 4, column: 32 }], }, ]); @@ -879,7 +879,7 @@ describe('Validate: Values of correct type', () => { `).to.deep.equal([ { message: - 'Field "ComplexInput.requiredField" of required type "Boolean!" was not provided.', + 'Expected value of type "ComplexInput" to include required field "requiredField", found {intField: 4}', locations: [{ line: 4, column: 41 }], }, ]); @@ -915,7 +915,7 @@ describe('Validate: Values of correct type', () => { } `).to.deep.equal([ { - message: 'Expected value of type "Boolean!", found null.', + message: 'Expected value of non-null type "Boolean!" not to be null.', locations: [{ line: 6, column: 29 }], }, ]); @@ -934,7 +934,7 @@ describe('Validate: Values of correct type', () => { `).to.deep.equal([ { message: - 'Field "invalidField" is not defined by type "ComplexInput". Did you mean "intField"?', + 'Expected value of type "ComplexInput" not to include unknown field "invalidField". Did you mean "intField"? Found {requiredField: true, invalidField: "value"}', locations: [{ line: 6, column: 15 }], }, ]); @@ -1108,15 +1108,15 @@ describe('Validate: Values of correct type', () => { } `).to.deep.equal([ { - message: 'Expected value of type "Int!", found null.', + message: 'Expected value of non-null type "Int!" not to be null.', locations: [{ line: 3, column: 22 }], }, { - message: 'Expected value of type "String!", found null.', + message: 'Expected value of non-null type "String!" not to be null.', locations: [{ line: 4, column: 25 }], }, { - message: 'Expected value of type "Boolean!", found null.', + message: 'Expected value of non-null type "Boolean!" not to be null.', locations: [{ line: 5, column: 47 }], }, ]); @@ -1142,7 +1142,7 @@ describe('Validate: Values of correct type', () => { }, { message: - 'Expected value of type "ComplexInput", found "NotVeryComplex".', + 'Expected value of type "ComplexInput" to be an object, found "NotVeryComplex".', locations: [{ line: 5, column: 30 }], }, ]); @@ -1175,7 +1175,7 @@ describe('Validate: Values of correct type', () => { `).to.deep.equal([ { message: - 'Field "ComplexInput.requiredField" of required type "Boolean!" was not provided.', + 'Expected value of type "ComplexInput" to include required field "requiredField", found {intField: 3}', locations: [{ line: 2, column: 55 }], }, ]); diff --git a/src/validation/rules/ValuesOfCorrectTypeRule.js b/src/validation/rules/ValuesOfCorrectTypeRule.js index e2e291cbdbe..9aef43542d9 100644 --- a/src/validation/rules/ValuesOfCorrectTypeRule.js +++ b/src/validation/rules/ValuesOfCorrectTypeRule.js @@ -1,26 +1,9 @@ -import { keyMap } from '../../jsutils/keyMap'; -import { inspect } from '../../jsutils/inspect'; -import { didYouMean } from '../../jsutils/didYouMean'; -import { suggestionList } from '../../jsutils/suggestionList'; - -import { GraphQLError } from '../../error/GraphQLError'; - -import type { ValueNode } from '../../language/ast'; import type { ASTVisitor } from '../../language/visitor'; -import { print } from '../../language/printer'; - -import { - isLeafType, - isInputObjectType, - isListType, - isNonNullType, - isRequiredInput, - getNullableType, - getNamedType, -} from '../../type/definition'; +import { Kind } from '../../language/kinds'; +import { isValueNode } from '../../language/predicates'; import type { ValidationContext } from '../ValidationContext'; -import { replaceVariables } from '../../utilities/replaceVariables'; +import { validateInputLiteral } from '../../utilities/validateInputLiteral'; /** * Value literals of correct type @@ -32,127 +15,19 @@ export function ValuesOfCorrectTypeRule( context: ValidationContext, ): ASTVisitor { return { - ListValue(node) { - // Note: TypeInfo will traverse into a list's item type, so look to the - // parent input type to check if it is a list. - const type = getNullableType(context.getParentInputType()); - if (!isListType(type)) { - isValidValueNode(context, node); - return false; // Don't traverse further. - } - }, - ObjectValue(node) { - const type = getNamedType(context.getInputType()); - if (!isInputObjectType(type)) { - isValidValueNode(context, node); - return false; // Don't traverse further. - } - // Ensure every required field exists. - const fieldNodeMap = keyMap(node.fields, (field) => field.name.value); - for (const fieldDef of Object.values(type.getFields())) { - const fieldNode = fieldNodeMap[fieldDef.name]; - if (!fieldNode && isRequiredInput(fieldDef)) { - const typeStr = inspect(fieldDef.type); - context.reportError( - new GraphQLError( - `Field "${type.name}.${fieldDef.name}" of required type "${typeStr}" was not provided.`, - node, - ), - ); + enter(node) { + if (isValueNode(node)) { + const inputType = + node.kind === Kind.LIST + ? context.getParentInputType() + : context.getInputType(); + if (inputType) { + validateInputLiteral(node, inputType, undefined, (error) => { + context.reportError(error); + }); } + return false; } }, - ObjectField(node) { - const parentType = getNamedType(context.getParentInputType()); - const fieldType = context.getInputType(); - if (!fieldType && isInputObjectType(parentType)) { - const suggestions = suggestionList( - node.name.value, - Object.keys(parentType.getFields()), - ); - context.reportError( - new GraphQLError( - `Field "${node.name.value}" is not defined by type "${parentType.name}".` + - didYouMean(suggestions), - node, - ), - ); - } - }, - NullValue(node) { - const type = context.getInputType(); - if (isNonNullType(type)) { - context.reportError( - new GraphQLError( - `Expected value of type "${inspect(type)}", found ${print(node)}.`, - node, - ), - ); - } - }, - EnumValue: (node) => isValidValueNode(context, node), - IntValue: (node) => isValidValueNode(context, node), - FloatValue: (node) => isValidValueNode(context, node), - StringValue: (node) => isValidValueNode(context, node), - BooleanValue: (node) => isValidValueNode(context, node), }; } - -/** - * Any value literal may be a valid representation of a Scalar, depending on - * that scalar type. - */ -function isValidValueNode(context: ValidationContext, node: ValueNode): void { - // Report any error at the full type expected by the location. - const locationType = context.getInputType(); - if (!locationType) { - return; - } - - const type = getNamedType(locationType); - - if (!isLeafType(type)) { - const typeStr = inspect(locationType); - context.reportError( - new GraphQLError( - `Expected value of type "${typeStr}", found ${print(node)}.`, - node, - ), - ); - return; - } - - const constValueNode = replaceVariables(node); - - // Scalars and Enums determine if a literal value is valid via parseLiteral(), - // which may throw or return undefined to indicate an invalid value. - try { - const parseResult = type.parseLiteral(constValueNode); - if (parseResult === undefined) { - const typeStr = inspect(locationType); - context.reportError( - new GraphQLError( - `Expected value of type "${typeStr}", found ${print(node)}.`, - node, - ), - ); - } - } catch (error) { - const typeStr = inspect(locationType); - if (error instanceof GraphQLError) { - context.reportError(error); - } else { - context.reportError( - new GraphQLError( - `Expected value of type "${typeStr}", found ${print(node)}; ` + - error.message, - node, - undefined, - undefined, - undefined, - error, // Ensure a reference to the original error is maintained. - ), - ); - } - } -}