diff --git a/src/type/__tests__/scalars-test.ts b/src/type/__tests__/scalars-test.ts index c0aafed1cb..437c5fd8b0 100644 --- a/src/type/__tests__/scalars-test.ts +++ b/src/type/__tests__/scalars-test.ts @@ -21,6 +21,9 @@ describe('Type System: Specified scalar types', () => { expect(coerceInputValue(1)).to.equal(1); expect(coerceInputValue(0)).to.equal(0); expect(coerceInputValue(-1)).to.equal(-1); + expect(coerceInputValue(1n)).to.equal(1); + expect(coerceInputValue(0n)).to.equal(0); + expect(coerceInputValue(-1n)).to.equal(-1); expect(() => coerceInputValue(9876504321)).to.throw( 'Int cannot represent non 32-bit signed integer value: 9876504321', @@ -28,6 +31,12 @@ describe('Type System: Specified scalar types', () => { expect(() => coerceInputValue(-9876504321)).to.throw( 'Int cannot represent non 32-bit signed integer value: -9876504321', ); + expect(() => coerceInputValue(2147483648n)).to.throw( + 'Int cannot represent non 32-bit signed integer value: 2147483648', + ); + expect(() => coerceInputValue(-2147483649n)).to.throw( + 'Int cannot represent non 32-bit signed integer value: -2147483649', + ); expect(() => coerceInputValue(0.1)).to.throw( 'Int cannot represent non-integer value: 0.1', ); @@ -119,6 +128,9 @@ describe('Type System: Specified scalar types', () => { expect(coerceOutputValue(1e5)).to.equal(100000); expect(coerceOutputValue(false)).to.equal(0); expect(coerceOutputValue(true)).to.equal(1); + expect(coerceOutputValue(1n)).to.equal(1); + expect(coerceOutputValue(0n)).to.equal(0); + expect(coerceOutputValue(-1n)).to.equal(-1); const customValueOfObj = { value: 5, @@ -157,6 +169,12 @@ describe('Type System: Specified scalar types', () => { expect(() => coerceOutputValue('-9876504321')).to.throw( 'Int cannot represent non 32-bit signed integer value: "-9876504321"', ); + expect(() => coerceOutputValue(2147483648n)).to.throw( + 'Int cannot represent non 32-bit signed integer value: 2147483648', + ); + expect(() => coerceOutputValue(-2147483649n)).to.throw( + 'Int cannot represent non 32-bit signed integer value: -2147483649', + ); // Too big to represent as an Int in JavaScript or GraphQL expect(() => coerceOutputValue(1e100)).to.throw( @@ -196,6 +214,10 @@ describe('Type System: Specified scalar types', () => { expect(coerceInputValue(-1)).to.equal(-1); expect(coerceInputValue(0.1)).to.equal(0.1); expect(coerceInputValue(Math.PI)).to.equal(Math.PI); + expect(coerceInputValue(1n)).to.equal(1); + expect(coerceInputValue(0n)).to.equal(0); + expect(coerceInputValue(-1n)).to.equal(-1); + expect(coerceInputValue(9007199254740992n)).to.equal(9007199254740992); expect(() => coerceInputValue(NaN)).to.throw( 'Float cannot represent non numeric value: NaN', @@ -203,6 +225,12 @@ describe('Type System: Specified scalar types', () => { expect(() => coerceInputValue(Infinity)).to.throw( 'Float cannot represent non numeric value: Infinity', ); + expect(() => coerceInputValue(9007199254740993n)).to.throw( + 'Float cannot represent non numeric value: 9007199254740993 (value would lose precision)', + ); + expect(() => coerceInputValue(2n ** 1024n)).to.throw( + 'Float cannot represent non numeric value: 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216 (value is too large)', + ); expect(() => coerceInputValue(undefined)).to.throw( 'Float cannot represent non numeric value: undefined', @@ -286,6 +314,10 @@ describe('Type System: Specified scalar types', () => { expect(coerceOutputValue('-1.1')).to.equal(-1.1); expect(coerceOutputValue(false)).to.equal(0.0); expect(coerceOutputValue(true)).to.equal(1.0); + expect(coerceOutputValue(1n)).to.equal(1.0); + expect(coerceOutputValue(0n)).to.equal(0.0); + expect(coerceOutputValue(-1n)).to.equal(-1.0); + expect(coerceOutputValue(9007199254740992n)).to.equal(9007199254740992); const customValueOfObj = { value: 5.5, @@ -301,6 +333,12 @@ describe('Type System: Specified scalar types', () => { expect(() => coerceOutputValue(Infinity)).to.throw( 'Float cannot represent non numeric value: Infinity', ); + expect(() => coerceOutputValue(9007199254740993n)).to.throw( + 'Float cannot represent non numeric value: 9007199254740993 (value would lose precision)', + ); + expect(() => coerceOutputValue(2n ** 1024n)).to.throw( + 'Float cannot represent non numeric value: 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216 (value is too large)', + ); expect(() => coerceOutputValue('one')).to.throw( 'Float cannot represent non numeric value: "one"', ); @@ -386,6 +424,7 @@ describe('Type System: Specified scalar types', () => { expect(coerceOutputValue(-1.1)).to.equal('-1.1'); expect(coerceOutputValue(true)).to.equal('true'); expect(coerceOutputValue(false)).to.equal('false'); + expect(coerceOutputValue(123n)).to.equal('123'); const valueOf = () => 'valueOf string'; const toJSON = () => 'toJSON string'; @@ -503,6 +542,8 @@ describe('Type System: Specified scalar types', () => { expect(coerceOutputValue(0)).to.equal(false); expect(coerceOutputValue(true)).to.equal(true); expect(coerceOutputValue(false)).to.equal(false); + expect(coerceOutputValue(1n)).to.equal(true); + expect(coerceOutputValue(0n)).to.equal(false); expect( coerceOutputValue({ value: true, @@ -542,6 +583,9 @@ describe('Type System: Specified scalar types', () => { expect(coerceInputValue(1)).to.equal('1'); expect(coerceInputValue(0)).to.equal('0'); expect(coerceInputValue(-1)).to.equal('-1'); + expect(coerceInputValue(1n)).to.equal('1'); + expect(coerceInputValue(0n)).to.equal('0'); + expect(coerceInputValue(-1n)).to.equal('-1'); // Maximum and minimum safe numbers in JS expect(coerceInputValue(9007199254740991)).to.equal('9007199254740991'); @@ -626,6 +670,9 @@ describe('Type System: Specified scalar types', () => { expect(coerceOutputValue(123)).to.equal('123'); expect(coerceOutputValue(0)).to.equal('0'); expect(coerceOutputValue(-1)).to.equal('-1'); + expect(coerceOutputValue(123n)).to.equal('123'); + expect(coerceOutputValue(0n)).to.equal('0'); + expect(coerceOutputValue(-1n)).to.equal('-1'); const valueOf = () => 'valueOf ID'; const toJSON = () => 'toJSON ID'; diff --git a/src/type/scalars.ts b/src/type/scalars.ts index 39915cff55..ddf2c3294c 100644 --- a/src/type/scalars.ts +++ b/src/type/scalars.ts @@ -40,6 +40,9 @@ export const GraphQLInt = new GraphQLScalarType({ if (typeof coercedValue === 'string') { return coerceIntFromString(coercedValue); } + if (typeof coercedValue === 'bigint') { + return coerceIntFromBigInt(coercedValue); + } throw new GraphQLError( `Int cannot represent non-integer value: ${inspect(coercedValue)}`, ); @@ -49,6 +52,9 @@ export const GraphQLInt = new GraphQLScalarType({ if (typeof inputValue === 'number') { return coerceIntFromNumber(inputValue); } + if (typeof inputValue === 'bigint') { + return coerceIntFromBigInt(inputValue); + } throw new GraphQLError( `Int cannot represent non-integer value: ${inspect(inputValue)}`, ); @@ -72,8 +78,8 @@ export const GraphQLInt = new GraphQLScalarType({ }, valueToLiteral(value) { if ( - typeof value === 'number' && - Number.isInteger(value) && + ((typeof value === 'number' && Number.isInteger(value)) || + typeof value === 'bigint') && value <= GRAPHQL_MAX_INT && value >= GRAPHQL_MIN_INT ) { @@ -99,6 +105,9 @@ export const GraphQLFloat = new GraphQLScalarType({ if (typeof coercedValue === 'string') { return coerceFloatFromString(coercedValue); } + if (typeof coercedValue === 'bigint') { + return coerceFloatFromBigInt(coercedValue); + } throw new GraphQLError( `Float cannot represent non numeric value: ${inspect(coercedValue)}`, ); @@ -108,6 +117,9 @@ export const GraphQLFloat = new GraphQLScalarType({ if (typeof inputValue === 'number') { return coerceFloatFromNumber(inputValue); } + if (typeof inputValue === 'bigint') { + return coerceFloatFromBigInt(inputValue); + } throw new GraphQLError( `Float cannot represent non numeric value: ${inspect(inputValue)}`, ); @@ -149,6 +161,9 @@ export const GraphQLString = new GraphQLScalarType({ if (typeof coercedValue === 'number') { return coerceStringFromNumber(coercedValue); } + if (typeof coercedValue === 'bigint') { + return String(coercedValue); + } throw new GraphQLError( `String cannot represent value: ${inspect(outputValue)}`, ); @@ -193,6 +208,9 @@ export const GraphQLBoolean = new GraphQLScalarType({ if (typeof coercedValue === 'number') { return coerceBooleanFromNumber(coercedValue); } + if (typeof coercedValue === 'bigint') { + return coercedValue !== 0n; + } throw new GraphQLError( `Boolean cannot represent a non boolean value: ${inspect(coercedValue)}`, ); @@ -238,6 +256,9 @@ export const GraphQLID = new GraphQLScalarType({ if (typeof coercedValue === 'number') { return coerceIDFromNumber(coercedValue); } + if (typeof coercedValue === 'bigint') { + return String(coercedValue); + } throw new GraphQLError( `ID cannot represent value: ${inspect(outputValue)}`, ); @@ -250,6 +271,9 @@ export const GraphQLID = new GraphQLScalarType({ if (typeof inputValue === 'number') { return coerceIDFromNumber(inputValue); } + if (typeof inputValue === 'bigint') { + return String(inputValue); + } throw new GraphQLError(`ID cannot represent value: ${inspect(inputValue)}`); }, @@ -274,6 +298,9 @@ export const GraphQLID = new GraphQLScalarType({ if (typeof value === 'number') { return { kind: Kind.INT, value: coerceIDFromNumber(value) }; } + if (typeof value === 'bigint') { + return { kind: Kind.INT, value: String(value) }; + } }, }); @@ -342,6 +369,15 @@ function coerceIntFromString(value: string): number { return num; } +function coerceIntFromBigInt(value: bigint): number { + if (value > GRAPHQL_MAX_INT || value < GRAPHQL_MIN_INT) { + throw new GraphQLError( + `Int cannot represent non 32-bit signed integer value: ${String(value)}`, + ); + } + return Number(value); +} + function coerceFloatFromNumber(value: number): number { if (!Number.isFinite(value)) { throw new GraphQLError( @@ -366,6 +402,21 @@ function coerceFloatFromString(value: string): number { return num; } +function coerceFloatFromBigInt(coercedValue: bigint): number { + const num = Number(coercedValue); + if (!Number.isFinite(num)) { + throw new GraphQLError( + `Float cannot represent non numeric value: ${inspect(coercedValue)} (value is too large)`, + ); + } + if (BigInt(num) !== coercedValue) { + throw new GraphQLError( + `Float cannot represent non numeric value: ${inspect(coercedValue)} (value would lose precision)`, + ); + } + return num; +} + function coerceStringFromNumber(value: number): string { if (!Number.isFinite(value)) { throw new GraphQLError(`String cannot represent value: ${inspect(value)}`); diff --git a/src/utilities/__tests__/astFromValue-test.ts b/src/utilities/__tests__/astFromValue-test.ts index c5aba4a956..18ab2b5a1f 100644 --- a/src/utilities/__tests__/astFromValue-test.ts +++ b/src/utilities/__tests__/astFromValue-test.ts @@ -46,6 +46,14 @@ describe('astFromValue', () => { kind: 'BooleanValue', value: true, }); + expect(astFromValue(0n, GraphQLBoolean)).to.deep.equal({ + kind: 'BooleanValue', + value: false, + }); + expect(astFromValue(1n, GraphQLBoolean)).to.deep.equal({ + kind: 'BooleanValue', + value: true, + }); const NonNullBoolean = new GraphQLNonNull(GraphQLBoolean); expect(astFromValue(0, NonNullBoolean)).to.deep.equal({ @@ -59,6 +67,10 @@ describe('astFromValue', () => { kind: 'IntValue', value: '-1', }); + expect(astFromValue(-1n, GraphQLInt)).to.deep.equal({ + kind: 'IntValue', + value: '-1', + }); expect(astFromValue(123.0, GraphQLInt)).to.deep.equal({ kind: 'IntValue', @@ -80,6 +92,9 @@ describe('astFromValue', () => { expect(() => astFromValue(1e40, GraphQLInt)).to.throw( 'Int cannot represent non 32-bit signed integer value: 1e+40', ); + expect(() => astFromValue(2147483648n, GraphQLInt)).to.throw( + 'Int cannot represent non 32-bit signed integer value: 2147483648', + ); expect(() => astFromValue(NaN, GraphQLInt)).to.throw( 'Int cannot represent non-integer value: NaN', @@ -91,6 +106,10 @@ describe('astFromValue', () => { kind: 'IntValue', value: '-1', }); + expect(astFromValue(-1n, GraphQLFloat)).to.deep.equal({ + kind: 'IntValue', + value: '-1', + }); expect(astFromValue(123.0, GraphQLFloat)).to.deep.equal({ kind: 'IntValue', @@ -111,6 +130,16 @@ describe('astFromValue', () => { kind: 'FloatValue', value: '1e+40', }); + expect(astFromValue(9007199254740992n, GraphQLFloat)).to.deep.equal({ + kind: 'IntValue', + value: '9007199254740992', + }); + expect(() => astFromValue(9007199254740993n, GraphQLFloat)).to.throw( + 'Float cannot represent non numeric value: 9007199254740993 (value would lose precision)', + ); + expect(() => astFromValue(2n ** 1024n, GraphQLFloat)).to.throw( + 'Float cannot represent non numeric value: 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216 (value is too large)', + ); }); it('converts String values to String ASTs', () => { @@ -133,6 +162,10 @@ describe('astFromValue', () => { kind: 'StringValue', value: '123', }); + expect(astFromValue(123n, GraphQLString)).to.deep.equal({ + kind: 'StringValue', + value: '123', + }); expect(astFromValue(false, GraphQLString)).to.deep.equal({ kind: 'StringValue', @@ -173,6 +206,10 @@ describe('astFromValue', () => { kind: 'IntValue', value: '123', }); + expect(astFromValue(123n, GraphQLID)).to.deep.equal({ + kind: 'IntValue', + value: '123', + }); expect(astFromValue('123', GraphQLID)).to.deep.equal({ kind: 'IntValue', @@ -205,6 +242,10 @@ describe('astFromValue', () => { kind: 'StringValue', value: 'value', }); + expect(astFromValue(123n, passthroughScalar)).to.deep.equal({ + kind: 'IntValue', + value: '123', + }); expect(() => astFromValue(NaN, passthroughScalar)).to.throw( 'Cannot convert value to AST: NaN.', diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index bda8225b57..c1440fc4e0 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -121,6 +121,13 @@ describe('coerceInputValue', () => { }); }); + it('converts BigInt values for numeric scalars', () => { + test(1n, GraphQLInt, 1); + test(1n, GraphQLFloat, 1); + test(1n, GraphQLID, '1'); + test(9007199254740993n, GraphQLFloat, undefined); + }); + describe('for GraphQLInputObject', () => { const TestInputObject: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'TestInputObject', diff --git a/src/utilities/__tests__/valueToLiteral-test.ts b/src/utilities/__tests__/valueToLiteral-test.ts index 8db4f869f8..d553993562 100644 --- a/src/utilities/__tests__/valueToLiteral-test.ts +++ b/src/utilities/__tests__/valueToLiteral-test.ts @@ -58,6 +58,13 @@ describe('valueToLiteral', () => { test(0.5, GraphQLInt, undefined); }); + it('converts bigint values to Int ASTs', () => { + test(0n, GraphQLInt, '0'); + test(-1n, GraphQLInt, '-1'); + test(2147483647n, GraphQLInt, '2147483647'); + test(2147483648n, GraphQLInt, undefined); + }); + it('converts float number values to Float ASTs', () => { test(123.5, GraphQLFloat, '123.5'); test(2e40, GraphQLFloat, '2e+40'); @@ -77,6 +84,8 @@ describe('valueToLiteral', () => { test('hello world', GraphQLID, '"hello world"'); test('123', GraphQLID, '123'); test(123, GraphQLID, '123'); + test(123n, GraphQLID, '123'); + test(-123n, GraphQLID, '-123'); test( '123456789123456789123456789123456789', GraphQLID, @@ -202,6 +211,12 @@ describe('valueToLiteral', () => { testDefault('hello world', '"hello world"'); }); + it('converts bigint values to Int ASTs', () => { + testDefault(0n, '0'); + testDefault(-1n, '-1'); + testDefault(9007199254740993n, '9007199254740993'); + }); + it('converts array values to List ASTs', () => { testDefault(['abc', 123], '["abc", 123]'); }); diff --git a/src/utilities/astFromValue.ts b/src/utilities/astFromValue.ts index 72fc70c427..2a647a9bdd 100644 --- a/src/utilities/astFromValue.ts +++ b/src/utilities/astFromValue.ts @@ -34,6 +34,7 @@ import { GraphQLID } from '../type/scalars.js'; * | Boolean | Boolean | * | String | String / Enum Value | * | Number | Int / Float | + * | BigInt | Int | * | Unknown | Enum Value | * | null | NullValue | * @@ -119,6 +120,10 @@ export function astFromValue( : { kind: Kind.FLOAT, value: stringNum }; } + if (typeof coerced === 'bigint') { + return { kind: Kind.INT, value: String(coerced) }; + } + if (typeof coerced === 'string') { // Enum types use Enum literals. if (isEnumType(type)) { diff --git a/src/utilities/valueToLiteral.ts b/src/utilities/valueToLiteral.ts index bb61b41ca5..1ad99832ea 100644 --- a/src/utilities/valueToLiteral.ts +++ b/src/utilities/valueToLiteral.ts @@ -113,6 +113,7 @@ export function valueToLiteral( * | Boolean | Boolean | * | String | String | * | Number | Int / Float | + * | BigInt | Int | * | null / undefined | Null | * * @internal @@ -129,6 +130,8 @@ export function defaultScalarValueToLiteral(value: unknown): ConstValueNode { return { kind: Kind.BOOLEAN, value }; case 'string': return { kind: Kind.STRING, value, block: false }; + case 'bigint': + return { kind: Kind.INT, value: value.toString() }; case 'number': { if (!Number.isFinite(value)) { // Like JSON, a null literal is produced for non-finite values.