Skip to content

Commit f8b17a9

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 4ab393b commit f8b17a9

12 files changed

Lines changed: 500 additions & 51 deletions

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;
@@ -566,3 +579,264 @@ describe('coerceInputValue', () => {
566579
});
567580
});
568581
});
582+
583+
describe('coerceInputLiteral', () => {
584+
function test(
585+
valueText: string,
586+
type: GraphQLInputType,
587+
expected: unknown,
588+
variables?: ObjMap<unknown>,
589+
) {
590+
const ast = parseValue(valueText);
591+
const value = coerceInputLiteral(ast, type, variables);
592+
expect(value).to.deep.equal(expected);
593+
}
594+
595+
function testWithVariables(
596+
variables: ObjMap<unknown>,
597+
valueText: string,
598+
type: GraphQLInputType,
599+
expected: unknown,
600+
) {
601+
test(valueText, type, expected, variables);
602+
}
603+
604+
it('converts according to input coercion rules', () => {
605+
test('true', GraphQLBoolean, true);
606+
test('false', GraphQLBoolean, false);
607+
test('123', GraphQLInt, 123);
608+
test('123', GraphQLFloat, 123);
609+
test('123.456', GraphQLFloat, 123.456);
610+
test('"abc123"', GraphQLString, 'abc123');
611+
test('123456', GraphQLID, '123456');
612+
test('"123456"', GraphQLID, '123456');
613+
});
614+
615+
it('does not convert when input coercion rules reject a value', () => {
616+
test('123', GraphQLBoolean, undefined);
617+
test('123.456', GraphQLInt, undefined);
618+
test('true', GraphQLInt, undefined);
619+
test('"123"', GraphQLInt, undefined);
620+
test('"123"', GraphQLFloat, undefined);
621+
test('123', GraphQLString, undefined);
622+
test('true', GraphQLString, undefined);
623+
test('123.456', GraphQLString, undefined);
624+
test('123.456', GraphQLID, undefined);
625+
});
626+
627+
it('convert using parseLiteral from a custom scalar type', () => {
628+
const passthroughScalar = new GraphQLScalarType({
629+
name: 'PassthroughScalar',
630+
parseLiteral(node) {
631+
invariant(node.kind === 'StringValue');
632+
return node.value;
633+
},
634+
parseValue: identityFunc,
635+
});
636+
637+
test('"value"', passthroughScalar, 'value');
638+
639+
const printScalar = new GraphQLScalarType({
640+
name: 'PrintScalar',
641+
parseLiteral(node) {
642+
return `~~~${print(node)}~~~`;
643+
},
644+
parseValue: identityFunc,
645+
});
646+
647+
test('"value"', printScalar, '~~~"value"~~~');
648+
649+
const throwScalar = new GraphQLScalarType({
650+
name: 'ThrowScalar',
651+
parseLiteral() {
652+
throw new Error('Test');
653+
},
654+
parseValue: identityFunc,
655+
});
656+
657+
test('value', throwScalar, undefined);
658+
659+
const returnUndefinedScalar = new GraphQLScalarType({
660+
name: 'ReturnUndefinedScalar',
661+
parseLiteral() {
662+
return undefined;
663+
},
664+
parseValue: identityFunc,
665+
});
666+
667+
test('value', returnUndefinedScalar, undefined);
668+
});
669+
670+
it('converts enum values according to input coercion rules', () => {
671+
const testEnum = new GraphQLEnumType({
672+
name: 'TestColor',
673+
values: {
674+
RED: { value: 1 },
675+
GREEN: { value: 2 },
676+
BLUE: { value: 3 },
677+
NULL: { value: null },
678+
NAN: { value: NaN },
679+
NO_CUSTOM_VALUE: { value: undefined },
680+
},
681+
});
682+
683+
test('RED', testEnum, 1);
684+
test('BLUE', testEnum, 3);
685+
test('3', testEnum, undefined);
686+
test('"BLUE"', testEnum, undefined);
687+
test('null', testEnum, null);
688+
test('NULL', testEnum, null);
689+
test('NULL', new GraphQLNonNull(testEnum), null);
690+
test('NAN', testEnum, NaN);
691+
test('NO_CUSTOM_VALUE', testEnum, 'NO_CUSTOM_VALUE');
692+
});
693+
694+
// Boolean!
695+
const nonNullBool = new GraphQLNonNull(GraphQLBoolean);
696+
// [Boolean]
697+
const listOfBool = new GraphQLList(GraphQLBoolean);
698+
// [Boolean!]
699+
const listOfNonNullBool = new GraphQLList(nonNullBool);
700+
// [Boolean]!
701+
const nonNullListOfBool = new GraphQLNonNull(listOfBool);
702+
// [Boolean!]!
703+
const nonNullListOfNonNullBool = new GraphQLNonNull(listOfNonNullBool);
704+
705+
it('coerces to null unless non-null', () => {
706+
test('null', GraphQLBoolean, null);
707+
test('null', nonNullBool, undefined);
708+
});
709+
710+
it('coerces lists of values', () => {
711+
test('true', listOfBool, [true]);
712+
test('123', listOfBool, undefined);
713+
test('null', listOfBool, null);
714+
test('[true, false]', listOfBool, [true, false]);
715+
test('[true, 123]', listOfBool, undefined);
716+
test('[true, null]', listOfBool, [true, null]);
717+
test('{ true: true }', listOfBool, undefined);
718+
});
719+
720+
it('coerces non-null lists of values', () => {
721+
test('true', nonNullListOfBool, [true]);
722+
test('123', nonNullListOfBool, undefined);
723+
test('null', nonNullListOfBool, undefined);
724+
test('[true, false]', nonNullListOfBool, [true, false]);
725+
test('[true, 123]', nonNullListOfBool, undefined);
726+
test('[true, null]', nonNullListOfBool, [true, null]);
727+
});
728+
729+
it('coerces lists of non-null values', () => {
730+
test('true', listOfNonNullBool, [true]);
731+
test('123', listOfNonNullBool, undefined);
732+
test('null', listOfNonNullBool, null);
733+
test('[true, false]', listOfNonNullBool, [true, false]);
734+
test('[true, 123]', listOfNonNullBool, undefined);
735+
test('[true, null]', listOfNonNullBool, undefined);
736+
});
737+
738+
it('coerces non-null lists of non-null values', () => {
739+
test('true', nonNullListOfNonNullBool, [true]);
740+
test('123', nonNullListOfNonNullBool, undefined);
741+
test('null', nonNullListOfNonNullBool, undefined);
742+
test('[true, false]', nonNullListOfNonNullBool, [true, false]);
743+
test('[true, 123]', nonNullListOfNonNullBool, undefined);
744+
test('[true, null]', nonNullListOfNonNullBool, undefined);
745+
});
746+
747+
it('uses default values for unprovided fields', () => {
748+
const type = new GraphQLInputObjectType({
749+
name: 'TestInput',
750+
fields: {
751+
int: { type: GraphQLInt, defaultValue: 42 },
752+
},
753+
});
754+
755+
test('{}', type, { int: 42 });
756+
});
757+
758+
const testInputObj = new GraphQLInputObjectType({
759+
name: 'TestInput',
760+
fields: {
761+
int: { type: GraphQLInt, defaultValue: 42 },
762+
bool: { type: GraphQLBoolean },
763+
requiredBool: { type: nonNullBool },
764+
},
765+
});
766+
const testOneOfInputObj = new GraphQLInputObjectType({
767+
name: 'TestOneOfInput',
768+
fields: {
769+
a: { type: GraphQLString },
770+
b: { type: GraphQLString },
771+
},
772+
isOneOf: true,
773+
});
774+
775+
it('coerces input objects according to input coercion rules', () => {
776+
test('null', testInputObj, null);
777+
test('123', testInputObj, undefined);
778+
test('[]', testInputObj, undefined);
779+
test('{ requiredBool: true }', testInputObj, {
780+
int: 42,
781+
requiredBool: true,
782+
});
783+
test('{ int: null, requiredBool: true }', testInputObj, {
784+
int: null,
785+
requiredBool: true,
786+
});
787+
test('{ int: 123, requiredBool: false }', testInputObj, {
788+
int: 123,
789+
requiredBool: false,
790+
});
791+
test('{ bool: true, requiredBool: false }', testInputObj, {
792+
int: 42,
793+
bool: true,
794+
requiredBool: false,
795+
});
796+
test('{ int: true, requiredBool: true }', testInputObj, undefined);
797+
test('{ requiredBool: null }', testInputObj, undefined);
798+
test('{ bool: true }', testInputObj, undefined);
799+
test('{ requiredBool: true, unknown: 123 }', testInputObj, undefined);
800+
test('{ a: "abc" }', testOneOfInputObj, {
801+
a: 'abc',
802+
});
803+
test('{ b: "def" }', testOneOfInputObj, {
804+
b: 'def',
805+
});
806+
test('{ a: "abc", b: null }', testOneOfInputObj, undefined);
807+
test('{ a: null }', testOneOfInputObj, undefined);
808+
test('{ a: 1 }', testOneOfInputObj, undefined);
809+
test('{ a: "abc", b: "def" }', testOneOfInputObj, undefined);
810+
test('{}', testOneOfInputObj, undefined);
811+
test('{ c: "abc" }', testOneOfInputObj, undefined);
812+
});
813+
814+
it('accepts variable values assuming already coerced', () => {
815+
test('$var', GraphQLBoolean, undefined);
816+
testWithVariables({ var: true }, '$var', GraphQLBoolean, true);
817+
testWithVariables({ var: null }, '$var', GraphQLBoolean, null);
818+
testWithVariables({ var: null }, '$var', nonNullBool, undefined);
819+
});
820+
821+
it('asserts variables are provided as items in lists', () => {
822+
test('[ $foo ]', listOfBool, [null]);
823+
test('[ $foo ]', listOfNonNullBool, undefined);
824+
testWithVariables({ foo: true }, '[ $foo ]', listOfNonNullBool, [true]);
825+
// Note: variables are expected to have already been coerced, so we
826+
// do not expect the singleton wrapping behavior for variables.
827+
testWithVariables({ foo: true }, '$foo', listOfNonNullBool, true);
828+
testWithVariables({ foo: [true] }, '$foo', listOfNonNullBool, [true]);
829+
});
830+
831+
it('omits input object fields for unprovided variables', () => {
832+
test('{ int: $foo, bool: $foo, requiredBool: true }', testInputObj, {
833+
int: 42,
834+
requiredBool: true,
835+
});
836+
test('{ requiredBool: $foo }', testInputObj, undefined);
837+
testWithVariables({ foo: true }, '{ requiredBool: $foo }', testInputObj, {
838+
int: 42,
839+
requiredBool: true,
840+
});
841+
});
842+
});

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)