From 9b449d94b28d8a6d61b254d8d9f1b4bb8a47db40 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Tue, 11 May 2021 09:54:57 -0700 Subject: [PATCH] Refactor to incorporate review feedback. Always require types. --- src/index.d.ts | 2 +- src/index.js | 2 +- src/jsutils/isSignedInt32.d.ts | 8 + src/jsutils/isSignedInt32.js | 18 + src/type/definition.js | 14 +- src/type/scalars.js | 79 +++- .../__tests__/literalToValue-test.js | 204 ++++++---- .../__tests__/replaceASTVariables-test.js | 52 --- .../__tests__/replaceVariables-test.js | 80 ++++ .../__tests__/valueToLiteral-test.js | 356 +++++++----------- src/utilities/index.d.ts | 2 +- src/utilities/index.js | 2 +- src/utilities/literalToValue.d.ts | 24 +- src/utilities/literalToValue.js | 123 ++++-- ...STVariables.d.ts => replaceVariables.d.ts} | 7 +- ...aceASTVariables.js => replaceVariables.js} | 22 +- src/utilities/valueToLiteral.d.ts | 32 +- src/utilities/valueToLiteral.js | 148 ++++++-- .../rules/ValuesOfCorrectTypeRule.js | 4 +- 19 files changed, 697 insertions(+), 482 deletions(-) create mode 100644 src/jsutils/isSignedInt32.d.ts create mode 100644 src/jsutils/isSignedInt32.js delete mode 100644 src/utilities/__tests__/replaceASTVariables-test.js create mode 100644 src/utilities/__tests__/replaceVariables-test.js rename src/utilities/{replaceASTVariables.d.ts => replaceVariables.d.ts} (71%) rename src/utilities/{replaceASTVariables.js => replaceVariables.js} (60%) diff --git a/src/index.d.ts b/src/index.d.ts index 24d3bfbcf11..ce27f80f7c9 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -417,7 +417,7 @@ export { TypeInfo, visitWithTypeInfo, // Converts a value to a const value by replacing variables. - replaceASTVariables, + replaceVariables, // Create a GraphQL Literal AST from a JavaScript input value. valueToLiteral, // Create a JavaScript input value from a GraphQL Literal AST. diff --git a/src/index.js b/src/index.js index 576f8df1025..dc1ec6b3f21 100644 --- a/src/index.js +++ b/src/index.js @@ -406,7 +406,7 @@ export { TypeInfo, visitWithTypeInfo, // Converts a value to a const value by replacing variables. - replaceASTVariables, + replaceVariables, // Create a GraphQL Literal AST from a JavaScript input value. valueToLiteral, // Create a JavaScript input value from a GraphQL Literal AST. diff --git a/src/jsutils/isSignedInt32.d.ts b/src/jsutils/isSignedInt32.d.ts new file mode 100644 index 00000000000..8a5f0229e69 --- /dev/null +++ b/src/jsutils/isSignedInt32.d.ts @@ -0,0 +1,8 @@ +/** + * As per the GraphQL Spec, Integers are only treated as valid when a valid + * 32-bit signed integer, providing the broadest support across platforms. + * + * n.b. JavaScript's integers are safe between -(2^53 - 1) and 2^53 - 1 because + * they are internally represented as IEEE 754 doubles. + */ +export function isSignedInt32(value: unknown): value is number; diff --git a/src/jsutils/isSignedInt32.js b/src/jsutils/isSignedInt32.js new file mode 100644 index 00000000000..b7656f0c59b --- /dev/null +++ b/src/jsutils/isSignedInt32.js @@ -0,0 +1,18 @@ +const MAX_INT32 = 2147483647; +const MIN_INT32 = -2147483648; + +/** + * As per the GraphQL Spec, Integers are only treated as valid when a valid + * 32-bit signed integer, providing the broadest support across platforms. + * + * n.b. JavaScript's integers are safe between -(2^53 - 1) and 2^53 - 1 because + * they are internally represented as IEEE 754 doubles. + */ +export function isSignedInt32(value: mixed): boolean %checks { + return ( + typeof value === 'number' && + Number.isInteger(value) && + value <= MAX_INT32 && + value >= MIN_INT32 + ); +} diff --git a/src/type/definition.js b/src/type/definition.js index dd8079842b5..b721a7e81c8 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -1383,12 +1383,14 @@ export class GraphQLEnumType /* */ { } valueToLiteral(value: mixed): ?ConstValueNode { - if (typeof value === 'string') { - // https://spec.graphql.org/draft/#Name - if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(value)) { - return { kind: Kind.ENUM, value }; - } - return { kind: Kind.STRING, value }; + if (typeof value === 'string' && this.getValue(value)) { + return { kind: Kind.ENUM, value }; + } + } + + literalToValue(valueNode: ConstValueNode): mixed { + if (valueNode.kind === Kind.ENUM && this.getValue(valueNode.value)) { + return valueNode.value; } } diff --git a/src/type/scalars.js b/src/type/scalars.js index 1286e2ce84a..b138787d64a 100644 --- a/src/type/scalars.js +++ b/src/type/scalars.js @@ -1,5 +1,6 @@ import { inspect } from '../jsutils/inspect'; import { isObjectLike } from '../jsutils/isObjectLike'; +import { isSignedInt32 } from '../jsutils/isSignedInt32'; import type { ConstValueNode } from '../language/ast'; import { Kind } from '../language/kinds'; @@ -7,17 +8,12 @@ import { print } from '../language/printer'; import { GraphQLError } from '../error/GraphQLError'; +import { defaultScalarLiteralToValue } from '../utilities/literalToValue'; +import { defaultScalarValueToLiteral } from '../utilities/valueToLiteral'; + import type { GraphQLNamedType } from './definition'; import { GraphQLScalarType } from './definition'; -// As per the GraphQL Spec, Integers are only treated as valid when a valid -// 32-bit signed integer, providing the broadest support across platforms. -// -// n.b. JavaScript's integers are safe between -(2^53 - 1) and 2^53 - 1 because -// they are internally represented as IEEE 754 doubles. -const MAX_INT = 2147483647; -const MIN_INT = -2147483648; - function serializeInt(outputValue: mixed): number { const coercedValue = serializeObject(outputValue); @@ -35,7 +31,7 @@ function serializeInt(outputValue: mixed): number { `Int cannot represent non-integer value: ${inspect(coercedValue)}`, ); } - if (num > MAX_INT || num < MIN_INT) { + if (!isSignedInt32(num)) { throw new GraphQLError( 'Int cannot represent non 32-bit signed integer value: ' + inspect(coercedValue), @@ -50,7 +46,7 @@ function coerceInt(inputValue: mixed): number { `Int cannot represent non-integer value: ${inspect(inputValue)}`, ); } - if (inputValue > MAX_INT || inputValue < MIN_INT) { + if (!isSignedInt32(inputValue)) { throw new GraphQLError( `Int cannot represent non 32-bit signed integer value: ${inputValue}`, ); @@ -72,7 +68,7 @@ export const GraphQLInt: GraphQLScalarType = new GraphQLScalarType({ ); } const num = parseInt(valueNode.value, 10); - if (num > MAX_INT || num < MIN_INT) { + if (!isSignedInt32(num)) { throw new GraphQLError( `Int cannot represent non 32-bit signed integer value: ${valueNode.value}`, valueNode, @@ -80,6 +76,19 @@ export const GraphQLInt: GraphQLScalarType = new GraphQLScalarType({ } return num; }, + valueToLiteral(value) { + if (isSignedInt32(value)) { + return defaultScalarValueToLiteral(value); + } + }, + literalToValue(valueNode) { + if (valueNode.kind === Kind.INT) { + const value = defaultScalarLiteralToValue(valueNode); + if (isSignedInt32(value)) { + return value; + } + } + }, }); function serializeFloat(outputValue: mixed): number { @@ -126,6 +135,17 @@ 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; + } + }, + literalToValue(valueNode) { + if (valueNode.kind === Kind.FLOAT || valueNode.kind === Kind.INT) { + return defaultScalarLiteralToValue(valueNode); + } + }, }); // Support serializing objects with custom valueOf() or toJSON() functions - @@ -190,6 +210,17 @@ export const GraphQLString: GraphQLScalarType = new GraphQLScalarType({ } return valueNode.value; }, + valueToLiteral(value) { + const literal = defaultScalarValueToLiteral(value); + if (literal.kind === Kind.STRING) { + return literal; + } + }, + literalToValue(valueNode) { + if (valueNode.kind === Kind.STRING) { + return defaultScalarLiteralToValue(valueNode); + } + }, }); function serializeBoolean(outputValue: mixed): boolean { @@ -229,6 +260,17 @@ export const GraphQLBoolean: GraphQLScalarType = new GraphQLScalarType({ } return valueNode.value; }, + valueToLiteral(value) { + const literal = defaultScalarValueToLiteral(value); + if (literal.kind === Kind.BOOLEAN) { + return literal; + } + }, + literalToValue(valueNode) { + if (valueNode.kind === Kind.BOOLEAN) { + return defaultScalarLiteralToValue(valueNode); + } + }, }); function serializeID(outputValue: mixed): string { @@ -270,17 +312,18 @@ export const GraphQLID: GraphQLScalarType = new GraphQLScalarType({ return valueNode.value; }, valueToLiteral(value: mixed): ?ConstValueNode { - // ID types can use Int literals. - if (typeof value === 'string') { - if (/^-?(?:0|[1-9][0-9]*)$/.test(value)) { - return { kind: Kind.INT, value }; - } - return { kind: Kind.STRING, 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 }; } }, literalToValue(valueNode: ConstValueNode): mixed { // ID Int literals are represented as string values. - if (valueNode.kind === Kind.INT || valueNode.kind === Kind.STRING) { + if (valueNode.kind === Kind.STRING || valueNode.kind === Kind.INT) { return valueNode.value; } }, diff --git a/src/utilities/__tests__/literalToValue-test.js b/src/utilities/__tests__/literalToValue-test.js index 8f6f60c37c4..5ccf809b0ab 100644 --- a/src/utilities/__tests__/literalToValue-test.js +++ b/src/utilities/__tests__/literalToValue-test.js @@ -1,12 +1,6 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; -import { - GraphQLID, - GraphQLInt, - GraphQLFloat, - GraphQLBoolean, -} from '../../type/scalars'; import { GraphQLList, GraphQLNonNull, @@ -14,66 +8,70 @@ import { GraphQLEnumType, GraphQLInputObjectType, } from '../../type/definition'; +import { + GraphQLBoolean, + GraphQLInt, + GraphQLFloat, + GraphQLString, + GraphQLID, +} from '../../type/scalars'; -import { parseValue, parseConstValue } from '../../language/parser'; -import { literalToValue } from '../literalToValue'; import { Kind } from '../../language/kinds'; +import { parseValue, parseConstValue } from '../../language/parser'; + +import { literalToValue, defaultScalarLiteralToValue } from '../literalToValue'; describe('literalToValue', () => { - it('does not allow variables', () => { - // $FlowExpectedError[incompatible-call] - expect(() => literalToValue(parseValue('$var'))).to.throw('Unexpected'); - }); + function test(value, type, expected) { + return expect(literalToValue(parseConstValue(value), type)).to.deep.equal( + expected, + ); + } it('converts null ASTs to values', () => { - expect(literalToValue(parseConstValue('null'))).to.equal(null); + test('null', GraphQLString, null); + test('null', new GraphQLNonNull(GraphQLString), undefined); }); it('converts boolean ASTs to values', () => { - expect(literalToValue(parseConstValue('true'))).to.equal(true); - expect(literalToValue(parseConstValue('false'))).to.equal(false); + test('true', GraphQLBoolean, true); + test('false', GraphQLBoolean, false); + test('"false"', GraphQLBoolean, undefined); }); it('converts Int ASTs to Int values', () => { - expect(literalToValue(parseConstValue('0'))).to.equal(0); - expect(literalToValue(parseConstValue('-1'))).to.equal(-1); - expect(literalToValue(parseConstValue('1000'))).to.equal(1000); + test('0', GraphQLInt, 0); + test('-1', GraphQLInt, -1); + test('2147483647', GraphQLInt, 2147483647); + test('2147483648', GraphQLInt, undefined); + test('0.5', GraphQLInt, undefined); }); - it('converts Float ASTs to Float values', () => { - expect(literalToValue(parseConstValue('123.5'))).to.equal(123.5); - expect(literalToValue(parseConstValue('2e40'))).to.equal(2e40); + it('converts Int/Float ASTs to Float values', () => { + test('123.5', GraphQLFloat, 123.5); + test('2e40', GraphQLFloat, 2e40); + test('1099511627776', GraphQLFloat, 1099511627776); + test('"0.5"', GraphQLFloat, undefined); }); it('converts String ASTs to String values', () => { - expect(literalToValue(parseConstValue('"hello world"'))).to.equal( - 'hello world', - ); - expect(literalToValue(parseConstValue('"NAME"'))).to.equal('NAME'); - }); - - it('does not do type checking or coercion', () => { - expect(literalToValue(parseConstValue('0'), GraphQLBoolean)).to.equal(0); - expect(literalToValue(parseConstValue('1.23'), GraphQLInt)).to.equal(1.23); - expect(literalToValue(parseConstValue('"123"'), GraphQLInt)).to.equal( - '123', - ); - const NonNullBoolean = new GraphQLNonNull(GraphQLBoolean); - expect(literalToValue(parseConstValue('null'), NonNullBoolean)).to.equal( - null, - ); + test('"hello world"', GraphQLString, 'hello world'); + test('"NAME"', GraphQLString, 'NAME'); + test('"""multiline"""', GraphQLString, 'multiline'); + test('123', GraphQLString, undefined); }); it('converts ID Int/String ASTs to string values', () => { - expect( - literalToValue(parseConstValue('"hello world"'), GraphQLID), - ).to.equal('hello world'); - expect(literalToValue(parseConstValue('"NAME"'), GraphQLID)).to.equal( - 'NAME', + test('"hello world"', GraphQLID, 'hello world'); + test('123', GraphQLID, '123'); + test('"123"', GraphQLID, '123'); + test( + '123456789123456789123456789123456789', + GraphQLID, + '123456789123456789123456789123456789', ); - expect(literalToValue(parseConstValue('123'), GraphQLID)).to.equal('123'); - expect(literalToValue(parseConstValue('"123"'), GraphQLID)).to.equal('123'); - expect(literalToValue(parseConstValue('123.0'), GraphQLID)).to.equal(123); + test('123.0', GraphQLID, undefined); + test('NAME', GraphQLID, undefined); }); const myEnum = new GraphQLEnumType({ @@ -85,45 +83,42 @@ describe('literalToValue', () => { }); it('converts Enum ASTs to string values', () => { - expect(literalToValue(parseConstValue('HELLO'))).to.equal('HELLO'); - expect(literalToValue(parseConstValue('HELLO'), myEnum)).to.equal('HELLO'); - expect(literalToValue(parseConstValue('COMPLEX'), myEnum)).to.equal( - 'COMPLEX', - ); - expect(literalToValue(parseConstValue('GOODBYE'), myEnum)).to.equal( - 'GOODBYE', - ); + test('HELLO', myEnum, 'HELLO'); + test('COMPLEX', myEnum, 'COMPLEX'); + // Undefined Enum + test('GOODBYE', myEnum, undefined); + // String value is not an Enum + test('"HELLO"', myEnum, undefined); }); it('converts List ASTs to array values', () => { - expect(literalToValue(parseConstValue('["FOO", BAR]'))).to.deep.equal([ - 'FOO', - 'BAR', - ]); - - expect( - literalToValue( - parseConstValue('["123", 123]'), - new GraphQLList(GraphQLID), - ), - ).to.deep.equal(['123', '123']); + test('["FOO", "BAR"]', new GraphQLList(GraphQLString), ['FOO', 'BAR']); + test('["123", 123]', new GraphQLList(GraphQLID), ['123', '123']); + // Invalid items create an invalid result + test('["FOO", BAR]', new GraphQLList(GraphQLString), undefined); + // Does not coerce items to list singletons + test('"FOO"', new GraphQLList(GraphQLString), 'FOO'); }); const inputObj = new GraphQLInputObjectType({ name: 'MyInputObj', fields: { - foo: { type: GraphQLFloat }, + foo: { type: new GraphQLNonNull(GraphQLFloat) }, bar: { type: GraphQLID }, }, }); it('converts input objects', () => { - expect( - literalToValue(parseConstValue('{ foo: 3, bar: 3 }'), inputObj), - ).to.deep.equal({ - foo: 3, - bar: '3', - }); + test('{ foo: 3, bar: 3 }', inputObj, { foo: 3, bar: '3' }); + test('{ foo: 3 }', inputObj, { foo: 3 }); + // Non-object is invalid + test('123', inputObj, undefined); + // Invalid fields create an invalid result + test('{ foo: "3" }', inputObj, undefined); + // Missing required fields create an invalid result + test('{ bar: 3 }', inputObj, undefined); + // Additional fields create an invalid result + test('{ foo: 3, unknown: 3 }', inputObj, undefined); }); it('custom scalar types may define literalToValue', () => { @@ -136,11 +131,66 @@ describe('literalToValue', () => { }, }); - expect(literalToValue(parseConstValue('FOO'), customScalar)).to.equal( - '#FOO', - ); - expect(literalToValue(parseConstValue('"FOO"'), customScalar)).to.equal( - 'FOO', - ); + test('FOO', customScalar, '#FOO'); + test('"FOO"', customScalar, undefined); + }); + + it('custom scalar types may fall back on default literalToValue', () => { + const customScalar = new GraphQLScalarType({ + name: 'CustomScalar', + }); + + test('{ foo: "bar" }', customScalar, { foo: 'bar' }); + }); + + describe('defaultScalarLiteralToValue', () => { + function testDefault(value, expected) { + return expect( + defaultScalarLiteralToValue(parseConstValue(value)), + ).to.deep.equal(expected); + } + + it('does not allow variables', () => { + // $FlowExpectedError[incompatible-call] + expect(() => defaultScalarLiteralToValue(parseValue('$var'))).to.throw( + 'Unexpected', + ); + }); + + it('converts null ASTs to null values', () => { + testDefault('null', null); + }); + + it('converts boolean ASTs to boolean values', () => { + testDefault('true', true); + testDefault('false', false); + }); + + it('converts Int ASTs to number values', () => { + testDefault('0', 0); + testDefault('-1', -1); + testDefault('1099511627776', 1099511627776); + }); + + it('converts Float ASTs to number values', () => { + testDefault('123.5', 123.5); + testDefault('2e40', 2e40); + }); + + it('converts String ASTs to string values', () => { + testDefault('"hello world"', 'hello world'); + }); + + it('converts Enum ASTs to string values', () => { + testDefault('HELLO_WORLD', 'HELLO_WORLD'); + }); + + it('converts List ASTs to array values', () => { + testDefault('["abc", 123, BAR]', ['abc', 123, 'BAR']); + }); + + it('converts Objects ASTs to object values', () => { + testDefault('{ foo: "abc", bar: 123 }', { foo: 'abc', bar: 123 }); + }); }); }); diff --git a/src/utilities/__tests__/replaceASTVariables-test.js b/src/utilities/__tests__/replaceASTVariables-test.js deleted file mode 100644 index 7b620e34fe1..00000000000 --- a/src/utilities/__tests__/replaceASTVariables-test.js +++ /dev/null @@ -1,52 +0,0 @@ -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/__tests__/replaceVariables-test.js b/src/utilities/__tests__/replaceVariables-test.js new file mode 100644 index 00000000000..4b62884df29 --- /dev/null +++ b/src/utilities/__tests__/replaceVariables-test.js @@ -0,0 +1,80 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import type { ReadOnlyObjMap } from '../../jsutils/ObjMap'; +import { invariant } from '../../jsutils/invariant'; + +import type { ValueNode } from '../../language/ast'; +import { parseValue as _parseValue, Parser } from '../../language/parser'; + +import { GraphQLInt } from '../../type/scalars'; +import { GraphQLSchema } from '../../type/schema'; + +import { getVariableValues } from '../../execution/values'; + +import { replaceVariables } from '../replaceVariables'; + +function parseValue(ast: string): ValueNode { + return _parseValue(ast, { noLocation: true }); +} + +function testVariables( + variableDefs: string, + variableValues: ReadOnlyObjMap, +) { + const parser = new Parser(variableDefs, { noLocation: true }); + parser.expectToken(''); + const coercedVariables = getVariableValues( + new GraphQLSchema({ types: [GraphQLInt] }), + parser.parseVariableDefinitions(), + variableValues, + ); + invariant(coercedVariables.coerced); + return coercedVariables.coerced; +} + +describe('replaceVariables', () => { + it('does not change simple AST', () => { + const ast = parseValue('null'); + expect(replaceVariables(ast, undefined)).to.equal(ast); + }); + + it('replaces simple Variables', () => { + const ast = parseValue('$var'); + const vars = testVariables('($var: Int)', { var: 123 }); + expect(replaceVariables(ast, vars)).to.deep.equal(parseValue('123')); + }); + + it('replaces Variables with default values', () => { + const ast = parseValue('$var'); + const vars = testVariables('($var: Int = 123)', {}); + expect(replaceVariables(ast, vars)).to.deep.equal(parseValue('123')); + }); + + it('replaces nested Variables', () => { + const ast = parseValue('{ foo: [ $var ], bar: $var }'); + const vars = testVariables('($var: Int)', { var: 123 }); + expect(replaceVariables(ast, vars)).to.deep.equal( + parseValue('{ foo: [ 123 ], bar: 123 }'), + ); + }); + + it('replaces missing Variables with null', () => { + const ast = parseValue('$var'); + expect(replaceVariables(ast, undefined)).to.deep.equal(parseValue('null')); + }); + + it('replaces missing Variables in lists with null', () => { + const ast = parseValue('[1, $var]'); + expect(replaceVariables(ast, undefined)).to.deep.equal( + parseValue('[1, null]'), + ); + }); + + it('omits missing Variables from objects', () => { + const ast = parseValue('{ foo: 1, bar: $var }'); + expect(replaceVariables(ast, undefined)).to.deep.equal( + parseValue('{ foo: 1 }'), + ); + }); +}); diff --git a/src/utilities/__tests__/valueToLiteral-test.js b/src/utilities/__tests__/valueToLiteral-test.js index f565f85f014..3d4502f5fcc 100644 --- a/src/utilities/__tests__/valueToLiteral-test.js +++ b/src/utilities/__tests__/valueToLiteral-test.js @@ -1,13 +1,6 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; -import { - GraphQLID, - GraphQLInt, - GraphQLFloat, - GraphQLString, - GraphQLBoolean, -} from '../../type/scalars'; import { GraphQLList, GraphQLNonNull, @@ -15,120 +8,71 @@ import { GraphQLEnumType, GraphQLInputObjectType, } from '../../type/definition'; +import { + GraphQLBoolean, + GraphQLInt, + GraphQLFloat, + GraphQLString, + GraphQLID, +} from '../../type/scalars'; + +import { Kind } from '../../language/kinds'; +import { parseConstValue } from '../../language/parser'; -import { valueToLiteral } from '../valueToLiteral'; +import { valueToLiteral, defaultScalarValueToLiteral } from '../valueToLiteral'; describe('valueToLiteral', () => { - it('does not convert some value types', () => { - expect(() => valueToLiteral(Symbol('test'))).to.throw( - 'Cannot convert value to AST: Symbol(test).', + function test(value, type, expected) { + return expect(valueToLiteral(value, type)).to.deep.equal( + expected && parseConstValue(expected, { noLocation: true }), ); - }); + } - it('converts null values to ASTs', () => { - expect(valueToLiteral(null)).to.deep.equal({ - kind: 'NullValue', - }); - - // Note: undefined values are represented as null. - expect(valueToLiteral(undefined)).to.deep.equal({ - kind: 'NullValue', - }); + it('converts null values to Null AST', () => { + test(null, GraphQLString, 'null'); + test(undefined, GraphQLString, 'null'); + test(null, new GraphQLNonNull(GraphQLString), undefined); }); - it('converts boolean values to ASTs', () => { - expect(valueToLiteral(true)).to.deep.equal({ - kind: 'BooleanValue', - value: true, - }); - - expect(valueToLiteral(false)).to.deep.equal({ - kind: 'BooleanValue', - value: false, - }); + it('converts boolean values to Boolean ASTs', () => { + test(true, GraphQLBoolean, 'true'); + test(false, GraphQLBoolean, 'false'); + test('false', GraphQLBoolean, undefined); }); - it('converts non-finite values to null', () => { - expect(valueToLiteral(NaN)).to.deep.equal({ - kind: 'NullValue', - }); - - expect(valueToLiteral(Infinity)).to.deep.equal({ - kind: 'NullValue', - }); + it('converts int number values to Int ASTs', () => { + test(0, GraphQLInt, '0'); + test(-1, GraphQLInt, '-1'); + test(2147483647, GraphQLInt, '2147483647'); + test(2147483648, GraphQLInt, undefined); + test(0.5, GraphQLInt, undefined); }); - it('converts Int values to Int ASTs', () => { - expect(valueToLiteral(-1)).to.deep.equal({ - kind: 'IntValue', - value: '-1', - }); - - expect(valueToLiteral(123.0)).to.deep.equal({ - kind: 'IntValue', - value: '123', - }); - - expect(valueToLiteral(1e4)).to.deep.equal({ - kind: 'IntValue', - value: '10000', - }); - }); - - it('converts Float values to Int/Float ASTs', () => { - expect(valueToLiteral(123.5)).to.deep.equal({ - kind: 'FloatValue', - value: '123.5', - }); - - expect(valueToLiteral(1e40)).to.deep.equal({ - kind: 'FloatValue', - value: '1e+40', - }); + it('converts float number values to Float ASTs', () => { + test(123.5, GraphQLFloat, '123.5'); + test(2e40, GraphQLFloat, '2e+40'); + test(1099511627776, GraphQLFloat, '1099511627776'); + test('0.5', GraphQLFloat, undefined); + // Non-finite + test(NaN, GraphQLFloat, undefined); + test(Infinity, GraphQLFloat, undefined); }); - it('converts String values to String ASTs', () => { - expect(valueToLiteral('hello world')).to.deep.equal({ - kind: 'StringValue', - value: 'hello world', - }); - - expect(valueToLiteral('NAME')).to.deep.equal({ - kind: 'StringValue', - value: 'NAME', - }); + it('converts String ASTs to String values', () => { + test('hello world', GraphQLString, '"hello world"'); + test(123, GraphQLString, undefined); }); it('converts ID values to Int/String ASTs', () => { - expect(valueToLiteral('hello world', GraphQLID)).to.deep.equal({ - kind: 'StringValue', - value: 'hello world', - }); - - expect(valueToLiteral('NAME', GraphQLID)).to.deep.equal({ - kind: 'StringValue', - value: 'NAME', - }); - - expect(valueToLiteral(123, GraphQLID)).to.deep.equal({ - kind: 'IntValue', - value: '123', - }); - - expect(valueToLiteral('123', GraphQLID)).to.deep.equal({ - kind: 'IntValue', - value: '123', - }); - - expect(valueToLiteral('123.5', GraphQLID)).to.deep.equal({ - kind: 'StringValue', - value: '123.5', - }); - - expect(valueToLiteral('001', GraphQLID)).to.deep.equal({ - kind: 'StringValue', - value: '001', - }); + test('hello world', GraphQLID, '"hello world"'); + test('123', GraphQLID, '123'); + test(123, GraphQLID, '123'); + test( + '123456789123456789123456789123456789', + GraphQLID, + '123456789123456789123456789123456789', + ); + test(123.5, GraphQLID, undefined); }); const myEnum = new GraphQLEnumType({ @@ -139,145 +83,119 @@ describe('valueToLiteral', () => { }, }); - it('converts string values to Enum ASTs if possible', () => { - expect(valueToLiteral('HELLO', myEnum)).to.deep.equal({ - kind: 'EnumValue', - value: 'HELLO', - }); - - expect(valueToLiteral('COMPLEX', myEnum)).to.deep.equal({ - kind: 'EnumValue', - value: 'COMPLEX', - }); + it('converts Enum names to Enum ASTs', () => { + test('HELLO', myEnum, 'HELLO'); + test('COMPLEX', myEnum, 'COMPLEX'); + // Undefined Enum + test('GOODBYE', myEnum, undefined); + test(123, myEnum, undefined); + }); - expect(valueToLiteral('GOODBYE', myEnum)).to.deep.equal({ - kind: 'EnumValue', - value: 'GOODBYE', - }); + it('converts List ASTs to array values', () => { + test(['FOO', 'BAR'], new GraphQLList(GraphQLString), '["FOO", "BAR"]'); + test(['123', 123], new GraphQLList(GraphQLID), '[123, 123]'); + // Invalid items create an invalid result + test(['FOO', 123], new GraphQLList(GraphQLString), undefined); + // Does not coerce items to list singletons + test('FOO', new GraphQLList(GraphQLString), '"FOO"'); + }); - // Non-names are string value - expect(valueToLiteral('hello friend', myEnum)).to.deep.equal({ - kind: 'StringValue', - value: 'hello friend', - }); + const inputObj = new GraphQLInputObjectType({ + name: 'MyInputObj', + fields: { + foo: { type: new GraphQLNonNull(GraphQLFloat) }, + bar: { type: GraphQLID }, + }, }); - it('does not do type checking or coercion', () => { - expect(valueToLiteral(0, GraphQLBoolean)).to.deep.equal({ - kind: 'IntValue', - value: '0', - }); + it('converts input objects', () => { + test({ foo: 3, bar: '3' }, inputObj, '{ foo: 3, bar: 3 }'); + test({ foo: 3 }, inputObj, '{ foo: 3 }'); - expect(valueToLiteral(1.23, GraphQLInt)).to.deep.equal({ - kind: 'FloatValue', - value: '1.23', - }); + // Non-object is invalid + test('123', inputObj, undefined); - expect(valueToLiteral('123', GraphQLInt)).to.deep.equal({ - kind: 'StringValue', - value: '123', - }); + // Invalid fields create an invalid result + test({ foo: '3' }, inputObj, undefined); - const NonNullBoolean = new GraphQLNonNull(GraphQLBoolean); - expect(valueToLiteral(null, NonNullBoolean)).to.deep.equal({ - kind: 'NullValue', - }); + // Missing required fields create an invalid result + test({ bar: 3 }, inputObj, undefined); - expect(valueToLiteral(123, myEnum)).to.deep.equal({ - kind: 'IntValue', - value: '123', - }); + // Additional fields create an invalid result + test({ foo: 3, unknown: 3 }, inputObj, undefined); }); - it('converts array values to List ASTs', () => { - expect( - valueToLiteral(['FOO', 'BAR'], new GraphQLList(GraphQLString)), - ).to.deep.equal({ - kind: 'ListValue', - values: [ - { kind: 'StringValue', value: 'FOO' }, - { kind: 'StringValue', value: 'BAR' }, - ], + it('custom scalar types may define valueToLiteral', () => { + const customScalar = new GraphQLScalarType({ + name: 'CustomScalar', + valueToLiteral(value) { + if (typeof value === 'string' && value[0] === '#') { + return { kind: Kind.ENUM, value: value.slice(1) }; + } + }, }); - expect( - valueToLiteral(['HELLO', 'GOODBYE'], new GraphQLList(myEnum)), - ).to.deep.equal({ - kind: 'ListValue', - values: [ - { kind: 'EnumValue', value: 'HELLO' }, - { kind: 'EnumValue', value: 'GOODBYE' }, - ], + test('#FOO', customScalar, 'FOO'); + test('FOO', customScalar, undefined); + }); + + it('custom scalar types may fall back on default valueToLiteral', () => { + const customScalar = new GraphQLScalarType({ + name: 'CustomScalar', }); - function* listGenerator() { - yield 1; - yield 2; - yield 3; + test({ foo: 'bar' }, customScalar, '{ foo: "bar" }'); + }); + + describe('defaultScalarValueToLiteral', () => { + function testDefault(value, expected) { + return expect(defaultScalarValueToLiteral(value)).to.deep.equal( + expected && parseConstValue(expected, { noLocation: true }), + ); } - expect( - valueToLiteral(listGenerator(), new GraphQLList(GraphQLInt)), - ).to.deep.equal({ - kind: 'ListValue', - values: [ - { kind: 'IntValue', value: '1' }, - { kind: 'IntValue', value: '2' }, - { kind: 'IntValue', value: '3' }, - ], + it('converts null values to Null ASTs', () => { + testDefault(null, 'null'); + testDefault(undefined, 'null'); }); - }); - const inputObj = new GraphQLInputObjectType({ - name: 'MyInputObj', - fields: { - foo: { type: GraphQLFloat }, - bar: { type: myEnum }, - }, - }); + it('converts boolean values to Boolean ASTs', () => { + testDefault(true, 'true'); + testDefault(false, 'false'); + }); - it('converts input objects', () => { - expect(valueToLiteral({ foo: 3, bar: 'HELLO' }, inputObj)).to.deep.equal({ - kind: 'ObjectValue', - fields: [ - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'foo' }, - value: { kind: 'IntValue', value: '3' }, - }, - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'bar' }, - value: { kind: 'EnumValue', value: 'HELLO' }, - }, - ], + it('converts number values to Int/Float ASTs', () => { + testDefault(0, '0'); + testDefault(-1, '-1'); + testDefault(1099511627776, '1099511627776'); + testDefault(123.5, '123.5'); + testDefault(2e40, '2e+40'); }); - }); - it('converts input objects with explicit nulls, omitting undefined', () => { - expect(valueToLiteral({ foo: null, bar: undefined })).to.deep.equal({ - kind: 'ObjectValue', - fields: [ - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'foo' }, - value: { kind: 'NullValue' }, - }, - ], + it('converts non-finite number values to Null ASTs', () => { + testDefault(NaN, 'null'); + testDefault(Infinity, 'null'); }); - }); - it('custom scalar types may define valueToLiteral', () => { - const customScalar = new GraphQLScalarType({ - name: 'CustomScalar', - valueToLiteral(value) { - return { kind: 'StringValue', value: String(value) }; - }, + it('converts String values to String ASTs', () => { + testDefault('hello world', '"hello world"'); + }); + + it('converts array values to List ASTs', () => { + testDefault(['abc', 123], '["abc", 123]'); + }); + + it('converts object values to Object ASTs', () => { + testDefault( + { foo: 'abc', bar: null, baz: undefined }, + '{ foo: "abc", bar: null }', + ); }); - expect(valueToLiteral(123, customScalar)).to.deep.equal({ - kind: 'StringValue', - value: '123', + it('throws on values it cannot convert', () => { + expect(() => defaultScalarValueToLiteral(Symbol())).to.throw( + 'Cannot convert value to AST: Symbol().', + ); }); }); }); diff --git a/src/utilities/index.d.ts b/src/utilities/index.d.ts index ce767cf0e00..a28c8af2295 100644 --- a/src/utilities/index.d.ts +++ b/src/utilities/index.d.ts @@ -73,7 +73,7 @@ export { astFromValue } from './astFromValue'; export { TypeInfo, visitWithTypeInfo } from './TypeInfo'; // Converts a value to a const value by replacing variables. -export { replaceASTVariables } from './replaceASTVariables'; +export { replaceVariables } from './replaceVariables'; // Create a GraphQL Literal AST from a JavaScript input value. export { valueToLiteral } from './valueToLiteral'; diff --git a/src/utilities/index.js b/src/utilities/index.js index d4f5e183ae5..5d94c5a0125 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -71,7 +71,7 @@ export { astFromValue } from './astFromValue'; export { TypeInfo, visitWithTypeInfo } from './TypeInfo'; // Converts a value to a const value by replacing variables. -export { replaceASTVariables } from './replaceASTVariables'; +export { replaceVariables } from './replaceVariables'; // Create a GraphQL Literal AST from a JavaScript input value. export { valueToLiteral } from './valueToLiteral'; diff --git a/src/utilities/literalToValue.d.ts b/src/utilities/literalToValue.d.ts index bd52e6cdcac..e5fc34ebd0d 100644 --- a/src/utilities/literalToValue.d.ts +++ b/src/utilities/literalToValue.d.ts @@ -2,10 +2,21 @@ import { ConstValueNode } from '../language/ast'; import { GraphQLInputType } from '../type/definition'; /** - * Produces a JavaScript value given a GraphQL Value AST. + * Produces a JavaScript value given a GraphQL Value AST and a GraphQL type. * - * A GraphQL type may be provided, which will be used to interpret different - * JavaScript values if it defines a `literalToValue` method. + * Scalar types are converted by calling the `literalToValue` method on that + * type, otherwise the default scalar `literalToValue` method is used, defined + * below. + * + * Note: This function does not perform any coercion. + */ +export function literalToValue( + valueNode: ConstValueNode, + type: GraphQLInputType, +): unknown; + +/** + * The default implementation to convert scalar literals to values. * * | GraphQL Value | JavaScript Value | * | -------------------- | ---------------- | @@ -16,9 +27,6 @@ import { GraphQLInputType } from '../type/definition'; * | Int / Float | Number | * | Null | null | * - * Note: This function does not perform any type validation or coercion. + * @internal */ -export function literalToValue( - valueNode: ConstValueNode, - type?: GraphQLInputType, -): unknown; +export function defaultScalarLiteralToValue(valueNode: ConstValueNode): unknown; diff --git a/src/utilities/literalToValue.js b/src/utilities/literalToValue.js index 3a2bd58a339..702958d9b9e 100644 --- a/src/utilities/literalToValue.js +++ b/src/utilities/literalToValue.js @@ -1,56 +1,119 @@ +import { hasOwnProperty } from '../jsutils/hasOwnProperty'; import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; +import { keyMap } from '../jsutils/keyMap'; import { keyValMap } from '../jsutils/keyValMap'; import { Kind } from '../language/kinds'; import type { ConstValueNode } from '../language/ast'; + import type { GraphQLInputType } from '../type/definition'; import { - getNamedType, - isScalarType, + isNonNullType, + isListType, isInputObjectType, + isLeafType, + isRequiredInput, } from '../type/definition'; /** - * Produces a JavaScript value given a GraphQL Value AST. + * Produces a JavaScript value given a GraphQL Value AST and a GraphQL type. * - * A GraphQL type may be provided, which will be used to interpret different - * JavaScript values if it defines a `literalToValue` method. + * Scalar types are converted by calling the `literalToValue` method on that + * type, otherwise the default scalar `literalToValue` method is used, defined + * below. * - * | 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. + * Note: This function does not perform any coercion. */ export function literalToValue( valueNode: ConstValueNode, - type?: GraphQLInputType, + type: GraphQLInputType, ): mixed { + if (isNonNullType(type)) { + if (valueNode.kind === Kind.NULL) { + return; // Invalid: intentionally return no value. + } + return literalToValue(valueNode, type.ofType); + } + 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)); + if (isListType(type)) { + if (valueNode.kind !== Kind.LIST) { + return literalToValue(valueNode, type.ofType); + } + const value = []; + for (const itemNode of valueNode.values) { + const itemValue = literalToValue(itemNode, type.ofType); + if (itemValue === undefined) { + return; // Invalid: intentionally return no value. + } + value.push(itemValue); + } + return value; } - // Does this type (if provided) define `literalToValue` which returns a value? - if (isScalarType(namedType) && namedType.literalToValue != null) { - const literal = namedType.literalToValue(valueNode); - if (literal !== undefined) { - return literal; + if (isInputObjectType(type)) { + if (valueNode.kind !== Kind.OBJECT) { + return; // Invalid: intentionally return no value. + } + const value = {}; + const fieldDefs = type.getFields(); + const hasUndefinedField = valueNode.fields.some( + (field) => !hasOwnProperty(fieldDefs, field.name.value), + ); + if (hasUndefinedField) { + return; // Invalid: intentionally return no value. } + const fieldNodes = keyMap(valueNode.fields, (field) => field.name.value); + for (const field of Object.values(fieldDefs)) { + const fieldNode = fieldNodes[field.name]; + if (!fieldNode) { + if (isRequiredInput(field)) { + return; // Invalid: intentionally return no value. + } + } else { + const fieldValue = literalToValue(fieldNode.value, field.type); + if (fieldValue === undefined) { + return; // Invalid: intentionally return no value. + } + value[field.name] = fieldValue; + } + } + return value; + } + + // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') + if (isLeafType(type)) { + return type.literalToValue + ? type.literalToValue(valueNode) + : defaultScalarLiteralToValue(valueNode); } + // istanbul ignore next (Not reachable. All possible input types have been considered) + invariant(false, 'Unexpected input type: ' + inspect((type: empty))); +} + +/** + * The default implementation to convert scalar literals to values. + * + * | GraphQL Value | JavaScript Value | + * | -------------------- | ---------------- | + * | Input Object | Object | + * | List | Array | + * | Boolean | Boolean | + * | String / Enum | String | + * | Int / Float | Number | + * | Null | null | + * + * @internal + */ +export function defaultScalarLiteralToValue(valueNode: ConstValueNode): mixed { switch (valueNode.kind) { + case Kind.NULL: + return null; case Kind.BOOLEAN: case Kind.STRING: case Kind.ENUM: @@ -59,17 +122,13 @@ export function literalToValue( return parseInt(valueNode.value, 10); case Kind.FLOAT: return parseFloat(valueNode.value); + case Kind.LIST: + return valueNode.values.map((node) => defaultScalarLiteralToValue(node)); 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); - }, + (field) => defaultScalarLiteralToValue(field.value), ); } } diff --git a/src/utilities/replaceASTVariables.d.ts b/src/utilities/replaceVariables.d.ts similarity index 71% rename from src/utilities/replaceASTVariables.d.ts rename to src/utilities/replaceVariables.d.ts index f07c5f30408..5eb002f2c93 100644 --- a/src/utilities/replaceASTVariables.d.ts +++ b/src/utilities/replaceVariables.d.ts @@ -1,5 +1,4 @@ -import { Maybe } from '../jsutils/Maybe'; -import { ObjMap } from '../jsutils/ObjMap'; +import { VariableValues } from '../execution/values'; import { ValueNode, ConstValueNode } from '../language/ast'; @@ -10,7 +9,7 @@ import { ValueNode, ConstValueNode } from '../language/ast'; * Used primarily to ensure only complete constant values are used during input * coercion of custom scalars which accept complex literals. */ -export function replaceASTVariables( +export function replaceVariables( valueNode: ValueNode, - variables: Maybe>, + variables: VariableValues, ): ConstValueNode; diff --git a/src/utilities/replaceASTVariables.js b/src/utilities/replaceVariables.js similarity index 60% rename from src/utilities/replaceASTVariables.js rename to src/utilities/replaceVariables.js index 5e4fb3feb3e..d686f752a08 100644 --- a/src/utilities/replaceASTVariables.js +++ b/src/utilities/replaceVariables.js @@ -1,9 +1,9 @@ -import type { ObjMap } from '../jsutils/ObjMap'; - import type { ValueNode, ConstValueNode } from '../language/ast'; import { Kind } from '../language/kinds'; import { visit } from '../language/visitor'; +import type { VariableValues } from '../execution/values'; + import { valueToLiteral } from './valueToLiteral'; /** @@ -13,13 +13,23 @@ import { valueToLiteral } from './valueToLiteral'; * Used primarily to ensure only complete constant values are used during input * coercion of custom scalars which accept complex literals. */ -export function replaceASTVariables( +export function replaceVariables( valueNode: ValueNode, - variables: ?ObjMap, + variables: ?VariableValues, ): ConstValueNode { return visit(valueNode, { Variable(node) { - return valueToLiteral(variables?.[node.name.value]); + const variableSource = variables?.sources[node.name.value]; + if (!variableSource) { + return { kind: Kind.NULL }; + } + if ( + variableSource.value === undefined && + variableSource.variable.defaultValue + ) { + return variableSource.variable.defaultValue; + } + return valueToLiteral(variableSource.value, variableSource.type); }, ObjectValue(node) { return { @@ -28,7 +38,7 @@ export function replaceASTVariables( fields: node.fields.filter( (field) => field.value.kind !== Kind.VARIABLE || - variables?.[field.value.name.value] !== undefined, + variables?.sources[field.value.name.value], ), }; }, diff --git a/src/utilities/valueToLiteral.d.ts b/src/utilities/valueToLiteral.d.ts index 1872f31453c..398a3c08d1a 100644 --- a/src/utilities/valueToLiteral.d.ts +++ b/src/utilities/valueToLiteral.d.ts @@ -1,28 +1,34 @@ +import { Maybe } from '../jsutils/Maybe'; + import { ConstValueNode } from '../language/ast'; import { GraphQLInputType } 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: + * Produces a GraphQL Value AST given a JavaScript value and a GraphQL type. * - * valueToLiteral("value", GraphQLString) + * Scalar types are converted by calling the `valueToLiteral` method on that + * type, otherwise the default scalar `valueToLiteral` method is used, defined + * below. * - * A GraphQL type may be provided, which will be used to interpret different - * JavaScript values if it defines a `valueToLiteral` method. + * Note: This function does not perform any coercion. + */ +export function valueToLiteral( + value: unknown, + type: GraphQLInputType, +): Maybe; + +/** + * The default implementation to convert scalar values to literals. * * | JavaScript Value | GraphQL Value | * | ----------------- | -------------------- | * | 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. + * @internal */ -export function valueToLiteral( - value: unknown, - type?: GraphQLInputType, -): ConstValueNode; +export function defaultScalarValueToLiteral(value: unknown): ConstValueNode; diff --git a/src/utilities/valueToLiteral.js b/src/utilities/valueToLiteral.js index a4c13f18884..913b48a48cd 100644 --- a/src/utilities/valueToLiteral.js +++ b/src/utilities/valueToLiteral.js @@ -1,21 +1,107 @@ +import { hasOwnProperty } from '../jsutils/hasOwnProperty'; import { inspect } from '../jsutils/inspect'; +import { invariant } from '../jsutils/invariant'; import { isIterableObject } from '../jsutils/isIterableObject'; +import { isObjectLike } from '../jsutils/isObjectLike'; -import type { ConstValueNode } from '../language/ast'; import { Kind } from '../language/kinds'; +import type { ConstValueNode } from '../language/ast'; import type { GraphQLInputType } from '../type/definition'; import { - getNamedType, - isLeafType, + isNonNullType, + isListType, isInputObjectType, + isLeafType, + isRequiredInput, } from '../type/definition'; /** - * Produces a GraphQL Value AST given a JavaScript value. + * Produces a GraphQL Value AST given a JavaScript value and a GraphQL type. * - * A GraphQL type may be provided, which will be used to interpret different - * JavaScript values if it defines a `valueToLiteral` method. + * Scalar types are converted by calling the `valueToLiteral` method on that + * type, otherwise the default scalar `valueToLiteral` method is used, defined + * below. + * + * Note: This function does not perform any coercion. + */ +export function valueToLiteral( + value: mixed, + type: GraphQLInputType, +): ?ConstValueNode { + if (isNonNullType(type)) { + if (value == null) { + return; // Invalid: intentionally return no value. + } + return valueToLiteral(value, type.ofType); + } + + // Like JSON, a null literal is produced for null and undefined. + if (value == null) { + return { kind: Kind.NULL }; + } + + if (isListType(type)) { + if (!isIterableObject(value)) { + return valueToLiteral(value, type.ofType); + } + const values = []; + for (const itemValue of value) { + const itemNode = valueToLiteral(itemValue, type.ofType); + if (!itemNode) { + return; // Invalid: intentionally return no value. + } + values.push(itemNode); + } + return { kind: Kind.LIST, values }; + } + + if (isInputObjectType(type)) { + if (!isObjectLike(value)) { + return; // Invalid: intentionally return no value. + } + const fields = []; + const fieldDefs = type.getFields(); + const hasUndefinedField = Object.keys(value).some( + (name) => !hasOwnProperty(fieldDefs, name), + ); + if (hasUndefinedField) { + return; // Invalid: intentionally return no value. + } + for (const field of Object.values(type.getFields())) { + const fieldValue = value[field.name]; + if (fieldValue === undefined) { + if (isRequiredInput(field)) { + return; // Invalid: intentionally return no value. + } + } else { + const fieldNode = valueToLiteral(value[field.name], field.type); + if (!fieldNode) { + return; // Invalid: intentionally return no value. + } + fields.push({ + kind: Kind.OBJECT_FIELD, + name: { kind: Kind.NAME, value: field.name }, + value: fieldNode, + }); + } + } + return { kind: Kind.OBJECT, fields }; + } + + // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') + if (isLeafType(type)) { + return type.valueToLiteral + ? type.valueToLiteral(value) + : defaultScalarValueToLiteral(value); + } + + // istanbul ignore next (Not reachable. All possible input types have been considered) + invariant(false, 'Unexpected input type: ' + inspect((type: empty))); +} + +/** + * The default implementation to convert scalar values to literals. * * | JavaScript Value | GraphQL Value | * | ----------------- | -------------------- | @@ -26,66 +112,46 @@ import { * | Number | Int / Float | * | null / undefined | Null | * - * Note: This function does not perform any type validation or coercion. + * @internal */ -export function valueToLiteral( - value: mixed, - type?: GraphQLInputType, -): ConstValueNode { +export function defaultScalarValueToLiteral(value: mixed): ConstValueNode { // Like JSON, a null literal is produced for null and undefined. if (value == null) { return { kind: Kind.NULL }; } - const namedType = type && getNamedType(type); - - // Convert JavaScript array to GraphQL list. - if (isIterableObject(value)) { - return { - kind: Kind.LIST, - values: Array.from(value, (item) => valueToLiteral(item, namedType)), - }; - } - - // Does this type (if provided) define `valueToLiteral` which returns a value? - if (isLeafType(namedType) && namedType.valueToLiteral != null) { - const literal = namedType.valueToLiteral(value); - if (literal) { - return literal; - } - } - - // Otherwise, perform a JS-to-Literal default equivalency. switch (typeof value) { case 'boolean': return { kind: Kind.BOOLEAN, value }; case 'string': - return { kind: Kind.STRING, value }; + return { kind: Kind.STRING, value, block: false }; case 'number': { if (!Number.isFinite(value)) { // Like JSON, a null literal is produced for non-finite values. return { kind: Kind.NULL }; } - const stringNum = String(value); - // Use Int literals for integer numbers. - return /^-?(?:0|[1-9][0-9]*)$/.test(stringNum) - ? { kind: Kind.INT, value: stringNum } - : { kind: Kind.FLOAT, value: stringNum }; + const stringValue = String(value); + // Will parse as an IntValue. + return /^-?(?:0|[1-9][0-9]*)$/.test(stringValue) + ? { kind: Kind.INT, value: stringValue } + : { kind: Kind.FLOAT, value: stringValue }; } case 'object': { + if (isIterableObject(value)) { + return { + kind: Kind.LIST, + values: Array.from(value, defaultScalarValueToLiteral), + }; + } const fields = []; - const fieldDefs = isInputObjectType(namedType) - ? namedType.getFields() - : undefined; for (const fieldName of Object.keys(value)) { const fieldValue = value[fieldName]; // Like JSON, undefined fields are not included in the literal result. if (fieldValue !== undefined) { - const fieldDef = fieldDefs && fieldDefs[fieldName]; fields.push({ kind: Kind.OBJECT_FIELD, name: { kind: Kind.NAME, value: fieldName }, - value: valueToLiteral(value[fieldName], fieldDef && fieldDef.type), + value: defaultScalarValueToLiteral(value[fieldName]), }); } } diff --git a/src/validation/rules/ValuesOfCorrectTypeRule.js b/src/validation/rules/ValuesOfCorrectTypeRule.js index 5b55b275483..fa3d024d7df 100644 --- a/src/validation/rules/ValuesOfCorrectTypeRule.js +++ b/src/validation/rules/ValuesOfCorrectTypeRule.js @@ -21,7 +21,7 @@ import { import type { ValidationContext } from '../ValidationContext'; import { literalToValue } from '../../utilities/literalToValue'; -import { replaceASTVariables } from '../../utilities/replaceASTVariables'; +import { replaceVariables } from '../../utilities/replaceVariables'; /** * Value literals of correct type @@ -123,7 +123,7 @@ function isValidValueNode(context: ValidationContext, node: ValueNode): void { return; } - const constValueNode = replaceASTVariables(node, undefined /* variables */); + const constValueNode = replaceVariables(node, undefined /* variables */); // Scalars and Enums determine if a literal value is valid via parseLiteral(), // or parseValue() which may throw or return an invalid value to indicate