Skip to content

feat(subdocument): support schematype-level minimize option to disable minimizing empty subdocuments #15336

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 4 commits into from
Apr 6, 2025
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
2 changes: 2 additions & 0 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -3796,6 +3796,8 @@ Document.prototype.$toObject = function(options, json) {
let _minimize;
if (options._calledWithOptions.minimize != null) {
_minimize = options.minimize;
} else if (this.$__schemaTypeOptions?.minimize != null) {
_minimize = this.$__schemaTypeOptions.minimize;
} else if (defaultOptions != null && defaultOptions.minimize != null) {
_minimize = defaultOptions.minimize;
} else {
Expand Down
5 changes: 0 additions & 5 deletions lib/helpers/clone.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ function clone(obj, options, isArrayChild) {
return clonedDoc;
}
}
const isSingleNested = obj.$isSingleNested;

if (isPOJO(obj) && obj.$__ != null && obj._doc != null) {
return obj._doc;
Expand All @@ -70,10 +69,6 @@ function clone(obj, options, isArrayChild) {
ret = obj.toObject(options);
}

if (options && options.minimize && !obj.constructor.$__required && isSingleNested && Object.keys(ret).length === 0) {
return undefined;
}

return ret;
}

Expand Down
26 changes: 25 additions & 1 deletion lib/options/schemaSubdocumentOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,36 @@ const opts = require('./propertyOptions');
* parentSchema.path('child').schema.options._id; // false
*
* @api public
* @property of
* @property _id
* @memberOf SchemaSubdocumentOptions
* @type {Function|string}
* @instance
*/

Object.defineProperty(SchemaSubdocumentOptions.prototype, '_id', opts);

/**
* If set, overwrites the child schema's `minimize` option. In addition, configures whether the entire
* subdocument can be minimized out.
*
* #### Example:
*
* const childSchema = Schema({ name: String });
* const parentSchema = Schema({
* child: { type: childSchema, minimize: false }
* });
* const ParentModel = mongoose.model('Parent', parentSchema);
* // Saves `{ child: {} }` to the db. Without `minimize: false`, Mongoose would remove the empty
* // object and save `{}` to the db.
* await ParentModel.create({ child: {} });
*
* @api public
* @property minimize
* @memberOf SchemaSubdocumentOptions
* @type {Function|string}
* @instance
*/

Object.defineProperty(SchemaSubdocumentOptions.prototype, 'minimize', opts);

module.exports = SchemaSubdocumentOptions;
1 change: 1 addition & 0 deletions lib/schema/subdocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ function _createConstructor(schema, baseClass, options) {
_embedded.prototype = Object.create(proto);
_embedded.prototype.$__setSchema(schema);
_embedded.prototype.constructor = _embedded;
_embedded.prototype.$__schemaTypeOptions = options;
_embedded.$__required = options?.required;
_embedded.base = schema.base;
_embedded.schema = schema;
Expand Down
19 changes: 19 additions & 0 deletions lib/types/subdocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,25 @@ if (util.inspect.custom) {
Subdocument.prototype[util.inspect.custom] = Subdocument.prototype.inspect;
}

/**
* Override `$toObject()` to handle minimizing the whole path. Should not minimize if schematype-level minimize
* is set to false re: gh-11247, gh-14058, gh-14151
*/

Subdocument.prototype.$toObject = function $toObject(options, json) {
const ret = Document.prototype.$toObject.call(this, options, json);

// If `$toObject()` was called recursively, respect the minimize option, including schematype level minimize.
// If minimize is set, then we can minimize out the whole object.
if (Object.keys(ret).length === 0 && options?._calledWithOptions != null) {
const minimize = options._calledWithOptions?.minimize ?? this?.$__schemaTypeOptions?.minimize ?? options.minimize;
if (minimize && !this.constructor.$__required) {
return undefined;
}
}
return ret;
};

/**
* Registers remove event listeners for triggering
* on subdocuments.
Expand Down
12 changes: 12 additions & 0 deletions test/types.subdocument.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,16 @@ describe('types.subdocument', function() {
assert.ok(doc.child.isModified('id'));
});
});

it('respects schematype-level minimize (gh-15313)', function() {
const MySubSchema = new Schema({}, { strict: false, _id: false });
const MySchema = new Schema({ myfield: { type: MySubSchema, minimize: false } });
const MyModel = db.model('MyModel', MySchema);

const doc = new MyModel({ myfield: {} });
assert.deepStrictEqual(doc.toObject().myfield, {});

const doc2 = new MyModel({ myfield: { empty: {} } });
assert.deepStrictEqual(doc2.toObject().myfield, { empty: {} });
});
});
3 changes: 3 additions & 0 deletions types/schematypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ declare module 'mongoose' {
/** The maximum value allowed for this path. Only allowed for numbers and dates. */
max?: number | NativeDate | [number, string] | [NativeDate, string] | readonly [number, string] | readonly [NativeDate, string];

/** Set to false to disable minimizing empty single nested subdocuments by default */
minimize?: boolean;

/** Defines a TTL index on this path. Only allowed for dates. */
expires?: string | number;

Expand Down