A plugin is a function that takes a schema and an options object and mutates the schema. That is the entire API, and it is where shared hooks belong: schema.plugin(fn) runs immediately, before the model is compiled, so Middleware and Hooks's "hook registered too late" failure cannot happen.
export function softDelete(schema, { indexed = true } = {}) {
schema.add({ deletedAt: { type: Date, default: null, index: indexed } });
schema.pre(/^find/, function () {
if (!this.getOptions().withDeleted) this.where({ deletedAt: null });
});
schema.methods.softRemove = function () { this.deletedAt = new Date(); return this.save(); };
schema.statics.countDeleted = function () {
return this.countDocuments({ deletedAt: { $ne: null } }).setOptions({ withDeleted: true });
};
}
noteSchema.plugin(softDelete);
const [n1] = await Note.create([{ body: 'keep' }, { body: 'drop' }]);
await n1.softRemove();
console.log('visible ->', (await Note.find()).map((d) => d.body).join(', '));
console.log('withDeleted ->', (await Note.find().setOptions({ withDeleted: true })).length,
'| countDeleted ->', await Note.countDeleted());visible -> drop withDeleted -> 2 | countDeleted -> 1
Four techniques in a dozen lines. schema.add contributes paths. The regular expression /^find/ covers find, findOne, findOneAndUpdate and the rest of that family in one registration. A custom query option carried in setOptions gives callers an escape hatch that reads clearly at the call site. And the plugin takes options, so a schema that indexes deletedAt differently can pass { indexed: false }. mongoose.plugin(fn) applies one to every schema compiled afterwards — right for a createdBy stamp, wrong for anything surprising.
| Plugin | Version | License | What it adds |
|---|---|---|---|
| mongoose-paginate-v2 | 1.9.5 | MIT | Model.paginate() with page metadata |
| mongoose-autopopulate | 1.2.1 | Apache 2.0 | autopopulate: true on a ref path |
| mongoose-lean-virtuals | 2.1.0 | Apache 2.0 | virtuals on lean() results |
| mongoose-delete | 1.0.7 | MIT | soft delete, more thorough than the above |
Be selective. mongoose-autopopulate is the fastest way to reintroduce the cost measured in References and populate, because it makes an extra query happen on reads that never asked for one. A plugin that adds a path is cheap; one that adds a query is a performance decision made in a file nobody reads.