Skip to content

Commit

Permalink
Add coerceInputLiteral()
Browse files Browse the repository at this point in the history
Deprecates `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.
  • Loading branch information
leebyron authored and yaacovCR committed Sep 14, 2024
1 parent 2b42a70 commit 4b21d1b
Show file tree
Hide file tree
Showing 13 changed files with 499 additions and 200 deletions.
8 changes: 6 additions & 2 deletions src/execution/getVariableSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { print } from '../language/printer.js';
import { isInputType } from '../type/definition.js';
import type { GraphQLInputType, GraphQLSchema } from '../type/index.js';

import { coerceInputLiteral } from '../utilities/coerceInputValue.js';
import { typeFromAST } from '../utilities/typeFromAST.js';
import { valueFromAST } from '../utilities/valueFromAST.js';

/**
* A GraphQLVariableSignature is required to coerce a variable value.
Expand Down Expand Up @@ -38,9 +38,13 @@ export function getVariableSignature(
);
}

const defaultValue = varDefNode.defaultValue;

return {
name: varName,
type: varType,
defaultValue: valueFromAST(varDefNode.defaultValue, varType),
defaultValue: defaultValue
? coerceInputLiteral(varDefNode.defaultValue, varType)
: undefined,
};
}
10 changes: 6 additions & 4 deletions src/execution/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import { isNonNullType } from '../type/definition.js';
import type { GraphQLDirective } from '../type/directives.js';
import type { GraphQLSchema } from '../type/schema.js';

import { coerceInputValue } from '../utilities/coerceInputValue.js';
import { valueFromAST } from '../utilities/valueFromAST.js';
import {
coerceInputLiteral,
coerceInputValue,
} from '../utilities/coerceInputValue.js';

import type { FragmentVariables } from './collectFields.js';
import type { GraphQLVariableSignature } from './getVariableSignature.js';
Expand Down Expand Up @@ -217,11 +219,11 @@ export function experimentalGetArgumentValues(
);
}

const coercedValue = valueFromAST(
const coercedValue = coerceInputLiteral(
valueNode,
argType,
variableValues,
fragmentVariables?.values,
fragmentVariables,
);
if (coercedValue === undefined) {
// Note: ValuesOfCorrectTypeRule validation should catch this before
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ export {
// Create a GraphQLType from a GraphQL language AST.
typeFromAST,
// Create a JavaScript value from a GraphQL language AST with a Type.
/** @deprecated use `coerceInputLiteral()` instead - `valueFromAST()` will be removed in v18 */
valueFromAST,
// Create a JavaScript value from a GraphQL language AST without a Type.
valueFromASTUntyped,
Expand All @@ -450,6 +451,8 @@ export {
visitWithTypeInfo,
// Coerces a JavaScript value to a GraphQL type, or produces errors.
coerceInputValue,
// Coerces a GraphQL literal (AST) to a GraphQL type, or returns undefined.
coerceInputLiteral,
// Concatenates multiple AST together.
concatAST,
// Separates an AST into an AST per Operation.
Expand Down
2 changes: 0 additions & 2 deletions src/language/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,6 @@ export function parse(
*
* This is useful within tools that operate upon GraphQL Values directly and
* in isolation of complete GraphQL documents.
*
* Consider providing the results to the utility function: valueFromAST().
*/
export function parseValue(
source: string | Source,
Expand Down
4 changes: 4 additions & 0 deletions src/type/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import type {
import { Kind } from '../language/kinds.js';
import { print } from '../language/printer.js';

import type { FragmentVariables } from '../execution/collectFields.js';

import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped.js';

import { assertEnumValueName, assertName } from './assertName.js';
Expand Down Expand Up @@ -618,6 +620,7 @@ export type GraphQLScalarValueParser<TInternal> = (
export type GraphQLScalarLiteralParser<TInternal> = (
valueNode: ValueNode,
variables?: Maybe<ObjMap<unknown>>,
fragmentVariables?: Maybe<FragmentVariables>,
) => TInternal;

export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
Expand Down Expand Up @@ -1355,6 +1358,7 @@ export class GraphQLEnumType /* <T> */ {
parseLiteral(
valueNode: ValueNode,
_variables: Maybe<ObjMap<unknown>>,
_fragmentVariables: Maybe<FragmentVariables>,
): Maybe<any> /* T */ {
// Note: variables will be resolved to a value before calling this function.
if (valueNode.kind !== Kind.ENUM) {
Expand Down
278 changes: 276 additions & 2 deletions src/utilities/__tests__/coerceInputValue-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';

import { identityFunc } from '../../jsutils/identityFunc.js';
import { invariant } from '../../jsutils/invariant.js';
import type { ObjMap } from '../../jsutils/ObjMap.js';

import { parseValue } from '../../language/parser.js';
import { print } from '../../language/printer.js';

import type { GraphQLInputType } from '../../type/definition.js';
import {
GraphQLEnumType,
Expand All @@ -9,9 +16,15 @@ import {
GraphQLNonNull,
GraphQLScalarType,
} from '../../type/definition.js';
import { GraphQLInt } from '../../type/scalars.js';
import {
GraphQLBoolean,
GraphQLFloat,
GraphQLID,
GraphQLInt,
GraphQLString,
} from '../../type/scalars.js';

import { coerceInputValue } from '../coerceInputValue.js';
import { coerceInputLiteral, coerceInputValue } from '../coerceInputValue.js';

interface CoerceResult {
value: unknown;
Expand Down Expand Up @@ -533,3 +546,264 @@ describe('coerceInputValue', () => {
});
});
});

describe('coerceInputLiteral', () => {
function test(
valueText: string,
type: GraphQLInputType,
expected: unknown,
variables?: ObjMap<unknown>,
) {
const ast = parseValue(valueText);
const value = coerceInputLiteral(ast, type, variables);
expect(value).to.deep.equal(expected);
}

function testWithVariables(
variables: ObjMap<unknown>,
valueText: string,
type: GraphQLInputType,
expected: unknown,
) {
test(valueText, type, expected, variables);
}

it('converts according to input coercion rules', () => {
test('true', GraphQLBoolean, true);
test('false', GraphQLBoolean, false);
test('123', GraphQLInt, 123);
test('123', GraphQLFloat, 123);
test('123.456', GraphQLFloat, 123.456);
test('"abc123"', GraphQLString, 'abc123');
test('123456', GraphQLID, '123456');
test('"123456"', GraphQLID, '123456');
});

it('does not convert when input coercion rules reject a value', () => {
test('123', GraphQLBoolean, undefined);
test('123.456', GraphQLInt, undefined);
test('true', GraphQLInt, undefined);
test('"123"', GraphQLInt, undefined);
test('"123"', GraphQLFloat, undefined);
test('123', GraphQLString, undefined);
test('true', GraphQLString, undefined);
test('123.456', GraphQLString, undefined);
test('123.456', GraphQLID, undefined);
});

it('convert using parseLiteral from a custom scalar type', () => {
const passthroughScalar = new GraphQLScalarType({
name: 'PassthroughScalar',
parseLiteral(node) {
invariant(node.kind === 'StringValue');
return node.value;
},
parseValue: identityFunc,
});

test('"value"', passthroughScalar, 'value');

const printScalar = new GraphQLScalarType({
name: 'PrintScalar',
parseLiteral(node) {
return `~~~${print(node)}~~~`;
},
parseValue: identityFunc,
});

test('"value"', printScalar, '~~~"value"~~~');

const throwScalar = new GraphQLScalarType({
name: 'ThrowScalar',
parseLiteral() {
throw new Error('Test');
},
parseValue: identityFunc,
});

test('value', throwScalar, undefined);

const returnUndefinedScalar = new GraphQLScalarType({
name: 'ReturnUndefinedScalar',
parseLiteral() {
return undefined;
},
parseValue: identityFunc,
});

test('value', returnUndefinedScalar, undefined);
});

it('converts enum values according to input coercion rules', () => {
const testEnum = new GraphQLEnumType({
name: 'TestColor',
values: {
RED: { value: 1 },
GREEN: { value: 2 },
BLUE: { value: 3 },
NULL: { value: null },
NAN: { value: NaN },
NO_CUSTOM_VALUE: { value: undefined },
},
});

test('RED', testEnum, 1);
test('BLUE', testEnum, 3);
test('3', testEnum, undefined);
test('"BLUE"', testEnum, undefined);
test('null', testEnum, null);
test('NULL', testEnum, null);
test('NULL', new GraphQLNonNull(testEnum), null);
test('NAN', testEnum, NaN);
test('NO_CUSTOM_VALUE', testEnum, 'NO_CUSTOM_VALUE');
});

// Boolean!
const nonNullBool = new GraphQLNonNull(GraphQLBoolean);
// [Boolean]
const listOfBool = new GraphQLList(GraphQLBoolean);
// [Boolean!]
const listOfNonNullBool = new GraphQLList(nonNullBool);
// [Boolean]!
const nonNullListOfBool = new GraphQLNonNull(listOfBool);
// [Boolean!]!
const nonNullListOfNonNullBool = new GraphQLNonNull(listOfNonNullBool);

it('coerces to null unless non-null', () => {
test('null', GraphQLBoolean, null);
test('null', nonNullBool, undefined);
});

it('coerces lists of values', () => {
test('true', listOfBool, [true]);
test('123', listOfBool, undefined);
test('null', listOfBool, null);
test('[true, false]', listOfBool, [true, false]);
test('[true, 123]', listOfBool, undefined);
test('[true, null]', listOfBool, [true, null]);
test('{ true: true }', listOfBool, undefined);
});

it('coerces non-null lists of values', () => {
test('true', nonNullListOfBool, [true]);
test('123', nonNullListOfBool, undefined);
test('null', nonNullListOfBool, undefined);
test('[true, false]', nonNullListOfBool, [true, false]);
test('[true, 123]', nonNullListOfBool, undefined);
test('[true, null]', nonNullListOfBool, [true, null]);
});

it('coerces lists of non-null values', () => {
test('true', listOfNonNullBool, [true]);
test('123', listOfNonNullBool, undefined);
test('null', listOfNonNullBool, null);
test('[true, false]', listOfNonNullBool, [true, false]);
test('[true, 123]', listOfNonNullBool, undefined);
test('[true, null]', listOfNonNullBool, undefined);
});

it('coerces non-null lists of non-null values', () => {
test('true', nonNullListOfNonNullBool, [true]);
test('123', nonNullListOfNonNullBool, undefined);
test('null', nonNullListOfNonNullBool, undefined);
test('[true, false]', nonNullListOfNonNullBool, [true, false]);
test('[true, 123]', nonNullListOfNonNullBool, undefined);
test('[true, null]', nonNullListOfNonNullBool, undefined);
});

it('uses default values for unprovided fields', () => {
const type = new GraphQLInputObjectType({
name: 'TestInput',
fields: {
int: { type: GraphQLInt, defaultValue: 42 },
},
});

test('{}', type, { int: 42 });
});

const testInputObj = new GraphQLInputObjectType({
name: 'TestInput',
fields: {
int: { type: GraphQLInt, defaultValue: 42 },
bool: { type: GraphQLBoolean },
requiredBool: { type: nonNullBool },
},
});
const testOneOfInputObj = new GraphQLInputObjectType({
name: 'TestOneOfInput',
fields: {
a: { type: GraphQLString },
b: { type: GraphQLString },
},
isOneOf: true,
});

it('coerces input objects according to input coercion rules', () => {
test('null', testInputObj, null);
test('123', testInputObj, undefined);
test('[]', testInputObj, undefined);
test('{ requiredBool: true }', testInputObj, {
int: 42,
requiredBool: true,
});
test('{ int: null, requiredBool: true }', testInputObj, {
int: null,
requiredBool: true,
});
test('{ int: 123, requiredBool: false }', testInputObj, {
int: 123,
requiredBool: false,
});
test('{ bool: true, requiredBool: false }', testInputObj, {
int: 42,
bool: true,
requiredBool: false,
});
test('{ int: true, requiredBool: true }', testInputObj, undefined);
test('{ requiredBool: null }', testInputObj, undefined);
test('{ bool: true }', testInputObj, undefined);
test('{ requiredBool: true, unknown: 123 }', testInputObj, undefined);
test('{ a: "abc" }', testOneOfInputObj, {
a: 'abc',
});
test('{ b: "def" }', testOneOfInputObj, {
b: 'def',
});
test('{ a: "abc", b: null }', testOneOfInputObj, undefined);
test('{ a: null }', testOneOfInputObj, undefined);
test('{ a: 1 }', testOneOfInputObj, undefined);
test('{ a: "abc", b: "def" }', testOneOfInputObj, undefined);
test('{}', testOneOfInputObj, undefined);
test('{ c: "abc" }', testOneOfInputObj, undefined);
});

it('accepts variable values assuming already coerced', () => {
test('$var', GraphQLBoolean, undefined);
testWithVariables({ var: true }, '$var', GraphQLBoolean, true);
testWithVariables({ var: null }, '$var', GraphQLBoolean, null);
testWithVariables({ var: null }, '$var', nonNullBool, undefined);
});

it('asserts variables are provided as items in lists', () => {
test('[ $foo ]', listOfBool, [null]);
test('[ $foo ]', listOfNonNullBool, undefined);
testWithVariables({ foo: true }, '[ $foo ]', listOfNonNullBool, [true]);
// Note: variables are expected to have already been coerced, so we
// do not expect the singleton wrapping behavior for variables.
testWithVariables({ foo: true }, '$foo', listOfNonNullBool, true);
testWithVariables({ foo: [true] }, '$foo', listOfNonNullBool, [true]);
});

it('omits input object fields for unprovided variables', () => {
test('{ int: $foo, bool: $foo, requiredBool: true }', testInputObj, {
int: 42,
requiredBool: true,
});
test('{ requiredBool: $foo }', testInputObj, undefined);
testWithVariables({ foo: true }, '{ requiredBool: $foo }', testInputObj, {
int: 42,
requiredBool: true,
});
});
});
Loading

0 comments on commit 4b21d1b

Please sign in to comment.