From e4243d8e9925fe69d1078b9519be046b9f67e93b Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Fri, 14 May 2021 21:25:33 -0700 Subject: [PATCH] Add coerceInputLiteral() Depends on #3067 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.js | 13 +- src/index.d.ts | 4 +- src/index.js | 4 +- src/jsutils/hasOwnProperty.d.ts | 4 + src/jsutils/hasOwnProperty.js | 6 + src/language/parser.js | 2 - .../__tests__/coerceInputValue-test.js | 256 ++++++++++++++++- src/utilities/__tests__/valueFromAST-test.js | 265 ------------------ src/utilities/buildClientSchema.js | 8 +- src/utilities/coerceInputValue.d.ts | 18 ++ src/utilities/coerceInputValue.js | 133 +++++++++ src/utilities/extendSchema.js | 6 +- src/utilities/index.d.ts | 11 +- src/utilities/index.js | 11 +- src/utilities/valueFromAST.d.ts | 31 -- src/utilities/valueFromAST.js | 161 ----------- src/utilities/valueFromASTUntyped.d.ts | 4 +- src/utilities/valueFromASTUntyped.js | 4 +- 18 files changed, 454 insertions(+), 487 deletions(-) create mode 100644 src/jsutils/hasOwnProperty.d.ts create mode 100644 src/jsutils/hasOwnProperty.js delete mode 100644 src/utilities/__tests__/valueFromAST-test.js delete mode 100644 src/utilities/valueFromAST.d.ts delete mode 100644 src/utilities/valueFromAST.js diff --git a/src/execution/values.js b/src/execution/values.js index 3b6728254ba..b738e745efd 100644 --- a/src/execution/values.js +++ b/src/execution/values.js @@ -19,8 +19,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 |} @@ -95,7 +97,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( @@ -216,7 +221,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.d.ts b/src/index.d.ts index 317602293e5..d2a0c53ea41 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -406,8 +406,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. @@ -418,6 +416,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/index.js b/src/index.js index b46f10cd3a0..6d74453d786 100644 --- a/src/index.js +++ b/src/index.js @@ -395,8 +395,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. @@ -407,6 +405,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.d.ts b/src/jsutils/hasOwnProperty.d.ts new file mode 100644 index 00000000000..a832bfa3097 --- /dev/null +++ b/src/jsutils/hasOwnProperty.d.ts @@ -0,0 +1,4 @@ +/** + * Determines if a provided object has a given property name. + */ +export function hasOwnProperty(obj: unknown, prop: string): boolean; diff --git a/src/jsutils/hasOwnProperty.js b/src/jsutils/hasOwnProperty.js new file mode 100644 index 00000000000..4ce29141e0c --- /dev/null +++ b/src/jsutils/hasOwnProperty.js @@ -0,0 +1,6 @@ +/** + * Determines if a provided object has a given property name. + */ +export function hasOwnProperty(obj: mixed, prop: string): boolean { + return Object.prototype.hasOwnProperty.call(obj, prop); +} diff --git a/src/language/parser.js b/src/language/parser.js index a2b5a0b0b7b..db9507a0809 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -105,8 +105,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.js b/src/utilities/__tests__/coerceInputValue-test.js index a5aa72096c8..5023b4974b2 100644 --- a/src/utilities/__tests__/coerceInputValue-test.js +++ b/src/utilities/__tests__/coerceInputValue-test.js @@ -1,10 +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, @@ -13,7 +24,7 @@ import { GraphQLInputObjectType, } from '../../type/definition'; -import { coerceInputValue } from '../coerceInputValue'; +import { coerceInputValue, coerceInputLiteral } from '../coerceInputValue'; type CoerceResult = {| value: mixed, @@ -425,3 +436,244 @@ describe('coerceInputValue', () => { }); }); }); + +describe('coerceInputLiteral', () => { + function test( + valueText: string, + type: GraphQLInputType, + expected: mixed, + 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: mixed, + ) { + 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.js b/src/utilities/__tests__/valueFromAST-test.js deleted file mode 100644 index a4443927770..00000000000 --- a/src/utilities/__tests__/valueFromAST-test.js +++ /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.js b/src/utilities/buildClientSchema.js index 487ee6d16e2..cf6b2a084ec 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -47,7 +47,8 @@ import type { IntrospectionTypeRef, IntrospectionNamedTypeRef, } from './getIntrospectionQuery'; -import { valueFromAST } from './valueFromAST'; + +import { coerceInputLiteral } from './coerceInputValue'; /** * Build a GraphQLSchema for use by client tools. @@ -369,7 +370,10 @@ export function buildClientSchema( const defaultValue = inputValueIntrospection.defaultValue != null - ? valueFromAST(parseValue(inputValueIntrospection.defaultValue), type) + ? coerceInputLiteral( + parseValue(inputValueIntrospection.defaultValue), + type, + ) : undefined; return { description: inputValueIntrospection.description, diff --git a/src/utilities/coerceInputValue.d.ts b/src/utilities/coerceInputValue.d.ts index 78dafb257fc..45785330cef 100644 --- a/src/utilities/coerceInputValue.d.ts +++ b/src/utilities/coerceInputValue.d.ts @@ -1,4 +1,10 @@ +import { Maybe } from '../jsutils/Maybe'; +import { ObjMap } from '../jsutils/ObjMap'; + +import { ValueNode } from '../language/ast'; + import { GraphQLInputType } from '../type/definition'; + import { GraphQLError } from '../error/GraphQLError'; type OnErrorCB = ( @@ -15,3 +21,15 @@ export function coerceInputValue( type: GraphQLInputType, onError?: OnErrorCB, ): unknown; + +/** + * 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; diff --git a/src/utilities/coerceInputValue.js b/src/utilities/coerceInputValue.js index 9c9546fbc2e..4ba348a42b7 100644 --- a/src/utilities/coerceInputValue.js +++ b/src/utilities/coerceInputValue.js @@ -1,8 +1,11 @@ +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 +19,12 @@ import { isInputObjectType, isListType, isNonNullType, + isRequiredInput, } from '../type/definition'; +import type { ValueNode } from '../language/ast'; +import { Kind } from '../language/kinds'; + type OnErrorCB = ( path: $ReadOnlyArray, invalidValue: mixed, @@ -186,3 +193,129 @@ function coerceInputValueImpl( // istanbul ignore next (Not reachable. All possible input types have been considered) invariant(false, 'Unexpected input type: ' + inspect((type: empty))); } + +/** + * 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?: ?ObjMap, +): mixed { + 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 = []; + 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 = {}; + 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 (isRequiredInput(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, 'Unexpected input type: ' + inspect((type: empty))); +} + +// Returns true if the provided valueNode is a variable which is not defined +// in the set of variables. +function isMissingVariable( + valueNode: ValueNode, + variables: ?ObjMap, +): boolean { + return ( + valueNode.kind === Kind.VARIABLE && + (variables == null || variables[valueNode.name.value] === undefined) + ); +} diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 9c55417d95e..d53b2e64678 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -79,7 +79,7 @@ import { GraphQLInputObjectType, } from '../type/definition'; -import { valueFromAST } from './valueFromAST'; +import { coerceInputLiteral } from './coerceInputValue'; type Options = {| ...GraphQLSchemaValidationOptions, @@ -494,7 +494,9 @@ export function extendSchemaImpl( configMap[node.name.value] = { type, description: node.description?.value, - defaultValue: valueFromAST(node.defaultValue, type), + defaultValue: node.defaultValue + ? coerceInputLiteral(node.defaultValue, type) + : undefined, deprecationReason: getDeprecationReason(node), astNode: node, }; diff --git a/src/utilities/index.d.ts b/src/utilities/index.d.ts index a690e640e95..6cd259563ac 100644 --- a/src/utilities/index.d.ts +++ b/src/utilities/index.d.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'; @@ -74,8 +71,12 @@ export { astFromValue } from './astFromValue'; // 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/index.js b/src/utilities/index.js index b4c8372f04e..c3fe08c14e1 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -59,9 +59,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'; @@ -72,8 +69,12 @@ export { astFromValue } from './astFromValue'; // 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.d.ts b/src/utilities/valueFromAST.d.ts deleted file mode 100644 index 6ea38000e12..00000000000 --- a/src/utilities/valueFromAST.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Maybe } from '../jsutils/Maybe'; -import { ObjMap } from '../jsutils/ObjMap'; - -import { ValueNode } from '../language/ast'; -import { GraphQLInputType } from '../type/definition'; - -/** - * 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 | Mixed | - * | NullValue | null | - * - */ -export function valueFromAST( - valueNode: Maybe, - type: GraphQLInputType, - variables?: Maybe>, -): unknown; diff --git a/src/utilities/valueFromAST.js b/src/utilities/valueFromAST.js deleted file mode 100644 index 258976462b9..00000000000 --- a/src/utilities/valueFromAST.js +++ /dev/null @@ -1,161 +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'; - -/** - * 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 | Mixed | - * | NullValue | null | - * - */ -export function valueFromAST( - valueNode: ?ValueNode, - type: GraphQLInputType, - variables?: ?ObjMap, -): mixed | void { - 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: empty))); -} - -// Returns true if the provided valueNode is a variable which is not defined -// in the set of variables. -function isMissingVariable( - valueNode: ValueNode, - variables: ?ObjMap, -): boolean { - return ( - valueNode.kind === Kind.VARIABLE && - (variables == null || variables[valueNode.name.value] === undefined) - ); -} diff --git a/src/utilities/valueFromASTUntyped.d.ts b/src/utilities/valueFromASTUntyped.d.ts index 495e2797234..7a57bf0d574 100644 --- a/src/utilities/valueFromASTUntyped.d.ts +++ b/src/utilities/valueFromASTUntyped.d.ts @@ -6,8 +6,8 @@ import { 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 | * | -------------------- | ---------------- | diff --git a/src/utilities/valueFromASTUntyped.js b/src/utilities/valueFromASTUntyped.js index 05f5db71b2e..41873266464 100644 --- a/src/utilities/valueFromASTUntyped.js +++ b/src/utilities/valueFromASTUntyped.js @@ -9,8 +9,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 | * | -------------------- | ---------------- |