diff --git a/src/jsutils/isInteger.ts b/src/jsutils/isInteger.ts new file mode 100644 index 0000000000..12adb946bf --- /dev/null +++ b/src/jsutils/isInteger.ts @@ -0,0 +1,7 @@ +export function isInteger(value: unknown): value is number | bigint { + const valueTypeOf = typeof value; + if (valueTypeOf === 'number') { + return Number.isInteger(value); + } + return valueTypeOf === 'bigint'; +} diff --git a/src/jsutils/isNumeric.ts b/src/jsutils/isNumeric.ts new file mode 100644 index 0000000000..f65068b625 --- /dev/null +++ b/src/jsutils/isNumeric.ts @@ -0,0 +1,7 @@ +export function isNumeric(value: unknown): value is number | bigint { + const valueTypeOf = typeof value; + if (valueTypeOf === 'number') { + return Number.isFinite(value); + } + return valueTypeOf === 'bigint'; +} diff --git a/src/type/__tests__/scalars-test.ts b/src/type/__tests__/scalars-test.ts index fbb2cd9087..07753f873c 100644 --- a/src/type/__tests__/scalars-test.ts +++ b/src/type/__tests__/scalars-test.ts @@ -21,6 +21,7 @@ describe('Type System: Specified scalar types', () => { expect(parseValue(1)).to.equal(1); expect(parseValue(0)).to.equal(0); expect(parseValue(-1)).to.equal(-1); + expect(parseValue(1n)).to.equal(1); expect(() => parseValue(9876504321)).to.throw( 'Int cannot represent non 32-bit signed integer value: 9876504321', @@ -119,6 +120,7 @@ describe('Type System: Specified scalar types', () => { expect(serialize(1e5)).to.equal(100000); expect(serialize(false)).to.equal(0); expect(serialize(true)).to.equal(1); + expect(serialize(1n)).to.equal(1); const customValueOfObj = { value: 5, @@ -190,6 +192,7 @@ describe('Type System: Specified scalar types', () => { expect(parseValue(-1)).to.equal(-1); expect(parseValue(0.1)).to.equal(0.1); expect(parseValue(Math.PI)).to.equal(Math.PI); + expect(parseValue(1n)).to.equal(1); expect(() => parseValue(NaN)).to.throw( 'Float cannot represent non numeric value: NaN', @@ -280,6 +283,7 @@ describe('Type System: Specified scalar types', () => { expect(serialize('-1.1')).to.equal(-1.1); expect(serialize(false)).to.equal(0.0); expect(serialize(true)).to.equal(1.0); + expect(serialize(1n)).to.equal(1n); const customValueOfObj = { value: 5.5, @@ -380,6 +384,7 @@ describe('Type System: Specified scalar types', () => { expect(serialize(-1.1)).to.equal('-1.1'); expect(serialize(true)).to.equal('true'); expect(serialize(false)).to.equal('false'); + expect(serialize(9007199254740993n)).to.equal('9007199254740993'); const valueOf = () => 'valueOf string'; const toJSON = () => 'toJSON string'; @@ -493,6 +498,8 @@ describe('Type System: Specified scalar types', () => { expect(serialize(1)).to.equal(true); expect(serialize(0)).to.equal(false); + expect(serialize(1n)).to.equal(true); + expect(serialize(0n)).to.equal(false); expect(serialize(true)).to.equal(true); expect(serialize(false)).to.equal(false); expect( @@ -539,6 +546,9 @@ describe('Type System: Specified scalar types', () => { expect(parseValue(9007199254740991)).to.equal('9007199254740991'); expect(parseValue(-9007199254740991)).to.equal('-9007199254740991'); + // Can handle bigint in JS + expect(parseValue(9007199254740993n)).to.equal('9007199254740993'); + expect(() => parseValue(undefined)).to.throw( 'ID cannot represent value: undefined', ); @@ -614,6 +624,7 @@ describe('Type System: Specified scalar types', () => { expect(serialize(123)).to.equal('123'); expect(serialize(0)).to.equal('0'); expect(serialize(-1)).to.equal('-1'); + expect(serialize(9007199254740993n)).to.equal('9007199254740993'); const valueOf = () => 'valueOf ID'; const toJSON = () => 'toJSON ID'; diff --git a/src/type/scalars.ts b/src/type/scalars.ts index a9003f7d54..814e2ba346 100644 --- a/src/type/scalars.ts +++ b/src/type/scalars.ts @@ -1,4 +1,6 @@ import { inspect } from '../jsutils/inspect.js'; +import { isInteger } from '../jsutils/isInteger.js'; +import { isNumeric } from '../jsutils/isNumeric.js'; import { isObjectLike } from '../jsutils/isObjectLike.js'; import { GraphQLError } from '../error/GraphQLError.js'; @@ -40,7 +42,7 @@ export const GraphQLInt = new GraphQLScalarType({ num = Number(coercedValue); } - if (typeof num !== 'number' || !Number.isInteger(num)) { + if (!isInteger(num)) { throw new GraphQLError( `Int cannot represent non-integer value: ${inspect(coercedValue)}`, ); @@ -51,21 +53,22 @@ export const GraphQLInt = new GraphQLScalarType({ inspect(coercedValue), ); } - return num; + return Number(num); }, parseValue(inputValue) { - if (typeof inputValue !== 'number' || !Number.isInteger(inputValue)) { + if (!isInteger(inputValue)) { throw new GraphQLError( `Int cannot represent non-integer value: ${inspect(inputValue)}`, ); } - if (inputValue > GRAPHQL_MAX_INT || inputValue < GRAPHQL_MIN_INT) { + const coercedVal = Number(inputValue); + if (coercedVal > GRAPHQL_MAX_INT || coercedVal < GRAPHQL_MIN_INT) { throw new GraphQLError( `Int cannot represent non 32-bit signed integer value: ${inputValue}`, ); } - return inputValue; + return coercedVal; }, parseConstLiteral(valueNode) { @@ -96,7 +99,7 @@ export const GraphQLInt = new GraphQLScalarType({ }, }); -export const GraphQLFloat = new GraphQLScalarType({ +export const GraphQLFloat = new GraphQLScalarType({ name: 'Float', description: 'The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).', @@ -113,16 +116,17 @@ export const GraphQLFloat = new GraphQLScalarType({ num = Number(coercedValue); } - if (typeof num !== 'number' || !Number.isFinite(num)) { + if (!isNumeric(num)) { throw new GraphQLError( `Float cannot represent non numeric value: ${inspect(coercedValue)}`, ); } + return num; }, parseValue(inputValue) { - if (typeof inputValue !== 'number' || !Number.isFinite(inputValue)) { + if (!isNumeric(inputValue)) { throw new GraphQLError( `Float cannot represent non numeric value: ${inspect(inputValue)}`, ); @@ -163,8 +167,8 @@ export const GraphQLString = new GraphQLScalarType({ if (typeof coercedValue === 'boolean') { return coercedValue ? 'true' : 'false'; } - if (typeof coercedValue === 'number' && Number.isFinite(coercedValue)) { - return coercedValue.toString(); + if (isNumeric(coercedValue)) { + return String(coercedValue); } throw new GraphQLError( `String cannot represent value: ${inspect(outputValue)}`, @@ -207,8 +211,8 @@ export const GraphQLBoolean = new GraphQLScalarType({ if (typeof coercedValue === 'boolean') { return coercedValue; } - if (Number.isFinite(coercedValue)) { - return coercedValue !== 0; + if (isNumeric(coercedValue)) { + return Number(coercedValue) !== 0; } throw new GraphQLError( `Boolean cannot represent a non boolean value: ${inspect(coercedValue)}`, @@ -252,7 +256,7 @@ export const GraphQLID = new GraphQLScalarType({ if (typeof coercedValue === 'string') { return coercedValue; } - if (Number.isInteger(coercedValue)) { + if (isInteger(coercedValue)) { return String(coercedValue); } throw new GraphQLError( @@ -264,8 +268,8 @@ export const GraphQLID = new GraphQLScalarType({ if (typeof inputValue === 'string') { return inputValue; } - if (typeof inputValue === 'number' && Number.isInteger(inputValue)) { - return inputValue.toString(); + if (isInteger(inputValue)) { + return String(inputValue); } throw new GraphQLError(`ID cannot represent value: ${inspect(inputValue)}`); }, diff --git a/src/utilities/__tests__/astFromValue-test.ts b/src/utilities/__tests__/astFromValue-test.ts index 0f9d474256..69d0b3b97d 100644 --- a/src/utilities/__tests__/astFromValue-test.ts +++ b/src/utilities/__tests__/astFromValue-test.ts @@ -46,6 +46,16 @@ describe('astFromValue', () => { 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({ kind: 'BooleanValue', @@ -69,6 +79,11 @@ describe('astFromValue', () => { value: '10000', }); + expect(astFromValue(1n, GraphQLInt)).to.deep.equal({ + kind: 'IntValue', + value: '1', + }); + // GraphQL spec does not allow coercing non-integer values to Int to avoid // accidental data loss. expect(() => astFromValue(123.5, GraphQLInt)).to.throw( @@ -80,6 +95,16 @@ describe('astFromValue', () => { 'Int cannot represent non 32-bit signed integer value: 1e+40', ); + // Note: outside the bounds of 32bit signed int. + expect(() => astFromValue(9007199254740991, GraphQLInt)).to.throw( + 'Int cannot represent non 32-bit signed integer value: 9007199254740991', + ); + + // Note: outside the bounds of 32bit signed int as BigInt. + expect(() => astFromValue(9007199254740991n, GraphQLInt)).to.throw( + 'Int cannot represent non 32-bit signed integer value: 9007199254740991', + ); + expect(() => astFromValue(NaN, GraphQLInt)).to.throw( 'Int cannot represent non-integer value: NaN', ); @@ -96,6 +121,11 @@ describe('astFromValue', () => { value: '123', }); + expect(astFromValue(9007199254740993n, GraphQLFloat)).to.deep.equal({ + kind: 'IntValue', + value: '9007199254740993', + }); + expect(astFromValue(123.5, GraphQLFloat)).to.deep.equal({ kind: 'FloatValue', value: '123.5', @@ -133,6 +163,11 @@ describe('astFromValue', () => { value: '123', }); + expect(astFromValue(9007199254740993n, GraphQLString)).to.deep.equal({ + kind: 'StringValue', + value: '9007199254740993', + }); + expect(astFromValue(false, GraphQLString)).to.deep.equal({ kind: 'StringValue', value: 'false', @@ -183,6 +218,11 @@ describe('astFromValue', () => { value: '01', }); + expect(astFromValue(9007199254740993n, GraphQLID)).to.deep.equal({ + kind: 'IntValue', + value: '9007199254740993', + }); + expect(() => astFromValue(false, GraphQLID)).to.throw( 'ID cannot represent value: false', ); diff --git a/src/utilities/astFromValue.ts b/src/utilities/astFromValue.ts index bb03baf232..cab61c8a6b 100644 --- a/src/utilities/astFromValue.ts +++ b/src/utilities/astFromValue.ts @@ -118,6 +118,11 @@ export function astFromValue( : { kind: Kind.FLOAT, value: stringNum }; } + if (typeof serialized === 'bigint') { + const stringNum = String(serialized); + return { kind: Kind.INT, value: stringNum }; + } + if (typeof serialized === 'string') { // Enum types use Enum literals. if (isEnumType(type)) {