From 2f04eed7b745e529cb71958e80a3969980642cd0 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Fri, 14 May 2021 21:25:33 -0700 Subject: [PATCH] Add coerceInputLiteral() Removes `valueFromAST()` and adds `coerceInputLiteral()` as an additional export from `coerceInputValue`. The implementation is almost exactly the same as `valueFromAST()` with a slightly more strict type signature and refactored tests to improve coverage (the file unit test has 100% coverage) While this does not change any behavior, it could be breaking if you rely directly on the valueFromAST() method. Use `coerceInputLiteral()` as a direct replacement. --- src/execution/values.ts | 13 +- src/index.ts | 4 +- src/jsutils/hasOwnProperty.ts | 6 + src/language/parser.ts | 2 - .../__tests__/coerceInputValue-test.ts | 258 ++++++++++++++++- src/utilities/__tests__/valueFromAST-test.ts | 265 ------------------ src/utilities/buildClientSchema.ts | 9 +- src/utilities/coerceInputValue.ts | 134 +++++++++ src/utilities/extendSchema.ts | 10 +- src/utilities/index.ts | 11 +- src/utilities/valueFromAST.ts | 163 ----------- src/utilities/valueFromASTUntyped.ts | 4 +- 12 files changed, 428 insertions(+), 451 deletions(-) create mode 100644 src/jsutils/hasOwnProperty.ts delete mode 100644 src/utilities/__tests__/valueFromAST-test.ts delete mode 100644 src/utilities/valueFromAST.ts diff --git a/src/execution/values.ts b/src/execution/values.ts index 6d4c95b85a1..4b6640c5afb 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -20,8 +20,10 @@ import type { GraphQLDirective } from '../type/directives'; import { isInputType, isNonNullType } from '../type/definition'; import { typeFromAST } from '../utilities/typeFromAST'; -import { valueFromAST } from '../utilities/valueFromAST'; -import { coerceInputValue } from '../utilities/coerceInputValue'; +import { + coerceInputValue, + coerceInputLiteral, +} from '../utilities/coerceInputValue'; type CoercedVariableValues = | { errors: ReadonlyArray; coerced?: never } @@ -96,7 +98,10 @@ function coerceVariableValues( if (!hasOwnProperty(inputs, varName)) { if (varDefNode.defaultValue) { - coercedValues[varName] = valueFromAST(varDefNode.defaultValue, varType); + coercedValues[varName] = coerceInputLiteral( + varDefNode.defaultValue, + varType, + ); } else if (isNonNullType(varType)) { const varTypeStr = inspect(varType); onError( @@ -217,7 +222,7 @@ export function getArgumentValues( ); } - const coercedValue = valueFromAST(valueNode, argType, variableValues); + 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 diff --git a/src/index.ts b/src/index.ts index 5ed1c34dc89..183d249a890 100644 --- a/src/index.ts +++ b/src/index.ts @@ -408,8 +408,6 @@ export { printIntrospectionSchema, /** Create a GraphQLType from a GraphQL language AST. */ typeFromAST, - /** Create a JavaScript value from a GraphQL language AST with a Type. */ - valueFromAST, /** Create a JavaScript value from a GraphQL language AST without a Type. */ valueFromASTUntyped, /** Create a GraphQL language AST from a JavaScript value. */ @@ -419,6 +417,8 @@ export { visitWithTypeInfo, /** Coerces a JavaScript value to a GraphQL type, or produces errors. */ coerceInputValue, + /** Coerces a GraphQL literal (AST) to a GraphQL type, or returns undefined. */ + coerceInputLiteral, /** Concatenates multiple AST together. */ concatAST, /** Separates an AST into an AST per Operation. */ diff --git a/src/jsutils/hasOwnProperty.ts b/src/jsutils/hasOwnProperty.ts new file mode 100644 index 00000000000..1ae88706318 --- /dev/null +++ b/src/jsutils/hasOwnProperty.ts @@ -0,0 +1,6 @@ +/** + * Determines if a provided object has a given property name. + */ +export function hasOwnProperty(obj: {}, prop: string): boolean { + return Object.prototype.hasOwnProperty.call(obj, prop); +} diff --git a/src/language/parser.ts b/src/language/parser.ts index 37904b5d2ec..90a24bef5e2 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -118,8 +118,6 @@ export function parse( * * This is useful within tools that operate upon GraphQL Values directly and * in isolation of complete GraphQL documents. - * - * Consider providing the results to the utility function: valueFromAST(). */ export function parseValue( source: string | Source, diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index 32e127619aa..c76bf0cc5d1 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -1,8 +1,21 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; +import type { ObjMap } from '../../jsutils/ObjMap'; +import { invariant } from '../../jsutils/invariant'; +import { identityFunc } from '../../jsutils/identityFunc'; + +import { print } from '../../language/printer'; +import { parseValue } from '../../language/parser'; + import type { GraphQLInputType } from '../../type/definition'; -import { GraphQLInt } from '../../type/scalars'; +import { + GraphQLInt, + GraphQLFloat, + GraphQLString, + GraphQLBoolean, + GraphQLID, +} from '../../type/scalars'; import { GraphQLList, GraphQLNonNull, @@ -11,7 +24,7 @@ import { GraphQLInputObjectType, } from '../../type/definition'; -import { coerceInputValue } from '../coerceInputValue'; +import { coerceInputValue, coerceInputLiteral } from '../coerceInputValue'; interface CoerceResult { value: unknown; @@ -427,3 +440,244 @@ describe('coerceInputValue', () => { }); }); }); + +describe('coerceInputLiteral', () => { + function test( + valueText: string, + type: GraphQLInputType, + expected: unknown, + variables?: ObjMap, + ) { + const ast = parseValue(valueText); + const value = coerceInputLiteral(ast, type, variables); + expect(value).to.deep.equal(expected); + } + + function testWithVariables( + variables: ObjMap, + valueText: string, + type: GraphQLInputType, + expected: unknown, + ) { + test(valueText, type, expected, variables); + } + + it('converts according to input coercion rules', () => { + test('true', GraphQLBoolean, true); + test('false', GraphQLBoolean, false); + test('123', GraphQLInt, 123); + test('123', GraphQLFloat, 123); + test('123.456', GraphQLFloat, 123.456); + test('"abc123"', GraphQLString, 'abc123'); + test('123456', GraphQLID, '123456'); + test('"123456"', GraphQLID, '123456'); + }); + + it('does not convert when input coercion rules reject a value', () => { + test('123', GraphQLBoolean, undefined); + test('123.456', GraphQLInt, undefined); + test('true', GraphQLInt, undefined); + test('"123"', GraphQLInt, undefined); + test('"123"', GraphQLFloat, undefined); + test('123', GraphQLString, undefined); + test('true', GraphQLString, undefined); + test('123.456', GraphQLString, undefined); + test('123.456', GraphQLID, undefined); + }); + + it('convert using parseLiteral from a custom scalar type', () => { + const passthroughScalar = new GraphQLScalarType({ + name: 'PassthroughScalar', + parseLiteral(node) { + invariant(node.kind === 'StringValue'); + return node.value; + }, + parseValue: identityFunc, + }); + + test('"value"', passthroughScalar, 'value'); + + const printScalar = new GraphQLScalarType({ + name: 'PrintScalar', + parseLiteral(node) { + return `~~~${print(node)}~~~`; + }, + parseValue: identityFunc, + }); + + test('"value"', printScalar, '~~~"value"~~~'); + + const throwScalar = new GraphQLScalarType({ + name: 'ThrowScalar', + parseLiteral() { + throw new Error('Test'); + }, + parseValue: identityFunc, + }); + + test('value', throwScalar, undefined); + + const returnUndefinedScalar = new GraphQLScalarType({ + name: 'ReturnUndefinedScalar', + parseLiteral() { + return undefined; + }, + parseValue: identityFunc, + }); + + test('value', returnUndefinedScalar, undefined); + }); + + it('converts enum values according to input coercion rules', () => { + const testEnum = new GraphQLEnumType({ + name: 'TestColor', + values: { + RED: { value: 1 }, + GREEN: { value: 2 }, + BLUE: { value: 3 }, + NULL: { value: null }, + NAN: { value: NaN }, + NO_CUSTOM_VALUE: { value: undefined }, + }, + }); + + test('RED', testEnum, 1); + test('BLUE', testEnum, 3); + test('3', testEnum, undefined); + test('"BLUE"', testEnum, undefined); + test('null', testEnum, null); + test('NULL', testEnum, null); + test('NULL', new GraphQLNonNull(testEnum), null); + test('NAN', testEnum, NaN); + test('NO_CUSTOM_VALUE', testEnum, 'NO_CUSTOM_VALUE'); + }); + + // Boolean! + const nonNullBool = new GraphQLNonNull(GraphQLBoolean); + // [Boolean] + const listOfBool = new GraphQLList(GraphQLBoolean); + // [Boolean!] + const listOfNonNullBool = new GraphQLList(nonNullBool); + // [Boolean]! + const nonNullListOfBool = new GraphQLNonNull(listOfBool); + // [Boolean!]! + const nonNullListOfNonNullBool = new GraphQLNonNull(listOfNonNullBool); + + it('coerces to null unless non-null', () => { + test('null', GraphQLBoolean, null); + test('null', nonNullBool, undefined); + }); + + it('coerces lists of values', () => { + test('true', listOfBool, [true]); + test('123', listOfBool, undefined); + test('null', listOfBool, null); + test('[true, false]', listOfBool, [true, false]); + test('[true, 123]', listOfBool, undefined); + test('[true, null]', listOfBool, [true, null]); + test('{ true: true }', listOfBool, undefined); + }); + + it('coerces non-null lists of values', () => { + test('true', nonNullListOfBool, [true]); + test('123', nonNullListOfBool, undefined); + test('null', nonNullListOfBool, undefined); + test('[true, false]', nonNullListOfBool, [true, false]); + test('[true, 123]', nonNullListOfBool, undefined); + test('[true, null]', nonNullListOfBool, [true, null]); + }); + + it('coerces lists of non-null values', () => { + test('true', listOfNonNullBool, [true]); + test('123', listOfNonNullBool, undefined); + test('null', listOfNonNullBool, null); + test('[true, false]', listOfNonNullBool, [true, false]); + test('[true, 123]', listOfNonNullBool, undefined); + test('[true, null]', listOfNonNullBool, undefined); + }); + + it('coerces non-null lists of non-null values', () => { + test('true', nonNullListOfNonNullBool, [true]); + test('123', nonNullListOfNonNullBool, undefined); + test('null', nonNullListOfNonNullBool, undefined); + test('[true, false]', nonNullListOfNonNullBool, [true, false]); + test('[true, 123]', nonNullListOfNonNullBool, undefined); + test('[true, null]', nonNullListOfNonNullBool, undefined); + }); + + it('uses default values for unprovided fields', () => { + const type = new GraphQLInputObjectType({ + name: 'TestInput', + fields: { + int: { type: GraphQLInt, defaultValue: 42 }, + }, + }); + + test('{}', type, { int: 42 }); + }); + + const testInputObj = new GraphQLInputObjectType({ + name: 'TestInput', + fields: { + int: { type: GraphQLInt, defaultValue: 42 }, + bool: { type: GraphQLBoolean }, + requiredBool: { type: nonNullBool }, + }, + }); + + it('coerces input objects according to input coercion rules', () => { + test('null', testInputObj, null); + test('123', testInputObj, undefined); + test('[]', testInputObj, undefined); + test('{ requiredBool: true }', testInputObj, { + int: 42, + requiredBool: true, + }); + test('{ int: null, requiredBool: true }', testInputObj, { + int: null, + requiredBool: true, + }); + test('{ int: 123, requiredBool: false }', testInputObj, { + int: 123, + requiredBool: false, + }); + test('{ bool: true, requiredBool: false }', testInputObj, { + int: 42, + bool: true, + requiredBool: false, + }); + test('{ int: true, requiredBool: true }', testInputObj, undefined); + test('{ requiredBool: null }', testInputObj, undefined); + test('{ bool: true }', testInputObj, undefined); + test('{ requiredBool: true, unknown: 123 }', testInputObj, undefined); + }); + + it('accepts variable values assuming already coerced', () => { + test('$var', GraphQLBoolean, undefined); + testWithVariables({ var: true }, '$var', GraphQLBoolean, true); + testWithVariables({ var: null }, '$var', GraphQLBoolean, null); + testWithVariables({ var: null }, '$var', nonNullBool, undefined); + }); + + it('asserts variables are provided as items in lists', () => { + test('[ $foo ]', listOfBool, [null]); + test('[ $foo ]', listOfNonNullBool, undefined); + testWithVariables({ foo: true }, '[ $foo ]', listOfNonNullBool, [true]); + // Note: variables are expected to have already been coerced, so we + // do not expect the singleton wrapping behavior for variables. + testWithVariables({ foo: true }, '$foo', listOfNonNullBool, true); + testWithVariables({ foo: [true] }, '$foo', listOfNonNullBool, [true]); + }); + + it('omits input object fields for unprovided variables', () => { + test('{ int: $foo, bool: $foo, requiredBool: true }', testInputObj, { + int: 42, + requiredBool: true, + }); + test('{ requiredBool: $foo }', testInputObj, undefined); + testWithVariables({ foo: true }, '{ requiredBool: $foo }', testInputObj, { + int: 42, + requiredBool: true, + }); + }); +}); diff --git a/src/utilities/__tests__/valueFromAST-test.ts b/src/utilities/__tests__/valueFromAST-test.ts deleted file mode 100644 index 6c08ccf15c3..00000000000 --- a/src/utilities/__tests__/valueFromAST-test.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import type { ObjMap } from '../../jsutils/ObjMap'; -import { invariant } from '../../jsutils/invariant'; -import { identityFunc } from '../../jsutils/identityFunc'; - -import { parseValue } from '../../language/parser'; - -import type { GraphQLInputType } from '../../type/definition'; -import { - GraphQLInt, - GraphQLFloat, - GraphQLString, - GraphQLBoolean, - GraphQLID, -} from '../../type/scalars'; -import { - GraphQLList, - GraphQLNonNull, - GraphQLScalarType, - GraphQLEnumType, - GraphQLInputObjectType, -} from '../../type/definition'; - -import { valueFromAST } from '../valueFromAST'; - -describe('valueFromAST', () => { - function expectValueFrom( - valueText: string, - type: GraphQLInputType, - variables?: ObjMap, - ) { - const ast = parseValue(valueText); - const value = valueFromAST(ast, type, variables); - return expect(value); - } - - it('rejects empty input', () => { - expect(valueFromAST(null, GraphQLBoolean)).to.deep.equal(undefined); - }); - - it('converts according to input coercion rules', () => { - expectValueFrom('true', GraphQLBoolean).to.equal(true); - expectValueFrom('false', GraphQLBoolean).to.equal(false); - expectValueFrom('123', GraphQLInt).to.equal(123); - expectValueFrom('123', GraphQLFloat).to.equal(123); - expectValueFrom('123.456', GraphQLFloat).to.equal(123.456); - expectValueFrom('"abc123"', GraphQLString).to.equal('abc123'); - expectValueFrom('123456', GraphQLID).to.equal('123456'); - expectValueFrom('"123456"', GraphQLID).to.equal('123456'); - }); - - it('does not convert when input coercion rules reject a value', () => { - expectValueFrom('123', GraphQLBoolean).to.equal(undefined); - expectValueFrom('123.456', GraphQLInt).to.equal(undefined); - expectValueFrom('true', GraphQLInt).to.equal(undefined); - expectValueFrom('"123"', GraphQLInt).to.equal(undefined); - expectValueFrom('"123"', GraphQLFloat).to.equal(undefined); - expectValueFrom('123', GraphQLString).to.equal(undefined); - expectValueFrom('true', GraphQLString).to.equal(undefined); - expectValueFrom('123.456', GraphQLString).to.equal(undefined); - }); - - it('convert using parseLiteral from a custom scalar type', () => { - const passthroughScalar = new GraphQLScalarType({ - name: 'PassthroughScalar', - parseLiteral(node) { - invariant(node.kind === 'StringValue'); - return node.value; - }, - parseValue: identityFunc, - }); - - expectValueFrom('"value"', passthroughScalar).to.equal('value'); - - const throwScalar = new GraphQLScalarType({ - name: 'ThrowScalar', - parseLiteral() { - throw new Error('Test'); - }, - parseValue: identityFunc, - }); - - expectValueFrom('value', throwScalar).to.equal(undefined); - - const returnUndefinedScalar = new GraphQLScalarType({ - name: 'ReturnUndefinedScalar', - parseLiteral() { - return undefined; - }, - parseValue: identityFunc, - }); - - expectValueFrom('value', returnUndefinedScalar).to.equal(undefined); - }); - - it('converts enum values according to input coercion rules', () => { - const testEnum = new GraphQLEnumType({ - name: 'TestColor', - values: { - RED: { value: 1 }, - GREEN: { value: 2 }, - BLUE: { value: 3 }, - NULL: { value: null }, - NAN: { value: NaN }, - NO_CUSTOM_VALUE: { value: undefined }, - }, - }); - - expectValueFrom('RED', testEnum).to.equal(1); - expectValueFrom('BLUE', testEnum).to.equal(3); - expectValueFrom('3', testEnum).to.equal(undefined); - expectValueFrom('"BLUE"', testEnum).to.equal(undefined); - expectValueFrom('null', testEnum).to.equal(null); - expectValueFrom('NULL', testEnum).to.equal(null); - expectValueFrom('NULL', new GraphQLNonNull(testEnum)).to.equal(null); - expectValueFrom('NAN', testEnum).to.deep.equal(NaN); - expectValueFrom('NO_CUSTOM_VALUE', testEnum).to.equal('NO_CUSTOM_VALUE'); - }); - - // Boolean! - const nonNullBool = new GraphQLNonNull(GraphQLBoolean); - // [Boolean] - const listOfBool = new GraphQLList(GraphQLBoolean); - // [Boolean!] - const listOfNonNullBool = new GraphQLList(nonNullBool); - // [Boolean]! - const nonNullListOfBool = new GraphQLNonNull(listOfBool); - // [Boolean!]! - const nonNullListOfNonNullBool = new GraphQLNonNull(listOfNonNullBool); - - it('coerces to null unless non-null', () => { - expectValueFrom('null', GraphQLBoolean).to.equal(null); - expectValueFrom('null', nonNullBool).to.equal(undefined); - }); - - it('coerces lists of values', () => { - expectValueFrom('true', listOfBool).to.deep.equal([true]); - expectValueFrom('123', listOfBool).to.equal(undefined); - expectValueFrom('null', listOfBool).to.equal(null); - expectValueFrom('[true, false]', listOfBool).to.deep.equal([true, false]); - expectValueFrom('[true, 123]', listOfBool).to.equal(undefined); - expectValueFrom('[true, null]', listOfBool).to.deep.equal([true, null]); - expectValueFrom('{ true: true }', listOfBool).to.equal(undefined); - }); - - it('coerces non-null lists of values', () => { - expectValueFrom('true', nonNullListOfBool).to.deep.equal([true]); - expectValueFrom('123', nonNullListOfBool).to.equal(undefined); - expectValueFrom('null', nonNullListOfBool).to.equal(undefined); - expectValueFrom('[true, false]', nonNullListOfBool).to.deep.equal([ - true, - false, - ]); - expectValueFrom('[true, 123]', nonNullListOfBool).to.equal(undefined); - expectValueFrom('[true, null]', nonNullListOfBool).to.deep.equal([ - true, - null, - ]); - }); - - it('coerces lists of non-null values', () => { - expectValueFrom('true', listOfNonNullBool).to.deep.equal([true]); - expectValueFrom('123', listOfNonNullBool).to.equal(undefined); - expectValueFrom('null', listOfNonNullBool).to.equal(null); - expectValueFrom('[true, false]', listOfNonNullBool).to.deep.equal([ - true, - false, - ]); - expectValueFrom('[true, 123]', listOfNonNullBool).to.equal(undefined); - expectValueFrom('[true, null]', listOfNonNullBool).to.equal(undefined); - }); - - it('coerces non-null lists of non-null values', () => { - expectValueFrom('true', nonNullListOfNonNullBool).to.deep.equal([true]); - expectValueFrom('123', nonNullListOfNonNullBool).to.equal(undefined); - expectValueFrom('null', nonNullListOfNonNullBool).to.equal(undefined); - expectValueFrom('[true, false]', nonNullListOfNonNullBool).to.deep.equal([ - true, - false, - ]); - expectValueFrom('[true, 123]', nonNullListOfNonNullBool).to.equal( - undefined, - ); - expectValueFrom('[true, null]', nonNullListOfNonNullBool).to.equal( - undefined, - ); - }); - - const testInputObj = new GraphQLInputObjectType({ - name: 'TestInput', - fields: { - int: { type: GraphQLInt, defaultValue: 42 }, - bool: { type: GraphQLBoolean }, - requiredBool: { type: nonNullBool }, - }, - }); - - it('coerces input objects according to input coercion rules', () => { - expectValueFrom('null', testInputObj).to.equal(null); - expectValueFrom('123', testInputObj).to.equal(undefined); - expectValueFrom('[]', testInputObj).to.equal(undefined); - expectValueFrom( - '{ int: 123, requiredBool: false }', - testInputObj, - ).to.deep.equal({ - int: 123, - requiredBool: false, - }); - expectValueFrom( - '{ bool: true, requiredBool: false }', - testInputObj, - ).to.deep.equal({ - int: 42, - bool: true, - requiredBool: false, - }); - expectValueFrom('{ int: true, requiredBool: true }', testInputObj).to.equal( - undefined, - ); - expectValueFrom('{ requiredBool: null }', testInputObj).to.equal(undefined); - expectValueFrom('{ bool: true }', testInputObj).to.equal(undefined); - }); - - it('accepts variable values assuming already coerced', () => { - expectValueFrom('$var', GraphQLBoolean, {}).to.equal(undefined); - expectValueFrom('$var', GraphQLBoolean, { var: true }).to.equal(true); - expectValueFrom('$var', GraphQLBoolean, { var: null }).to.equal(null); - expectValueFrom('$var', nonNullBool, { var: null }).to.equal(undefined); - }); - - it('asserts variables are provided as items in lists', () => { - expectValueFrom('[ $foo ]', listOfBool, {}).to.deep.equal([null]); - expectValueFrom('[ $foo ]', listOfNonNullBool, {}).to.equal(undefined); - expectValueFrom('[ $foo ]', listOfNonNullBool, { - foo: true, - }).to.deep.equal([true]); - // Note: variables are expected to have already been coerced, so we - // do not expect the singleton wrapping behavior for variables. - expectValueFrom('$foo', listOfNonNullBool, { foo: true }).to.equal(true); - expectValueFrom('$foo', listOfNonNullBool, { foo: [true] }).to.deep.equal([ - true, - ]); - }); - - it('omits input object fields for unprovided variables', () => { - expectValueFrom( - '{ int: $foo, bool: $foo, requiredBool: true }', - testInputObj, - {}, - ).to.deep.equal({ int: 42, requiredBool: true }); - - expectValueFrom('{ requiredBool: $foo }', testInputObj, {}).to.equal( - undefined, - ); - - expectValueFrom('{ requiredBool: $foo }', testInputObj, { - foo: true, - }).to.deep.equal({ - int: 42, - requiredBool: true, - }); - }); -}); diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index e2d55eecb1b..1bfe2e69012 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -3,7 +3,7 @@ import { devAssert } from '../jsutils/devAssert'; import { keyValMap } from '../jsutils/keyValMap'; import { isObjectLike } from '../jsutils/isObjectLike'; -import { parseValue } from '../language/parser'; +import { parseConstValue } from '../language/parser'; import type { GraphQLSchemaValidationOptions } from '../type/schema'; import type { @@ -47,7 +47,7 @@ import type { IntrospectionTypeRef, IntrospectionNamedTypeRef, } from './getIntrospectionQuery'; -import { valueFromAST } from './valueFromAST'; +import { coerceInputLiteral } from './coerceInputValue'; /** * Build a GraphQLSchema for use by client tools. @@ -368,7 +368,10 @@ export function buildClientSchema( const defaultValue = inputValueIntrospection.defaultValue != null - ? valueFromAST(parseValue(inputValueIntrospection.defaultValue), type) + ? coerceInputLiteral( + parseConstValue(inputValueIntrospection.defaultValue), + type, + ) : undefined; return { description: inputValueIntrospection.description, diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index 5515b2c6255..a52741583fc 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -1,8 +1,12 @@ +import type { Maybe } from '../jsutils/Maybe'; +import type { ObjMap } from '../jsutils/ObjMap'; 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 { keyMap } from '../jsutils/keyMap'; import { suggestionList } from '../jsutils/suggestionList'; import { printPathArray } from '../jsutils/printPathArray'; import { addPath, pathToArray } from '../jsutils/Path'; @@ -16,8 +20,12 @@ import { isInputObjectType, isListType, isNonNullType, + isRequiredInputField, } from '../type/definition'; +import type { ValueNode } from '../language/ast'; +import { Kind } from '../language/kinds'; + type OnErrorCB = ( path: ReadonlyArray, invalidValue: unknown, @@ -186,3 +194,129 @@ function coerceInputValueImpl( // istanbul ignore next (Not reachable. All possible input types have been considered) invariant(false, 'Unexpected input type: ' + inspect(type)); } + +/** + * Produces a coerced "internal" JavaScript value given a GraphQL Value AST. + * + * Returns `undefined` when the value could not be validly coerced according to + * the provided type. + */ +export function coerceInputLiteral( + valueNode: ValueNode, + type: GraphQLInputType, + variables?: Maybe>, +): unknown { + if (valueNode.kind === Kind.VARIABLE) { + if (!variables || isMissingVariable(valueNode, variables)) { + return; // Invalid: intentionally return no value. + } + const variableValue = variables[valueNode.name.value]; + if (variableValue === null && isNonNullType(type)) { + return; // Invalid: intentionally return no value. + } + // Note: This does no further checking that this variable is correct. + // This assumes validated has checked this variable is of the correct type. + return variableValue; + } + + if (isNonNullType(type)) { + if (valueNode.kind === Kind.NULL) { + return; // Invalid: intentionally return no value. + } + return coerceInputLiteral(valueNode, type.ofType, variables); + } + + if (valueNode.kind === Kind.NULL) { + return null; // Explicitly return the value null. + } + + if (isListType(type)) { + if (valueNode.kind !== Kind.LIST) { + // Lists accept a non-list value as a list of one. + const itemValue = coerceInputLiteral(valueNode, type.ofType, variables); + if (itemValue === undefined) { + return; // Invalid: intentionally return no value. + } + return [itemValue]; + } + const coercedValue: Array = []; + for (const itemNode of valueNode.values) { + let itemValue = coerceInputLiteral(itemNode, type.ofType, variables); + if (itemValue === undefined) { + if ( + isMissingVariable(itemNode, variables) && + !isNonNullType(type.ofType) + ) { + // A missing variable within a list is coerced to null. + itemValue = null; + } else { + return; // Invalid: intentionally return no value. + } + } + coercedValue.push(itemValue); + } + return coercedValue; + } + + if (isInputObjectType(type)) { + if (valueNode.kind !== Kind.OBJECT) { + return; // Invalid: intentionally return no value. + } + + const coercedValue: { [field: string]: unknown } = {}; + const fieldDefs = type.getFields(); + const hasUndefinedField = valueNode.fields.some( + (field) => !hasOwnProperty(fieldDefs, field.name.value), + ); + if (hasUndefinedField) { + return; // Invalid: intentionally return no value. + } + const fieldNodes = keyMap(valueNode.fields, (field) => field.name.value); + for (const field of Object.values(fieldDefs)) { + const fieldNode = fieldNodes[field.name]; + if (!fieldNode || isMissingVariable(fieldNode.value, variables)) { + if (isRequiredInputField(field)) { + return; // Invalid: intentionally return no value. + } + if (field.defaultValue !== undefined) { + coercedValue[field.name] = field.defaultValue; + } + } else { + const fieldValue = coerceInputLiteral( + fieldNode.value, + field.type, + variables, + ); + if (fieldValue === undefined) { + return; // Invalid: intentionally return no value. + } + coercedValue[field.name] = fieldValue; + } + } + return coercedValue; + } + + // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') + if (isLeafType(type)) { + try { + return type.parseLiteral(valueNode, variables); + } catch (_error) { + return; // Invalid: ignore error and intentionally return no value. + } + } + + // istanbul ignore next (Not reachable. All possible input types have been considered) + invariant(false, `Expected input type: ${inspect(type)}`); +} + +// Returns true if the provided valueNode is a variable which is not defined +// in the set of variables. +function isMissingVariable( + valueNode: ValueNode, + variables: Maybe>, +): boolean { + return ( + valueNode.kind === Kind.VARIABLE && + (variables == null || variables[valueNode.name.value] === undefined) + ); +} diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 7b82e2ce397..2597998478d 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -80,7 +80,7 @@ import { GraphQLInputObjectType, } from '../type/definition'; -import { valueFromAST } from './valueFromAST'; +import { coerceInputLiteral } from './coerceInputValue'; interface Options extends GraphQLSchemaValidationOptions { /** @@ -491,7 +491,9 @@ export function extendSchemaImpl( argConfigMap[arg.name.value] = { type, description: arg.description?.value, - defaultValue: valueFromAST(arg.defaultValue, type), + defaultValue: arg.defaultValue + ? coerceInputLiteral(arg.defaultValue, type) + : undefined, deprecationReason: getDeprecationReason(arg), astNode: arg, }; @@ -518,7 +520,9 @@ export function extendSchemaImpl( inputFieldMap[field.name.value] = { type, description: field.description?.value, - defaultValue: valueFromAST(field.defaultValue, type), + defaultValue: field.defaultValue + ? coerceInputLiteral(field.defaultValue, type) + : undefined, deprecationReason: getDeprecationReason(field), astNode: field, }; diff --git a/src/utilities/index.ts b/src/utilities/index.ts index e9197f946b7..5f952edeaaf 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -61,9 +61,6 @@ export { /** Create a GraphQLType from a GraphQL language AST. */ export { typeFromAST } from './typeFromAST'; -/** Create a JavaScript value from a GraphQL language AST with a type. */ -export { valueFromAST } from './valueFromAST'; - /** Create a JavaScript value from a GraphQL language AST without a type. */ export { valueFromASTUntyped } from './valueFromASTUntyped'; @@ -73,8 +70,12 @@ export { astFromValue } from './astFromValue'; /** A helper to use within recursive-descent visitors which need to be aware of the GraphQL type system. */ export { TypeInfo, visitWithTypeInfo } from './TypeInfo'; -/** Coerces a JavaScript value to a GraphQL type, or produces errors. */ -export { coerceInputValue } from './coerceInputValue'; +export { + /** Coerces a JavaScript value to a GraphQL type, or produces errors. */ + coerceInputValue, + /** Coerces a GraphQL literal (AST) to a GraphQL type, or returns undefined. */ + coerceInputLiteral, +} from './coerceInputValue'; /** Concatenates multiple AST together. */ export { concatAST } from './concatAST'; diff --git a/src/utilities/valueFromAST.ts b/src/utilities/valueFromAST.ts deleted file mode 100644 index 359d2145bcc..00000000000 --- a/src/utilities/valueFromAST.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { ObjMap } from '../jsutils/ObjMap'; -import { keyMap } from '../jsutils/keyMap'; -import { inspect } from '../jsutils/inspect'; -import { invariant } from '../jsutils/invariant'; - -import type { ValueNode } from '../language/ast'; -import { Kind } from '../language/kinds'; - -import type { GraphQLInputType } from '../type/definition'; -import { - isLeafType, - isInputObjectType, - isListType, - isNonNullType, -} from '../type/definition'; - -import type { Maybe } from '../jsutils/Maybe'; - -/** - * Produces a JavaScript value given a GraphQL Value AST. - * - * A GraphQL type must be provided, which will be used to interpret different - * GraphQL Value literals. - * - * Returns `undefined` when the value could not be validly coerced according to - * the provided type. - * - * | GraphQL Value | JSON Value | - * | -------------------- | ------------- | - * | Input Object | Object | - * | List | Array | - * | Boolean | Boolean | - * | String | String | - * | Int / Float | Number | - * | Enum Value | Unknown | - * | NullValue | null | - * - */ -export function valueFromAST( - valueNode: Maybe, - type: GraphQLInputType, - variables?: Maybe>, -): unknown { - if (!valueNode) { - // When there is no node, then there is also no value. - // Importantly, this is different from returning the value null. - return; - } - - if (valueNode.kind === Kind.VARIABLE) { - const variableName = valueNode.name.value; - if (variables == null || variables[variableName] === undefined) { - // No valid return value. - return; - } - const variableValue = variables[variableName]; - if (variableValue === null && isNonNullType(type)) { - return; // Invalid: intentionally return no value. - } - // Note: This does no further checking that this variable is correct. - // This assumes that this query has been validated and the variable - // usage here is of the correct type. - return variableValue; - } - - if (isNonNullType(type)) { - if (valueNode.kind === Kind.NULL) { - return; // Invalid: intentionally return no value. - } - return valueFromAST(valueNode, type.ofType, variables); - } - - if (valueNode.kind === Kind.NULL) { - // This is explicitly returning the value null. - return null; - } - - if (isListType(type)) { - const itemType = type.ofType; - if (valueNode.kind === Kind.LIST) { - const coercedValues = []; - for (const itemNode of valueNode.values) { - if (isMissingVariable(itemNode, variables)) { - // If an array contains a missing variable, it is either coerced to - // null or if the item type is non-null, it considered invalid. - if (isNonNullType(itemType)) { - return; // Invalid: intentionally return no value. - } - coercedValues.push(null); - } else { - const itemValue = valueFromAST(itemNode, itemType, variables); - if (itemValue === undefined) { - return; // Invalid: intentionally return no value. - } - coercedValues.push(itemValue); - } - } - return coercedValues; - } - const coercedValue = valueFromAST(valueNode, itemType, variables); - if (coercedValue === undefined) { - return; // Invalid: intentionally return no value. - } - return [coercedValue]; - } - - if (isInputObjectType(type)) { - if (valueNode.kind !== Kind.OBJECT) { - return; // Invalid: intentionally return no value. - } - const coercedObj = Object.create(null); - const fieldNodes = keyMap(valueNode.fields, (field) => field.name.value); - for (const field of Object.values(type.getFields())) { - const fieldNode = fieldNodes[field.name]; - if (!fieldNode || isMissingVariable(fieldNode.value, variables)) { - if (field.defaultValue !== undefined) { - coercedObj[field.name] = field.defaultValue; - } else if (isNonNullType(field.type)) { - return; // Invalid: intentionally return no value. - } - continue; - } - const fieldValue = valueFromAST(fieldNode.value, field.type, variables); - if (fieldValue === undefined) { - return; // Invalid: intentionally return no value. - } - coercedObj[field.name] = fieldValue; - } - return coercedObj; - } - - // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') - if (isLeafType(type)) { - // Scalars and Enums fulfill parsing a literal value via parseLiteral(). - // Invalid values represent a failure to parse correctly, in which case - // no value is returned. - let result; - try { - result = type.parseLiteral(valueNode, variables); - } catch (_error) { - return; // Invalid: intentionally return no value. - } - if (result === undefined) { - return; // Invalid: intentionally return no value. - } - return result; - } - - // istanbul ignore next (Not reachable. All possible input types have been considered) - invariant(false, 'Unexpected input type: ' + inspect(type)); -} - -// Returns true if the provided valueNode is a variable which is not defined -// in the set of variables. -function isMissingVariable( - valueNode: ValueNode, - variables: Maybe>, -): boolean { - return ( - valueNode.kind === Kind.VARIABLE && - (variables == null || variables[valueNode.name.value] === undefined) - ); -} diff --git a/src/utilities/valueFromASTUntyped.ts b/src/utilities/valueFromASTUntyped.ts index c3e2e92e49d..e168d888f8f 100644 --- a/src/utilities/valueFromASTUntyped.ts +++ b/src/utilities/valueFromASTUntyped.ts @@ -10,8 +10,8 @@ import type { ValueNode } from '../language/ast'; /** * Produces a JavaScript value given a GraphQL Value AST. * - * Unlike `valueFromAST()`, no type is provided. The resulting JavaScript value - * will reflect the provided GraphQL value AST. + * No type is provided. The resulting JavaScript value will reflect the + * provided GraphQL value AST. * * | GraphQL Value | JavaScript Value | * | -------------------- | ---------------- |