From 9a9d330bca35d66e9f461f5e3e8049d41a7ea0e3 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Thu, 13 May 2021 10:30:16 -0700 Subject: [PATCH] Input Value Validation 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 Potentially breaking if you rely on the existing behavior of `coerceInputValue` to call a callback function, as the call signature has changed. GraphQL behavior should not change, though error messages are now slightly different. --- src/execution/__tests__/nonnull-test.ts | 6 +- src/execution/__tests__/variables-test.ts | 34 +- src/execution/values.ts | 144 ++- src/index.ts | 6 +- src/jsutils/printPathArray.ts | 11 +- src/subscription/__tests__/subscribe-test.ts | 2 +- src/type/__tests__/enumType-test.ts | 2 +- .../__tests__/coerceInputValue-test.ts | 321 ++----- .../__tests__/validateInputValue-test.ts | 837 ++++++++++++++++++ src/utilities/coerceInputValue.ts | 185 +--- src/utilities/index.ts | 9 +- src/utilities/validateInputValue.ts | 385 ++++++++ .../__tests__/ValuesOfCorrectTypeRule-test.ts | 32 +- .../rules/ValuesOfCorrectTypeRule.ts | 148 +--- 14 files changed, 1459 insertions(+), 663 deletions(-) create mode 100644 src/utilities/__tests__/validateInputValue-test.ts create mode 100644 src/utilities/validateInputValue.ts diff --git a/src/execution/__tests__/nonnull-test.ts b/src/execution/__tests__/nonnull-test.ts index dff67abd63..43ac8a4659 100644 --- a/src/execution/__tests__/nonnull-test.ts +++ b/src/execution/__tests__/nonnull-test.ts @@ -643,7 +643,7 @@ describe('Execute: handles non-nullable types', () => { errors: [ { message: - 'Argument Query.withNonNullArg(cannotBeNull:) of non-null type String! must not be null.', + 'Argument Query.withNonNullArg(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 Query.withNonNullArg(cannotBeNull:) of required type String! was provided the variable "$testVar" which was not provided a runtime value.', + 'Argument Query.withNonNullArg(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 Query.withNonNullArg(cannotBeNull:) of non-null type String! must not be null.', + 'Argument Query.withNonNullArg(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.ts b/src/execution/__tests__/variables-test.ts index 27404ea5a2..7ac91b20c2 100644 --- a/src/execution/__tests__/variables-test.ts +++ b/src/execution/__tests__/variables-test.ts @@ -203,7 +203,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Argument TestType.fieldWithObjectInput(input:) of type TestInputObject has invalid value ["foo", "bar", "baz"].', + 'Argument TestType.fieldWithObjectInput(input:) has invalid value: Expected value of type TestInputObject to be an object, found: ["foo", "bar", "baz"].', path: ['fieldWithObjectInput'], locations: [{ line: 3, column: 41 }], }, @@ -373,7 +373,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 }], }, ], @@ -387,7 +387,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 }], }, ], @@ -401,7 +401,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 }], }, ], @@ -420,12 +420,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 }], }, ], @@ -442,7 +442,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 }], }, ], @@ -617,7 +617,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 }], }, ], @@ -636,7 +636,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 }], }, ], @@ -702,7 +702,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 }], }, ], @@ -730,7 +730,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Argument TestType.fieldWithNonNullableStringInput(input:) of required type String! was provided the variable "$foo" which was not provided a runtime value.', + 'Argument TestType.fieldWithNonNullableStringInput(input:) has invalid value: Expected variable "$foo" provided to type String! to provide a runtime value.', locations: [{ line: 3, column: 50 }], path: ['fieldWithNonNullableStringInput'], }, @@ -785,7 +785,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 }], }, ], @@ -848,7 +848,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 }], }, ], @@ -867,7 +867,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 }], }, ], @@ -897,7 +897,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 }], }, ], @@ -982,7 +982,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Argument TestType.fieldWithDefaultArgumentValue(input:) of type String has invalid value WRONG_TYPE.', + 'Argument TestType.fieldWithDefaultArgumentValue(input:) has invalid value: String cannot represent a non string value: WRONG_TYPE', locations: [{ line: 3, column: 48 }], path: ['fieldWithDefaultArgumentValue'], }, @@ -1022,7 +1022,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.ts b/src/execution/values.ts index b71d2d624d..99ef30147a 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -1,7 +1,7 @@ import type { ObjMap, ReadOnlyObjMap } from '../jsutils/ObjMap'; import type { Maybe } from '../jsutils/Maybe'; import { keyMap } from '../jsutils/keyMap'; -import { inspect } from '../jsutils/inspect'; +import { invariant } from '../jsutils/invariant'; import { printPathArray } from '../jsutils/printPathArray'; import { GraphQLError } from '../error/GraphQLError'; @@ -17,7 +17,11 @@ 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, + isRequiredArgument, +} from '../type/definition'; import { typeFromAST } from '../utilities/typeFromAST'; import { @@ -25,6 +29,10 @@ import { coerceInputLiteral, coerceDefaultValue, } from '../utilities/coerceInputValue'; +import { + validateInputValue, + validateInputLiteral, +} from '../utilities/validateInputValue'; export interface VariableValues { readonly sources: ReadOnlyObjMap; @@ -109,50 +117,29 @@ function coerceVariableValues( continue; } - if (!hasOwnProperty(inputs, varName)) { - const defaultValue = varDefNode.defaultValue; - if (defaultValue) { - sources[varName] = { - variable: varDefNode, - type: varType, - value: undefined, - }; - coerced[varName] = coerceInputLiteral(defaultValue, varType); - } else if (isNonNullType(varType)) { - onError( - new GraphQLError( - `Variable "$${varName}" of required type ${varType} was not provided.`, - varDefNode, - ), - ); - } - continue; - } + const value = hasOwnProperty(inputs, varName) ? inputs[varName] : undefined; + sources[varName] = { variable: varDefNode, type: varType, value }; - const value = inputs[varName]; - if (value === null && isNonNullType(varType)) { - onError( - new GraphQLError( - `Variable "$${varName}" of non-null type ${varType} must not be null.`, - varDefNode, - ), - ); - continue; + if (value === undefined) { + if (varDefNode.defaultValue) { + coerced[varName] = coerceInputLiteral(varDefNode.defaultValue, varType); + continue; + } else if (!isNonNullType(varType)) { + // Non-provided values for nullable variables are omitted. + 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, @@ -160,8 +147,8 @@ function coerceVariableValues( error.originalError, ), ); - }, - ); + }); + } } return { sources, coerced }; @@ -193,65 +180,52 @@ export function getArgumentValues( const argType = argDef.type; const argumentNode = argNodeMap[name]; - if (!argumentNode) { + if (!argumentNode && isRequiredArgument(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 ${argDef} of required type ${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 && + !isRequiredArgument(argDef)) + ) { if (argDef.defaultValue) { coercedValues[name] = coerceDefaultValue( argDef.defaultValue, argDef.type, ); - } else if (isNonNullType(argType)) { - throw new GraphQLError( - `Argument ${argDef} of required type ${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] = coerceDefaultValue( - argDef.defaultValue, - argDef.type, - ); - } else if (isNonNullType(argType)) { - throw new GraphQLError( - `Argument ${argDef} of required type ${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 ${argDef} of non-null type ${argType} must not be null.`, - valueNode, - ); - } - const coercedValue = coerceInputLiteral(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 ${argDef} of type ${argType} has invalid value ${print( - valueNode, - )}.`, + validateInputLiteral( valueNode, + argType, + variableValues, + (error, path) => { + error.message = `Argument ${argDef} has invalid value${printPathArray( + path, + )}: ${error.message}`; + throw error; + }, ); + // istanbul ignore next (validateInputLiteral should throw) + invariant(false, 'Invalid argument'); } coercedValues[name] = coercedValue; } diff --git a/src/index.ts b/src/index.ts index 0fae538294..d9f5e8eda8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -420,10 +420,14 @@ export { replaceVariables, /** Create a GraphQL literal (AST) from a JavaScript input value. */ valueToLiteral, - /** Coerces a JavaScript value to a GraphQL type, or produces errors. */ + /** Coerces a JavaScript value to a GraphQL type, or returns undefined. */ coerceInputValue, /** Coerces a GraphQL literal (AST) to a GraphQL type, or returns undefined. */ coerceInputLiteral, + /** 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.ts b/src/jsutils/printPathArray.ts index 0d9fcc2b19..e51abf67dc 100644 --- a/src/jsutils/printPathArray.ts +++ b/src/jsutils/printPathArray.ts @@ -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.ts b/src/subscription/__tests__/subscribe-test.ts index 85d6400a3d..e9b6ada499 100644 --- a/src/subscription/__tests__/subscribe-test.ts +++ b/src/subscription/__tests__/subscribe-test.ts @@ -474,7 +474,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.ts b/src/type/__tests__/enumType-test.ts index 00a4ec18dc..fd3a0617e2 100644 --- a/src/type/__tests__/enumType-test.ts +++ b/src/type/__tests__/enumType-test.ts @@ -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.ts b/src/utilities/__tests__/coerceInputValue-test.ts index 318eaa3ea4..1c24a7ead0 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -34,71 +34,28 @@ import { coerceDefaultValue, } from '../coerceInputValue'; -interface CoerceResult { - value: unknown; - errors: ReadonlyArray; -} - -interface CoerceError { - path: ReadonlyArray; - value: unknown; - error: string; -} - -function coerceValue( - inputValue: unknown, - type: GraphQLInputType, -): CoerceResult { - const errors: Array = []; - 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: unknown, + type: GraphQLInputType, + expected: unknown, + ) { + 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); }); }); @@ -113,42 +70,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); }); }); @@ -162,44 +104,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); }); }); @@ -213,84 +129,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); }); }); @@ -307,23 +166,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); }); }); @@ -331,8 +188,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', () => { @@ -342,29 +198,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', () => { @@ -377,24 +219,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); }); }); @@ -402,49 +235,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__/validateInputValue-test.ts b/src/utilities/__tests__/validateInputValue-test.ts new file mode 100644 index 0000000000..97532a3682 --- /dev/null +++ b/src/utilities/__tests__/validateInputValue-test.ts @@ -0,0 +1,837 @@ +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 { + validateInputValue, + validateInputLiteral, +} from '../validateInputValue'; + +describe('validateInputValue', () => { + function test( + inputValue: unknown, + type: GraphQLInputType, + expected: unknown, + ) { + const errors: any = []; + 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: any) { + 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; Some error message. Found: { error: "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 @typescript-eslint/no-throw-literal + throw 'Not an error object.'; + }, + }); + + test({}, TestThrowScalar, [ + { + error: + 'Expected value of type TestScalar; Not an error object. Found: {}.', + 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', () => { + function makeTestInputObject(defaultValue: unknown) { + return 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, []); + }); + }); +}); + +describe('validateInputLiteral', () => { + function test( + inputValue: string, + type: GraphQLInputType, + expected: unknown, + variableValues?: VariableValues, + ) { + const errors: any = []; + 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: unknown, + ) { + const parser = new Parser(variableDefs); + parser.expectToken(''); + const variableValuesOrErrors = getVariableValues( + new GraphQLSchema({ types: [GraphQLInt] }), + parser.parseVariableDefinitions(), + values, + ); + invariant(variableValuesOrErrors.variableValues); + test(inputValue, type, expected, variableValuesOrErrors.variableValues); + } + + 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: any) { + 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; Some error message. Found: {error: "Some error message."}.', + path: [], + }, + ]); + }); + + it('reports thrown non-error', () => { + const TestThrowScalar = new GraphQLScalarType({ + name: 'TestScalar', + parseValue() { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw 'Not an error object.'; + }, + }); + + test('{}', TestThrowScalar, [ + { + error: + 'Expected value of type TestScalar; Not an error object. Found: {}.', + 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', () => { + function makeTestInputObject(defaultValue: unknown) { + return 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/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index af057d302a..b297dea262 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -1,24 +1,15 @@ import type { Maybe } from '../jsutils/Maybe'; -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 { isIterableObject } from '../jsutils/isIterableObject'; import { isObjectLike } from '../jsutils/isObjectLike'; import { keyMap } from '../jsutils/keyMap'; -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, GraphQLDefaultValueUsage, } from '../type/definition'; import { - isLeafType, assertLeafType, isInputObjectType, isListType, @@ -33,176 +24,90 @@ import type { VariableValues } from '../execution/values'; import { replaceVariables } from './replaceVariables'; -type OnErrorCB = ( - path: ReadonlyArray, - invalidValue: unknown, - error: GraphQLError, -) => void; - /** * Coerces a JavaScript value given a GraphQL Input Type. + * + * Returns `undefined` when the value could not be validly coerced according to + * the provided type. */ export function coerceInputValue( inputValue: unknown, type: GraphQLInputType, - onError: OnErrorCB = defaultOnError, -): unknown { - return coerceInputValueImpl(inputValue, type, onError, undefined); -} - -function defaultOnError( - path: ReadonlyArray, - invalidValue: unknown, - 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: unknown, - type: GraphQLInputType, - onError: OnErrorCB, - path: Path | undefined, ): unknown { 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]; + } + 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); } - // Lists accept a non-list value as a list of one. - return [coerceInputValueImpl(inputValue, itemType, onError, path)]; + 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: any = {}; 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 (isRequiredInputField(field)) { + return; // Invalid: intentionally return no value. + } if (field.defaultValue) { coercedValue[field.name] = coerceDefaultValue( 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; } - // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') - if (isLeafType(type)) { - let parseResult; + const leafType = assertLeafType(type); - // 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 parseResult; + try { + return leafType.parseValue(inputValue); + } catch (_error) { + // Invalid: ignore error and intentionally return no value. } - - // istanbul ignore next (Not reachable. All possible input types have been considered) - invariant(false, 'Unexpected input type: ' + inspect(type)); } /** diff --git a/src/utilities/index.ts b/src/utilities/index.ts index a2246ad85c..6f25494c62 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -77,12 +77,19 @@ export { replaceVariables } from './replaceVariables'; export { valueToLiteral } from './valueToLiteral'; export { - /** Coerces a JavaScript value to a GraphQL type, or produces errors. */ + /** Coerces a JavaScript value to a GraphQL type, or returns undefined. */ coerceInputValue, /** Coerces a GraphQL literal (AST) to a GraphQL type, or returns undefined. */ coerceInputLiteral, } from './coerceInputValue'; +export { + /** 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, +} from './validateInputValue'; + /** Concatenates multiple AST together. */ export { concatAST } from './concatAST'; diff --git a/src/utilities/validateInputValue.ts b/src/utilities/validateInputValue.ts new file mode 100644 index 0000000000..db9419bc0b --- /dev/null +++ b/src/utilities/validateInputValue.ts @@ -0,0 +1,385 @@ +import type { Maybe } from '../jsutils/Maybe'; +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 { isIterableObject } from '../jsutils/isIterableObject'; +import { isObjectLike } from '../jsutils/isObjectLike'; +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 { + assertLeafType, + isInputObjectType, + isListType, + isNonNullType, + isRequiredInputField, +} from '../type/definition'; + +import type { VariableValues } from '../execution/values'; + +import { replaceVariables } from './replaceVariables'; + +/** + * 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, + path?: Path, +): void { + if (isNonNullType(type)) { + if (inputValue === undefined) { + reportInvalidValue( + onError, + `Expected a value of non-null type ${type} to be provided.`, + path, + ); + return; + } + if (inputValue === null) { + reportInvalidValue( + onError, + `Expected value of non-null type ${type} not to be null.`, + path, + ); + return; + } + return 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} to be an object, found: ${inspect( + inputValue, + )}.`, + path, + ); + return; + } + + const fieldDefs = type.getFields(); + + for (const field of Object.values(fieldDefs)) { + const fieldValue = inputValue[field.name]; + if (fieldValue === undefined) { + if (isRequiredInputField(field)) { + reportInvalidValue( + onError, + `Expected value of type ${type} to include required field "${ + field.name + }", found: ${inspect(inputValue)}.`, + path, + ); + } + } else { + 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} not to include unknown field "${fieldName}"${ + suggestions.length > 0 + ? `.${didYouMean(suggestions)} Found` + : ', found' + }: ${inspect(inputValue)}.`, + path, + ); + } + } + } else { + assertLeafType(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}${ + caughtError + ? `; ${caughtError.message || caughtError} Found` + : ', found' + }: ${inspect(inputValue)}.`, + path, + caughtError, + ); + } + } +} + +function reportInvalidValue( + onError: (error: GraphQLError, path: ReadonlyArray) => void, + message: string, + path: Path | undefined, + originalError?: GraphQLError | undefined, +): void { + onError( + new GraphQLError(message, null, null, null, null, originalError), + pathToArray(path), + ); +} + +/** + * 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, + 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)) { + reportInvalidLiteral( + onError, + `Expected variable "$${valueNode.name.value}" provided to type ${type} to provide a runtime value.`, + valueNode, + path, + ); + } else if ( + isNonNullType(type) && + variables.coerced[valueNode.name.value] === null + ) { + reportInvalidLiteral( + onError, + `Expected variable "$${valueNode.name.value}" provided to non-null type ${type} not to be null.`, + valueNode, + path, + ); + } + // Note: This does no further checking that this variable is correct. + // This assumes this variable usage has already been validated. + return; + } + + if (isNonNullType(type)) { + if (valueNode.kind === Kind.NULL) { + reportInvalidLiteral( + onError, + `Expected value of non-null type ${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) { + reportInvalidLiteral( + onError, + `Expected value of type ${type} to be an object, found: ${print( + valueNode, + )}.`, + valueNode, + path, + ); + return; + } + + const fieldDefs = type.getFields(); + const fieldNodes = keyMap(valueNode.fields, (field) => field.name.value); + + for (const field of Object.values(fieldDefs)) { + const fieldNode = fieldNodes[field.name]; + if (fieldNode === undefined) { + if (isRequiredInputField(field)) { + reportInvalidLiteral( + onError, + `Expected value of type ${type} 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) && + !isRequiredInputField(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)); + reportInvalidLiteral( + onError, + `Expected value of type ${type} not to include unknown field "${fieldName}"${ + suggestions.length > 0 + ? `.${didYouMean(suggestions)} Found` + : ', found' + }: ${print(valueNode)}.`, + fieldNode, + path, + ); + } + } + } else { + assertLeafType(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) { + reportInvalidLiteral( + onError, + `Expected value of type ${type}${ + caughtError + ? `; ${caughtError.message || caughtError} Found` + : ', found' + }: ${print(valueNode)}.`, + valueNode, + path, + caughtError, + ); + } + } +} + +// 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 reportInvalidLiteral( + onError: (error: GraphQLError, path: ReadonlyArray) => void, + message: string, + valueNode: ASTNode, + path: Path | undefined, + originalError?: GraphQLError | undefined, +): void { + onError( + new GraphQLError(message, valueNode, null, null, null, originalError), + pathToArray(path), + ); +} diff --git a/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts b/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts index 1f3cbab8e0..d739de37a1 100644 --- a/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts +++ b/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts @@ -1,7 +1,5 @@ import { describe, it } from 'mocha'; -import { inspect } from '../../jsutils/inspect'; - import { GraphQLSchema } from '../../type/schema'; import { GraphQLString } from '../../type/scalars'; import { GraphQLScalarType, GraphQLObjectType } from '../../type/definition'; @@ -787,7 +785,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 +877,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 +913,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 +932,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 }], }, ]); @@ -943,10 +941,8 @@ describe('Validate: Values of correct type', () => { it('reports original error for custom scalar which throws', () => { const customScalar = new GraphQLScalarType({ name: 'Invalid', - parseValue(value) { - throw new Error( - `Invalid scalar is always invalid: ${inspect(value)}`, - ); + parseValue() { + throw new Error('Invalid scalar is always invalid.'); }, }); @@ -970,14 +966,14 @@ describe('Validate: Values of correct type', () => { expectedErrors.to.deep.equal([ { message: - 'Expected value of type "Invalid", found 123; Invalid scalar is always invalid: 123', + 'Expected value of type Invalid; Invalid scalar is always invalid. Found: 123.', locations: [{ line: 1, column: 19 }], }, ]); expectedErrors.to.have.nested.property( '[0].originalError.message', - 'Invalid scalar is always invalid: 123', + 'Invalid scalar is always invalid.', ); }); @@ -1003,7 +999,7 @@ describe('Validate: Values of correct type', () => { expectErrorsWithSchema(schema, '{ invalidArg(arg: 123) }').to.deep.equal([ { - message: 'Expected value of type "CustomScalar", found 123.', + message: 'Expected value of type CustomScalar, found: 123.', locations: [{ line: 1, column: 19 }], }, ]); @@ -1108,15 +1104,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 +1138,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 +1171,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.ts b/src/validation/rules/ValuesOfCorrectTypeRule.ts index d29530cffd..b6a7a07e94 100644 --- a/src/validation/rules/ValuesOfCorrectTypeRule.ts +++ b/src/validation/rules/ValuesOfCorrectTypeRule.ts @@ -1,25 +1,9 @@ -import { keyMap } from '../../jsutils/keyMap'; -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, - isRequiredInputField, - 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/validateInputValue'; /** * Value literals of correct type @@ -31,123 +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 && isRequiredInputField(fieldDef)) { - context.reportError( - new GraphQLError( - `Field "${fieldDef}" of required type "${fieldDef.type}" 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}".` + - didYouMean(suggestions), - node, - ), - ); - } - }, - NullValue(node) { - const type = context.getInputType(); - if (isNonNullType(type)) { - context.reportError( - new GraphQLError( - `Expected value of type "${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)) { - context.reportError( - new GraphQLError( - `Expected value of type "${locationType}", 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) { - context.reportError( - new GraphQLError( - `Expected value of type "${locationType}", found ${print(node)}.`, - node, - ), - ); - } - } catch (error) { - if (error instanceof GraphQLError) { - context.reportError(error); - } else { - context.reportError( - new GraphQLError( - `Expected value of type "${locationType}", found ${print(node)}; ` + - error.message, - node, - undefined, - undefined, - undefined, - error, // Ensure a reference to the original error is maintained. - ), - ); - } - } -}