Skip to content
Open
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
2 changes: 2 additions & 0 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
8 changes: 8 additions & 0 deletions lib/helpers/clone.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ exports.internalToObjectOptions = {
flattenDecimals: false,
useProjection: false,
versionKey: true,
flattenObjectIds: false
flattenObjectIds: false,
flattenUUIDs: false
};
104 changes: 104 additions & 0 deletions test/document.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
52 changes: 52 additions & 0 deletions test/types/document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,3 +651,55 @@ async function gh15578() {
expectType<number>(jsonWithVersionKey.taco);
}
}

function testFlattenUUIDs() {
interface RawDocType {
_id: Types.UUID;
uuid: Types.UUID;
}

const ASchema = new Schema<RawDocType>({
_id: Schema.Types.UUID,
uuid: Schema.Types.UUID
});

const AModel = model<RawDocType>('UUIDModel', ASchema);

const a = new AModel({
uuid: new Types.UUID()
});

// Test flattenUUIDs: true converts UUIDs to strings
const toObjectFlattened = a.toObject({ flattenUUIDs: true });
const toJSONFlattened = a.toJSON({ flattenUUIDs: true });

expectType<string>(toObjectFlattened._id);
expectType<string>(toObjectFlattened.uuid);
expectType<string>(toJSONFlattened._id);
expectType<string>(toJSONFlattened.uuid);

// Test with virtuals
const toObjectWithVirtuals = a.toObject({ flattenUUIDs: true, virtuals: true });
const toJSONWithVirtuals = a.toJSON({ flattenUUIDs: true, virtuals: true });

expectType<string>(toObjectWithVirtuals._id);
expectType<string>(toObjectWithVirtuals.uuid);
expectType<string>(toJSONWithVirtuals._id);
expectType<string>(toJSONWithVirtuals.uuid);

// Test flattenUUIDs: false (default behavior - should remain UUID)
const toObjectNotFlattened = a.toObject({ flattenUUIDs: false });
const toJSONNotFlattened = a.toJSON({ flattenUUIDs: false });
expectType<Types.UUID>(toObjectNotFlattened._id);
expectType<Types.UUID>(toObjectNotFlattened.uuid);
expectType<Types.UUID>(toJSONNotFlattened._id);
expectType<Types.UUID>(toJSONNotFlattened.uuid);

// Test default (no flattenUUIDs option - should remain UUID)
const toObjectDefault = a.toObject();
const toJSONDefault = a.toJSON();
expectType<Types.UUID>(toObjectDefault._id);
expectType<Types.UUID>(toObjectDefault.uuid);
expectType<Types.UUID>(toJSONDefault._id);
expectType<Types.UUID>(toJSONDefault.uuid);
}
8 changes: 8 additions & 0 deletions types/document.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ declare module 'mongoose' {
toJSON(options: ToObjectOptions & { flattenMaps: true, virtuals: true }): FlattenMaps<Require_id<DocType & TVirtuals>>;
toJSON(options: ToObjectOptions & { flattenMaps: true }): FlattenMaps<Require_id<DocType>>;

// flattenUUIDs overloads
toJSON(options: ToObjectOptions & { flattenUUIDs: true, virtuals: true }): UUIDToString<Require_id<DocType & TVirtuals>>;
toJSON(options: ToObjectOptions & { flattenUUIDs: true }): UUIDToString<Require_id<DocType>>;

// 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<Omit<FlattenMaps<Require_id<DocType & TVirtuals>>, '__v'>>;
toJSON(options: ToObjectOptions & { versionKey: false, flattenMaps: false, flattenObjectIds: true, virtuals: true }): ObjectIdToString<Omit<Require_id<DocType & TVirtuals>, '__v'>>;
Expand Down Expand Up @@ -335,6 +339,10 @@ declare module 'mongoose' {
toObject(options: ToObjectOptions & { flattenMaps: true, virtuals: true }): FlattenMaps<Require_id<DocType & TVirtuals>>;
toObject(options: ToObjectOptions & { flattenMaps: true }): FlattenMaps<Require_id<DocType>>;

// flattenUUIDs overloads
toObject(options: ToObjectOptions & { flattenUUIDs: true, virtuals: true }): UUIDToString<Require_id<DocType & TVirtuals>>;
toObject(options: ToObjectOptions & { flattenUUIDs: true }): UUIDToString<Require_id<DocType>>;

// 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<Omit<FlattenMaps<Require_id<DocType & TVirtuals>>, '__v'>>;
toObject(options: ToObjectOptions & { versionKey: false, flattenMaps: false, flattenObjectIds: true, virtuals: true }): ObjectIdToString<Omit<Require_id<DocType & TVirtuals>, '__v'>>;
Expand Down
42 changes: 27 additions & 15 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand Down Expand Up @@ -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> = T extends mongodb.UUID
export type UUIDToString<T> = T extends Types.UUID
? string
: T extends Document
? T
: T extends TreatAsPrimitives
: T extends mongodb.UUID
? string
: T extends Document
? T
: T extends Record<string, any> ? {
[K in keyof T]: T[K] extends mongodb.UUID
? string
: T[K] extends Types.DocumentArray<infer ItemType>
? Types.DocumentArray<UUIDToJSON<ItemType>>
: T[K] extends Types.Subdocument<unknown, unknown, infer SubdocType>
? HydratedSingleSubdocument<SubdocType>
: UUIDToJSON<T[K]>;
} : T;
: T extends TreatAsPrimitives
? T
: T extends Record<string, any> ? {
[K in keyof T]: T[K] extends Types.UUID
? string
: T[K] extends mongodb.UUID
? string
: T[K] extends Types.DocumentArray<infer ItemType>
? Types.DocumentArray<UUIDToString<ItemType>>
: T[K] extends Types.Subdocument<unknown, unknown, infer SubdocType>
? HydratedSingleSubdocument<UUIDToString<SubdocType>>
: UUIDToString<T[K]>;
} : T;

/**
* Alias for UUIDToString for backwards compatibility.
* @deprecated Use UUIDToString instead.
*/
export type UUIDToJSON<T> = UUIDToString<T>;

/**
* Converts any ObjectId properties into strings for JSON serialization
Expand Down Expand Up @@ -1004,7 +1016,7 @@ declare module 'mongoose' {
FlattenMaps<
BufferToJSON<
ObjectIdToString<
UUIDToJSON<
UUIDToString<
DateToString<T>
>
>
Expand Down
Loading