Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f03f2c5

Browse files
leebyronyaacovCR
authored andcommittedJan 1, 2023
Add valueToLiteral()
* Adds `valueToLiteral()` which takes an external value and translates it to a literal, allowing for custom scalars to define this behavior. This also adds important changes to Input Coercion, especially for custom scalars: * The value provided to `parseLiteral` is now `ConstValueNode` and the second `variables` argument has been removed. For all built-in scalars this has no effect, but any custom scalars which use complex literals no longer need to do variable reconciliation manually (in fact most do not -- this has been an easy subtle bug to miss). This behavior is possible with the addition of `replaceASTVariables`
1 parent 9e6cf99 commit f03f2c5

13 files changed

+651
-50
lines changed
 

‎src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,10 @@ export {
445445
// A helper to use within recursive-descent visitors which need to be aware of the GraphQL type system.
446446
TypeInfo,
447447
visitWithTypeInfo,
448+
// Converts a value to a const value by replacing variables.
449+
replaceVariables,
450+
// Create a GraphQL literal (AST) from a JavaScript input value.
451+
valueToLiteral,
448452
// Coerces a JavaScript value to a GraphQL type, or produces errors.
449453
coerceInputValue,
450454
// Coerces a GraphQL literal (AST) to a GraphQL type, or returns undefined.

‎src/type/__tests__/definition-test.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { identityFunc } from '../../jsutils/identityFunc.js';
55
import { inspect } from '../../jsutils/inspect.js';
66

77
import { Kind } from '../../language/kinds.js';
8-
import { parseValue } from '../../language/parser.js';
8+
import { parseConstValue } from '../../language/parser.js';
99

1010
import type { GraphQLNullableType, GraphQLType } from '../definition.js';
1111
import {
@@ -82,15 +82,12 @@ describe('Type System: Scalars', () => {
8282
},
8383
});
8484

85-
expect(scalar.parseLiteral(parseValue('null'))).to.equal(
85+
expect(scalar.parseLiteral(parseConstValue('null'))).to.equal(
8686
'parseValue: null',
8787
);
88-
expect(scalar.parseLiteral(parseValue('{ foo: "bar" }'))).to.equal(
88+
expect(scalar.parseLiteral(parseConstValue('{ foo: "bar" }'))).to.equal(
8989
'parseValue: { foo: "bar" }',
9090
);
91-
expect(
92-
scalar.parseLiteral(parseValue('{ foo: { bar: $var } }'), { var: 'baz' }),
93-
).to.equal('parseValue: { foo: { bar: "baz" } }');
9491
});
9592

9693
it('rejects a Scalar type defining parseLiteral but not parseValue', () => {

‎src/type/__tests__/scalars-test.ts

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

4-
import { parseValue as parseValueToAST } from '../../language/parser.js';
4+
import { parseConstValue } from '../../language/parser.js';
55

66
import {
77
GraphQLBoolean,
@@ -66,7 +66,7 @@ describe('Type System: Specified scalar types', () => {
6666

6767
it('parseLiteral', () => {
6868
function parseLiteral(str: string) {
69-
return GraphQLInt.parseLiteral(parseValueToAST(str), undefined);
69+
return GraphQLInt.parseLiteral(parseConstValue(str));
7070
}
7171

7272
expect(parseLiteral('1')).to.equal(1);
@@ -104,9 +104,6 @@ describe('Type System: Specified scalar types', () => {
104104
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
105105
'Int cannot represent non-integer value: ENUM_VALUE',
106106
);
107-
expect(() => parseLiteral('$var')).to.throw(
108-
'Int cannot represent non-integer value: $var',
109-
);
110107
});
111108

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

232229
it('parseLiteral', () => {
233230
function parseLiteral(str: string) {
234-
return GraphQLFloat.parseLiteral(parseValueToAST(str), undefined);
231+
return GraphQLFloat.parseLiteral(parseConstValue(str));
235232
}
236233

237234
expect(parseLiteral('1')).to.equal(1);
@@ -264,9 +261,6 @@ describe('Type System: Specified scalar types', () => {
264261
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
265262
'Float cannot represent non numeric value: ENUM_VALUE',
266263
);
267-
expect(() => parseLiteral('$var')).to.throw(
268-
'Float cannot represent non numeric value: $var',
269-
);
270264
});
271265

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

345339
it('parseLiteral', () => {
346340
function parseLiteral(str: string) {
347-
return GraphQLString.parseLiteral(parseValueToAST(str), undefined);
341+
return GraphQLString.parseLiteral(parseConstValue(str));
348342
}
349343

350344
expect(parseLiteral('"foo"')).to.equal('foo');
@@ -371,9 +365,6 @@ describe('Type System: Specified scalar types', () => {
371365
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
372366
'String cannot represent a non string value: ENUM_VALUE',
373367
);
374-
expect(() => parseLiteral('$var')).to.throw(
375-
'String cannot represent a non string value: $var',
376-
);
377368
});
378369

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

457448
it('parseLiteral', () => {
458449
function parseLiteral(str: string) {
459-
return GraphQLBoolean.parseLiteral(parseValueToAST(str), undefined);
450+
return GraphQLBoolean.parseLiteral(parseConstValue(str));
460451
}
461452

462453
expect(parseLiteral('true')).to.equal(true);
@@ -489,9 +480,6 @@ describe('Type System: Specified scalar types', () => {
489480
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
490481
'Boolean cannot represent a non boolean value: ENUM_VALUE',
491482
);
492-
expect(() => parseLiteral('$var')).to.throw(
493-
'Boolean cannot represent a non boolean value: $var',
494-
);
495483
});
496484

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

572560
it('parseLiteral', () => {
573561
function parseLiteral(str: string) {
574-
return GraphQLID.parseLiteral(parseValueToAST(str), undefined);
562+
return GraphQLID.parseLiteral(parseConstValue(str));
575563
}
576564

577565
expect(parseLiteral('""')).to.equal('');
@@ -604,9 +592,6 @@ describe('Type System: Specified scalar types', () => {
604592
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
605593
'ID cannot represent a non-string and non-integer value: ENUM_VALUE',
606594
);
607-
expect(() => parseLiteral('$var')).to.throw(
608-
'ID cannot represent a non-string and non-integer value: $var',
609-
);
610595
});
611596

612597
it('serialize', () => {

‎src/type/definition.ts

+59-20
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import type {
3535
ScalarTypeExtensionNode,
3636
UnionTypeDefinitionNode,
3737
UnionTypeExtensionNode,
38-
ValueNode,
3938
} from '../language/ast.js';
4039
import { Kind } from '../language/kinds.js';
4140
import { print } from '../language/printer.js';
@@ -551,22 +550,52 @@ export interface GraphQLScalarTypeExtensions {
551550
* Example:
552551
*
553552
* ```ts
553+
* function ensureOdd(value) {
554+
* if (!Number.isFinite(value)) {
555+
* throw new Error(
556+
* `Scalar "Odd" cannot represent "${value}" since it is not a finite number.`,
557+
* );
558+
* }
559+
*
560+
* if (value % 2 === 0) {
561+
* throw new Error(`Scalar "Odd" cannot represent "${value}" since it is even.`);
562+
* }
563+
* }
564+
*
554565
* const OddType = new GraphQLScalarType({
555566
* name: 'Odd',
556567
* serialize(value) {
557-
* if (!Number.isFinite(value)) {
558-
* throw new Error(
559-
* `Scalar "Odd" cannot represent "${value}" since it is not a finite number.`,
560-
* );
561-
* }
562-
*
563-
* if (value % 2 === 0) {
564-
* throw new Error(`Scalar "Odd" cannot represent "${value}" since it is even.`);
565-
* }
566-
* return value;
568+
* return ensureOdd(value);
569+
* },
570+
* parseValue(value) {
571+
* return ensureOdd(value);
572+
* }
573+
* valueToLiteral(value) {
574+
* return parse(`${ensureOdd(value)`);
567575
* }
568576
* });
569577
* ```
578+
*
579+
* Custom scalars behavior is defined via the following functions:
580+
*
581+
* - serialize(value): Implements "Result Coercion". Given an internal value,
582+
* produces an external value valid for this type. Returns undefined or
583+
* throws an error to indicate invalid values.
584+
*
585+
* - parseValue(value): Implements "Input Coercion" for values. Given an
586+
* external value (for example, variable values), produces an internal value
587+
* valid for this type. Returns undefined or throws an error to indicate
588+
* invalid values.
589+
*
590+
* - parseLiteral(ast): Implements "Input Coercion" for literals. Given an
591+
* GraphQL literal (AST) (for example, an argument value), produces an
592+
* internal value valid for this type. Returns undefined or throws an error
593+
* to indicate invalid values.
594+
*
595+
* - valueToLiteral(value): Converts an external value to a GraphQL
596+
* literal (AST). Returns undefined or throws an error to indicate
597+
* invalid values.
598+
*
570599
*/
571600
export class GraphQLScalarType<
572601
TInternal = unknown,
@@ -578,6 +607,7 @@ export class GraphQLScalarType<
578607
serialize: GraphQLScalarSerializer<TExternal>;
579608
parseValue: GraphQLScalarValueParser<TInternal>;
580609
parseLiteral: GraphQLScalarLiteralParser<TInternal>;
610+
valueToLiteral: GraphQLScalarValueToLiteral | undefined;
581611
extensions: Readonly<GraphQLScalarTypeExtensions>;
582612
astNode: Maybe<ScalarTypeDefinitionNode>;
583613
extensionASTNodes: ReadonlyArray<ScalarTypeExtensionNode>;
@@ -595,8 +625,8 @@ export class GraphQLScalarType<
595625
config.serialize ?? (identityFunc as GraphQLScalarSerializer<TExternal>);
596626
this.parseValue = parseValue;
597627
this.parseLiteral =
598-
config.parseLiteral ??
599-
((node, variables) => parseValue(valueFromASTUntyped(node, variables)));
628+
config.parseLiteral ?? ((node) => parseValue(valueFromASTUntyped(node)));
629+
this.valueToLiteral = config.valueToLiteral;
600630
this.extensions = toObjMap(config.extensions);
601631
this.astNode = config.astNode;
602632
this.extensionASTNodes = config.extensionASTNodes ?? [];
@@ -622,6 +652,7 @@ export class GraphQLScalarType<
622652
serialize: this.serialize,
623653
parseValue: this.parseValue,
624654
parseLiteral: this.parseLiteral,
655+
valueToLiteral: this.valueToLiteral,
625656
extensions: this.extensions,
626657
astNode: this.astNode,
627658
extensionASTNodes: this.extensionASTNodes,
@@ -638,9 +669,12 @@ export type GraphQLScalarValueParser<TInternal> = (
638669
) => TInternal;
639670

640671
export type GraphQLScalarLiteralParser<TInternal> = (
641-
valueNode: ValueNode,
642-
variables?: Maybe<ObjMap<unknown>>,
643-
) => TInternal;
672+
valueNode: ConstValueNode,
673+
) => Maybe<TInternal>;
674+
675+
export type GraphQLScalarValueToLiteral = (
676+
inputValue: unknown,
677+
) => ConstValueNode | undefined;
644678

645679
export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
646680
name: string;
@@ -652,6 +686,8 @@ export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
652686
parseValue?: GraphQLScalarValueParser<TInternal> | undefined;
653687
/** Parses an externally provided literal value to use as an input. */
654688
parseLiteral?: GraphQLScalarLiteralParser<TInternal> | undefined;
689+
/** Translates an externally provided value to a literal (AST). */
690+
valueToLiteral?: GraphQLScalarValueToLiteral | undefined;
655691
extensions?: Maybe<Readonly<GraphQLScalarTypeExtensions>>;
656692
astNode?: Maybe<ScalarTypeDefinitionNode>;
657693
extensionASTNodes?: Maybe<ReadonlyArray<ScalarTypeExtensionNode>>;
@@ -1381,10 +1417,7 @@ export class GraphQLEnumType /* <T> */ extends GraphQLSchemaElement {
13811417
return enumValue.value;
13821418
}
13831419

1384-
parseLiteral(
1385-
valueNode: ValueNode,
1386-
_variables: Maybe<ObjMap<unknown>>,
1387-
): Maybe<any> /* T */ {
1420+
parseLiteral(valueNode: ConstValueNode): Maybe<any> /* T */ {
13881421
// Note: variables will be resolved to a value before calling this function.
13891422
if (valueNode.kind !== Kind.ENUM) {
13901423
const valueStr = print(valueNode);
@@ -1407,6 +1440,12 @@ export class GraphQLEnumType /* <T> */ extends GraphQLSchemaElement {
14071440
return enumValue.value;
14081441
}
14091442

1443+
valueToLiteral(value: unknown): ConstValueNode | undefined {
1444+
if (typeof value === 'string' && this.getValue(value)) {
1445+
return { kind: Kind.ENUM, value };
1446+
}
1447+
}
1448+
14101449
toConfig(): GraphQLEnumTypeNormalizedConfig {
14111450
return {
14121451
name: this.name,

‎src/type/scalars.ts

+40
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { GraphQLError } from '../error/GraphQLError.js';
66
import { Kind } from '../language/kinds.js';
77
import { print } from '../language/printer.js';
88

9+
import { defaultScalarValueToLiteral } from '../utilities/valueToLiteral.js';
10+
911
import type { GraphQLNamedType } from './definition.js';
1012
import { GraphQLScalarType } from './definition.js';
1113

@@ -82,6 +84,16 @@ export const GraphQLInt = new GraphQLScalarType<number>({
8284
}
8385
return num;
8486
},
87+
valueToLiteral(value) {
88+
if (
89+
typeof value === 'number' &&
90+
Number.isInteger(value) &&
91+
value <= GRAPHQL_MAX_INT &&
92+
value >= GRAPHQL_MIN_INT
93+
) {
94+
return { kind: Kind.INT, value: String(value) };
95+
}
96+
},
8597
});
8698

8799
export const GraphQLFloat = new GraphQLScalarType<number>({
@@ -127,6 +139,12 @@ export const GraphQLFloat = new GraphQLScalarType<number>({
127139
}
128140
return parseFloat(valueNode.value);
129141
},
142+
valueToLiteral(value) {
143+
const literal = defaultScalarValueToLiteral(value);
144+
if (literal.kind === Kind.FLOAT || literal.kind === Kind.INT) {
145+
return literal;
146+
}
147+
},
130148
});
131149

132150
export const GraphQLString = new GraphQLScalarType<string>({
@@ -171,6 +189,12 @@ export const GraphQLString = new GraphQLScalarType<string>({
171189
}
172190
return valueNode.value;
173191
},
192+
valueToLiteral(value) {
193+
const literal = defaultScalarValueToLiteral(value);
194+
if (literal.kind === Kind.STRING) {
195+
return literal;
196+
}
197+
},
174198
});
175199

176200
export const GraphQLBoolean = new GraphQLScalarType<boolean>({
@@ -209,6 +233,12 @@ export const GraphQLBoolean = new GraphQLScalarType<boolean>({
209233
}
210234
return valueNode.value;
211235
},
236+
valueToLiteral(value) {
237+
const literal = defaultScalarValueToLiteral(value);
238+
if (literal.kind === Kind.BOOLEAN) {
239+
return literal;
240+
}
241+
},
212242
});
213243

214244
export const GraphQLID = new GraphQLScalarType<string>({
@@ -250,6 +280,16 @@ export const GraphQLID = new GraphQLScalarType<string>({
250280
}
251281
return valueNode.value;
252282
},
283+
valueToLiteral(value) {
284+
// ID types can use number values and Int literals.
285+
const stringValue = Number.isInteger(value) ? String(value) : value;
286+
if (typeof stringValue === 'string') {
287+
// Will parse as an IntValue.
288+
return /^-?(?:0|[1-9][0-9]*)$/.test(stringValue)
289+
? { kind: Kind.INT, value: stringValue }
290+
: { kind: Kind.STRING, value: stringValue, block: false };
291+
}
292+
},
253293
});
254294

255295
export const specifiedScalarTypes: ReadonlyArray<GraphQLScalarType> =

‎src/utilities/__tests__/coerceInputValue-test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,13 @@ describe('coerceInputLiteral', () => {
525525
});
526526

527527
test('"value"', printScalar, '~~~"value"~~~');
528+
testWithVariables(
529+
'($var: String)',
530+
{ var: 'value' },
531+
'{ field: $var }',
532+
printScalar,
533+
'~~~{ field: "value" }~~~',
534+
);
528535

529536
const throwScalar = new GraphQLScalarType({
530537
name: 'ThrowScalar',

0 commit comments

Comments
 (0)
Please sign in to comment.