Skip to content

Commit 4171fc6

Browse files
committed
Add valueToLiteral() and literalToValue()
* Adds `valueToLiteral()` which takes an external value and translates it to a literal, allowing for custom scalars to define this behavior. * Adds `literalToValue()` which does the same in reverse, also allowing custom scalars to define it. * Deprecates `valueFromASTUntyped()` in favor of `literalToValue()`, replacing all use of it in the codebase and adding a printed warning (via a new `deprecationWarning` method). 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` * The `parseLiteral` function is no longer filled with a default implementation. Callsites need to check for it before calling it. This untangles what would otherwise be a circular dependency. Both callsites are updated.
1 parent 0a91e63 commit 4171fc6

26 files changed

+951
-69
lines changed

src/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,13 +413,20 @@ export {
413413
// Create a JavaScript value from a GraphQL language AST with a Type.
414414
valueFromAST,
415415
// Create a JavaScript value from a GraphQL language AST without a Type.
416+
// DEPRECATED: use literalToValue
416417
valueFromASTUntyped,
417418
// Create a GraphQL language AST from a JavaScript value.
418419
astFromValue,
419420
// A helper to use within recursive-descent visitors which need to be aware of
420421
// the GraphQL type system.
421422
TypeInfo,
422423
visitWithTypeInfo,
424+
// Converts a value to a const value by replacing variables.
425+
replaceASTVariables,
426+
// Create a GraphQL Literal AST from a JavaScript input value.
427+
valueToLiteral,
428+
// Create a JavaScript input value from a GraphQL Literal AST.
429+
literalToValue,
423430
// Coerces a JavaScript value to a GraphQL type, or produces errors.
424431
coerceInputValue,
425432
// Concatenates multiple AST together.

src/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,13 +402,20 @@ export {
402402
// Create a JavaScript value from a GraphQL language AST with a Type.
403403
valueFromAST,
404404
// Create a JavaScript value from a GraphQL language AST without a Type.
405+
// DEPRECATED: use literalToValue
405406
valueFromASTUntyped,
406407
// Create a GraphQL language AST from a JavaScript value.
407408
astFromValue,
408409
// A helper to use within recursive-descent visitors which need to be aware of
409410
// the GraphQL type system.
410411
TypeInfo,
411412
visitWithTypeInfo,
413+
// Converts a value to a const value by replacing variables.
414+
replaceASTVariables,
415+
// Create a GraphQL Literal AST from a JavaScript input value.
416+
valueToLiteral,
417+
// Create a JavaScript input value from a GraphQL Literal AST.
418+
literalToValue,
412419
// Coerces a JavaScript value to a GraphQL type, or produces errors.
413420
coerceInputValue,
414421
// Concatenates multiple AST together.

src/jsutils/deprecationWarning.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* eslint-disable no-console */
2+
const canWarn = console && console.warn;
3+
const hasIssuedWarning = {};
4+
5+
export function deprecationWarning(
6+
deprecatedFunction: string,
7+
resolution: string,
8+
): void {
9+
if (canWarn && !hasIssuedWarning[deprecatedFunction]) {
10+
hasIssuedWarning[deprecatedFunction] = true;
11+
console.warn(
12+
`DEPRECATION WARNING: The function "${deprecatedFunction}" is deprecated and may be removed in a future version. ${resolution}`,
13+
);
14+
}
15+
}

src/type/__tests__/definition-test.js

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import { expect } from 'chai';
22
import { describe, it } from 'mocha';
33

4-
import { inspect } from '../../jsutils/inspect';
54
import { identityFunc } from '../../jsutils/identityFunc';
65

7-
import { parseValue } from '../../language/parser';
8-
96
import type { GraphQLType, GraphQLNullableType } from '../definition';
107
import {
118
GraphQLList,
@@ -72,26 +69,6 @@ describe('Type System: Scalars', () => {
7269

7370
expect(scalar.serialize).to.equal(identityFunc);
7471
expect(scalar.parseValue).to.equal(identityFunc);
75-
expect(scalar.parseLiteral).to.be.a('function');
76-
});
77-
78-
it('use parseValue for parsing literals if parseLiteral omitted', () => {
79-
const scalar = new GraphQLScalarType({
80-
name: 'Foo',
81-
parseValue(value) {
82-
return 'parseValue: ' + inspect(value);
83-
},
84-
});
85-
86-
expect(scalar.parseLiteral(parseValue('null'))).to.equal(
87-
'parseValue: null',
88-
);
89-
expect(scalar.parseLiteral(parseValue('{ foo: "bar" }'))).to.equal(
90-
'parseValue: { foo: "bar" }',
91-
);
92-
expect(
93-
scalar.parseLiteral(parseValue('{ foo: { bar: $var } }'), { var: 'baz' }),
94-
).to.equal('parseValue: { foo: { bar: "baz" } }');
9572
});
9673

9774
it('rejects a Scalar type without name', () => {

src/type/__tests__/scalars-test.js

Lines changed: 11 additions & 21 deletions
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';
4+
import { parseConstValue } from '../../language/parser';
55

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

6767
it('parseLiteral', () => {
6868
function parseLiteral(str: string) {
69-
return GraphQLInt.parseLiteral(parseValueToAST(str), undefined);
69+
// $FlowExpectedError[not-a-function]
70+
return GraphQLInt.parseLiteral(parseConstValue(str));
7071
}
7172

7273
expect(parseLiteral('1')).to.equal(1);
@@ -104,9 +105,6 @@ describe('Type System: Specified scalar types', () => {
104105
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
105106
'Int cannot represent non-integer value: ENUM_VALUE',
106107
);
107-
expect(() => parseLiteral('$var')).to.throw(
108-
'Int cannot represent non-integer value: $var',
109-
);
110108
});
111109

112110
it('serialize', () => {
@@ -231,7 +229,8 @@ describe('Type System: Specified scalar types', () => {
231229

232230
it('parseLiteral', () => {
233231
function parseLiteral(str: string) {
234-
return GraphQLFloat.parseLiteral(parseValueToAST(str), undefined);
232+
// $FlowExpectedError[not-a-function]
233+
return GraphQLFloat.parseLiteral(parseConstValue(str));
235234
}
236235

237236
expect(parseLiteral('1')).to.equal(1);
@@ -264,9 +263,6 @@ describe('Type System: Specified scalar types', () => {
264263
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
265264
'Float cannot represent non numeric value: ENUM_VALUE',
266265
);
267-
expect(() => parseLiteral('$var')).to.throw(
268-
'Float cannot represent non numeric value: $var',
269-
);
270266
});
271267

272268
it('serialize', () => {
@@ -344,7 +340,8 @@ describe('Type System: Specified scalar types', () => {
344340

345341
it('parseLiteral', () => {
346342
function parseLiteral(str: string) {
347-
return GraphQLString.parseLiteral(parseValueToAST(str), undefined);
343+
// $FlowExpectedError[not-a-function]
344+
return GraphQLString.parseLiteral(parseConstValue(str));
348345
}
349346

350347
expect(parseLiteral('"foo"')).to.equal('foo');
@@ -371,9 +368,6 @@ describe('Type System: Specified scalar types', () => {
371368
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
372369
'String cannot represent a non string value: ENUM_VALUE',
373370
);
374-
expect(() => parseLiteral('$var')).to.throw(
375-
'String cannot represent a non string value: $var',
376-
);
377371
});
378372

379373
it('serialize', () => {
@@ -456,7 +450,8 @@ describe('Type System: Specified scalar types', () => {
456450

457451
it('parseLiteral', () => {
458452
function parseLiteral(str: string) {
459-
return GraphQLBoolean.parseLiteral(parseValueToAST(str), undefined);
453+
// $FlowExpectedError[not-a-function]
454+
return GraphQLBoolean.parseLiteral(parseConstValue(str));
460455
}
461456

462457
expect(parseLiteral('true')).to.equal(true);
@@ -489,9 +484,6 @@ describe('Type System: Specified scalar types', () => {
489484
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
490485
'Boolean cannot represent a non boolean value: ENUM_VALUE',
491486
);
492-
expect(() => parseLiteral('$var')).to.throw(
493-
'Boolean cannot represent a non boolean value: $var',
494-
);
495487
});
496488

497489
it('serialize', () => {
@@ -571,7 +563,8 @@ describe('Type System: Specified scalar types', () => {
571563

572564
it('parseLiteral', () => {
573565
function parseLiteral(str: string) {
574-
return GraphQLID.parseLiteral(parseValueToAST(str), undefined);
566+
// $FlowExpectedError[not-a-function]
567+
return GraphQLID.parseLiteral(parseConstValue(str));
575568
}
576569

577570
expect(parseLiteral('""')).to.equal('');
@@ -604,9 +597,6 @@ describe('Type System: Specified scalar types', () => {
604597
expect(() => parseLiteral('ENUM_VALUE')).to.throw(
605598
'ID cannot represent a non-string and non-integer value: ENUM_VALUE',
606599
);
607-
expect(() => parseLiteral('$var')).to.throw(
608-
'ID cannot represent a non-string and non-integer value: $var',
609-
);
610600
});
611601

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

src/type/definition.d.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {
2222
OperationDefinitionNode,
2323
FieldNode,
2424
FragmentDefinitionNode,
25-
ValueNode,
2625
ConstValueNode,
2726
ScalarTypeExtensionNode,
2827
UnionTypeExtensionNode,
@@ -316,7 +315,9 @@ export class GraphQLScalarType {
316315
specifiedByURL: Maybe<string>;
317316
serialize: GraphQLScalarSerializer<unknown>;
318317
parseValue: GraphQLScalarValueParser<unknown>;
319-
parseLiteral: GraphQLScalarLiteralParser<unknown>;
318+
parseLiteral: Maybe<GraphQLScalarLiteralParser<unknown>>;
319+
valueToLiteral: Maybe<GraphQLScalarValueToLiteral>;
320+
literalToValue: Maybe<GraphQLScalarLiteralToValue>;
320321
extensions: Maybe<Readonly<GraphQLScalarTypeExtensions>>;
321322
astNode: Maybe<ScalarTypeDefinitionNode>;
322323
extensionASTNodes: ReadonlyArray<ScalarTypeExtensionNode>;
@@ -327,7 +328,9 @@ export class GraphQLScalarType {
327328
specifiedByURL: Maybe<string>;
328329
serialize: GraphQLScalarSerializer<unknown>;
329330
parseValue: GraphQLScalarValueParser<unknown>;
330-
parseLiteral: GraphQLScalarLiteralParser<unknown>;
331+
parseLiteral: Maybe<GraphQLScalarLiteralParser<unknown>>;
332+
valueToLiteral: Maybe<GraphQLScalarValueToLiteral>;
333+
literalToValue: Maybe<GraphQLScalarLiteralToValue>;
331334
extensions: Maybe<Readonly<GraphQLScalarTypeExtensions>>;
332335
extensionASTNodes: ReadonlyArray<ScalarTypeExtensionNode>;
333336
};
@@ -344,9 +347,14 @@ export type GraphQLScalarValueParser<TInternal> = (
344347
value: unknown,
345348
) => Maybe<TInternal>;
346349
export type GraphQLScalarLiteralParser<TInternal> = (
347-
valueNode: ValueNode,
348-
variables: Maybe<ObjMap<unknown>>,
350+
valueNode: ConstValueNode,
349351
) => Maybe<TInternal>;
352+
export type GraphQLScalarValueToLiteral = (
353+
inputValue: unknown,
354+
) => Maybe<ConstValueNode>;
355+
export type GraphQLScalarLiteralToValue = (
356+
valueNode: ConstValueNode,
357+
) => unknown;
350358

351359
export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
352360
name: string;
@@ -358,6 +366,10 @@ export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
358366
parseValue?: GraphQLScalarValueParser<TInternal>;
359367
// Parses an externally provided literal value to use as an input.
360368
parseLiteral?: GraphQLScalarLiteralParser<TInternal>;
369+
// Translates an external input value to a literal (AST).
370+
valueToLiteral?: Maybe<GraphQLScalarValueToLiteral>;
371+
// Translates a literal (AST) to external input value.
372+
literalToValue?: Maybe<GraphQLScalarLiteralToValue>;
361373
extensions?: Maybe<Readonly<GraphQLScalarTypeExtensions>>;
362374
astNode?: Maybe<ScalarTypeDefinitionNode>;
363375
extensionASTNodes?: Maybe<ReadonlyArray<ScalarTypeExtensionNode>>;
@@ -788,10 +800,9 @@ export class GraphQLEnumType {
788800
getValue(name: string): Maybe<GraphQLEnumValue>;
789801
serialize(value: unknown): Maybe<string>;
790802
parseValue(value: unknown): Maybe<any>;
791-
parseLiteral(
792-
valueNode: ValueNode,
793-
_variables: Maybe<ObjMap<unknown>>,
794-
): Maybe<any>;
803+
parseLiteral(valueNode: ConstValueNode): Maybe<any>;
804+
valueToLiteral(value: unknown): Maybe<ConstValueNode>;
805+
literalToValue(valueNode: ConstValueNode): unknown;
795806

796807
toConfig(): GraphQLEnumTypeConfig & {
797808
extensions: Maybe<Readonly<GraphQLEnumTypeExtensions>>;

src/type/definition.js

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,9 @@ import type {
4040
OperationDefinitionNode,
4141
FieldNode,
4242
FragmentDefinitionNode,
43-
ValueNode,
4443
ConstValueNode,
4544
} from '../language/ast';
4645

47-
import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped';
48-
4946
import type { GraphQLSchema } from './schema';
5047

5148
// Predicates & Assertions
@@ -561,7 +558,9 @@ export class GraphQLScalarType {
561558
specifiedByURL: ?string;
562559
serialize: GraphQLScalarSerializer<mixed>;
563560
parseValue: GraphQLScalarValueParser<mixed>;
564-
parseLiteral: GraphQLScalarLiteralParser<mixed>;
561+
parseLiteral: ?GraphQLScalarLiteralParser<mixed>;
562+
valueToLiteral: ?GraphQLScalarValueToLiteral;
563+
literalToValue: ?GraphQLScalarLiteralToValue;
565564
extensions: ?ReadOnlyObjMap<mixed>;
566565
astNode: ?ScalarTypeDefinitionNode;
567566
extensionASTNodes: $ReadOnlyArray<ScalarTypeExtensionNode>;
@@ -573,9 +572,9 @@ export class GraphQLScalarType {
573572
this.specifiedByURL = config.specifiedByURL;
574573
this.serialize = config.serialize ?? identityFunc;
575574
this.parseValue = parseValue;
576-
this.parseLiteral =
577-
config.parseLiteral ??
578-
((node, variables) => parseValue(valueFromASTUntyped(node, variables)));
575+
this.parseLiteral = config.parseLiteral;
576+
this.valueToLiteral = config.valueToLiteral;
577+
this.literalToValue = config.literalToValue;
579578
this.extensions = config.extensions && toObjMap(config.extensions);
580579
this.astNode = config.astNode;
581580
this.extensionASTNodes = config.extensionASTNodes ?? [];
@@ -611,6 +610,8 @@ export class GraphQLScalarType {
611610
serialize: this.serialize,
612611
parseValue: this.parseValue,
613612
parseLiteral: this.parseLiteral,
613+
valueToLiteral: this.valueToLiteral,
614+
literalToValue: this.literalToValue,
614615
extensions: this.extensions,
615616
astNode: this.astNode,
616617
extensionASTNodes: this.extensionASTNodes,
@@ -640,10 +641,15 @@ export type GraphQLScalarValueParser<TInternal> = (
640641
) => ?TInternal;
641642
642643
export type GraphQLScalarLiteralParser<TInternal> = (
643-
valueNode: ValueNode,
644-
variables: ?ObjMap<mixed>,
644+
valueNode: ConstValueNode,
645645
) => ?TInternal;
646646
647+
export type GraphQLScalarValueToLiteral = (
648+
inputValue: mixed,
649+
) => ?ConstValueNode;
650+
651+
export type GraphQLScalarLiteralToValue = (valueNode: ConstValueNode) => mixed;
652+
647653
export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
648654
name: string,
649655
description?: ?string,
@@ -653,7 +659,11 @@ export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
653659
// Parses an externally provided value to use as an input.
654660
parseValue?: GraphQLScalarValueParser<TInternal>,
655661
// Parses an externally provided literal value to use as an input.
656-
parseLiteral?: GraphQLScalarLiteralParser<TInternal>,
662+
parseLiteral?: ?GraphQLScalarLiteralParser<TInternal>,
663+
// Translates an external input value to a literal (AST).
664+
valueToLiteral?: ?GraphQLScalarValueToLiteral,
665+
// Translates a literal (AST) to external input value.
666+
literalToValue?: ?GraphQLScalarLiteralToValue,
657667
extensions?: ?ReadOnlyObjMapLike<mixed>,
658668
astNode?: ?ScalarTypeDefinitionNode,
659669
extensionASTNodes?: ?$ReadOnlyArray<ScalarTypeExtensionNode>,
@@ -663,7 +673,6 @@ type GraphQLScalarTypeNormalizedConfig = {|
663673
...GraphQLScalarTypeConfig<mixed, mixed>,
664674
serialize: GraphQLScalarSerializer<mixed>,
665675
parseValue: GraphQLScalarValueParser<mixed>,
666-
parseLiteral: GraphQLScalarLiteralParser<mixed>,
667676
extensions: ?ReadOnlyObjMap<mixed>,
668677
extensionASTNodes: $ReadOnlyArray<ScalarTypeExtensionNode>,
669678
|};
@@ -1345,8 +1354,7 @@ export class GraphQLEnumType /* <T> */ {
13451354
return enumValue.value;
13461355
}
13471356

1348-
parseLiteral(valueNode: ValueNode, _variables: ?ObjMap<mixed>): ?any /* T */ {
1349-
// Note: variables will be resolved to a value before calling this function.
1357+
parseLiteral(valueNode: ConstValueNode): ?any /* T */ {
13501358
if (valueNode.kind !== Kind.ENUM) {
13511359
const valueStr = print(valueNode);
13521360
throw new GraphQLError(
@@ -1368,6 +1376,16 @@ export class GraphQLEnumType /* <T> */ {
13681376
return enumValue.value;
13691377
}
13701378

1379+
valueToLiteral(value: mixed): ?ConstValueNode {
1380+
if (typeof value === 'string') {
1381+
// https://spec.graphql.org/draft/#Name
1382+
if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(value)) {
1383+
return { kind: Kind.ENUM, value };
1384+
}
1385+
return { kind: Kind.STRING, value };
1386+
}
1387+
}
1388+
13711389
toConfig(): GraphQLEnumTypeNormalizedConfig {
13721390
const values = keyValMap(
13731391
this.getValues(),

0 commit comments

Comments
 (0)