Skip to content

feat(NODE-4873): support EJSON stringify from BigInt to $numberLong #547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/extended_json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,13 @@ function serializeValue(value: any, options: EJSONSerializeOptions): any {
return { $numberDouble: Object.is(value, -0) ? '-0.0' : value.toString() };
}

if (typeof value === 'bigint') {
if (!options.relaxed) {
return { $numberLong: BigInt.asIntN(64, value).toString() };
}
return Number(BigInt.asIntN(64, value));
}

if (value instanceof RegExp || isRegExp(value)) {
let flags = value.flags;
if (flags === undefined) {
Expand Down
101 changes: 100 additions & 1 deletion test/node/bigint.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BSON, BSONError } from '../register-bson';
import { BSON, EJSON, BSONError } from '../register-bson';
import { bufferFromHexArray } from './tools/utils';
import { expect } from 'chai';
import { BSON_DATA_LONG } from '../../src/constants';
Expand Down Expand Up @@ -263,4 +263,103 @@ describe('BSON BigInt support', function () {
expect(serializedMap).to.deep.equal(expectedSerialization);
});
});

describe('EJSON.stringify()', function () {
context('canonical mode (relaxed=false)', function () {
it('truncates bigint values when they are outside the range [BSON_INT64_MIN, BSON_INT64_MAX]', function () {
const numbers = { a: 2n ** 64n + 1n, b: -(2n ** 64n) - 1n };
const serialized = EJSON.stringify(numbers, { relaxed: false });
expect(serialized).to.equal('{"a":{"$numberLong":"1"},"b":{"$numberLong":"-1"}}');
});

it('truncates bigint values in the same way as BSON.serialize', function () {
const number = { a: 0x1234_5678_1234_5678_9999n };
const stringified = EJSON.stringify(number, { relaxed: false });
const serialized = BSON.serialize(number);

const VALUE_OFFSET = 7;
const dataView = BSONDataView.fromUint8Array(serialized);
const serializedValue = dataView.getBigInt64(VALUE_OFFSET, true);
const parsed = JSON.parse(stringified);

expect(parsed).to.have.property('a');
expect(parsed['a']).to.have.property('$numberLong');
expect(parsed.a.$numberLong).to.equal(0x5678_1234_5678_9999n.toString());

expect(parsed.a.$numberLong).to.equal(serializedValue.toString());
});
it('serializes bigint values to numberLong in canonical mode', function () {
const number = { a: 2n };
const serialized = EJSON.stringify(number, { relaxed: false });
expect(serialized).to.equal('{"a":{"$numberLong":"2"}}');
});
});

context('relaxed mode (relaxed=true)', function () {
it('truncates bigint values in the same way as BSON.serialize', function () {
const number = { a: 0x1234_0000_1234_5678_9999n }; // Ensure that the truncated number can be exactly represented as a JS number
const stringified = EJSON.stringify(number, { relaxed: true });
const serializedDoc = BSON.serialize(number);

const VALUE_OFFSET = 7;
const dataView = BSONDataView.fromUint8Array(serializedDoc);
const parsed = JSON.parse(stringified);

expect(parsed).to.have.property('a');
expect(parsed.a).to.equal(0x0000_1234_5678_9999);

expect(parsed.a).to.equal(Number(dataView.getBigInt64(VALUE_OFFSET, true)));
});

it('serializes bigint values to Number', function () {
const number = { a: 10000n };
const serialized = EJSON.stringify(number, { relaxed: true });
expect(serialized).to.equal('{"a":10000}');
});

it('loses precision when serializing bigint values outside of range [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]', function () {
const numbers = { a: -(2n ** 53n) - 1n, b: 2n ** 53n + 2n };
const serialized = EJSON.stringify(numbers, { relaxed: true });
expect(serialized).to.equal('{"a":-9007199254740992,"b":9007199254740994}');
});
});

context('when passed bigint values that are 64 bits wide or less', function () {
let parsed;

before(function () {
const number = { a: 12345n };
const serialized = EJSON.stringify(number, { relaxed: false });
parsed = JSON.parse(serialized);
});

it('passes loose equality checks with native bigint values', function () {
// eslint-disable-next-line eqeqeq
expect(parsed.a.$numberLong == 12345n).true;
});

it('equals the result of BigInt.toString', function () {
expect(parsed.a.$numberLong).to.equal(12345n.toString());
});
});

context('when passed bigint values that are more than 64 bits wide', function () {
let parsed;

before(function () {
const number = { a: 0x1234_5678_1234_5678_9999n };
const serialized = EJSON.stringify(number, { relaxed: false });
parsed = JSON.parse(serialized);
});

it('fails loose equality checks with native bigint values', function () {
// eslint-disable-next-line eqeqeq
expect(parsed.a.$numberLong == 0x1234_5678_1234_5678_9999n).false;
});

it('not equal to results of BigInt.toString', function () {
expect(parsed.a.$numberLong).to.not.equal(0x1234_5678_1234_5678_9999n.toString());
});
});
});
});