diff --git a/src/long.ts b/src/long.ts index 9a0d9959..cd2ba6a4 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1,6 +1,6 @@ -import type { Timestamp } from './timestamp'; import type { EJSONOptions } from './extended_json'; import { isObjectLike } from './parser/utils'; +import type { Timestamp } from './timestamp'; interface LongWASMHelpers { /** Gets the high bits of the last operation performed */ @@ -187,6 +187,16 @@ export class Long { return Long.fromBits(value % TWO_PWR_32_DBL | 0, (value / TWO_PWR_32_DBL) | 0, unsigned); } + /** + * Returns a Long representing the given value, provided that it is a finite number. Otherwise, zero is returned. + * @param value - The number in question + * @param unsigned - Whether unsigned or not, defaults to signed + * @returns The corresponding Long value + */ + static fromBigInt(value: bigint, unsigned?: boolean): Long { + return Long.fromString(value.toString(), unsigned); + } + /** * Returns a Long representation of the given string, written using the specified radix. * @param str - The textual representation of the Long @@ -793,6 +803,11 @@ export class Long { return this.high * TWO_PWR_32_DBL + (this.low >>> 0); } + /** Converts the Long to a BigInt (arbitrary precision). */ + toBigInt(): bigint { + return BigInt(this.toString()); + } + /** * Converts this Long to its byte representation. * @param le - Whether little or big endian, defaults to big endian diff --git a/src/parser/serializer.ts b/src/parser/serializer.ts index 0bfdde6b..b3f7a7d0 100644 --- a/src/parser/serializer.ts +++ b/src/parser/serializer.ts @@ -15,7 +15,13 @@ import { Map } from '../map'; import type { MinKey } from '../min_key'; import type { ObjectId } from '../objectid'; import type { BSONRegExp } from '../regexp'; -import { isDate, isUint8Array, normalizedFunctionString } from './utils'; +import { + isBigInt64Array, + isBigUInt64Array, + isDate, + isUint8Array, + normalizedFunctionString +} from './utils'; export interface SerializeOptions { /** the serializer will check if keys are valid. */ @@ -807,6 +813,8 @@ export function serializeInto( index = serializeString(buffer, key, value, index, true); } else if (typeof value === 'number') { index = serializeNumber(buffer, key, value, index, true); + } else if (typeof value === 'bigint') { + throw new TypeError('Unsupported type BigInt, please use Decimal128'); } else if (typeof value === 'boolean') { index = serializeBoolean(buffer, key, value, index, true); } else if (value instanceof Date || isDate(value)) { @@ -913,6 +921,8 @@ export function serializeInto( index = serializeString(buffer, key, value, index); } else if (type === 'number') { index = serializeNumber(buffer, key, value, index); + } else if (type === 'bigint' || isBigInt64Array(value) || isBigUInt64Array(value)) { + throw new TypeError('Unsupported type BigInt, please use Decimal128'); } else if (type === 'boolean') { index = serializeBoolean(buffer, key, value, index); } else if (value instanceof Date || isDate(value)) { @@ -1015,6 +1025,8 @@ export function serializeInto( index = serializeString(buffer, key, value, index); } else if (type === 'number') { index = serializeNumber(buffer, key, value, index); + } else if (type === 'bigint') { + throw new TypeError('Unsupported type BigInt, please use Decimal128'); } else if (type === 'boolean') { index = serializeBoolean(buffer, key, value, index); } else if (value instanceof Date || isDate(value)) { diff --git a/src/parser/utils.ts b/src/parser/utils.ts index b7ee7aba..00befef8 100644 --- a/src/parser/utils.ts +++ b/src/parser/utils.ts @@ -18,6 +18,8 @@ function insecureRandomBytes(size: number): Uint8Array { // eslint-disable-next-line @typescript-eslint/no-explicit-any declare let window: any; declare let require: Function; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +declare let global: any; export let randomBytes = insecureRandomBytes; if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) { @@ -40,6 +42,24 @@ export function isUint8Array(value: unknown): value is Uint8Array { return Object.prototype.toString.call(value) === '[object Uint8Array]'; } +export function isBigInt64Array(value: unknown): value is BigInt64Array { + return Object.prototype.toString.call(value) === '[object BigInt64Array]'; +} + +export function isBigUInt64Array(value: unknown): value is BigUint64Array { + return Object.prototype.toString.call(value) === '[object BigUint64Array]'; +} + +/** Call to check if your environment has `Buffer` */ +export function haveBuffer(): boolean { + return typeof global !== 'undefined' && typeof global.Buffer !== 'undefined'; +} + +/** Callable in any environment to check if value is a Buffer */ +export function isBuffer(value: unknown): value is Buffer { + return haveBuffer() && Buffer.isBuffer(value); +} + // To ensure that 0.4 of node works correctly export function isDate(d: unknown): d is Date { return isObjectLike(d) && Object.prototype.toString.call(d) === '[object Date]'; diff --git a/test/node/bigint_tests.js b/test/node/bigint_tests.js new file mode 100644 index 00000000..b8d17bd3 --- /dev/null +++ b/test/node/bigint_tests.js @@ -0,0 +1,58 @@ +/* globals BigInt */ +'use strict'; + +const BSON = require('../register-bson'); + +describe('BSON BigInt Support', function () { + before(function () { + try { + BigInt(0); + } catch (_) { + this.skip('JS VM does not support BigInt'); + } + }); + it('Should serialize an int that fits in int32', function () { + const testDoc = { b: BigInt(32) }; + expect(() => BSON.serialize(testDoc)).to.throw(TypeError); + + // const serializedDoc = BSON.serialize(testDoc); + // // prettier-ignore + // const resultBuffer = Buffer.from([0x0C, 0x00, 0x00, 0x00, 0x10, 0x62, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00]); + // const resultDoc = BSON.deserialize(serializedDoc); + // expect(Array.from(serializedDoc)).to.have.members(Array.from(resultBuffer)); + // expect(BigInt(resultDoc.b)).to.equal(testDoc.b); + }); + + it('Should serialize an int that fits in int64', function () { + const testDoc = { b: BigInt(0x1ffffffff) }; + expect(() => BSON.serialize(testDoc)).to.throw(TypeError); + + // const serializedDoc = BSON.serialize(testDoc); + // // prettier-ignore + // const resultBuffer = Buffer.from([0x10, 0x00, 0x00, 0x00, 0x12, 0x62, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00]); + // const resultDoc = BSON.deserialize(serializedDoc); + // expect(Array.from(serializedDoc)).to.have.members(Array.from(resultBuffer)); + // expect(BigInt(resultDoc.b)).to.equal(testDoc.b); + }); + + it('Should serialize an int that fits in decimal128', function () { + const testDoc = { b: BigInt('9223372036854776001') }; // int64 max + 1 + expect(() => BSON.serialize(testDoc)).to.throw(TypeError); + + // const serializedDoc = BSON.serialize(testDoc); + // // prettier-ignore + // const resultBuffer = Buffer.from([0x18, 0x00, 0x00, 0x00, 0x13, 0x62, 0x00, 0xC1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x30, 0x00]); + // const resultDoc = BSON.deserialize(serializedDoc); + // expect(Array.from(serializedDoc)).to.have.members(Array.from(resultBuffer)); + // expect(resultDoc.b._bsontype).to.equal('Decimal128'); + // expect(BigInt(resultDoc.b.toString())).to.equal(testDoc.b); + }); + + it('Should throw if BigInt is too large to serialize', function () { + const testDoc = { + b: BigInt('9'.repeat(35)) + }; // decimal 128 can only encode 34 digits of precision + expect(() => BSON.serialize(testDoc)).to.throw(TypeError); + // expect(() => BSON.serialize(testDoc)).to.throw(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 020252b3..4752a1f9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,9 @@ "module": "commonjs", "moduleResolution": "node", "lib": [ - "ES2017" + "ES2017", + "ES2020.BigInt", + "ES2017.TypedArrays" ], "outDir": "lib", // We don't make use of tslib helpers