From 54f77df20172044932cc19178cf21d900b3d57e0 Mon Sep 17 00:00:00 2001
From: Jovi De Croock <decroockjovi@gmail.com>
Date: Thu, 31 Oct 2024 20:54:36 +0100
Subject: [PATCH] Adjust BigInt PR

---
 src/jsutils/isNumeric.ts                     | 15 ++++++++
 src/type/__tests__/scalars-test.ts           | 10 +++++
 src/type/scalars.ts                          | 39 +++++++++++---------
 src/utilities/__tests__/astFromValue-test.ts | 35 ++++++++++++++++++
 src/utilities/astFromValue.ts                |  5 +++
 5 files changed, 87 insertions(+), 17 deletions(-)
 create mode 100644 src/jsutils/isNumeric.ts

diff --git a/src/jsutils/isNumeric.ts b/src/jsutils/isNumeric.ts
new file mode 100644
index 0000000000..47503ad60b
--- /dev/null
+++ b/src/jsutils/isNumeric.ts
@@ -0,0 +1,15 @@
+export function isInteger(value: unknown): value is number | bigint {
+  const valueTypeOf = typeof value;
+  if (valueTypeOf === 'number') {
+    return Number.isInteger(value);
+  }
+  return valueTypeOf === 'bigint';
+}
+
+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 73d32b0efb..bb640b42d9 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(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(9876504321)).to.throw(
         'Int cannot represent non 32-bit signed integer value: 9876504321',
@@ -119,6 +120,7 @@ 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);
 
       const customValueOfObj = {
         value: 5,
@@ -190,6 +192,7 @@ 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(NaN)).to.throw(
         'Float cannot represent non numeric value: NaN',
@@ -280,6 +283,7 @@ 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(1n);
 
       const customValueOfObj = {
         value: 5.5,
@@ -380,6 +384,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(9007199254740993n)).to.equal('9007199254740993');
 
       const valueOf = () => 'valueOf string';
       const toJSON = () => 'toJSON string';
@@ -497,6 +502,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,
@@ -536,6 +543,8 @@ describe('Type System: Specified scalar types', () => {
       expect(coerceInputValue(1)).to.equal('1');
       expect(coerceInputValue(0)).to.equal('0');
       expect(coerceInputValue(-1)).to.equal('-1');
+      // Can handle bigint in JS
+      expect(coerceInputValue(9007199254740993n)).to.equal('9007199254740993');
 
       // Maximum and minimum safe numbers in JS
       expect(coerceInputValue(9007199254740991)).to.equal('9007199254740991');
@@ -620,6 +629,7 @@ 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(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 37428e7146..ae9b66dba5 100644
--- a/src/type/scalars.ts
+++ b/src/type/scalars.ts
@@ -1,4 +1,5 @@
 import { inspect } from '../jsutils/inspect.js';
+import { isInteger, isNumeric } from '../jsutils/isNumeric.js';
 import { isObjectLike } from '../jsutils/isObjectLike.js';
 
 import { GraphQLError } from '../error/GraphQLError.js';
@@ -40,32 +41,36 @@ export const GraphQLInt = new GraphQLScalarType<number>({
       num = Number(coercedValue);
     }
 
-    if (typeof num !== 'number' || !Number.isInteger(num)) {
+    if (!isInteger(num)) {
       throw new GraphQLError(
         `Int cannot represent non-integer value: ${inspect(coercedValue)}`,
       );
     }
+
     if (num > GRAPHQL_MAX_INT || num < GRAPHQL_MIN_INT) {
       throw new GraphQLError(
         'Int cannot represent non 32-bit signed integer value: ' +
           inspect(coercedValue),
       );
     }
-    return num;
+
+    return Number(num);
   },
 
   coerceInputValue(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}`,
+        `Int cannot represent non 32-bit signed integer value: ${coercedVal}`,
       );
     }
-    return inputValue;
+    return coercedVal;
   },
 
   coerceInputLiteral(valueNode) {
@@ -96,7 +101,7 @@ export const GraphQLInt = new GraphQLScalarType<number>({
   },
 });
 
-export const GraphQLFloat = new GraphQLScalarType<number>({
+export const GraphQLFloat = new GraphQLScalarType<number | bigint>({
   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,7 +118,7 @@ export const GraphQLFloat = new GraphQLScalarType<number>({
       num = Number(coercedValue);
     }
 
-    if (typeof num !== 'number' || !Number.isFinite(num)) {
+    if (!isNumeric(num)) {
       throw new GraphQLError(
         `Float cannot represent non numeric value: ${inspect(coercedValue)}`,
       );
@@ -122,12 +127,12 @@ export const GraphQLFloat = new GraphQLScalarType<number>({
   },
 
   coerceInputValue(inputValue) {
-    if (typeof inputValue !== 'number' || !Number.isFinite(inputValue)) {
+    if (!isNumeric(inputValue)) {
       throw new GraphQLError(
         `Float cannot represent non numeric value: ${inspect(inputValue)}`,
       );
     }
-    return inputValue;
+    return typeof inputValue === 'bigint' ? Number(inputValue) : inputValue;
   },
 
   coerceInputLiteral(valueNode) {
@@ -163,8 +168,8 @@ export const GraphQLString = new GraphQLScalarType<string>({
     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 +212,8 @@ export const GraphQLBoolean = new GraphQLScalarType<boolean>({
     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 +257,7 @@ export const GraphQLID = new GraphQLScalarType<string>({
     if (typeof coercedValue === 'string') {
       return coercedValue;
     }
-    if (Number.isInteger(coercedValue)) {
+    if (isInteger(coercedValue)) {
       return String(coercedValue);
     }
     throw new GraphQLError(
@@ -264,8 +269,8 @@ export const GraphQLID = new GraphQLScalarType<string>({
     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 c5aba4a956..94216bbf5b 100644
--- a/src/utilities/__tests__/astFromValue-test.ts
+++ b/src/utilities/__tests__/astFromValue-test.ts
@@ -31,6 +31,16 @@ describe('astFromValue', () => {
       value: false,
     });
 
+    expect(astFromValue(0n, GraphQLBoolean)).to.deep.equal({
+      kind: 'BooleanValue',
+      value: false,
+    });
+
+    expect(astFromValue(1n, GraphQLBoolean)).to.deep.equal({
+      kind: 'BooleanValue',
+      value: true,
+    });
+
     expect(astFromValue(undefined, GraphQLBoolean)).to.deep.equal(null);
 
     expect(astFromValue(null, GraphQLBoolean)).to.deep.equal({
@@ -65,6 +75,16 @@ describe('astFromValue', () => {
       value: '123',
     });
 
+    // 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(1e4, GraphQLInt)).to.deep.equal({
       kind: 'IntValue',
       value: '10000',
@@ -111,6 +131,11 @@ describe('astFromValue', () => {
       kind: 'FloatValue',
       value: '1e+40',
     });
+
+    expect(astFromValue(9007199254740993n, GraphQLFloat)).to.deep.equal({
+      kind: 'IntValue',
+      value: '9007199254740993',
+    });
   });
 
   it('converts String values to String ASTs', () => {
@@ -143,6 +168,11 @@ describe('astFromValue', () => {
       kind: 'NullValue',
     });
 
+    expect(astFromValue(9007199254740993n, GraphQLString)).to.deep.equal({
+      kind: 'StringValue',
+      value: '9007199254740993',
+    });
+
     expect(astFromValue(undefined, GraphQLString)).to.deep.equal(null);
   });
 
@@ -163,6 +193,11 @@ describe('astFromValue', () => {
       value: 'VA\nLUE',
     });
 
+    expect(astFromValue(9007199254740993n, GraphQLID)).to.deep.equal({
+      kind: 'IntValue',
+      value: '9007199254740993',
+    });
+
     // Note: IntValues are used when possible.
     expect(astFromValue(-1, GraphQLID)).to.deep.equal({
       kind: 'IntValue',
diff --git a/src/utilities/astFromValue.ts b/src/utilities/astFromValue.ts
index 3ace598d91..b20027e883 100644
--- a/src/utilities/astFromValue.ts
+++ b/src/utilities/astFromValue.ts
@@ -119,6 +119,11 @@ export function astFromValue(
         : { kind: Kind.FLOAT, value: stringNum };
     }
 
+    if (typeof coerced === 'bigint') {
+      const stringNum = String(coerced);
+      return { kind: Kind.INT, value: stringNum };
+    }
+
     if (typeof coerced === 'string') {
       // Enum types use Enum literals.
       if (isEnumType(type)) {