From 55e3e32e643012f95153efd4dc25a46851c83560 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Wed, 5 May 2021 16:58:37 -0700 Subject: [PATCH] WIP --- src/index.d.ts | 3 + src/index.js | 3 + src/type/__tests__/definition-test.js | 12 +-- src/type/__tests__/scalars-test.js | 12 +-- src/type/definition.d.ts | 24 +++--- src/type/definition.js | 31 +++++--- src/type/scalars.js | 6 ++ .../__tests__/literalToValue-test.js | 26 ++++++ .../__tests__/replaceASTVariables-test.js | 42 ++++++++++ src/utilities/buildClientSchema.js | 9 ++- src/utilities/coerceInputLiteral.js | 10 ++- src/utilities/extendSchema.js | 6 +- src/utilities/index.d.ts | 4 + src/utilities/index.js | 4 + src/utilities/literalToValue.d.ts | 24 ++++++ src/utilities/literalToValue.js | 79 +++++++++++++++++++ src/utilities/replaceASTVariables.d.ts | 16 ++++ src/utilities/replaceASTVariables.js | 31 ++++++++ src/utilities/valueFromASTUntyped.d.ts | 1 + src/utilities/valueFromASTUntyped.js | 4 + src/utilities/valueToLiteral.js | 10 +-- .../rules/ValuesOfCorrectTypeRule.js | 10 ++- 22 files changed, 316 insertions(+), 51 deletions(-) create mode 100644 src/utilities/__tests__/literalToValue-test.js create mode 100644 src/utilities/__tests__/replaceASTVariables-test.js create mode 100644 src/utilities/literalToValue.d.ts create mode 100644 src/utilities/literalToValue.js create mode 100644 src/utilities/replaceASTVariables.d.ts create mode 100644 src/utilities/replaceASTVariables.js diff --git a/src/index.d.ts b/src/index.d.ts index b36d36a3c1a..7ba70164cf9 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -407,6 +407,7 @@ export { // DEPRECATED: use coerceInputLiteral valueFromAST, // Create a JavaScript value from a GraphQL language AST without a Type. + // DEPRECATED: use literalToValue valueFromASTUntyped, // Create a GraphQL language AST from a JavaScript value. // DEPRECATED: use valueToLiteral @@ -417,6 +418,8 @@ export { visitWithTypeInfo, // Create a GraphQL Literal AST from a JavaScript input value. valueToLiteral, + // Create a JavaScript input value from a GraphQL Literal AST. + literalToValue, // Coerces a GraphQL Literal with a GraphQL type. coerceInputLiteral, // Coerces a JavaScript value with a GraphQL type, or produces errors. diff --git a/src/index.js b/src/index.js index ca24ed00563..f7c26073387 100644 --- a/src/index.js +++ b/src/index.js @@ -396,6 +396,7 @@ export { // DEPRECATED: use coerceInputLiteral valueFromAST, // Create a JavaScript value from a GraphQL language AST without a Type. + // DEPRECATED: use literalToValue valueFromASTUntyped, // Create a GraphQL language AST from a JavaScript value. // DEPRECATED: use valueToLiteral @@ -406,6 +407,8 @@ export { visitWithTypeInfo, // Create a GraphQL Literal AST from a JavaScript input value. valueToLiteral, + // Create a JavaScript input value from a GraphQL Literal AST. + literalToValue, // Coerces a GraphQL Literal with a GraphQL type. coerceInputLiteral, // Coerces a JavaScript value with a GraphQL type, or produces errors. diff --git a/src/type/__tests__/definition-test.js b/src/type/__tests__/definition-test.js index 809e7007d34..02c069423c1 100644 --- a/src/type/__tests__/definition-test.js +++ b/src/type/__tests__/definition-test.js @@ -17,6 +17,7 @@ import { GraphQLEnumType, GraphQLInputObjectType, } from '../definition'; +import { coerceInputLiteral } from '../../utilities/coerceInputLiteral'; const ScalarType = new GraphQLScalarType({ name: 'Scalar' }); const ObjectType = new GraphQLObjectType({ name: 'Object', fields: {} }); @@ -72,7 +73,6 @@ describe('Type System: Scalars', () => { expect(scalar.serialize).to.equal(identityFunc); expect(scalar.parseValue).to.equal(identityFunc); - expect(scalar.parseLiteral).to.be.a('function'); }); it('use parseValue for parsing literals if parseLiteral omitted', () => { @@ -83,14 +83,16 @@ describe('Type System: Scalars', () => { }, }); - expect(scalar.parseLiteral(parseValue('null'))).to.equal( - 'parseValue: null', + expect(coerceInputLiteral(parseValue('null'), scalar)).to.equal( + null, ); - expect(scalar.parseLiteral(parseValue('{ foo: "bar" }'))).to.equal( + expect(coerceInputLiteral(parseValue('{ foo: "bar" }'), scalar)).to.equal( 'parseValue: { foo: "bar" }', ); expect( - scalar.parseLiteral(parseValue('{ foo: { bar: $var } }'), { var: 'baz' }), + coerceInputLiteral(parseValue('{ foo: { bar: $var } }'), scalar, { + var: 'baz', + }), ).to.equal('parseValue: { foo: { bar: "baz" } }'); }); diff --git a/src/type/__tests__/scalars-test.js b/src/type/__tests__/scalars-test.js index 8eac0f6266e..6d456d1d939 100644 --- a/src/type/__tests__/scalars-test.js +++ b/src/type/__tests__/scalars-test.js @@ -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, @@ -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); @@ -231,7 +231,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); @@ -344,7 +344,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'); @@ -456,7 +456,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); @@ -571,7 +571,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(''); diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index c8ff4651d23..a57115d9fa7 100644 --- a/src/type/definition.d.ts +++ b/src/type/definition.d.ts @@ -22,7 +22,6 @@ import { OperationDefinitionNode, FieldNode, FragmentDefinitionNode, - ValueNode, ConstValueNode, ScalarTypeExtensionNode, UnionTypeExtensionNode, @@ -316,8 +315,9 @@ export class GraphQLScalarType { specifiedByURL: Maybe; serialize: GraphQLScalarSerializer; parseValue: GraphQLScalarValueParser; - parseLiteral: GraphQLScalarLiteralParser; + parseLiteral: Maybe>; valueToLiteral: Maybe; + literalToValue: Maybe; extensions: Maybe>; astNode: Maybe; extensionASTNodes: ReadonlyArray; @@ -328,8 +328,9 @@ export class GraphQLScalarType { specifiedByURL: Maybe; serialize: GraphQLScalarSerializer; parseValue: GraphQLScalarValueParser; - parseLiteral: GraphQLScalarLiteralParser; + parseLiteral: Maybe>; valueToLiteral: Maybe; + literalToValue: Maybe; extensions: Maybe>; extensionASTNodes: ReadonlyArray; }; @@ -346,13 +347,14 @@ export type GraphQLScalarValueParser = ( value: unknown, ) => Maybe; export type GraphQLScalarLiteralParser = ( - valueNode: ValueNode, - variables: Maybe>, + valueNode: ConstValueNode, ) => Maybe; - export type GraphQLScalarValueToLiteral = ( inputValue: unknown, ) => Maybe; +export type GraphQLScalarLiteralToValue = ( + valueNode: ConstValueNode, +) => unknown; export interface GraphQLScalarTypeConfig { name: string; @@ -364,8 +366,10 @@ export interface GraphQLScalarTypeConfig { parseValue?: GraphQLScalarValueParser; // Parses an externally provided literal value to use as an input. parseLiteral?: GraphQLScalarLiteralParser; - // Translates an external input value to an external literal (AST). + // Translates an external input value to a literal (AST). valueToLiteral?: Maybe; + // Translates a literal (AST) to external input value. + literalToValue?: Maybe; extensions?: Maybe>; astNode?: Maybe; extensionASTNodes?: Maybe>; @@ -791,11 +795,9 @@ export class GraphQLEnumType { getValue(name: string): Maybe; serialize(value: unknown): Maybe; parseValue(value: unknown): Maybe; - parseLiteral( - valueNode: ValueNode, - _variables: Maybe>, - ): Maybe; + parseLiteral(valueNode: ConstValueNode): Maybe; valueToLiteral(value: unknown): Maybe; + literalToValue(valueNode: ConstValueNode): unknown; toConfig(): GraphQLEnumTypeConfig & { extensions: Maybe>; diff --git a/src/type/definition.js b/src/type/definition.js index 9067893f06e..8889554572e 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -45,8 +45,6 @@ import type { ConstValueNode, } from '../language/ast'; -import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped'; - import type { GraphQLSchema } from './schema'; // Predicates & Assertions @@ -562,8 +560,9 @@ export class GraphQLScalarType { specifiedByURL: ?string; serialize: GraphQLScalarSerializer; parseValue: GraphQLScalarValueParser; - parseLiteral: GraphQLScalarLiteralParser; + parseLiteral: ?GraphQLScalarLiteralParser; valueToLiteral: ?GraphQLScalarValueToLiteral; + literalToValue: ?GraphQLScalarLiteralToValue; extensions: ?ReadOnlyObjMap; astNode: ?ScalarTypeDefinitionNode; extensionASTNodes: $ReadOnlyArray; @@ -575,10 +574,9 @@ export class GraphQLScalarType { this.specifiedByURL = config.specifiedByURL; this.serialize = config.serialize ?? identityFunc; this.parseValue = parseValue; - this.parseLiteral = - config.parseLiteral ?? - ((node, variables) => parseValue(valueFromASTUntyped(node, variables))); + this.parseLiteral = config.parseLiteral; this.valueToLiteral = config.valueToLiteral; + this.literalToValue = config.literalToValue; this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; @@ -615,6 +613,7 @@ export class GraphQLScalarType { parseValue: this.parseValue, parseLiteral: this.parseLiteral, valueToLiteral: this.valueToLiteral, + literalToValue: this.literalToValue, extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes, @@ -644,14 +643,15 @@ export type GraphQLScalarValueParser = ( ) => ?TInternal; export type GraphQLScalarLiteralParser = ( - valueNode: ValueNode, - variables: ?ObjMap, + valueNode: ConstValueNode, ) => ?TInternal; export type GraphQLScalarValueToLiteral = ( inputValue: mixed, ) => ?ConstValueNode; +export type GraphQLScalarLiteralToValue = (valueNode: ConstValueNode) => mixed; + export type GraphQLScalarTypeConfig = {| name: string, description?: ?string, @@ -661,9 +661,11 @@ export type GraphQLScalarTypeConfig = {| // Parses an externally provided value to use as an input. parseValue?: GraphQLScalarValueParser, // Parses an externally provided literal value to use as an input. - parseLiteral?: GraphQLScalarLiteralParser, - // Translates an external input value to an external literal (AST). + parseLiteral?: ?GraphQLScalarLiteralParser, + // Translates an external input value to a literal (AST). valueToLiteral?: ?GraphQLScalarValueToLiteral, + // Translates a literal (AST) to external input value. + literalToValue?: ?GraphQLScalarLiteralToValue, extensions?: ?ReadOnlyObjMapLike, astNode?: ?ScalarTypeDefinitionNode, extensionASTNodes?: ?$ReadOnlyArray, @@ -673,7 +675,6 @@ type GraphQLScalarTypeNormalizedConfig = {| ...GraphQLScalarTypeConfig, serialize: GraphQLScalarSerializer, parseValue: GraphQLScalarValueParser, - parseLiteral: GraphQLScalarLiteralParser, extensions: ?ReadOnlyObjMap, extensionASTNodes: $ReadOnlyArray, |}; @@ -1331,7 +1332,7 @@ export class GraphQLEnumType /* */ { return enumValue.value; } - parseLiteral(valueNode: ValueNode, _variables: ?ObjMap): ?any /* T */ { + parseLiteral(valueNode: ValueNode): ?any /* T */ { // Note: variables will be resolved to a value before calling this function. if (valueNode.kind !== Kind.ENUM) { const valueStr = print(valueNode); @@ -1364,6 +1365,12 @@ export class GraphQLEnumType /* */ { } } + literalToValue(valueNode: ConstValueNode): mixed { + if (valueNode.kind === Kind.ENUM) { + return valueNode.value; + } + } + toConfig(): GraphQLEnumTypeNormalizedConfig { const values = keyValMap( this.getValues(), diff --git a/src/type/scalars.js b/src/type/scalars.js index 8d2b5757bc7..1286e2ce84a 100644 --- a/src/type/scalars.js +++ b/src/type/scalars.js @@ -278,6 +278,12 @@ export const GraphQLID: GraphQLScalarType = new GraphQLScalarType({ return { kind: Kind.STRING, value }; } }, + literalToValue(valueNode: ConstValueNode): mixed { + // ID Int literals are represented as string values. + if (valueNode.kind === Kind.INT || valueNode.kind === Kind.STRING) { + return valueNode.value; + } + }, }); export const specifiedScalarTypes: $ReadOnlyArray = Object.freeze( diff --git a/src/utilities/__tests__/literalToValue-test.js b/src/utilities/__tests__/literalToValue-test.js new file mode 100644 index 00000000000..911ac071f27 --- /dev/null +++ b/src/utilities/__tests__/literalToValue-test.js @@ -0,0 +1,26 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +// import { +// GraphQLID, +// GraphQLInt, +// GraphQLFloat, +// GraphQLString, +// GraphQLBoolean, +// } from '../../type/scalars'; +// import { +// GraphQLList, +// GraphQLNonNull, +// GraphQLScalarType, +// GraphQLEnumType, +// GraphQLInputObjectType, +// } from '../../type/definition'; + +import { parseConstValue } from '../../language/parser'; +import { literalToValue } from '../literalToValue'; + +describe('literalToValue', () => { + it('converts null', () => { + expect(literalToValue(parseConstValue('null'))).to.equal(null); + }); +}); diff --git a/src/utilities/__tests__/replaceASTVariables-test.js b/src/utilities/__tests__/replaceASTVariables-test.js new file mode 100644 index 00000000000..f72638f26f8 --- /dev/null +++ b/src/utilities/__tests__/replaceASTVariables-test.js @@ -0,0 +1,42 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import type { ValueNode } from '../../language/ast'; +import { parseValue as _parseValue } from '../../language/parser'; +import { replaceASTVariables } from '../replaceASTVariables'; + +function parseValue(ast: string): ValueNode { + return _parseValue(ast, { noLocation: true }); +} + +describe('replaceASTVariables', () => { + it('does not change simple AST', () => { + const ast = parseValue('null') + expect(replaceASTVariables(ast, undefined)).to.equal(ast); + }); + + it('replaces simple Variables', () => { + const ast = parseValue('$var') + expect(replaceASTVariables(ast, {var:123})).to.deep.equal(parseValue('123')); + }); + + it('replaces nested Variables', () => { + const ast = parseValue('{ foo: [ $var ], bar: $var }') + expect(replaceASTVariables(ast, {var:123})).to.deep.equal(parseValue('{ foo: [ 123 ], bar: 123 }')); + }); + + it('replaces missing Variables with null', () => { + const ast = parseValue('$var') + expect(replaceASTVariables(ast, undefined)).to.deep.equal(parseValue('null')); + }); + + it('replaces missing Variables in lists with null', () => { + const ast = parseValue('[1, $var]') + expect(replaceASTVariables(ast, undefined)).to.deep.equal(parseValue('[1, null]')); + }); + + it('omits missing Variables from objects', () => { + const ast = parseValue('{ foo: 1, bar: $var }') + expect(replaceASTVariables(ast, undefined)).to.deep.equal(parseValue('{ foo: 1 }')); + }); +}); diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index 2e56170fd38..33dad516d3d 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -3,7 +3,7 @@ import { devAssert } from '../jsutils/devAssert'; import { keyValMap } from '../jsutils/keyValMap'; import { isObjectLike } from '../jsutils/isObjectLike'; -import { parseValue } from '../language/parser'; +import { parseConstValue } from '../language/parser'; import type { GraphQLSchemaValidationOptions } from '../type/schema'; import type { @@ -47,7 +47,7 @@ import type { IntrospectionTypeRef, IntrospectionNamedTypeRef, } from './getIntrospectionQuery'; -import { valueFromASTUntyped } from './valueFromASTUntyped'; +import { literalToValue } from './literalToValue'; /** * Build a GraphQLSchema for use by client tools. @@ -369,7 +369,10 @@ export function buildClientSchema( const defaultValue = inputValueIntrospection.defaultValue != null - ? valueFromASTUntyped(parseValue(inputValueIntrospection.defaultValue)) + ? literalToValue( + parseConstValue(inputValueIntrospection.defaultValue), + type, + ) : undefined; return { description: inputValueIntrospection.description, diff --git a/src/utilities/coerceInputLiteral.js b/src/utilities/coerceInputLiteral.js index b4f24430a5a..d3ed3c1bcae 100644 --- a/src/utilities/coerceInputLiteral.js +++ b/src/utilities/coerceInputLiteral.js @@ -15,6 +15,8 @@ import { } from '../type/definition'; import { coerceDefaultValue } from './coerceInputValue'; +import { literalToValue } from './literalToValue'; +import { replaceASTVariables } from './replaceASTVariables'; /** * Produces a coerced "internal" JavaScript value given a GraphQL Value AST. @@ -113,11 +115,15 @@ export function coerceInputLiteral( // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') if (isLeafType(type)) { - // Scalars and Enums fulfill parsing a literal value via parseLiteral(). + // Scalars and Enums fulfill parsing a literal value via parseLiteral() + // (or via parseValue() if not defined). // Invalid values represent a failure to parse correctly, in which case // no value is returned. + const constValueNode = replaceASTVariables(valueNode, variables); try { - return type.parseLiteral(valueNode, variables); + return type.parseLiteral + ? type.parseLiteral(constValueNode) + : type.parseValue(literalToValue(constValueNode, type)); } catch (_error) { return; // Invalid: ignore error and intentionally return no value. } diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 3abfd297040..8a9b77ef75e 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -80,7 +80,7 @@ import { GraphQLInputObjectType, } from '../type/definition'; -import { valueFromASTUntyped } from './valueFromASTUntyped'; +import { literalToValue } from './literalToValue'; type Options = {| ...GraphQLSchemaValidationOptions, @@ -498,7 +498,7 @@ export function extendSchemaImpl( description: arg.description?.value, defaultValue: arg.defaultValue != null - ? valueFromASTUntyped(arg.defaultValue) + ? literalToValue(arg.defaultValue, type) : undefined, deprecationReason: getDeprecationReason(arg), astNode: arg, @@ -528,7 +528,7 @@ export function extendSchemaImpl( description: field.description?.value, defaultValue: field.defaultValue != null - ? valueFromASTUntyped(field.defaultValue) + ? literalToValue(field.defaultValue, type) : undefined, deprecationReason: getDeprecationReason(field), astNode: field, diff --git a/src/utilities/index.d.ts b/src/utilities/index.d.ts index 8a4f183ede6..991c97e7bd6 100644 --- a/src/utilities/index.d.ts +++ b/src/utilities/index.d.ts @@ -66,6 +66,7 @@ export { typeFromAST } from './typeFromAST'; export { valueFromAST } from './valueFromAST'; // Create a JavaScript value from a GraphQL language AST without a type. +// DEPRECATED: use literalToValue export { valueFromASTUntyped } from './valueFromASTUntyped'; // Create a GraphQL language AST from a JavaScript value. @@ -79,6 +80,9 @@ export { TypeInfo, visitWithTypeInfo } from './TypeInfo'; // Create a GraphQL Literal AST from a JavaScript input value. export { valueToLiteral } from './valueToLiteral'; +// Create a JavaScript input value from a GraphQL Literal AST. +export { literalToValue } from './literalToValue'; + // Coerces a GraphQL Literal with a GraphQL type. export { coerceInputLiteral } from './coerceInputLiteral'; diff --git a/src/utilities/index.js b/src/utilities/index.js index c10d79470d6..a2115ed4837 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -64,6 +64,7 @@ export { typeFromAST } from './typeFromAST'; export { valueFromAST } from './valueFromAST'; // Create a JavaScript value from a GraphQL language AST without a type. +// DEPRECATED: use literalToValue export { valueFromASTUntyped } from './valueFromASTUntyped'; // Create a GraphQL language AST from a JavaScript value. @@ -77,6 +78,9 @@ export { TypeInfo, visitWithTypeInfo } from './TypeInfo'; // Create a GraphQL Literal AST from a JavaScript input value. export { valueToLiteral } from './valueToLiteral'; +// Create a JavaScript input value from a GraphQL Literal AST. +export { literalToValue } from './literalToValue'; + // Coerces a GraphQL Literal with a GraphQL type. export { coerceInputLiteral } from './coerceInputLiteral'; diff --git a/src/utilities/literalToValue.d.ts b/src/utilities/literalToValue.d.ts new file mode 100644 index 00000000000..bd52e6cdcac --- /dev/null +++ b/src/utilities/literalToValue.d.ts @@ -0,0 +1,24 @@ +import { ConstValueNode } from '../language/ast'; +import { GraphQLInputType } from '../type/definition'; + +/** + * Produces a JavaScript value given a GraphQL Value AST. + * + * A GraphQL type may be provided, which will be used to interpret different + * JavaScript values if it defines a `literalToValue` method. + * + * | GraphQL Value | JavaScript Value | + * | -------------------- | ---------------- | + * | Input Object | Object | + * | List | Array | + * | Boolean | Boolean | + * | String / Enum | String | + * | Int / Float | Number | + * | Null | null | + * + * Note: This function does not perform any type validation or coercion. + */ +export function literalToValue( + valueNode: ConstValueNode, + type?: GraphQLInputType, +): unknown; diff --git a/src/utilities/literalToValue.js b/src/utilities/literalToValue.js new file mode 100644 index 00000000000..67f6475ad5e --- /dev/null +++ b/src/utilities/literalToValue.js @@ -0,0 +1,79 @@ +import { inspect } from '../jsutils/inspect'; +import { invariant } from '../jsutils/invariant'; +import { keyValMap } from '../jsutils/keyValMap'; + +import { Kind } from '../language/kinds'; +import type { ConstValueNode } from '../language/ast'; +import type { GraphQLInputType } from '../type/definition'; +import { + getNamedType, + isLeafType, + isInputObjectType, +} from '../type/definition'; + +/** + * Produces a JavaScript value given a GraphQL Value AST. + * + * A GraphQL type may be provided, which will be used to interpret different + * JavaScript values if it defines a `literalToValue` method. + * + * | GraphQL Value | JavaScript Value | + * | -------------------- | ---------------- | + * | Input Object | Object | + * | List | Array | + * | Boolean | Boolean | + * | String / Enum | String | + * | Int / Float | Number | + * | Null | null | + * + * Note: This function does not perform any type validation or coercion. + */ +export function literalToValue( + valueNode: ConstValueNode, + type?: GraphQLInputType, +): mixed { + if (valueNode.kind === Kind.NULL) { + return null; + } + + const namedType = type && getNamedType(type); + + if (valueNode.kind === Kind.LIST) { + return valueNode.values.map((node) => literalToValue(node, namedType)); + } + + // Does this type (if provided) define `literalToValue` which returns a value? + if (isLeafType(namedType) && namedType.literalToValue != null) { + const literal = namedType.literalToValue(valueNode); + if (literal !== undefined) { + return literal; + } + } + + switch (valueNode.kind) { + case Kind.BOOLEAN: + case Kind.STRING: + case Kind.ENUM: + return valueNode.value; + case Kind.INT: + return parseInt(valueNode.value, 10); + case Kind.FLOAT: + return parseFloat(valueNode.value); + case Kind.OBJECT: { + const fieldDefs = isInputObjectType(namedType) + ? namedType.getFields() + : undefined; + return keyValMap( + valueNode.fields, + (field) => field.name.value, + (field) => { + const fieldDef = fieldDefs && fieldDefs[field.name.value]; + return literalToValue(field.value, fieldDef && fieldDef.type); + }, + ); + } + } + + // istanbul ignore next (Not reachable. All possible const value nodes have been considered) + invariant(false, 'Unexpected: ' + inspect((valueNode: empty))); +} diff --git a/src/utilities/replaceASTVariables.d.ts b/src/utilities/replaceASTVariables.d.ts new file mode 100644 index 00000000000..f07c5f30408 --- /dev/null +++ b/src/utilities/replaceASTVariables.d.ts @@ -0,0 +1,16 @@ +import { Maybe } from '../jsutils/Maybe'; +import { ObjMap } from '../jsutils/ObjMap'; + +import { ValueNode, ConstValueNode } from '../language/ast'; + +/** + * Replaces any Variables found within an AST Value literal with literals + * supplied from a map of variable values, returning a constant value. + * + * Used primarily to ensure only complete constant values are used during input + * coercion of custom scalars which accept complex literals. + */ +export function replaceASTVariables( + valueNode: ValueNode, + variables: Maybe>, +): ConstValueNode; diff --git a/src/utilities/replaceASTVariables.js b/src/utilities/replaceASTVariables.js new file mode 100644 index 00000000000..ca07699c077 --- /dev/null +++ b/src/utilities/replaceASTVariables.js @@ -0,0 +1,31 @@ +import type { ObjMap } from '../jsutils/ObjMap'; + +import type { ValueNode, ConstValueNode } from '../language/ast'; +import { Kind } from '../language/kinds'; +import { visit } from '../language/visitor'; + +import { valueToLiteral } from './valueToLiteral'; + +/** + * Replaces any Variables found within an AST Value literal with literals + * supplied from a map of variable values, returning a constant value. + * + * Used primarily to ensure only complete constant values are used during input + * coercion of custom scalars which accept complex literals. + */ +export function replaceASTVariables( + valueNode: ValueNode, + variables: ?ObjMap, +): ConstValueNode { + return visit(valueNode, { + Variable: (node) => valueToLiteral(variables?.[node.name.value]), + ObjectValue: (node) => ({ + ...node, + // Filter out any fields supplied with missing variables. + fields: node.fields.filter( + (field) => + field !== Kind.VARIABLE || variables?.[node.name.value] !== undefined, + ), + }), + }); +} diff --git a/src/utilities/valueFromASTUntyped.d.ts b/src/utilities/valueFromASTUntyped.d.ts index 495e2797234..c51fbf98381 100644 --- a/src/utilities/valueFromASTUntyped.d.ts +++ b/src/utilities/valueFromASTUntyped.d.ts @@ -18,6 +18,7 @@ import { ValueNode } from '../language/ast'; * | Int / Float | Number | * | Null | null | * + * @deprecated use literalToValue */ export function valueFromASTUntyped( valueNode: ValueNode, diff --git a/src/utilities/valueFromASTUntyped.js b/src/utilities/valueFromASTUntyped.js index 79eea47da5e..4a2a87ce6b3 100644 --- a/src/utilities/valueFromASTUntyped.js +++ b/src/utilities/valueFromASTUntyped.js @@ -1,4 +1,5 @@ import type { ObjMap } from '../jsutils/ObjMap'; +import { deprecationWarning } from '../jsutils/deprecationWarning'; import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; import { keyValMap } from '../jsutils/keyValMap'; @@ -21,11 +22,14 @@ import type { ValueNode } from '../language/ast'; * | Int / Float | Number | * | Null | null | * + * @deprecated use literalToValue */ export function valueFromASTUntyped( valueNode: ValueNode, variables?: ?ObjMap, ): mixed { + deprecationWarning('valueFromASTUntyped', 'Use "literalToValue".'); + switch (valueNode.kind) { case Kind.NULL: return null; diff --git a/src/utilities/valueToLiteral.js b/src/utilities/valueToLiteral.js index 435a187ed3b..a4c13f18884 100644 --- a/src/utilities/valueToLiteral.js +++ b/src/utilities/valueToLiteral.js @@ -12,11 +12,7 @@ import { } from '../type/definition'; /** - * Produces a GraphQL Value AST given a JavaScript object. - * Function will match JavaScript values to GraphQL AST schema format - * by using suggested GraphQLInputType. For example: - * - * valueToLiteral("value", GraphQLString) + * Produces a GraphQL Value AST given a JavaScript value. * * A GraphQL type may be provided, which will be used to interpret different * JavaScript values if it defines a `valueToLiteral` method. @@ -26,9 +22,9 @@ import { * | Object | Input Object | * | Array | List | * | Boolean | Boolean | - * | String | String Value | + * | String | String | * | Number | Int / Float | - * | null / undefined | NullValue | + * | null / undefined | Null | * * Note: This function does not perform any type validation or coercion. */ diff --git a/src/validation/rules/ValuesOfCorrectTypeRule.js b/src/validation/rules/ValuesOfCorrectTypeRule.js index 6d5dc5c1ca8..af3aeeb7ecf 100644 --- a/src/validation/rules/ValuesOfCorrectTypeRule.js +++ b/src/validation/rules/ValuesOfCorrectTypeRule.js @@ -20,6 +20,8 @@ import { } from '../../type/definition'; import type { ValidationContext } from '../ValidationContext'; +import { literalToValue } from '../../utilities/literalToValue'; +import { replaceASTVariables } from '../../utilities/replaceASTVariables'; /** * Value literals of correct type @@ -122,9 +124,13 @@ function isValidValueNode(context: ValidationContext, node: ValueNode): void { } // Scalars and Enums determine if a literal value is valid via parseLiteral(), - // which may throw or return an invalid value to indicate failure. + // or parseValue() which may throw or return an invalid value to indicate + // failure. try { - const parseResult = type.parseLiteral(node, undefined /* variables */); + const constValueNode = replaceASTVariables(node, undefined /* variables */); + const parseResult = type.parseLiteral + ? type.parseLiteral(constValueNode) + : type.parseValue(literalToValue(constValueNode, type)); if (parseResult === undefined) { const typeStr = inspect(locationType); context.reportError(