Skip to content

Commit 3c0ba0b

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 4954529 commit 3c0ba0b

26 files changed

+953
-69
lines changed

src/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,13 +408,20 @@ export {
408408
// Create a JavaScript value from a GraphQL language AST with a Type.
409409
valueFromAST,
410410
// Create a JavaScript value from a GraphQL language AST without a Type.
411+
// DEPRECATED: use literalToValue
411412
valueFromASTUntyped,
412413
// Create a GraphQL language AST from a JavaScript value.
413414
astFromValue,
414415
// A helper to use within recursive-descent visitors which need to be aware of
415416
// the GraphQL type system.
416417
TypeInfo,
417418
visitWithTypeInfo,
419+
// Converts a value to a const value by replacing variables.
420+
replaceASTVariables,
421+
// Create a GraphQL Literal AST from a JavaScript input value.
422+
valueToLiteral,
423+
// Create a JavaScript input value from a GraphQL Literal AST.
424+
literalToValue,
418425
// Coerces a JavaScript value to a GraphQL type, or produces errors.
419426
coerceInputValue,
420427
// Concatenates multiple AST together.

src/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,13 +397,20 @@ export {
397397
// Create a JavaScript value from a GraphQL language AST with a Type.
398398
valueFromAST,
399399
// Create a JavaScript value from a GraphQL language AST without a Type.
400+
// DEPRECATED: use literalToValue
400401
valueFromASTUntyped,
401402
// Create a GraphQL language AST from a JavaScript value.
402403
astFromValue,
403404
// A helper to use within recursive-descent visitors which need to be aware of
404405
// the GraphQL type system.
405406
TypeInfo,
406407
visitWithTypeInfo,
408+
// Converts a value to a const value by replacing variables.
409+
replaceASTVariables,
410+
// Create a GraphQL Literal AST from a JavaScript input value.
411+
valueToLiteral,
412+
// Create a JavaScript input value from a GraphQL Literal AST.
413+
literalToValue,
407414
// Coerces a JavaScript value to a GraphQL type, or produces errors.
408415
coerceInputValue,
409416
// 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: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
OperationDefinitionNode,
2323
FieldNode,
2424
FragmentDefinitionNode,
25-
ValueNode,
25+
ConstValueNode,
2626
ScalarTypeExtensionNode,
2727
UnionTypeExtensionNode,
2828
EnumTypeExtensionNode,
@@ -315,7 +315,9 @@ export class GraphQLScalarType {
315315
specifiedByURL: Maybe<string>;
316316
serialize: GraphQLScalarSerializer<unknown>;
317317
parseValue: GraphQLScalarValueParser<unknown>;
318-
parseLiteral: GraphQLScalarLiteralParser<unknown>;
318+
parseLiteral: Maybe<GraphQLScalarLiteralParser<unknown>>;
319+
valueToLiteral: Maybe<GraphQLScalarValueToLiteral>;
320+
literalToValue: Maybe<GraphQLScalarLiteralToValue>;
319321
extensions: Maybe<Readonly<GraphQLScalarTypeExtensions>>;
320322
astNode: Maybe<ScalarTypeDefinitionNode>;
321323
extensionASTNodes: ReadonlyArray<ScalarTypeExtensionNode>;
@@ -326,7 +328,9 @@ export class GraphQLScalarType {
326328
specifiedByURL: Maybe<string>;
327329
serialize: GraphQLScalarSerializer<unknown>;
328330
parseValue: GraphQLScalarValueParser<unknown>;
329-
parseLiteral: GraphQLScalarLiteralParser<unknown>;
331+
parseLiteral: Maybe<GraphQLScalarLiteralParser<unknown>>;
332+
valueToLiteral: Maybe<GraphQLScalarValueToLiteral>;
333+
literalToValue: Maybe<GraphQLScalarLiteralToValue>;
330334
extensions: Maybe<Readonly<GraphQLScalarTypeExtensions>>;
331335
extensionASTNodes: ReadonlyArray<ScalarTypeExtensionNode>;
332336
};
@@ -343,9 +347,14 @@ export type GraphQLScalarValueParser<TInternal> = (
343347
value: unknown,
344348
) => Maybe<TInternal>;
345349
export type GraphQLScalarLiteralParser<TInternal> = (
346-
valueNode: ValueNode,
347-
variables: Maybe<ObjMap<unknown>>,
350+
valueNode: ConstValueNode,
348351
) => Maybe<TInternal>;
352+
export type GraphQLScalarValueToLiteral = (
353+
inputValue: unknown,
354+
) => Maybe<ConstValueNode>;
355+
export type GraphQLScalarLiteralToValue = (
356+
valueNode: ConstValueNode,
357+
) => unknown;
349358

350359
export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
351360
name: string;
@@ -357,6 +366,10 @@ export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
357366
parseValue?: GraphQLScalarValueParser<TInternal>;
358367
// Parses an externally provided literal value to use as an input.
359368
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>;
360373
extensions?: Maybe<Readonly<GraphQLScalarTypeExtensions>>;
361374
astNode?: Maybe<ScalarTypeDefinitionNode>;
362375
extensionASTNodes?: Maybe<ReadonlyArray<ScalarTypeExtensionNode>>;
@@ -782,10 +795,9 @@ export class GraphQLEnumType {
782795
getValue(name: string): Maybe<GraphQLEnumValue>;
783796
serialize(value: unknown): Maybe<string>;
784797
parseValue(value: unknown): Maybe<any>;
785-
parseLiteral(
786-
valueNode: ValueNode,
787-
_variables: Maybe<ObjMap<unknown>>,
788-
): Maybe<any>;
798+
parseLiteral(valueNode: ConstValueNode): Maybe<any>;
799+
valueToLiteral(value: unknown): Maybe<ConstValueNode>;
800+
literalToValue(valueNode: ConstValueNode): unknown;
789801

790802
toConfig(): GraphQLEnumTypeConfig & {
791803
extensions: Maybe<Readonly<GraphQLEnumTypeExtensions>>;

src/type/definition.js

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

46-
import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped';
47-
4846
import type { GraphQLSchema } from './schema';
4947

5048
// Predicates & Assertions
@@ -560,7 +558,9 @@ export class GraphQLScalarType {
560558
specifiedByURL: ?string;
561559
serialize: GraphQLScalarSerializer<mixed>;
562560
parseValue: GraphQLScalarValueParser<mixed>;
563-
parseLiteral: GraphQLScalarLiteralParser<mixed>;
561+
parseLiteral: ?GraphQLScalarLiteralParser<mixed>;
562+
valueToLiteral: ?GraphQLScalarValueToLiteral;
563+
literalToValue: ?GraphQLScalarLiteralToValue;
564564
extensions: ?ReadOnlyObjMap<mixed>;
565565
astNode: ?ScalarTypeDefinitionNode;
566566
extensionASTNodes: $ReadOnlyArray<ScalarTypeExtensionNode>;
@@ -572,9 +572,9 @@ export class GraphQLScalarType {
572572
this.specifiedByURL = config.specifiedByURL;
573573
this.serialize = config.serialize ?? identityFunc;
574574
this.parseValue = parseValue;
575-
this.parseLiteral =
576-
config.parseLiteral ??
577-
((node, variables) => parseValue(valueFromASTUntyped(node, variables)));
575+
this.parseLiteral = config.parseLiteral;
576+
this.valueToLiteral = config.valueToLiteral;
577+
this.literalToValue = config.literalToValue;
578578
this.extensions = config.extensions && toObjMap(config.extensions);
579579
this.astNode = config.astNode;
580580
this.extensionASTNodes = config.extensionASTNodes ?? [];
@@ -610,6 +610,8 @@ export class GraphQLScalarType {
610610
serialize: this.serialize,
611611
parseValue: this.parseValue,
612612
parseLiteral: this.parseLiteral,
613+
valueToLiteral: this.valueToLiteral,
614+
literalToValue: this.literalToValue,
613615
extensions: this.extensions,
614616
astNode: this.astNode,
615617
extensionASTNodes: this.extensionASTNodes,
@@ -639,10 +641,15 @@ export type GraphQLScalarValueParser<TInternal> = (
639641
) => ?TInternal;
640642
641643
export type GraphQLScalarLiteralParser<TInternal> = (
642-
valueNode: ValueNode,
643-
variables: ?ObjMap<mixed>,
644+
valueNode: ConstValueNode,
644645
) => ?TInternal;
645646
647+
export type GraphQLScalarValueToLiteral = (
648+
inputValue: mixed,
649+
) => ?ConstValueNode;
650+
651+
export type GraphQLScalarLiteralToValue = (valueNode: ConstValueNode) => mixed;
652+
646653
export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
647654
name: string,
648655
description?: ?string,
@@ -652,7 +659,11 @@ export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
652659
// Parses an externally provided value to use as an input.
653660
parseValue?: GraphQLScalarValueParser<TInternal>,
654661
// Parses an externally provided literal value to use as an input.
655-
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,
656667
extensions?: ?ReadOnlyObjMapLike<mixed>,
657668
astNode?: ?ScalarTypeDefinitionNode,
658669
extensionASTNodes?: ?$ReadOnlyArray<ScalarTypeExtensionNode>,
@@ -662,7 +673,6 @@ type GraphQLScalarTypeNormalizedConfig = {|
662673
...GraphQLScalarTypeConfig<mixed, mixed>,
663674
serialize: GraphQLScalarSerializer<mixed>,
664675
parseValue: GraphQLScalarValueParser<mixed>,
665-
parseLiteral: GraphQLScalarLiteralParser<mixed>,
666676
extensions: ?ReadOnlyObjMap<mixed>,
667677
extensionASTNodes: $ReadOnlyArray<ScalarTypeExtensionNode>,
668678
|};
@@ -1320,8 +1330,7 @@ export class GraphQLEnumType /* <T> */ {
13201330
return enumValue.value;
13211331
}
13221332

1323-
parseLiteral(valueNode: ValueNode, _variables: ?ObjMap<mixed>): ?any /* T */ {
1324-
// Note: variables will be resolved to a value before calling this function.
1333+
parseLiteral(valueNode: ConstValueNode): ?any /* T */ {
13251334
if (valueNode.kind !== Kind.ENUM) {
13261335
const valueStr = print(valueNode);
13271336
throw new GraphQLError(
@@ -1343,6 +1352,16 @@ export class GraphQLEnumType /* <T> */ {
13431352
return enumValue.value;
13441353
}
13451354

1355+
valueToLiteral(value: mixed): ?ConstValueNode {
1356+
if (typeof value === 'string') {
1357+
// https://spec.graphql.org/draft/#Name
1358+
if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(value)) {
1359+
return { kind: Kind.ENUM, value };
1360+
}
1361+
return { kind: Kind.STRING, value };
1362+
}
1363+
}
1364+
13461365
toConfig(): GraphQLEnumTypeNormalizedConfig {
13471366
const values = keyValMap(
13481367
this.getValues(),

0 commit comments

Comments
 (0)