Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add valueToLiteral() #3065

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,10 @@ export {
/** A helper to use within recursive-descent visitors which need to be aware of the GraphQL type system. */
TypeInfo,
visitWithTypeInfo,
/** Converts a value to a const value by replacing variables. */
replaceVariables,
/** Create a GraphQL literal (AST) from a JavaScript input value. */
valueToLiteral,
/** Coerces a JavaScript value to a GraphQL type, or produces errors. */
coerceInputValue,
/** Coerces a GraphQL literal (AST) to a GraphQL type, or returns undefined. */
Expand Down
20 changes: 14 additions & 6 deletions src/type/__tests__/definition-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, it } from 'mocha';
import { inspect } from '../../jsutils/inspect';
import { identityFunc } from '../../jsutils/identityFunc';

import { parseValue } from '../../language/parser';
import { parseConstValue } from '../../language/parser';

import type { GraphQLType, GraphQLNullableType } from '../definition';
import {
Expand Down Expand Up @@ -83,15 +83,12 @@ describe('Type System: Scalars', () => {
},
});

expect(scalar.parseLiteral(parseValue('null'))).to.equal(
expect(scalar.parseLiteral(parseConstValue('null'))).to.equal(
'parseValue: null',
);
expect(scalar.parseLiteral(parseValue('{ foo: "bar" }'))).to.equal(
expect(scalar.parseLiteral(parseConstValue('{ foo: "bar" }'))).to.equal(
'parseValue: { foo: "bar" }',
);
expect(
scalar.parseLiteral(parseValue('{ foo: { bar: $var } }'), { var: 'baz' }),
).to.equal('parseValue: { foo: { bar: "baz" } }');
});

