Skip to content
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
40 changes: 34 additions & 6 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -3836,15 +3836,39 @@ Document.prototype.$toObject = function(options, json) {
// Parent options should only bubble down for subdocuments, not populated docs
options._parentOptions = this.$isSubdocument ? options : null;

// remember the root transform function
// to save it from being overwritten by sub-transform functions
// const originalTransform = options.transform;
const schemaFieldsOnly = options._calledWithOptions.schemaFieldsOnly
?? options.schemaFieldsOnly
?? defaultOptions.schemaFieldsOnly
?? false;

let ret;
if (hasOnlyPrimitiveValues && !options.flattenObjectIds) {
// Fast path: if we don't have any nested objects or arrays, we only need a
// shallow clone.
ret = this.$__toObjectShallow();
ret = this.$__toObjectShallow(schemaFieldsOnly);
} else if (schemaFieldsOnly) {
ret = {};
for (const path of Object.keys(this.$__schema.paths)) {
const value = this.$__getValue(path);
if (value === undefined) {
continue;
}
let pathToSet = path;
let objToSet = ret;
if (path.indexOf('.') !== -1) {
const segments = path.split('.');
pathToSet = segments[segments.length - 1];
for (let i = 0; i < segments.length - 1; ++i) {
objToSet[segments[i]] = objToSet[segments[i]] ?? {};
objToSet = objToSet[segments[i]];
}
}
if (value === null) {
objToSet[pathToSet] = null;
continue;
}
objToSet[pathToSet] = clone(value, options);
}
} else {
ret = clone(this._doc, options) || {};
}
Expand Down Expand Up @@ -3910,10 +3934,12 @@ Document.prototype.$toObject = function(options, json) {
* Internal shallow clone alternative to `$toObject()`: much faster, no options processing
*/

Document.prototype.$__toObjectShallow = function $__toObjectShallow() {
Document.prototype.$__toObjectShallow = function $__toObjectShallow(schemaFieldsOnly) {
const ret = {};
if (this._doc != null) {
for (const key of Object.keys(this._doc)) {
const keys = schemaFieldsOnly ? Object.keys(this.$__schema.paths) : Object.keys(this._doc);
for (const key of keys) {
// Safe to do this even in the schemaFieldsOnly case because we assume there's no nested paths
const value = this._doc[key];
if (value instanceof Date) {
ret[key] = new Date(value);
Expand Down Expand Up @@ -4066,6 +4092,7 @@ Document.prototype.$__toObjectShallow = function $__toObjectShallow() {
* @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.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.
* @see mongodb.Binary https://mongodb.github.io/node-mongodb-native/4.9/classes/Binary.html
* @api public
Expand Down Expand Up @@ -4336,6 +4363,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.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()
* @see JSON.stringify() in JavaScript https://thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript.html
Expand Down
84 changes: 84 additions & 0 deletions test/document.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14298,6 +14298,90 @@ describe('document', function() {
delete mongoose.Schema.Types.CustomType;
});

it('supports schemaFieldsOnly option for toObject() (gh-15258)', async function() {
const schema = new Schema({ key: String }, { discriminatorKey: 'key' });
const subschema1 = new Schema({ field1: String });
const subschema2 = new Schema({ field2: String });

const Discriminator = db.model('Test', schema);
Discriminator.discriminator('type1', subschema1);
Discriminator.discriminator('type2', subschema2);

const doc = await Discriminator.create({
key: 'type1',
field1: 'test value'
});

await Discriminator.updateOne(
{ _id: doc._id },
{
key: 'type2',
field2: 'test2'
},
{ overwriteDiscriminatorKey: true }
);

const doc2 = await Discriminator.findById(doc).orFail();
assert.strictEqual(doc2.field2, 'test2');
assert.strictEqual(doc2.field1, undefined);

const obj = doc2.toObject();
assert.strictEqual(obj.field2, 'test2');
assert.strictEqual(obj.field1, 'test value');

const obj2 = doc2.toObject({ schemaFieldsOnly: true });
assert.strictEqual(obj.field2, 'test2');
assert.strictEqual(obj2.field1, undefined);
});

it('supports schemaFieldsOnly on nested paths, subdocuments, and arrays (gh-15258)', async function() {
const subSchema = new Schema({
title: String,
description: String
}, { _id: false });
const taskSchema = new Schema({
name: String,
details: {
dueDate: Date,
priority: Number
},
subtask: subSchema,
tasks: [subSchema]
});
const Task = db.model('Test', taskSchema);

const doc = await Task.create({
_id: '0'.repeat(24),
name: 'Test Task',
details: {
dueDate: new Date('2024-01-01'),
priority: 1
},
subtask: {
title: 'Subtask 1',
description: 'Test Description'
},
tasks: [{
title: 'Array Task 1',
description: 'Array Description 1'
}]
});

doc._doc.details.extraField = 'extra';
doc._doc.subtask.extraField = 'extra';
doc._doc.tasks[0].extraField = 'extra';

const obj = doc.toObject({ schemaFieldsOnly: true });
assert.deepStrictEqual(obj, {
name: 'Test Task',
details: { dueDate: new Date('2024-01-01T00:00:00.000Z'), priority: 1 },
subtask: { title: 'Subtask 1', description: 'Test Description' },
tasks: [{ title: 'Array Task 1', description: 'Array Description 1' }],
_id: new mongoose.Types.ObjectId('0'.repeat(24)),
__v: 0
});
});

it('handles undoReset() on deep recursive subdocuments (gh-15255)', async function() {
const RecursiveSchema = new mongoose.Schema({});

Expand Down
26 changes: 14 additions & 12 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,30 +204,32 @@ declare module 'mongoose' {
}

export interface ToObjectOptions<THydratedDocumentType = HydratedDocument<unknown>> {
/** apply all getters (path and virtual getters) */
getters?: boolean;
/** apply virtual getters (can override getters option) */
virtuals?: boolean | string[];
/** if `options.virtuals = true`, you can set `options.aliases = false` to skip applying aliases. This option is a no-op if `options.virtuals = false`. */
aliases?: boolean;
/** if true, replace any conventionally populated paths with the original id in the output. Has no affect on virtual populated paths. */
depopulate?: boolean;
/** if true, convert Maps to POJOs. Useful if you want to `JSON.stringify()` the result of `toObject()`. */
flattenMaps?: boolean;
/** if true, convert any ObjectIds in the result to 24 character hex strings. */
flattenObjectIds?: boolean;
/** apply all getters (path and virtual getters) */
getters?: boolean;
/** remove empty objects (defaults to true) */
minimize?: boolean;
/** If true, the resulting object will only have fields that are defined in the document's schema. By default, `toJSON()` & `toObject()` returns all fields in the underlying document from MongoDB, including ones that are not listed in the schema. */
schemaFieldsOnly?: boolean;
/** if set, mongoose will call this function to allow you to transform the returned object */
transform?: boolean | ((
doc: THydratedDocumentType,
ret: Record<string, any>,
options: ToObjectOptions<THydratedDocumentType>
) => any);
/** if true, replace any conventionally populated paths with the original id in the output. Has no affect on virtual populated paths. */
depopulate?: boolean;
/** if false, exclude the version key (`__v` by default) from the output */
versionKey?: boolean;
/** if true, convert Maps to POJOs. Useful if you want to `JSON.stringify()` the result of `toObject()`. */
flattenMaps?: boolean;
/** if true, convert any ObjectIds in the result to 24 character hex strings. */
flattenObjectIds?: boolean;
/** 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. */
useProjection?: boolean;
/** if false, exclude the version key (`__v` by default) from the output */
versionKey?: boolean;
/** apply virtual getters (can override getters option) */
virtuals?: boolean | string[];
}

export type DiscriminatorModel<M, T> = T extends Model<infer T, infer TQueryHelpers, infer TInstanceMethods, infer TVirtuals>
Expand Down
Loading