From 2fc8629746e689c271424f4bd541d9a4e5779bd9 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 | 264 ------------------ src/utilities/buildClientSchema.ts | 11 +- src/utilities/coerceInputValue.ts | 131 +++++++++ src/utilities/extendSchema.ts | 10 +- src/utilities/index.ts | 11 +- src/utilities/valueFromAST.ts | 161 ----------- src/utilities/valueFromASTUntyped.ts | 4 +- 12 files changed, 426 insertions(+), 449 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 c06160ac87d..db67b2f2b37 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -19,9 +19,11 @@ import { isInputType, isNonNullType } from '../type/definition.js'; import type { GraphQLDirective } from '../type/directives.js'; import type { GraphQLSchema } from '../type/schema.js'; -import { coerceInputValue } from '../utilities/coerceInputValue.js'; +import { + coerceInputLiteral, + coerceInputValue, +} from '../utilities/coerceInputValue.js'; import { typeFromAST } from '../utilities/typeFromAST.js'; -import { valueFromAST } from '../utilities/valueFromAST.js'; type CoercedVariableValues = | { errors: ReadonlyArray; coerced?: never } @@ -94,7 +96,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)) { onError( new GraphQLError( @@ -206,7 +211,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 4fa82937c9d..ed051de17b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -437,8 +437,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. @@ -448,6 +446,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 1b630e0af1a..27cad61d098 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -152,8 +152,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 5e0199fead2..2aa0fe7458f 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -1,6 +1,13 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; +import { identityFunc } from '../../jsutils/identityFunc.js'; +import { invariant } from '../../jsutils/invariant.js'; +import type { ObjMap } from '../../jsutils/ObjMap.js'; + +import { parseValue } from '../../language/parser.js'; +import { print } from '../../language/printer.js'; + import type { GraphQLInputType } from '../../type/definition.js'; import { GraphQLEnumType, @@ -9,9 +16,15 @@ import { GraphQLNonNull, GraphQLScalarType, } from '../../type/definition.js'; -import { GraphQLInt } from '../../type/scalars.js'; +import { + GraphQLBoolean, + GraphQLFloat, + GraphQLID, + GraphQLInt, + GraphQLString, +} from '../../type/scalars.js'; -import { coerceInputValue } from '../coerceInputValue.js'; +import { coerceInputLiteral, coerceInputValue } from '../coerceInputValue.js'; 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 d0a8380d13a..00000000000 --- a/src/utilities/__tests__/valueFromAST-test.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { assert, expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { identityFunc } from '../../jsutils/identityFunc.js'; -import type { ObjMap } from '../../jsutils/ObjMap.js'; - -import { parseValue } from '../../language/parser.js'; - -import type { GraphQLInputType } from '../../type/definition.js'; -import { - GraphQLEnumType, - GraphQLInputObjectType, - GraphQLList, - GraphQLNonNull, - GraphQLScalarType, -} from '../../type/definition.js'; -import { - GraphQLBoolean, - GraphQLFloat, - GraphQLID, - GraphQLInt, - GraphQLString, -} from '../../type/scalars.js'; - -import { valueFromAST } from '../valueFromAST.js'; - -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) { - assert(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 b2f6771c63e..12800069643 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -3,7 +3,7 @@ import { inspect } from '../jsutils/inspect.js'; import { isObjectLike } from '../jsutils/isObjectLike.js'; import { keyValMap } from '../jsutils/keyValMap.js'; -import { parseValue } from '../language/parser.js'; +import { parseConstValue } from '../language/parser.js'; import type { GraphQLFieldConfig, @@ -32,6 +32,7 @@ import { specifiedScalarTypes } from '../type/scalars.js'; import type { GraphQLSchemaValidationOptions } from '../type/schema.js'; import { GraphQLSchema } from '../type/schema.js'; +import { coerceInputLiteral } from './coerceInputValue.js'; import type { IntrospectionDirective, IntrospectionEnumType, @@ -46,8 +47,7 @@ import type { IntrospectionType, IntrospectionTypeRef, IntrospectionUnionType, -} from './getIntrospectionQuery.js'; -import { valueFromAST } from './valueFromAST.js'; +} from './getIntrospectionQuery'; /** * Build a GraphQLSchema for use by client tools. @@ -372,7 +372,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 d1decf86a14..556fb1bc5ad 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -1,8 +1,12 @@ import { didYouMean } from '../jsutils/didYouMean.js'; +import { hasOwnProperty } from '../jsutils/hasOwnProperty.js'; import { inspect } from '../jsutils/inspect.js'; import { invariant } from '../jsutils/invariant.js'; import { isIterableObject } from '../jsutils/isIterableObject.js'; import { isObjectLike } from '../jsutils/isObjectLike.js'; +import { keyMap } from '../jsutils/keyMap.js'; +import type { Maybe } from '../jsutils/Maybe'; +import type { ObjMap } from '../jsutils/ObjMap'; import type { Path } from '../jsutils/Path.js'; import { addPath, pathToArray } from '../jsutils/Path.js'; import { printPathArray } from '../jsutils/printPathArray.js'; @@ -10,12 +14,17 @@ import { suggestionList } from '../jsutils/suggestionList.js'; import { GraphQLError } from '../error/GraphQLError.js'; +import type { ValueNode } from '../language/ast'; +import { Kind } from '../language/kinds.js'; + import type { GraphQLInputType } from '../type/definition.js'; import { + assertLeafType, isInputObjectType, isLeafType, isListType, isNonNullType, + isRequiredInputField, } from '../type/definition.js'; type OnErrorCB = ( @@ -180,3 +189,125 @@ function coerceInputValueImpl( // Not reachable, all possible 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; + } + + const leafType = assertLeafType(type); + + try { + return leafType.parseLiteral(valueNode, variables); + } catch (_error) { + // Invalid: ignore error and intentionally return no value. + } +} + +// 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 || !hasOwnProperty(variables, valueNode.name.value)) + ); +} diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 831733b69b7..9a127b4bc38 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -83,7 +83,7 @@ import { assertValidSDLExtension } from '../validation/validate.js'; import { getDirectiveValues } from '../execution/values.js'; -import { valueFromAST } from './valueFromAST.js'; +import { coerceInputLiteral } from './coerceInputValue.js'; interface Options extends GraphQLSchemaValidationOptions { /** @@ -536,7 +536,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, }; @@ -563,7 +565,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 59156554943..ad0cdabc4e6 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -55,9 +55,6 @@ export { // Create a GraphQLType from a GraphQL language AST. export { typeFromAST } from './typeFromAST.js'; -// Create a JavaScript value from a GraphQL language AST with a type. -export { valueFromAST } from './valueFromAST.js'; - // Create a JavaScript value from a GraphQL language AST without a type. export { valueFromASTUntyped } from './valueFromASTUntyped.js'; @@ -67,8 +64,12 @@ export { astFromValue } from './astFromValue.js'; // A helper to use within recursive-descent visitors which need to be aware of the GraphQL type system. export { TypeInfo, visitWithTypeInfo } from './TypeInfo.js'; -// Coerces a JavaScript value to a GraphQL type, or produces errors. -export { coerceInputValue } from './coerceInputValue.js'; +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.js'; // Concatenates multiple AST together. export { concatAST } from './concatAST.js'; diff --git a/src/utilities/valueFromAST.ts b/src/utilities/valueFromAST.ts deleted file mode 100644 index 18981712f41..00000000000 --- a/src/utilities/valueFromAST.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { inspect } from '../jsutils/inspect.js'; -import { invariant } from '../jsutils/invariant.js'; -import { keyMap } from '../jsutils/keyMap.js'; -import type { Maybe } from '../jsutils/Maybe.js'; -import type { ObjMap } from '../jsutils/ObjMap.js'; - -import type { ValueNode } from '../language/ast.js'; -import { Kind } from '../language/kinds.js'; - -import type { GraphQLInputType } from '../type/definition.js'; -import { - isInputObjectType, - isLeafType, - isListType, - isNonNullType, -} from '../type/definition.js'; - -/** - * 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; - } - - 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; - } - /* c8 ignore next 3 */ - // 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 87af11a9a3b..4c8e197821b 100644 --- a/src/utilities/valueFromASTUntyped.ts +++ b/src/utilities/valueFromASTUntyped.ts @@ -8,8 +8,8 @@ import { Kind } from '../language/kinds.js'; /** * 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 | * | -------------------- | ---------------- |