Skip to content

Commit 15223bc

Browse files
committed
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 4e4d16e commit 15223bc

12 files changed

+428
-451
lines changed

src/execution/values.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import type { GraphQLDirective } from '../type/directives';
2020
import { isInputType, isNonNullType } from '../type/definition';
2121

2222
import { typeFromAST } from '../utilities/typeFromAST';
23-
import { valueFromAST } from '../utilities/valueFromAST';
24-
import { coerceInputValue } from '../utilities/coerceInputValue';
23+
import {
24+
coerceInputValue,
25+
coerceInputLiteral,
26+
} from '../utilities/coerceInputValue';
2527

2628
type CoercedVariableValues =
2729
| { errors: ReadonlyArray<GraphQLError>; coerced?: never }
@@ -96,7 +98,10 @@ function coerceVariableValues(
9698

9799
if (!hasOwnProperty(inputs, varName)) {
98100
if (varDefNode.defaultValue) {
99-
coercedValues[varName] = valueFromAST(varDefNode.defaultValue, varType);
101+
coercedValues[varName] = coerceInputLiteral(
102+
varDefNode.defaultValue,
103+
varType,
104+
);
100105
} else if (isNonNullType(varType)) {
101106
onError(
102107
new GraphQLError(
@@ -213,7 +218,7 @@ export function getArgumentValues(
213218
);
214219
}
215220

216-
const coercedValue = valueFromAST(valueNode, argType, variableValues);
221+
const coercedValue = coerceInputLiteral(valueNode, argType, variableValues);
217222
if (coercedValue === undefined) {
218223
// Note: ValuesOfCorrectTypeRule validation should catch this before
219224
// execution. This is a runtime check to ensure execution does not

src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -408,8 +408,6 @@ export {
408408
printIntrospectionSchema,
409409
/** Create a GraphQLType from a GraphQL language AST. */
410410
typeFromAST,
411-
/** Create a JavaScript value from a GraphQL language AST with a Type. */
412-
valueFromAST,
413411
/** Create a JavaScript value from a GraphQL language AST without a Type. */
414412
valueFromASTUntyped,
415413
/** Create a GraphQL language AST from a JavaScript value. */
@@ -419,6 +417,8 @@ export {
419417
visitWithTypeInfo,
420418
/** Coerces a JavaScript value to a GraphQL type, or produces errors. */
421419
coerceInputValue,
420+
/** Coerces a GraphQL literal (AST) to a GraphQL type, or returns undefined. */
421+
coerceInputLiteral,
422422
/** Concatenates multiple AST together. */
423423
concatAST,
424424
/** Separates an AST into an AST per Operation. */

src/jsutils/hasOwnProperty.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* Determines if a provided object has a given property name.
3+
*/
4+
export function hasOwnProperty(obj: {}, prop: string): boolean {
5+
return Object.prototype.hasOwnProperty.call(obj, prop);
6+
}

src/language/parser.ts

-2
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,6 @@ export function parse(
118118
*
119119
* This is useful within tools that operate upon GraphQL Values directly and
120120
* in isolation of complete GraphQL documents.
121-
*
122-
* Consider providing the results to the utility function: valueFromAST().
123121
*/
124122
export function parseValue(
125123
source: string | Source,

src/utilities/__tests__/coerceInputValue-test.ts

+256-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
import { expect } from 'chai';
22
import { describe, it } from 'mocha';
33

4+
import type { ObjMap } from '../../jsutils/ObjMap';
5+
import { invariant } from '../../jsutils/invariant';
6+
import { identityFunc } from '../../jsutils/identityFunc';
7+
8+
import { print } from '../../language/printer';
9+
import { parseValue } from '../../language/parser';
10+
411
import type { GraphQLInputType } from '../../type/definition';
5-
import { GraphQLInt } from '../../type/scalars';
12+
import {
13+
GraphQLInt,
14+
GraphQLFloat,
15+
GraphQLString,
16+
GraphQLBoolean,
17+
GraphQLID,
18+
} from '../../type/scalars';
619
import {
720
GraphQLList,
821
GraphQLNonNull,
@@ -11,7 +24,7 @@ import {
1124
GraphQLInputObjectType,
1225
} from '../../type/definition';
1326

14-
import { coerceInputValue } from '../coerceInputValue';
27+
import { coerceInputValue, coerceInputLiteral } from '../coerceInputValue';
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)