Skip to content

Commit e143676

Browse files
committed
Add coerceInputLiteral() (graphql#3809)
Deprecates `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 . `coerceInputLiteral()` and only `coerceInputLiteral()` properly supports fragment variables in addition to operation variables.
1 parent 7e741b1 commit e143676

File tree

12 files changed

+500
-51
lines changed

12 files changed

+500
-51
lines changed

src/execution/getVariableSignature.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { print } from '../language/printer.js';
66
import { isInputType } from '../type/definition.js';
77
import type { GraphQLInputType, GraphQLSchema } from '../type/index.js';
88

9+
import { coerceInputLiteral } from '../utilities/coerceInputValue.js';
910
import { typeFromAST } from '../utilities/typeFromAST.js';
10-
import { valueFromAST } from '../utilities/valueFromAST.js';
1111

1212
/**
1313
* A GraphQLVariableSignature is required to coerce a variable value.
@@ -38,9 +38,13 @@ export function getVariableSignature(
3838
);
3939
}
4040

41+
const defaultValue = varDefNode.defaultValue;
42+
4143
return {
4244
name: varName,
4345
type: varType,
44-
defaultValue: valueFromAST(varDefNode.defaultValue, varType),
46+
defaultValue: defaultValue
47+
? coerceInputLiteral(varDefNode.defaultValue, varType)
48+
: undefined,
4549
};
4650
}

src/execution/values.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import { isNonNullType } from '../type/definition.js';
1919
import type { GraphQLDirective } from '../type/directives.js';
2020
import type { GraphQLSchema } from '../type/schema.js';
2121

22-
import { coerceInputValue } from '../utilities/coerceInputValue.js';
23-
import { valueFromAST } from '../utilities/valueFromAST.js';
22+
import {
23+
coerceInputLiteral,
24+
coerceInputValue,
25+
} from '../utilities/coerceInputValue.js';
2426

2527
import type { FragmentVariables } from './collectFields.js';
2628
import type { GraphQLVariableSignature } from './getVariableSignature.js';
@@ -217,11 +219,11 @@ export function experimentalGetArgumentValues(
217219
);
218220
}
219221

220-
const coercedValue = valueFromAST(
222+
const coercedValue = coerceInputLiteral(
221223
valueNode,
222224
argType,
223225
variableValues,
224-
fragmentVariables?.values,
226+
fragmentVariables,
225227
);
226228
if (coercedValue === undefined) {
227229
// Note: ValuesOfCorrectTypeRule validation should catch this before

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ export {
436436
// Create a GraphQLType from a GraphQL language AST.
437437
typeFromAST,
438438
// Create a JavaScript value from a GraphQL language AST with a Type.
439+
/** @deprecated use `coerceInputLiteral()` instead - will be removed in v18 */
439440
valueFromAST,
440441
// Create a JavaScript value from a GraphQL language AST without a Type.
441442
valueFromASTUntyped,
@@ -446,6 +447,8 @@ export {
446447
visitWithTypeInfo,
447448
// Coerces a JavaScript value to a GraphQL type, or produces errors.
448449
coerceInputValue,
450+
// Coerces a GraphQL literal (AST) to a GraphQL type, or returns undefined.
451+
coerceInputLiteral,
449452
// Concatenates multiple AST together.
450453
concatAST,
451454
// Separates an AST into an AST per Operation.

src/language/parser.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,6 @@ export function parse(
175175
*
176176
* This is useful within tools that operate upon GraphQL Values directly and
177177
* in isolation of complete GraphQL documents.
178-
*
179-
* Consider providing the results to the utility function: valueFromAST().
180178
*/
181179
export function parseValue(
182180
source: string | Source,

src/utilities/__tests__/coerceInputValue-test.ts

Lines changed: 276 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { expect } from 'chai';
22
import { describe, it } from 'mocha';
33

4+
import { identityFunc } from '../../jsutils/identityFunc.js';
5+
import { invariant } from '../../jsutils/invariant.js';
6+
import type { ObjMap } from '../../jsutils/ObjMap.js';
7+
8+
import { parseValue } from '../../language/parser.js';
9+
import { print } from '../../language/printer.js';
10+
411
import type { GraphQLInputType } from '../../type/definition.js';
512
import {
613
GraphQLEnumType,
@@ -9,9 +16,15 @@ import {
916
GraphQLNonNull,
1017
GraphQLScalarType,
1118
} from '../../type/definition.js';
12-
import { GraphQLInt } from '../../type/scalars.js';
19+
import {
20+
GraphQLBoolean,
21+
GraphQLFloat,
22+
GraphQLID,
23+
GraphQLInt,
24+
GraphQLString,
25+
} from '../../type/scalars.js';
1326

14-
import { coerceInputValue } from '../coerceInputValue.js';
27+
import { coerceInputLiteral, coerceInputValue } from '../coerceInputValue.js';
1528

1629
interface CoerceResult {
1730
value: unknown;
@@ -671,3 +684,264 @@ describe('coerceInputValue', () => {
671684
});
672685
});
673686
});
687+
688+
describe('coerceInputLiteral', () => {
689+
function test(
690+
valueText: string,
691+
type: GraphQLInputType,
692+
expected: unknown,
693+
variables?: ObjMap<unknown>,
694+
) {
695+
const ast = parseValue(valueText);
696+
const value = coerceInputLiteral(ast, type, variables);
697+
expect(value).to.deep.equal(expected);
698+
}
699+
700+
function testWithVariables(
701+
variables: ObjMap<unknown>,
702+
valueText: string,
703+
type: GraphQLInputType,
704+
expected: unknown,
705+
) {
706+
test(valueText, type, expected, variables);
707+
}
708+
709+
it('converts according to input coercion rules', () => {
710+
test('true', GraphQLBoolean, true);
711+
test('false', GraphQLBoolean, false);
712+
test('123', GraphQLInt, 123);
713+
test('123', GraphQLFloat, 123);
714+
test('123.456', GraphQLFloat, 123.456);
715+
test('"abc123"', GraphQLString, 'abc123');
716+
test('123456', GraphQLID, '123456');
717+
test('"123456"', GraphQLID, '123456');
718+
});
719+
720+
it('does not convert when input coercion rules reject a value', () => {
721+
test('123', GraphQLBoolean, undefined);
722+
test('123.456', GraphQLInt, undefined);
723+
test('true', GraphQLInt, undefined);
724+
test('"123"', GraphQLInt, undefined);
725+
test('"123"', GraphQLFloat, undefined);
726+
test('123', GraphQLString, undefined);
727+
test('true', GraphQLString, undefined);
728+
test('123.456', GraphQLString, undefined);
729+
test('123.456', GraphQLID, undefined);
730+
});
731+
732+
it('convert using parseLiteral from a custom scalar type', () => {
733+
const passthroughScalar = new GraphQLScalarType({
734+
name: 'PassthroughScalar',
735+
parseLiteral(node) {
736+
invariant(node.kind === 'StringValue');
737+
return node.value;
738+
},
739+
parseValue: identityFunc,
740+
});
741+
742+
test('"value"', passthroughScalar, 'value');
743+
744+
const printScalar = new GraphQLScalarType({
745+
name: 'PrintScalar',
746+
parseLiteral(node) {
747+
return `~~~${print(node)}~~~`;
748+
},
749+
parseValue: identityFunc,
750+
});
751+
752+
test('"value"', printScalar, '~~~"value"~~~');
753+
754+
const throwScalar = new GraphQLScalarType({
755+
name: 'ThrowScalar',
756+
parseLiteral() {
757+
throw new Error('Test');
758+
},
759+
parseValue: identityFunc,
760+
});
761+
762+
test('value', throwScalar, undefined);
763+
764+
const returnUndefinedScalar = new GraphQLScalarType({
765+
name: 'ReturnUndefinedScalar',
766+
parseLiteral() {
767+
return undefined;
768+
},
769+
parseValue: identityFunc,
770+
});
771+
772+
test('value', returnUndefinedScalar, undefined);
773+
});
774+
775+
it('converts enum values according to input coercion rules', () => {
776+
const testEnum = new GraphQLEnumType({
777+
name: 'TestColor',
778+
values: {
779+
RED: { value: 1 },
780+
GREEN: { value: 2 },
781+
BLUE: { value: 3 },
782+
NULL: { value: null },
783+
NAN: { value: NaN },
784+
NO_CUSTOM_VALUE: { value: undefined },
785+
},
786+
});
787+
788+
test('RED', testEnum, 1);
789+
test('BLUE', testEnum, 3);
790+
test('3', testEnum, undefined);
791+
test('"BLUE"', testEnum, undefined);
792+
test('null', testEnum, null);
793+
test('NULL', testEnum, null);
794+
test('NULL', new GraphQLNonNull(testEnum), null);
795+
test('NAN', testEnum, NaN);
796+
test('NO_CUSTOM_VALUE', testEnum, 'NO_CUSTOM_VALUE');
797+
});
798+
799+
// Boolean!
800+
const nonNullBool = new GraphQLNonNull(GraphQLBoolean);
801+
// [Boolean]
802+
const listOfBool = new GraphQLList(GraphQLBoolean);
803+
// [Boolean!]
804+
const listOfNonNullBool = new GraphQLList(nonNullBool);
805+
// [Boolean]!
806+
const nonNullListOfBool = new GraphQLNonNull(listOfBool);
807+
// [Boolean!]!
808+
const nonNullListOfNonNullBool = new GraphQLNonNull(listOfNonNullBool);
809+
810+
it('coerces to null unless non-null', () => {
811+
test('null', GraphQLBoolean, null);
812+
test('null', nonNullBool, undefined);
813+
});
814+
815+
it('coerces lists of values', () => {
816+
test('true', listOfBool, [true]);
817+
test('123', listOfBool, undefined);
818+
test('null', listOfBool, null);
819+
test('[true, false]', listOfBool, [true, false]);
820+
test('[true, 123]', listOfBool, undefined);
821+
test('[true, null]', listOfBool, [true, null]);
822+
test('{ true: true }', listOfBool, undefined);
823+
});
824+
825+
it('coerces non-null lists of values', () => {
826+
test('true', nonNullListOfBool, [true]);
827+
test('123', nonNullListOfBool, undefined);
828+
test('null', nonNullListOfBool, undefined);
829+
test('[true, false]', nonNullListOfBool, [true, false]);
830+
test('[true, 123]', nonNullListOfBool, undefined);
831+
test('[true, null]', nonNullListOfBool, [true, null]);
832+
});
833+
834+
it('coerces lists of non-null values', () => {
835+
test('true', listOfNonNullBool, [true]);
836+
test('123', listOfNonNullBool, undefined);
837+
test('null', listOfNonNullBool, null);
838+
test('[true, false]', listOfNonNullBool, [true, false]);
839+
test('[true, 123]', listOfNonNullBool, undefined);
840+
test('[true, null]', listOfNonNullBool, undefined);
841+
});
842+
843+
it('coerces non-null lists of non-null values', () => {
844+
test('true', nonNullListOfNonNullBool, [true]);
845+
test('123', nonNullListOfNonNullBool, undefined);
846+
test('null', nonNullListOfNonNullBool, undefined);
847+
test('[true, false]', nonNullListOfNonNullBool, [true, false]);
848+
test('[true, 123]', nonNullListOfNonNullBool, undefined);
849+
test('[true, null]', nonNullListOfNonNullBool, undefined);
850+
});
851+
852+
it('uses default values for unprovided fields', () => {
853+
const type = new GraphQLInputObjectType({
854+
name: 'TestInput',
855+
fields: {
856+
int: { type: GraphQLInt, defaultValue: 42 },
857+
},
858+
});
859+
860+
test('{}', type, { int: 42 });
861+
});
862+
863+
const testInputObj = new GraphQLInputObjectType({
864+
name: 'TestInput',
865+
fields: {
866+
int: { type: GraphQLInt, defaultValue: 42 },
867+
bool: { type: GraphQLBoolean },
868+
requiredBool: { type: nonNullBool },
869+
},
870+
});
871+
const testOneOfInputObj = new GraphQLInputObjectType({
872+
name: 'TestOneOfInput',
873+
fields: {
874+
a: { type: GraphQLString },
875+
b: { type: GraphQLString },
876+
},
877+
isOneOf: true,
878+
});
879+
880+
it('coerces input objects according to input coercion rules', () => {
881+
test('null', testInputObj, null);
882+
test('123', testInputObj, undefined);
883+
test('[]', testInputObj, undefined);
884+
test('{ requiredBool: true }', testInputObj, {
885+
int: 42,
886+
requiredBool: true,
887+
});
888+
test('{ int: null, requiredBool: true }', testInputObj, {
889+
int: null,
890+
requiredBool: true,
891+
});
892+
test('{ int: 123, requiredBool: false }', testInputObj, {
893+
int: 123,
894+
requiredBool: false,
895+
});
896+
test('{ bool: true, requiredBool: false }', testInputObj, {
897+
int: 42,
898+
bool: true,
899+
requiredBool: false,
900+
});
901+
test('{ int: true, requiredBool: true }', testInputObj, undefined);
902+
test('{ requiredBool: null }', testInputObj, undefined);
903+
test('{ bool: true }', testInputObj, undefined);
904+
test('{ requiredBool: true, unknown: 123 }', testInputObj, undefined);
905+
test('{ a: "abc" }', testOneOfInputObj, {
906+
a: 'abc',
907+
});
908+
test('{ b: "def" }', testOneOfInputObj, {
909+
b: 'def',
910+
});
911+
test('{ a: "abc", b: null }', testOneOfInputObj, undefined);
912+
test('{ a: null }', testOneOfInputObj, undefined);
913+
test('{ a: 1 }', testOneOfInputObj, undefined);
914+
test('{ a: "abc", b: "def" }', testOneOfInputObj, undefined);
915+
test('{}', testOneOfInputObj, undefined);
916+
test('{ c: "abc" }', testOneOfInputObj, undefined);
917+
});
918+
919+
it('accepts variable values assuming already coerced', () => {
920+
test('$var', GraphQLBoolean, undefined);
921+
testWithVariables({ var: true }, '$var', GraphQLBoolean, true);
922+
testWithVariables({ var: null }, '$var', GraphQLBoolean, null);
923+
testWithVariables({ var: null }, '$var', nonNullBool, undefined);
924+
});
925+
926+
it('asserts variables are provided as items in lists', () => {
927+
test('[ $foo ]', listOfBool, [null]);
928+
test('[ $foo ]', listOfNonNullBool, undefined);
929+
testWithVariables({ foo: true }, '[ $foo ]', listOfNonNullBool, [true]);
930+
// Note: variables are expected to have already been coerced, so we
931+
// do not expect the singleton wrapping behavior for variables.
932+
testWithVariables({ foo: true }, '$foo', listOfNonNullBool, true);
933+
testWithVariables({ foo: [true] }, '$foo', listOfNonNullBool, [true]);
934+
});
935+
936+
it('omits input object fields for unprovided variables', () => {
937+
test('{ int: $foo, bool: $foo, requiredBool: true }', testInputObj, {
938+
int: 42,
939+
requiredBool: true,
940+
});
941+
test('{ requiredBool: $foo }', testInputObj, undefined);
942+
testWithVariables({ foo: true }, '{ requiredBool: $foo }', testInputObj, {
943+
int: 42,
944+
requiredBool: true,
945+
});
946+
});
947+
});

src/utilities/__tests__/valueFromAST-test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424

2525
import { valueFromAST } from '../valueFromAST.js';
2626

27+
/** @deprecated use `coerceInputLiteral()` instead - will be removed in v18 */
2728
describe('valueFromAST', () => {
2829
function expectValueFrom(
2930
valueText: string,

0 commit comments

Comments
 (0)