it('rejects a Scalar type without name', () => {
Expand Down Expand Up @@ -139,6 +136,17 @@ describe('Type System: Scalars', () => {
);
});

it('rejects a Scalar type defining valueToLiteral with an incorrect type', () => {
expect(
() =>
new GraphQLScalarType({
name: 'SomeScalar',
// @ts-expect-error
valueToLiteral: {},
}),
).to.throw('SomeScalar must provide "valueToLiteral" as a function.');
});

it('rejects a Scalar type defining specifiedByURL with an incorrect type', () => {
expect(
() =>
Expand Down
27 changes: 6 additions & 21 deletions src/type/__tests__/scalars-test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';

import { parseValue as parseValueToAST } from '../../language/parser';
import { parseConstValue } from '../../language/parser';

import {
GraphQLID,
Expand Down Expand Up @@ -66,7 +66,7 @@ describe('Type System: Specified scalar types', () => {

it('parseLiteral', () => {
function parseLiteral(str: string) {
return GraphQLInt.parseLiteral(parseValueToAST(str), undefined);
return GraphQLInt.parseLiteral(parseConstValue(str));
}

expect(parseLiteral('1')).to.equal(1);
Expand Down Expand Up @@ -104,9 +104,6 @@ describe('Type System: Specified scalar types', () => {
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
'Int cannot represent non-integer value: ENUM_VALUE',
);
expect(() => parseLiteral('$var')).to.throw(
'Int cannot represent non-integer value: $var',
);
});

it('serialize', () => {
Expand Down Expand Up @@ -231,7 +228,7 @@ describe('Type System: Specified scalar types', () => {

it('parseLiteral', () => {
function parseLiteral(str: string) {
return GraphQLFloat.parseLiteral(parseValueToAST(str), undefined);
return GraphQLFloat.parseLiteral(parseConstValue(str));
}

expect(parseLiteral('1')).to.equal(1);
Expand Down Expand Up @@ -264,9 +261,6 @@ describe('Type System: Specified scalar types', () => {
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
'Float cannot represent non numeric value: ENUM_VALUE',
);
expect(() => parseLiteral('$var')).to.throw(
'Float cannot represent non numeric value: $var',
);
});

it('serialize', () => {
Expand Down Expand Up @@ -344,7 +338,7 @@ describe('Type System: Specified scalar types', () => {

it('parseLiteral', () => {
function parseLiteral(str: string) {
return GraphQLString.parseLiteral(parseValueToAST(str), undefined);
return GraphQLString.parseLiteral(parseConstValue(str));
}

expect(parseLiteral('"foo"')).to.equal('foo');
Expand All @@ -371,9 +365,6 @@ describe('Type System: Specified scalar types', () => {
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
'String cannot represent a non string value: ENUM_VALUE',
);
expect(() => parseLiteral('$var')).to.throw(
'String cannot represent a non string value: $var',
);
});

it('serialize', () => {
Expand Down Expand Up @@ -456,7 +447,7 @@ describe('Type System: Specified scalar types', () => {

it('parseLiteral', () => {
function parseLiteral(str: string) {
return GraphQLBoolean.parseLiteral(parseValueToAST(str), undefined);
return GraphQLBoolean.parseLiteral(parseConstValue(str));
}

expect(parseLiteral('true')).to.equal(true);
Expand Down Expand Up @@ -489,9 +480,6 @@ describe('Type System: Specified scalar types', () => {
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
'Boolean cannot represent a non boolean value: ENUM_VALUE',
);
expect(() => parseLiteral('$var')).to.throw(
'Boolean cannot represent a non boolean value: $var',
);
});

it('serialize', () => {
Expand Down Expand Up @@ -571,7 +559,7 @@ describe('Type System: Specified scalar types', () => {

it('parseLiteral', () => {
function parseLiteral(str: string) {
return GraphQLID.parseLiteral(parseValueToAST(str), undefined);
return GraphQLID.parseLiteral(parseConstValue(str));
}

expect(parseLiteral('""')).to.equal('');
Expand Down Expand Up @@ -604,9 +592,6 @@ describe('Type System: Specified scalar types', () => {
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
'ID cannot represent a non-string and non-integer value: ENUM_VALUE',
);
expect(() => parseLiteral('$var')).to.throw(
'ID cannot represent a non-string and non-integer value: $var',
);
});

it('serialize', () => {
Expand Down
64 changes: 55 additions & 9 deletions src/type/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { Kind } from '../language/kinds';
import { print } from '../language/printer';
import type {
FieldNode,
ValueNode,
ConstValueNode,
OperationDefinitionNode,
FragmentDefinitionNode,
Expand Down Expand Up @@ -590,9 +589,39 @@ export interface GraphQLScalarTypeExtensions {
* if (value % 2 === 1) {
* return value;
* }
* },
* parseValue(value) {
* if (value % 2 === 1) {
* return value;
* }
* }
* valueToLiteral(value) {
* if (value % 2 === 1) {
* return parse(`${value}`);
* }
* }
* });
*
* Custom scalars behavior is defined via the following functions:
*
* - serialize(value): Implements "Result Coercion". Given an internal value,
* produces an external value valid for this type. Returns undefined or
* throws an error to indicate invalid values.
*
* - parseValue(value): Implements "Input Coercion" for values. Given an
* external value (for example, variable values), produces an internal value
* valid for this type. Returns undefined or throws an error to indicate
* invalid values.
*
* - parseLiteral(ast): Implements "Input Coercion" for literals. Given an
* GraphQL literal (AST) (for example, an argument value), produces an
* internal value valid for this type. Returns undefined or throws an error
* to indicate invalid values.
*
* - valueToLiteral(value): Converts an external value to a GraphQL
* literal (AST). Returns undefined or throws an error to indicate
* invalid values.
*
*/
export class GraphQLScalarType extends GraphQLSchemaElement {
name: string;
Expand All @@ -601,6 +630,7 @@ export class GraphQLScalarType extends GraphQLSchemaElement {
serialize: GraphQLScalarSerializer<unknown>;
parseValue: GraphQLScalarValueParser<unknown>;
parseLiteral: GraphQLScalarLiteralParser<unknown>;
valueToLiteral: Maybe<GraphQLScalarValueToLiteral>;
extensions: Maybe<Readonly<GraphQLScalarTypeExtensions>>;
astNode: Maybe<ScalarTypeDefinitionNode>;
extensionASTNodes: ReadonlyArray<ScalarTypeExtensionNode>;
Expand All @@ -614,8 +644,8 @@ export class GraphQLScalarType extends GraphQLSchemaElement {
this.serialize = config.serialize ?? identityFunc;
this.parseValue = parseValue;
this.parseLiteral =
config.parseLiteral ??
((node, variables) => parseValue(valueFromASTUntyped(node, variables)));
config.parseLiteral ?? ((node) => parseValue(valueFromASTUntyped(node)));
this.valueToLiteral = config.valueToLiteral;
this.extensions = config.extensions && toObjMap(config.extensions);
this.astNode = config.astNode;
this.extensionASTNodes = config.extensionASTNodes ?? [];
Expand All @@ -641,6 +671,13 @@ export class GraphQLScalarType extends GraphQLSchemaElement {
`${this.name} must provide both "parseValue" and "parseLiteral" functions.`,
);
}

if (config.valueToLiteral) {
devAssert(
typeof config.valueToLiteral === 'function',
`${this.name} must provide "valueToLiteral" as a function.`,
);
}
}

toConfig(): GraphQLScalarTypeNormalizedConfig {
Expand All @@ -651,6 +688,7 @@ export class GraphQLScalarType extends GraphQLSchemaElement {
serialize: this.serialize,
parseValue: this.parseValue,
parseLiteral: this.parseLiteral,
valueToLiteral: this.valueToLiteral,
extensions: this.extensions,
astNode: this.astNode,
extensionASTNodes: this.extensionASTNodes,
Expand All @@ -671,10 +709,13 @@ export type GraphQLScalarValueParser<TInternal> = (
) => Maybe<TInternal>;

export type GraphQLScalarLiteralParser<TInternal> = (
valueNode: ValueNode,
variables?: Maybe<ObjMap<unknown>>,
valueNode: ConstValueNode,
) => Maybe<TInternal>;

export type GraphQLScalarValueToLiteral = (
inputValue: unknown,
) => ConstValueNode | undefined;

export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
name: string;
description?: Maybe<string>;
Expand All @@ -685,6 +726,8 @@ export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
parseValue?: GraphQLScalarValueParser<TInternal>;
/** Parses an externally provided literal value to use as an input. */
parseLiteral?: GraphQLScalarLiteralParser<TInternal>;
/** Translates an externally provided value to a literal (AST). */
valueToLiteral?: Maybe<GraphQLScalarValueToLiteral>;
extensions?: Maybe<Readonly<GraphQLScalarTypeExtensions>>;
astNode?: Maybe<ScalarTypeDefinitionNode>;
extensionASTNodes?: Maybe<ReadonlyArray<ScalarTypeExtensionNode>>;
Expand Down Expand Up @@ -1457,10 +1500,7 @@ export class GraphQLEnumType /* <T> */ extends GraphQLSchemaElement {
return enumValue.value;
}

parseLiteral(
valueNode: ValueNode,
_variables: Maybe<ObjMap<unknown>>,
): Maybe<any> /* T */ {
parseLiteral(valueNode: ConstValueNode): Maybe<any> /* T */ {
// Note: variables will be resolved to a value before calling this function.
if (valueNode.kind !== Kind.ENUM) {
const valueStr = print(valueNode);
Expand All @@ -1483,6 +1523,12 @@ export class GraphQLEnumType /* <T> */ extends GraphQLSchemaElement {
return enumValue.value;
}

valueToLiteral(value: unknown): ConstValueNode | undefined {
if (typeof value === 'string' && this.getValue(value)) {
return { kind: Kind.ENUM, value };
}
}

toConfig(): GraphQLEnumTypeNormalizedConfig {
return {
name: this.name,
Expand Down
40 changes: 40 additions & 0 deletions src/type/scalars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { print } from '../language/printer';

import { GraphQLError } from '../error/GraphQLError';

import { defaultScalarValueToLiteral } from '../utilities/valueToLiteral';

import type { GraphQLNamedType } from './definition';
import { GraphQLScalarType } from './definition';

Expand Down Expand Up @@ -79,6 +81,16 @@ export const GraphQLInt: GraphQLScalarType = new GraphQLScalarType({
}
return num;
},
valueToLiteral(value) {
if (
typeof value === 'number' &&
Number.isInteger(value) &&
value <= MAX_INT &&
value >= MIN_INT
) {
return { kind: Kind.INT, value: String(value) };
}
},
});

function serializeFloat(outputValue: unknown): number {
Expand Down Expand Up @@ -125,6 +137,12 @@ export const GraphQLFloat: GraphQLScalarType = new GraphQLScalarType({
}
return parseFloat(valueNode.value);
},
valueToLiteral(value) {
const literal = defaultScalarValueToLiteral(value);
if (literal.kind === Kind.FLOAT || literal.kind === Kind.INT) {
return literal;
}
},
});

// Support serializing objects with custom valueOf() or toJSON() functions -
Expand Down Expand Up @@ -188,6 +206,12 @@ export const GraphQLString: GraphQLScalarType = new GraphQLScalarType({
}
return valueNode.value;
},
valueToLiteral(value) {
const literal = defaultScalarValueToLiteral(value);
if (literal.kind === Kind.STRING) {
return literal;
}
},
});

function serializeBoolean(outputValue: unknown): boolean {
Expand Down Expand Up @@ -227,6 +251,12 @@ export const GraphQLBoolean: GraphQLScalarType = new GraphQLScalarType({
}
return valueNode.value;
},
valueToLiteral(value) {
const literal = defaultScalarValueToLiteral(value);
if (literal.kind === Kind.BOOLEAN) {
return literal;
}
},
});

function serializeID(outputValue: unknown): string {
Expand Down Expand Up @@ -267,6 +297,16 @@ export const GraphQLID: GraphQLScalarType = new GraphQLScalarType({
}
return valueNode.value;
},
valueToLiteral(value) {
// ID types can use number values and Int literals.
const stringValue = Number.isInteger(value) ? String(value) : value;
if (typeof stringValue === 'string') {
// Will parse as an IntValue.
return /^-?(?:0|[1-9][0-9]*)$/.test(stringValue)
? { kind: Kind.INT, value: stringValue }
: { kind: Kind.STRING, value: stringValue, block: false };
}
},
});

export const specifiedScalarTypes: ReadonlyArray<GraphQLScalarType> =
Expand Down
7 changes: 7 additions & 0 deletions src/utilities/__tests__/coerceInputValue-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,13 @@ describe('coerceInputLiteral', () => {
});

test('"value"', printScalar, '~~~"value"~~~');
testWithVariables(
'($var: String)',
{ var: 'value' },
'{ field: $var }',
printScalar,
'~~~{field: "value"}~~~',
);

const throwScalar = new GraphQLScalarType({
name: 'ThrowScalar',
Expand Down
Loading