Skip to content

Commit bf89146

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 3df173b commit bf89146

11 files changed

+419
-449
lines changed

src/execution/values.ts

+9-4
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

+2-2
Original file line numberDiff line numberDiff line change
@@ -439,8 +439,6 @@ export {
439439
printIntrospectionSchema,
440440
// Create a GraphQLType from a GraphQL language AST.
441441
typeFromAST,
442-
// Create a JavaScript value from a GraphQL language AST with a Type.
443-
valueFromAST,
444442
// Create a JavaScript value from a GraphQL language AST without a Type.
445443
valueFromASTUntyped,
446444
// Create a GraphQL language AST from a JavaScript value.
@@ -450,6 +448,8 @@ export {
450448
visitWithTypeInfo,
451449
// Coerces a JavaScript value to a GraphQL type, or produces errors.
452450
coerceInputValue,
451+
// Coerces a GraphQL literal (AST) to a GraphQL type, or returns undefined.
452+
coerceInputLiteral,
453453
// Concatenates multiple AST together.
454454
concatAST,
455455
// Separates an AST into an AST per Operation.

src/language/parser.ts

-2
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

+256-2
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;
@@ -427,3 +440,244 @@ describe('coerceInputValue', () => {
427440
});
428441
});
429442
});
443+
444+
describe('coerceInputLiteral', () => {
445+
function test(
446+
valueText: string,
447+
type: GraphQLInputType,
448+
expected: unknown,
449+
variables?: ObjMap<unknown>,
450+
) {
451+
const ast = parseValue(valueText);
452+
const value = coerceInputLiteral(ast, type, variables);
453+
expect(value).to.deep.equal(expected);
454+
}
455+
456+
function testWithVariables(
457+
variables: ObjMap<unknown>,
458+
valueText: string,
459+
type: GraphQLInputType,
460+
expected: unknown,
461+
) {
462+
test(valueText, type, expected, variables);
463+
}
464+
465+
it('converts according to input coercion rules', () => {
466+
test('true', GraphQLBoolean, true);
467+
test('false', GraphQLBoolean, false);
468+
test('123', GraphQLInt, 123);
469+
test('123', GraphQLFloat, 123);
470+
test('123.456', GraphQLFloat, 123.456);
471+
test('"abc123"', GraphQLString, 'abc123');
472+
test('123456', GraphQLID, '123456');
473+
test('"123456"', GraphQLID, '123456');
474+
});
475+
476+
it('does not convert when input coercion rules reject a value', () => {
477+
test('123', GraphQLBoolean, undefined);
478+
test('123.456', GraphQLInt, undefined);
479+
test('true', GraphQLInt, undefined);
480+
test('"123"', GraphQLInt, undefined);
481+
test('"123"', GraphQLFloat, undefined);
482+
test('123', GraphQLString, undefined);
483+
test('true', GraphQLString, undefined);
484+
test('123.456', GraphQLString, undefined);
485+
test('123.456', GraphQLID, undefined);
486+
});
487+
488+
it('convert using parseLiteral from a custom scalar type', () => {
489+
const passthroughScalar = new GraphQLScalarType({
490+
name: 'PassthroughScalar',
491+
parseLiteral(node) {
492+
invariant(node.kind === 'StringValue');
493+
return node.value;
494+
},
495+
parseValue: identityFunc,
496+
});
497+
498+
test('"value"', passthroughScalar, 'value');
499+
500+
const printScalar = new GraphQLScalarType({
501+
name: 'PrintScalar',
502+
parseLiteral(node) {
503+
return `~~~${print(node)}~~~`;
504+
},
505+
parseValue: identityFunc,
506+
});
507+
508+
test('"value"', printScalar, '~~~"value"~~~');
509+
510+
const throwScalar = new GraphQLScalarType({
511+
name: 'ThrowScalar',
512+
parseLiteral() {
513+
throw new Error('Test');
514+
},
515+
parseValue: identityFunc,
516+
});
517+
518+
test('value', throwScalar, undefined);
519+
520+
const returnUndefinedScalar = new GraphQLScalarType({
521+
name: 'ReturnUndefinedScalar',
522+
parseLiteral() {
523+
return undefined;
524+
},
525+
parseValue: identityFunc,
526+
});
527+
528+
test('value', returnUndefinedScalar, undefined);
529+
});
530+
531+
it('converts enum values according to input coercion rules', () => {
532+
const testEnum = new GraphQLEnumType({
533+
name: 'TestColor',
534+
values: {
535+
RED: { value: 1 },
536+
GREEN: { value: 2 },
537+
BLUE: { value: 3 },
538+
NULL: { value: null },
539+
NAN: { value: NaN },
540+
NO_CUSTOM_VALUE: { value: undefined },
541+
},
542+
});
543+
544+
test('RED', testEnum, 1);
545+
test('BLUE', testEnum, 3);
546+
test('3', testEnum, undefined);
547+
test('"BLUE"', testEnum, undefined);
548+
test('null', testEnum, null);
549+
test('NULL', testEnum, null);
550+
test('NULL', new GraphQLNonNull(testEnum), null);
551+
test('NAN', testEnum, NaN);
552+
test('NO_CUSTOM_VALUE', testEnum, 'NO_CUSTOM_VALUE');
553+
});
554+
555+
// Boolean!
556+
const nonNullBool = new GraphQLNonNull(GraphQLBoolean);
557+
// [Boolean]
558+
const listOfBool = new GraphQLList(GraphQLBoolean);
559+
// [Boolean!]
560+
const listOfNonNullBool = new GraphQLList(nonNullBool);
561+
// [Boolean]!
562+
const nonNullListOfBool = new GraphQLNonNull(listOfBool);
563+
// [Boolean!]!
564+
const nonNullListOfNonNullBool = new GraphQLNonNull(listOfNonNullBool);
565+
566+
it('coerces to null unless non-null', () => {
567+
test('null', GraphQLBoolean, null);
568+
test('null', nonNullBool, undefined);
569+
});
570+
571+
it('coerces lists of values', () => {
572+
test('true', listOfBool, [true]);
573+
test('123', listOfBool, undefined);
574+
test('null', listOfBool, null);
575+
test('[true, false]', listOfBool, [true, false]);
576+
test('[true, 123]', listOfBool, undefined);
577+
test('[true, null]', listOfBool, [true, null]);
578+
test('{ true: true }', listOfBool, undefined);
579+
});
580+
581+
it('coerces non-null lists of values', () => {
582+
test('true', nonNullListOfBool, [true]);
583+
test('123', nonNullListOfBool, undefined);
584+
test('null', nonNullListOfBool, undefined);
585+
test('[true, false]', nonNullListOfBool, [true, false]);
586+
test('[true, 123]', nonNullListOfBool, undefined);
587+
test('[true, null]', nonNullListOfBool, [true, null]);
588+
});
589+
590+
it('coerces lists of non-null values', () => {
591+
test('true', listOfNonNullBool, [true]);
592+
test('123', listOfNonNullBool, undefined);
593+
test('null', listOfNonNullBool, null);
594+
test('[true, false]', listOfNonNullBool, [true, false]);
595+
test('[true, 123]', listOfNonNullBool, undefined);
596+
test('[true, null]', listOfNonNullBool, undefined);
597+
});
598+
599+
it('coerces non-null lists of non-null values', () => {
600+
test('true', nonNullListOfNonNullBool, [true]);
601+
test('123', nonNullListOfNonNullBool, undefined);
602+
test('null', nonNullListOfNonNullBool, undefined);
603+
test('[true, false]', nonNullListOfNonNullBool, [true, false]);
604+
test('[true, 123]', nonNullListOfNonNullBool, undefined);
605+
test('[true, null]', nonNullListOfNonNullBool, undefined);
606+
});
607+
608+
it('uses default values for unprovided fields', () => {
609+
const type = new GraphQLInputObjectType({
610+
name: 'TestInput',
611+
fields: {
612+
int: { type: GraphQLInt, defaultValue: 42 },
613+
},
614+
});
615+
616+
test('{}', type, { int: 42 });
617+
});
618+
619+
const testInputObj = new GraphQLInputObjectType({
620+
name: 'TestInput',
621+
fields: {
622+
int: { type: GraphQLInt, defaultValue: 42 },
623+
bool: { type: GraphQLBoolean },
624+
requiredBool: { type: nonNullBool },
625+
},
626+
});
627+
628+
it('coerces input objects according to input coercion rules', () => {
629+
test('null', testInputObj, null);
630+
test('123', testInputObj, undefined);
631+
test('[]', testInputObj, undefined);
632+
test('{ requiredBool: true }', testInputObj, {
633+
int: 42,
634+
requiredBool: true,
635+
});
636+
test('{ int: null, requiredBool: true }', testInputObj, {
637+
int: null,
638+
requiredBool: true,
639+
});
640+
test('{ int: 123, requiredBool: false }', testInputObj, {
641+
int: 123,
642+
requiredBool: false,
643+
});
644+
test('{ bool: true, requiredBool: false }', testInputObj, {
645+
int: 42,
646+
bool: true,
647+
requiredBool: false,
648+
});
649+
test('{ int: true, requiredBool: true }', testInputObj, undefined);
650+
test('{ requiredBool: null }', testInputObj, undefined);
651+
test('{ bool: true }', testInputObj, undefined);
652+
test('{ requiredBool: true, unknown: 123 }', testInputObj, undefined);
653+
});
654+
655+
it('accepts variable values assuming already coerced', () => {
656+
test('$var', GraphQLBoolean, undefined);
657+
testWithVariables({ var: true }, '$var', GraphQLBoolean, true);
658+
testWithVariables({ var: null }, '$var', GraphQLBoolean, null);
659+
testWithVariables({ var: null }, '$var', nonNullBool, undefined);
660+
});
661+
662+
it('asserts variables are provided as items in lists', () => {
663+
test('[ $foo ]', listOfBool, [null]);
664+
test('[ $foo ]', listOfNonNullBool, undefined);
665+
testWithVariables({ foo: true }, '[ $foo ]', listOfNonNullBool, [true]);
666+
// Note: variables are expected to have already been coerced, so we
667+
// do not expect the singleton wrapping behavior for variables.
668+
testWithVariables({ foo: true }, '$foo', listOfNonNullBool, true);
669+
testWithVariables({ foo: [true] }, '$foo', listOfNonNullBool, [true]);
670+
});
671+
672+
it('omits input object fields for unprovided variables', () => {
673+
test('{ int: $foo, bool: $foo, requiredBool: true }', testInputObj, {
674+
int: 42,
675+
requiredBool: true,
676+
});
677+
test('{ requiredBool: $foo }', testInputObj, undefined);
678+
testWithVariables({ foo: true }, '{ requiredBool: $foo }', testInputObj, {
679+
int: 42,
680+
requiredBool: true,
681+
});
682+
});
683+
});

0 commit comments

Comments
 (0)