From 2f7db7c98abc583777d6101cd9647fbee8f9674c Mon Sep 17 00:00:00 2001 From: Hafez Date: Thu, 4 Dec 2025 02:02:36 +0100 Subject: [PATCH 1/2] feat(document): add flattenUUIDs option to `toObject()` and `toJSON()` --- lib/document.js | 2 + lib/helpers/clone.js | 8 +++ lib/options.js | 3 +- test/document.test.js | 104 ++++++++++++++++++++++++++++++++++++ test/types/document.test.ts | 52 ++++++++++++++++++ types/document.d.ts | 8 +++ types/index.d.ts | 42 +++++++++------ 7 files changed, 203 insertions(+), 16 deletions(-) diff --git a/lib/document.js b/lib/document.js index 45da5eb3f13..477bf158a15 100644 --- a/lib/document.js +++ b/lib/document.js @@ -4132,6 +4132,7 @@ Document.prototype.$__toObjectShallow = function $__toObjectShallow(schemaFields * @param {Boolean} [options.versionKey=true] if false, exclude the version key (`__v` by default) from the output * @param {Boolean} [options.flattenMaps=false] if true, convert Maps to POJOs. Useful if you want to `JSON.stringify()` the result of `toObject()`. * @param {Boolean} [options.flattenObjectIds=false] if true, convert any ObjectIds in the result to 24 character hex strings. + * @param {Boolean} [options.flattenUUIDs=false] if true, convert any UUIDs in the result to 36 character hex strings. * @param {Boolean} [options.useProjection=false] - If true, omits fields that are excluded in this document's projection. Unless you specified a projection, this will omit any field that has `select: false` in the schema. * @param {Boolean} [options.schemaFieldsOnly=false] - If true, the resulting object will only have fields that are defined in the document's schema. By default, `toObject()` returns all fields in the underlying document from MongoDB, including ones that are not listed in the schema. * @return {Object} document as a plain old JavaScript object (POJO). This object may contain ObjectIds, Maps, Dates, mongodb.Binary, Buffers, and other non-POJO values. @@ -4404,6 +4405,7 @@ function omitDeselectedFields(self, json) { * @param {Object} options * @param {Boolean} [options.flattenMaps=true] if true, convert Maps to [POJOs](https://masteringjs.io/tutorials/fundamentals/pojo). Useful if you want to `JSON.stringify()` the result. * @param {Boolean} [options.flattenObjectIds=false] if true, convert any ObjectIds in the result to 24 character hex strings. + * @param {Boolean} [options.flattenUUIDs=false] if true, convert any UUIDs in the result to 36 character hex strings. * @param {Boolean} [options.schemaFieldsOnly=false] - If true, the resulting object will only have fields that are defined in the document's schema. By default, `toJSON()` returns all fields in the underlying document from MongoDB, including ones that are not listed in the schema. * @return {Object} * @see Document#toObject https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject() diff --git a/lib/helpers/clone.js b/lib/helpers/clone.js index 575d78ca3cd..caa76b3f239 100644 --- a/lib/helpers/clone.js +++ b/lib/helpers/clone.js @@ -12,6 +12,7 @@ const isPOJO = require('./isPOJO'); const symbols = require('./symbols'); const trustedSymbol = require('./query/trusted').trustedSymbol; const BSON = require('mongodb/lib/bson'); +const UUID = BSON.UUID; /** * Object clone with Mongoose natives support. @@ -102,6 +103,13 @@ function clone(obj, options, isArrayChild) { return Decimal.fromString(obj.toString()); } + if (obj instanceof UUID) { + if (options?.flattenUUIDs) { + return obj.toJSON(); + } + return new UUID(obj.buffer); + } + // object created with Object.create(null) if (!objConstructor && isObject(obj)) { return cloneObject(obj, options, isArrayChild); diff --git a/lib/options.js b/lib/options.js index bbdcda8b97e..3651dd6db9f 100644 --- a/lib/options.js +++ b/lib/options.js @@ -13,5 +13,6 @@ exports.internalToObjectOptions = { flattenDecimals: false, useProjection: false, versionKey: true, - flattenObjectIds: false + flattenObjectIds: false, + flattenUUIDs: false }; diff --git a/test/document.test.js b/test/document.test.js index beb8c890244..1d4ee21f9a4 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -7041,6 +7041,110 @@ describe('document', function() { }); }); + describe('`flattenUUIDs` option (gh-15021)', function() { + it('converts UUIDs to strings in toObject()', function() { + // Arrange + const { User, UUID } = createTestContext(); + const user = new User({ + _id: new UUID('00000000-0000-0000-0000-000000000000'), + uuid: new UUID('11111111-1111-1111-1111-111111111111'), + nested: { + uuid: new UUID('22222222-2222-2222-2222-222222222222') + }, + subdocument: { + _id: new UUID('33333333-3333-3333-3333-333333333333') + }, + documentArray: [{ _id: new UUID('44444444-4444-4444-4444-444444444444') }] + }); + + // Act + const userObj = user.toObject({ flattenUUIDs: true }); + + // Assert + assert.deepStrictEqual(userObj, { + _id: '00000000-0000-0000-0000-000000000000', + uuid: '11111111-1111-1111-1111-111111111111', + nested: { + uuid: '22222222-2222-2222-2222-222222222222' + }, + subdocument: { + _id: '33333333-3333-3333-3333-333333333333' + }, + documentArray: [{ _id: '44444444-4444-4444-4444-444444444444' }] + }); + }); + + it('converts UUIDs to strings in toJSON()', function() { + // Arrange + const { User, UUID } = createTestContext(); + const user = new User({ + _id: new UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'), + uuid: new UUID('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'), + nested: { uuid: new UUID('cccccccc-cccc-cccc-cccc-cccccccccccc') }, + subdocument: { _id: new UUID('dddddddd-dddd-dddd-dddd-dddddddddddd') }, + documentArray: [{ _id: new UUID('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee') }] + }); + + // Act + const userJSON = user.toJSON({ flattenUUIDs: true }); + + // Assert + assert.deepStrictEqual(userJSON, { + _id: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + uuid: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + nested: { + uuid: 'cccccccc-cccc-cccc-cccc-cccccccccccc' + }, + subdocument: { + _id: 'dddddddd-dddd-dddd-dddd-dddddddddddd' + }, + documentArray: [{ _id: 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' }] + }); + }); + + for (const flattenUUIDs of [false, undefined]) { + it(`does not convert UUIDs when flattenUUIDs is \`${flattenUUIDs}\``, function() { + // Arrange + const { User, UUID } = createTestContext(); + const testUUID = new UUID('12345678-1234-1234-1234-123456789012'); + const user = new User({ + _id: testUUID, + uuid: testUUID, + nested: { uuid: testUUID }, + subdocument: { _id: testUUID }, + documentArray: [{ _id: testUUID }] + }); + + // Act + const obj = user.toObject({ flattenUUIDs }); + + // Assert + assert.ok(obj._id instanceof UUID); + assert.ok(obj.uuid instanceof UUID); + assert.ok(obj.nested.uuid instanceof UUID); + assert.ok(obj.subdocument._id instanceof UUID); + assert.ok(obj.documentArray[0]._id instanceof UUID); + }); + } + + + function createTestContext() { + const UUID = mongoose.Types.UUID; + const userSchema = new Schema({ + _id: 'UUID', + uuid: 'UUID', + nested: { + uuid: 'UUID' + }, + subdocument: new Schema({ _id: 'UUID' }), + documentArray: [new Schema({ _id: 'UUID' })] + }, { versionKey: false }); + + const User = db.model('User', userSchema); + return { User, UUID }; + } + }); + it('`collection` property with strict: false (gh-7276)', async function() { const schema = new Schema({}, { strict: false, versionKey: false }); const Model = db.model('Test', schema); diff --git a/test/types/document.test.ts b/test/types/document.test.ts index de0c742c246..6863c46f6a8 100644 --- a/test/types/document.test.ts +++ b/test/types/document.test.ts @@ -651,3 +651,55 @@ async function gh15578() { expectType(jsonWithVersionKey.taco); } } + +function testFlattenUUIDs() { + interface RawDocType { + _id: Types.UUID; + uuid: Types.UUID; + } + + const ASchema = new Schema({ + _id: Schema.Types.UUID, + uuid: Schema.Types.UUID + }); + + const AModel = model('UUIDModel', ASchema); + + const a = new AModel({ + uuid: new Types.UUID() + }); + + // Test flattenUUIDs: true converts UUIDs to strings + const toObjectFlattened: Omit & { _id: string, uuid: string } = a.toObject({ flattenUUIDs: true }); + const toJSONFlattened: Omit & { _id: string, uuid: string } = a.toJSON({ flattenUUIDs: true }); + + expectType(toObjectFlattened._id); + expectType(toObjectFlattened.uuid); + expectType(toJSONFlattened._id); + expectType(toJSONFlattened.uuid); + + // Test with virtuals + const toObjectWithVirtuals: Omit & { _id: string, uuid: string } = a.toObject({ flattenUUIDs: true, virtuals: true }); + const toJSONWithVirtuals: Omit & { _id: string, uuid: string } = a.toJSON({ flattenUUIDs: true, virtuals: true }); + + expectType(toObjectWithVirtuals._id); + expectType(toObjectWithVirtuals.uuid); + expectType(toJSONWithVirtuals._id); + expectType(toJSONWithVirtuals.uuid); + + // Test flattenUUIDs: false (default behavior - should remain UUID) + const toObjectNotFlattened = a.toObject({ flattenUUIDs: false }); + const toJSONNotFlattened = a.toJSON({ flattenUUIDs: false }); + expectType(toObjectNotFlattened._id); + expectType(toObjectNotFlattened.uuid); + expectType(toJSONNotFlattened._id); + expectType(toJSONNotFlattened.uuid); + + // Test default (no flattenUUIDs option - should remain UUID) + const toObjectDefault = a.toObject(); + const toJSONDefault = a.toJSON(); + expectType(toObjectDefault._id); + expectType(toObjectDefault.uuid); + expectType(toJSONDefault._id); + expectType(toJSONDefault.uuid); +} diff --git a/types/document.d.ts b/types/document.d.ts index 3332ba40079..a87fd0b2888 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -287,6 +287,10 @@ declare module 'mongoose' { toJSON(options: ToObjectOptions & { flattenMaps: true, virtuals: true }): FlattenMaps>; toJSON(options: ToObjectOptions & { flattenMaps: true }): FlattenMaps>; + // flattenUUIDs overloads + toJSON(options: ToObjectOptions & { flattenUUIDs: true, virtuals: true }): UUIDToString>; + toJSON(options: ToObjectOptions & { flattenUUIDs: true }): UUIDToString>; + // Handle versionKey: false (regardless of the others - most specific overloads should come first) toJSON(options: ToObjectOptions & { versionKey: false, flattenMaps: true, flattenObjectIds: true, virtuals: true }): ObjectIdToString>, '__v'>>; toJSON(options: ToObjectOptions & { versionKey: false, flattenMaps: false, flattenObjectIds: true, virtuals: true }): ObjectIdToString, '__v'>>; @@ -335,6 +339,10 @@ declare module 'mongoose' { toObject(options: ToObjectOptions & { flattenMaps: true, virtuals: true }): FlattenMaps>; toObject(options: ToObjectOptions & { flattenMaps: true }): FlattenMaps>; + // flattenUUIDs overloads + toObject(options: ToObjectOptions & { flattenUUIDs: true, virtuals: true }): UUIDToString>; + toObject(options: ToObjectOptions & { flattenUUIDs: true }): UUIDToString>; + // Handle versionKey: false (regardless of the others - most specific overloads should come first) toObject(options: ToObjectOptions & { versionKey: false, flattenMaps: true, flattenObjectIds: true, virtuals: true }): ObjectIdToString>, '__v'>>; toObject(options: ToObjectOptions & { versionKey: false, flattenMaps: false, flattenObjectIds: true, virtuals: true }): ObjectIdToString, '__v'>>; diff --git a/types/index.d.ts b/types/index.d.ts index a445427e4c7..d0dc992938e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -242,6 +242,8 @@ declare module 'mongoose' { flattenMaps?: boolean; /** if true, convert any ObjectIds in the result to 24 character hex strings. */ flattenObjectIds?: boolean; + /** if true, convert any UUIDs in the result to 36 character hex strings. */ + flattenUUIDs?: boolean; /** apply all getters (path and virtual getters) */ getters?: boolean; /** remove empty objects (defaults to true) */ @@ -927,23 +929,33 @@ declare module 'mongoose' { } : T; /** - * Converts any Buffer properties into "{ type: 'buffer', data: [1, 2, 3] }" format for JSON serialization + * Converts any UUID properties into strings for JSON serialization */ - export type UUIDToJSON = T extends mongodb.UUID + export type UUIDToString = T extends Types.UUID ? string - : T extends Document - ? T - : T extends TreatAsPrimitives + : T extends mongodb.UUID + ? string + : T extends Document ? T - : T extends Record ? { - [K in keyof T]: T[K] extends mongodb.UUID - ? string - : T[K] extends Types.DocumentArray - ? Types.DocumentArray> - : T[K] extends Types.Subdocument - ? HydratedSingleSubdocument - : UUIDToJSON; - } : T; + : T extends TreatAsPrimitives + ? T + : T extends Record ? { + [K in keyof T]: T[K] extends Types.UUID + ? string + : T[K] extends mongodb.UUID + ? string + : T[K] extends Types.DocumentArray + ? Types.DocumentArray> + : T[K] extends Types.Subdocument + ? HydratedSingleSubdocument> + : UUIDToString; + } : T; + + /** + * Alias for UUIDToString for backwards compatibility. + * @deprecated Use UUIDToString instead. + */ + export type UUIDToJSON = UUIDToString; /** * Converts any ObjectId properties into strings for JSON serialization @@ -1004,7 +1016,7 @@ declare module 'mongoose' { FlattenMaps< BufferToJSON< ObjectIdToString< - UUIDToJSON< + UUIDToString< DateToString > > From b3d449c03bde7924d74dc6b7d846c6a063ac8e3f Mon Sep 17 00:00:00 2001 From: Hafez Date: Thu, 4 Dec 2025 02:06:01 +0100 Subject: [PATCH 2/2] test(types): remove explicit type casting --- test/types/document.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/types/document.test.ts b/test/types/document.test.ts index 6863c46f6a8..c99f3270ea9 100644 --- a/test/types/document.test.ts +++ b/test/types/document.test.ts @@ -670,8 +670,8 @@ function testFlattenUUIDs() { }); // Test flattenUUIDs: true converts UUIDs to strings - const toObjectFlattened: Omit & { _id: string, uuid: string } = a.toObject({ flattenUUIDs: true }); - const toJSONFlattened: Omit & { _id: string, uuid: string } = a.toJSON({ flattenUUIDs: true }); + const toObjectFlattened = a.toObject({ flattenUUIDs: true }); + const toJSONFlattened = a.toJSON({ flattenUUIDs: true }); expectType(toObjectFlattened._id); expectType(toObjectFlattened.uuid); @@ -679,8 +679,8 @@ function testFlattenUUIDs() { expectType(toJSONFlattened.uuid); // Test with virtuals - const toObjectWithVirtuals: Omit & { _id: string, uuid: string } = a.toObject({ flattenUUIDs: true, virtuals: true }); - const toJSONWithVirtuals: Omit & { _id: string, uuid: string } = a.toJSON({ flattenUUIDs: true, virtuals: true }); + const toObjectWithVirtuals = a.toObject({ flattenUUIDs: true, virtuals: true }); + const toJSONWithVirtuals = a.toJSON({ flattenUUIDs: true, virtuals: true }); expectType(toObjectWithVirtuals._id); expectType(toObjectWithVirtuals.uuid);