Skip to content

Commit e520725

Browse files
leebyronyaacovCR
authored andcommitted
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.
1 parent 1931581 commit e520725

File tree

11 files changed

+451
-485
lines changed

11 files changed

+451
-485
lines changed

src/execution/values.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import { isInputType, isNonNullType } from '../type/definition.js';
1818
import type { GraphQLDirective } from '../type/directives.js';
1919
import type { GraphQLSchema } from '../type/schema.js';
2020

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

2527
type CoercedVariableValues =
2628
| { errors: ReadonlyArray<GraphQLError>; coerced?: never }
@@ -93,7 +95,10 @@ function coerceVariableValues(
9395

9496
if (!Object.hasOwn(inputs, varName)) {
9597
if (varDefNode.defaultValue) {
96-
coercedValues[varName] = valueFromAST(varDefNode.defaultValue, varType);
98+
coercedValues[varName] = coerceInputLiteral(
99+
varDefNode.defaultValue,
100+
varType,
101+
);
97102
} else if (isNonNullType(varType)) {
98103
onError(
99104
new GraphQLError(
@@ -205,7 +210,7 @@ export function getArgumentValues(
205210
);
206211
}
207212

208-
const coercedValue = valueFromAST(valueNode, argType, variableValues);
213+
const coercedValue = coerceInputLiteral(valueNode, argType, variableValues);
209214
if (coercedValue === undefined) {
210215
// Note: ValuesOfCorrectTypeRule validation should catch this before
211216
// execution. This is a runtime check to ensure execution does not

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,8 +440,6 @@ export {
440440
printIntrospectionSchema,
441441
// Create a GraphQLType from a GraphQL language AST.
442442
typeFromAST,
443-
// Create a JavaScript value from a GraphQL language AST with a Type.
444-
valueFromAST,
445443
// Create a JavaScript value from a GraphQL language AST without a Type.
446444
valueFromASTUntyped,
447445
// Create a GraphQL language AST from a JavaScript value.
@@ -451,6 +449,8 @@ export {
451449
visitWithTypeInfo,
452450
// Coerces a JavaScript value to a GraphQL type, or produces errors.
453451
coerceInputValue,
452+
// Coerces a GraphQL literal (AST) to a GraphQL type, or returns undefined.
453+
coerceInputLiteral,
454454
// Concatenates multiple AST together.
455455
concatAST,
456456
// 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
@@ -152,8 +152,6 @@ export function parse(
152152
*
153153
* This is useful within tools that operate upon GraphQL Values directly and
154154
* in isolation of complete GraphQL documents.
155-
*
156-
* Consider providing the results to the utility function: valueFromAST().
157155
*/
158156
export function parseValue(
159157
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;
@@ -532,3 +545,264 @@ describe('coerceInputValue', () => {
532545
});
533546
});
534547
});
548+
549+
describe('coerceInputLiteral', () => {
550+
function test(
551+
valueText: string,
552+
type: GraphQLInputType,
553+
expected: unknown,
554+
variables?: ObjMap<unknown>,
555+
) {
556+
const ast = parseValue(valueText);
557+
const value = coerceInputLiteral(ast, type, variables);
558+
expect(value).to.deep.equal(expected);
559+
}
560+
561+
function testWithVariables(
562+
variables: ObjMap<unknown>,
563+
valueText: string,
564+
type: GraphQLInputType,
565+
expected: unknown,
566+
) {
567+
test(valueText, type, expected, variables);
568+
}
569+
570+
it('converts according to input coercion rules', () => {
571+
test('true', GraphQLBoolean, true);
572+
test('false', GraphQLBoolean, false);
573+
test('123', GraphQLInt, 123);
574+
test('123', GraphQLFloat, 123);
575+
test('123.456', GraphQLFloat, 123.456);
576+
test('"abc123"', GraphQLString, 'abc123');
577+
test('123456', GraphQLID, '123456');
578+
test('"123456"', GraphQLID, '123456');
579+
});
580+
581+
it('does not convert when input coercion rules reject a value', () => {
582+
test('123', GraphQLBoolean, undefined);
583+
test('123.456', GraphQLInt, undefined);
584+
test('true', GraphQLInt, undefined);
585+
test('"123"', GraphQLInt, undefined);
586+
test('"123"', GraphQLFloat, undefined);
587+
test('123', GraphQLString, undefined);
588+
test('true', GraphQLString, undefined);
589+
test('123.456', GraphQLString, undefined);
590+
test('123.456', GraphQLID, undefined);
591+
});
592+
593+
it('convert using parseLiteral from a custom scalar type', () => {
594+
const passthroughScalar = new GraphQLScalarType({
595+
name: 'PassthroughScalar',
596+
parseLiteral(node) {
597+
invariant(node.kind === 'StringValue');
598+
return node.value;
599+
},
600+
parseValue: identityFunc,
601+
});
602+
603+
test('"value"', passthroughScalar, 'value');
604+
605+
const printScalar = new GraphQLScalarType({
606+
name: 'PrintScalar',
607+
parseLiteral(node) {
608+
return `~~~${print(node)}~~~`;
609+
},
610+
parseValue: identityFunc,
611+
});
612+
613+
test('"value"', printScalar, '~~~"value"~~~');
614+
615+
const throwScalar = new GraphQLScalarType({
616+
name: 'ThrowScalar',
617+
parseLiteral() {
618+
throw new Error('Test');
619+
},
620+
parseValue: identityFunc,
621+
});
622+
623+
test('value', throwScalar, undefined);
624+
625+
const returnUndefinedScalar = new GraphQLScalarType({
626+
name: 'ReturnUndefinedScalar',
627+
parseLiteral() {
628+
return undefined;
629+
},
630+
parseValue: identityFunc,
631+
});
632+
633+
test('value', returnUndefinedScalar, undefined);
634+
});
635+
636+
it('converts enum values according to input coercion rules', () => {
637+
const testEnum = new GraphQLEnumType({
638+
name: 'TestColor',
639+
values: {
640+
RED: { value: 1 },
641+
GREEN: { value: 2 },
642+
BLUE: { value: 3 },
643+
NULL: { value: null },
644+
NAN: { value: NaN },
645+
NO_CUSTOM_VALUE: { value: undefined },
646+
},
647+
});
648+
649+
test('RED', testEnum, 1);
650+
test('BLUE', testEnum, 3);
651+
test('3', testEnum, undefined);
652+
test('"BLUE"', testEnum, undefined);
653+
test('null', testEnum, null);
654+
test('NULL', testEnum, null);
655+
test('NULL', new GraphQLNonNull(testEnum), null);
656+
test('NAN', testEnum, NaN);
657+
test('NO_CUSTOM_VALUE', testEnum, 'NO_CUSTOM_VALUE');
658+
});
659+
660+
// Boolean!
661+
const nonNullBool = new GraphQLNonNull(GraphQLBoolean);
662+
// [Boolean]
663+
const listOfBool = new GraphQLList(GraphQLBoolean);
664+
// [Boolean!]
665+
const listOfNonNullBool = new GraphQLList(nonNullBool);
666+
// [Boolean]!
667+
const nonNullListOfBool = new GraphQLNonNull(listOfBool);
668+
// [Boolean!]!
669+
const nonNullListOfNonNullBool = new GraphQLNonNull(listOfNonNullBool);
670+
671+
it('coerces to null unless non-null', () => {
672+
test('null', GraphQLBoolean, null);
673+
test('null', nonNullBool, undefined);
674+
});
675+
676+
it('coerces lists of values', () => {
677+
test('true', listOfBool, [true]);
678+
test('123', listOfBool, undefined);
679+
test('null', listOfBool, null);
680+
test('[true, false]', listOfBool, [true, false]);
681+
test('[true, 123]', listOfBool, undefined);
682+
test('[true, null]', listOfBool, [true, null]);
683+
test('{ true: true }', listOfBool, undefined);
684+
});
685+
686+
it('coerces non-null lists of values', () => {
687+
test('true', nonNullListOfBool, [true]);
688+
test('123', nonNullListOfBool, undefined);
689+
test('null', nonNullListOfBool, undefined);
690+
test('[true, false]', nonNullListOfBool, [true, false]);
691+
test('[true, 123]', nonNullListOfBool, undefined);
692+
test('[true, null]', nonNullListOfBool, [true, null]);
693+
});
694+
695+
it('coerces lists of non-null values', () => {
696+
test('true', listOfNonNullBool, [true]);
697+
test('123', listOfNonNullBool, undefined);
698+
test('null', listOfNonNullBool, null);
699+
test('[true, false]', listOfNonNullBool, [true, false]);
700+
test('[true, 123]', listOfNonNullBool, undefined);
701+
test('[true, null]', listOfNonNullBool, undefined);
702+
});
703+
704+
it('coerces non-null lists of non-null values', () => {
705+
test('true', nonNullListOfNonNullBool, [true]);
706+
test('123', nonNullListOfNonNullBool, undefined);
707+
test('null', nonNullListOfNonNullBool, undefined);
708+
test('[true, false]', nonNullListOfNonNullBool, [true, false]);
709+
test('[true, 123]', nonNullListOfNonNullBool, undefined);
710+
test('[true, null]', nonNullListOfNonNullBool, undefined);
711+
});
712+
713+
it('uses default values for unprovided fields', () => {
714+
const type = new GraphQLInputObjectType({
715+
name: 'TestInput',
716+
fields: {
717+
int: { type: GraphQLInt, defaultValue: 42 },
718+
},
719+
});
720+
721+
test('{}', type, { int: 42 });
722+
});
723+
724+
const testInputObj = new GraphQLInputObjectType({
725+
name: 'TestInput',
726+
fields: {
727+
int: { type: GraphQLInt, defaultValue: 42 },
728+
bool: { type: GraphQLBoolean },
729+
requiredBool: { type: nonNullBool },
730+
},
731+
});
732+
const testOneOfInputObj = new GraphQLInputObjectType({
733+
name: 'TestOneOfInput',
734+
fields: {
735+
a: { type: GraphQLString },
736+
b: { type: GraphQLString },
737+
},
738+
isOneOf: true,
739+
});
740+
741+
it('coerces input objects according to input coercion rules', () => {
742+
test('null', testInputObj, null);
743+
test('123', testInputObj, undefined);
744+
test('[]', testInputObj, undefined);
745+
test('{ requiredBool: true }', testInputObj, {
746+
int: 42,
747+
requiredBool: true,
748+
});
749+
test('{ int: null, requiredBool: true }', testInputObj, {
750+
int: null,
751+
requiredBool: true,
752+
});
753+
test('{ int: 123, requiredBool: false }', testInputObj, {
754+
int: 123,
755+
requiredBool: false,
756+
});
757+
test('{ bool: true, requiredBool: false }', testInputObj, {
758+
int: 42,
759+
bool: true,
760+
requiredBool: false,
761+
});
762+
test('{ int: true, requiredBool: true }', testInputObj, undefined);
763+
test('{ requiredBool: null }', testInputObj, undefined);
764+
test('{ bool: true }', testInputObj, undefined);
765+
test('{ requiredBool: true, unknown: 123 }', testInputObj, undefined);
766+
test('{ a: "abc" }', testOneOfInputObj, {
767+
a: 'abc',
768+
});
769+
test('{ b: "def" }', testOneOfInputObj, {
770+
b: 'def',
771+
});
772+
test('{ a: "abc", b: null }', testOneOfInputObj, undefined);
773+
test('{ a: null }', testOneOfInputObj, undefined);
774+
test('{ a: 1 }', testOneOfInputObj, undefined);
775+
test('{ a: "abc", b: "def" }', testOneOfInputObj, undefined);
776+
test('{}', testOneOfInputObj, undefined);
777+
test('{ c: "abc" }', testOneOfInputObj, undefined);
778+
});
779+
780+
it('accepts variable values assuming already coerced', () => {
781+
test('$var', GraphQLBoolean, undefined);
782+
testWithVariables({ var: true }, '$var', GraphQLBoolean, true);
783+
testWithVariables({ var: null }, '$var', GraphQLBoolean, null);
784+
testWithVariables({ var: null }, '$var', nonNullBool, undefined);
785+
});
786+
787+
it('asserts variables are provided as items in lists', () => {
788+
test('[ $foo ]', listOfBool, [null]);
789+
test('[ $foo ]', listOfNonNullBool, undefined);
790+
testWithVariables({ foo: true }, '[ $foo ]', listOfNonNullBool, [true]);
791+
// Note: variables are expected to have already been coerced, so we
792+
// do not expect the singleton wrapping behavior for variables.
793+
testWithVariables({ foo: true }, '$foo', listOfNonNullBool, true);
794+
testWithVariables({ foo: [true] }, '$foo', listOfNonNullBool, [true]);
795+
});
796+
797+
it('omits input object fields for unprovided variables', () => {
798+
test('{ int: $foo, bool: $foo, requiredBool: true }', testInputObj, {
799+
int: 42,
800+
requiredBool: true,
801+
});
802+
test('{ requiredBool: $foo }', testInputObj, undefined);
803+
testWithVariables({ foo: true }, '{ requiredBool: $foo }', testInputObj, {
804+
int: 42,
805+
requiredBool: true,
806+
});
807+
});
808+
});

0 commit comments

Comments
 (0)