Skip to content

Commit 7dd6206

Browse files
committed
Add coerceInputLiteral()
Depends on #3067 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 9830fda commit 7dd6206

18 files changed

+455
-488
lines changed

src/execution/values.js

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

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

2527
type CoercedVariableValues =
2628
| {| errors: $ReadOnlyArray<GraphQLError> |}
@@ -95,7 +97,10 @@ function coerceVariableValues(
9597

9698
if (!hasOwnProperty(inputs, varName)) {
9799
if (varDefNode.defaultValue) {
98-
coercedValues[varName] = valueFromAST(varDefNode.defaultValue, varType);
100+
coercedValues[varName] = coerceInputLiteral(
101+
varDefNode.defaultValue,
102+
varType,
103+
);
99104
} else if (isNonNullType(varType)) {
100105
const varTypeStr = inspect(varType);
101106
onError(
@@ -216,7 +221,7 @@ export function getArgumentValues(
216221
);
217222
}
218223

219-
const coercedValue = valueFromAST(valueNode, argType, variableValues);
224+
const coercedValue = coerceInputLiteral(valueNode, argType, variableValues);
220225
if (coercedValue === undefined) {
221226
// Note: ValuesOfCorrectTypeRule validation should catch this before
222227
// execution. This is a runtime check to ensure execution does not

src/index.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,6 @@ export {
406406
printIntrospectionSchema,
407407
// Create a GraphQLType from a GraphQL language AST.
408408
typeFromAST,
409-
// Create a JavaScript value from a GraphQL language AST with a Type.
410-
valueFromAST,
411409
// Create a JavaScript value from a GraphQL language AST without a Type.
412410
valueFromASTUntyped,
413411
// Create a GraphQL language AST from a JavaScript value.
@@ -418,6 +416,8 @@ export {
418416
visitWithTypeInfo,
419417
// Coerces a JavaScript value to a GraphQL type, or produces errors.
420418
coerceInputValue,
419+
// Coerces a GraphQL literal (AST) to a GraphQL type, or returns undefined.
420+
coerceInputLiteral,
421421
// Concatenates multiple AST together.
422422
concatAST,
423423
// Separates an AST into an AST per Operation.

src/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -395,8 +395,6 @@ export {
395395
printIntrospectionSchema,
396396
// Create a GraphQLType from a GraphQL language AST.
397397
typeFromAST,
398-
// Create a JavaScript value from a GraphQL language AST with a Type.
399-
valueFromAST,
400398
// Create a JavaScript value from a GraphQL language AST without a Type.
401399
valueFromASTUntyped,
402400
// Create a GraphQL language AST from a JavaScript value.
@@ -407,6 +405,8 @@ export {
407405
visitWithTypeInfo,
408406
// Coerces a JavaScript value to a GraphQL type, or produces errors.
409407
coerceInputValue,
408+
// Coerces a GraphQL literal (AST) to a GraphQL type, or returns undefined.
409+
coerceInputLiteral,
410410
// Concatenates multiple AST together.
411411
concatAST,
412412
// Separates an AST into an AST per Operation.

src/jsutils/hasOwnProperty.d.ts

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

src/jsutils/hasOwnProperty.js

+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: mixed, prop: string): boolean {
5+
return Object.prototype.hasOwnProperty.call(obj, prop);
6+
}

src/language/parser.js

-2
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,6 @@ export function parse(
105105
*
106106
* This is useful within tools that operate upon GraphQL Values directly and
107107
* in isolation of complete GraphQL documents.
108-
*
109-
* Consider providing the results to the utility function: valueFromAST().
110108
*/
111109
export function parseValue(
112110
source: string | Source,

src/utilities/__tests__/coerceInputValue-test.js

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

4+
import type { ObjMap } from '../../jsutils/ObjMap';
45
import { invariant } from '../../jsutils/invariant';
6+
import { identityFunc } from '../../jsutils/identityFunc';
7+
8+
import { print } from '../../language/printer';
9+
import { parseValue } from '../../language/parser';
510

611
import type { GraphQLInputType } from '../../type/definition';
7-
import { GraphQLInt } from '../../type/scalars';
12+
import {
13+
GraphQLInt,
14+
GraphQLFloat,
15+
GraphQLString,
16+
GraphQLBoolean,
17+
GraphQLID,
18+
} from '../../type/scalars';
819
import {
920
GraphQLList,
1021
GraphQLNonNull,
@@ -13,7 +24,7 @@ import {
1324
GraphQLInputObjectType,
1425
} from '../../type/definition';
1526

16-
import { coerceInputValue } from '../coerceInputValue';
27+
import { coerceInputValue, coerceInputLiteral } from '../coerceInputValue';
1728

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

0 commit comments

Comments
 (0)