diff --git a/package-lock.json b/package-lock.json index c7cfd9d8..55a0ef03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@typescript-eslint/eslint-plugin": "^5.30.0", "@typescript-eslint/parser": "^5.30.0", "array-includes": "^3.1.3", + "array.prototype.flatmap": "^1.3.0", "benchmark": "^2.1.4", "chai": "^4.2.0", "eslint": "^8.18.0", @@ -2614,6 +2615,24 @@ "node": ">=8" } }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", + "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -3914,6 +3933,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, "node_modules/es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -11233,6 +11261,18 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "array.prototype.flatmap": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", + "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + } + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -12248,6 +12288,15 @@ "unbox-primitive": "^1.0.2" } }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", diff --git a/package.json b/package.json index a070c00b..669f4063 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@typescript-eslint/eslint-plugin": "^5.30.0", "@typescript-eslint/parser": "^5.30.0", "array-includes": "^3.1.3", + "array.prototype.flatmap": "^1.3.0", "benchmark": "^2.1.4", "chai": "^4.2.0", "eslint": "^8.18.0", diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index bd306c95..3f103616 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -35,6 +35,8 @@ export interface DeserializeOptions { promoteBuffers?: boolean; /** when deserializing will promote BSON values to their Node.js closest equivalent types. */ promoteValues?: boolean; + /** when deserializing will return UUID type, if promoteBuffers is also true then promoteUUIDs will take precedence and a buffer will not be returned */ + promoteUUIDs?: boolean; /** allow to specify if there what fields we wish to return as unserialized raw buffer. */ fieldsAsRaw?: Document; /** return BSON regular expressions as BSONRegExp instances. */ @@ -135,6 +137,7 @@ function deserializeObject( const promoteBuffers = options['promoteBuffers'] == null ? false : options['promoteBuffers']; const promoteLongs = options['promoteLongs'] == null ? true : options['promoteLongs']; const promoteValues = options['promoteValues'] == null ? true : options['promoteValues']; + const promoteUUIDs = options.promoteUUIDs == null ? false : options.promoteUUIDs; // Ensures default validation option if none given const validation = options.validation == null ? { utf8: true } : options.validation; @@ -413,7 +416,9 @@ function deserializeObject( throw new BSONError('Binary type with subtype 0x02 contains too short binary size'); } - if (promoteBuffers && promoteValues) { + if (promoteUUIDs && subType === 4) { + value = new Binary(buffer.slice(index, index + binarySize), subType).toUUID(); + } else if (promoteBuffers && promoteValues) { value = buffer.slice(index, index + binarySize); } else { value = new Binary(buffer.slice(index, index + binarySize), subType); @@ -440,7 +445,9 @@ function deserializeObject( _buffer[i] = buffer[index + i]; } - if (promoteBuffers && promoteValues) { + if (promoteUUIDs && subType === 4) { + value = new Binary(_buffer, subType).toUUID(); + } else if (promoteBuffers && promoteValues) { value = _buffer; } else { value = new Binary(_buffer, subType); diff --git a/test/node/uuid_tests.js b/test/node/uuid_tests.js index f48360d8..c35177c8 100644 --- a/test/node/uuid_tests.js +++ b/test/node/uuid_tests.js @@ -191,4 +191,65 @@ describe('UUID', () => { expect(plainUUIDSerialization).to.deep.equal(toBinarySerialization); }); }); + + describe('deserialize', () => { + const originalUUID = new BSON.UUID(); + const binaryUUID = originalUUID.toBinary(); + const serializedUUID = BSON.serialize({ uuid: originalUUID.toBinary() }); + + it('should promoteUUIDs when flag is true', () => { + const { uuid: promotedUUID } = BSON.deserialize(serializedUUID, { promoteUUIDs: true }); + expect(promotedUUID._bsontype).to.equal('UUID'); + expect(promotedUUID).to.deep.equal(originalUUID); + }); + + it('should not promoteUUIDs when flag is false', () => { + const { uuid: unpromotedUUID } = BSON.deserialize(serializedUUID, { promoteUUIDs: false }); + expect(unpromotedUUID._bsontype).to.equal('Binary'); + expect(unpromotedUUID).to.deep.equal(binaryUUID); + }); + + it('should not promoteUUIDs when flag is omitted', () => { + const { uuid: omittedFlagUUID } = BSON.deserialize(serializedUUID); + expect(omittedFlagUUID._bsontype).to.equal('Binary'); + expect(omittedFlagUUID).to.deep.equal(binaryUUID); + }); + + it('should throw BSONTypeError if _bsontype is not UUID and promoteUUIDs is true', () => { + const binaryVar = new Binary(Buffer.from('abc'), BSON_BINARY_SUBTYPE_UUID_NEW); + const serializedBinary = BSON.serialize({ d: binaryVar }); + expect(() => { + BSON.deserialize(serializedBinary, { promoteUUIDs: true }); + }).to.throw(BSONTypeError); + }); + + describe('promoteBuffers', () => { + const promoteUUIDValues = [true, false, undefined]; + const promoteBufferValues = [true, false, undefined]; + + const testCases = promoteUUIDValues.flatMap(promoteUUIDs => + promoteBufferValues.flatMap(promoteBuffers => ({ + options: { promoteUUIDs, promoteBuffers }, + // promoteBuffers: true returns a Buffer so _bsontype does not exist + outcome: promoteUUIDs ? 'UUID' : promoteBuffers ? undefined : 'Binary' + })) + ); + + for (const { options, outcome } of testCases) { + it(`should deserialize to ${outcome} type when promoteUUIDs is ${options.promoteUUIDs} and promoteBuffers is ${options.promoteBuffers}`, () => { + const { uuid } = BSON.deserialize(serializedUUID, options); + expect(uuid._bsontype).to.equal(outcome); + if (uuid._bsontype === 'UUID') { + expect(uuid.id).to.deep.equal(originalUUID.id); + } else if (uuid._bsontype === 'Binary') { + expect(uuid.buffer).to.deep.equal(originalUUID.id); + } else if (uuid._bsontype === undefined) { + expect(uuid).to.deep.equal(originalUUID.id); + } else { + expect.fail('Unexpected _bsontype'); + } + }); + } + }); + }); }); diff --git a/test/register-bson.js b/test/register-bson.js index a8240154..712641f1 100644 --- a/test/register-bson.js +++ b/test/register-bson.js @@ -10,6 +10,7 @@ require('chai/register-expect'); require('array-includes/auto'); require('object.entries/auto'); +require('array.prototype.flatmap/auto'); const BSON = require('../lib/bson'); const { ensureBuffer } = require('../lib/ensure_buffer');