Skip to content

Commit 5e2b6d5

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 5e2b6d5

18 files changed

+449
-487
lines changed

src/execution/values.js

Lines changed: 9 additions & 4 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 0 additions & 2 deletions
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

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

0 commit comments

Comments
 (0)