diff --git a/docs/populate.md b/docs/populate.md index 58226e65130..32dbca8b6ea 100644 --- a/docs/populate.md +++ b/docs/populate.md @@ -727,6 +727,29 @@ AuthorSchema.virtual('posts', { }); ``` +You can overwrite the `match` option when calling `populate()` as follows. + +```javascript +// Overwrite the `match` option specified in `AuthorSchema.virtual()` for this +// single `populate()` call. +await Author.findOne().populate({ path: posts, match: {} }); +``` + +You can also set the `match` option to a function in your `populate()` call. +If you want to merge your `populate()` match option, rather than overwriting, use the following. + +```javascript +await Author.findOne().populate({ + path: posts, + // Add `isDeleted: false` to the virtual's default `match`, so the `match` + // option would be `{ tags: author.favoriteTags, isDeleted: false }` + match: (author, virtual) => ({ + ...virtual.options.match(author), + isDeleted: false + }) +}); +``` +

Populating Maps

[Maps](schematypes.html#maps) are a type that represents an object with arbitrary diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js index dbe3707766a..7ac134266d7 100644 --- a/lib/helpers/populate/getModelsMapForPopulate.js +++ b/lib/helpers/populate/getModelsMapForPopulate.js @@ -436,13 +436,13 @@ function _virtualPopulate(model, docs, options, _virtualRes) { data.justOne = justOne; // `match` - let match = get(options, 'match', null) || - get(data, 'virtual.options.match', null) || + const baseMatch = get(data, 'virtual.options.match', null) || get(data, 'virtual.options.options.match', null); + let match = get(options, 'match', null) || baseMatch; let hasMatchFunction = typeof match === 'function'; if (hasMatchFunction) { - match = match.call(doc, doc); + match = match.call(doc, doc, data.virtual); } if (Array.isArray(localField) && Array.isArray(foreignField) && localField.length === foreignField.length) { diff --git a/lib/schema.js b/lib/schema.js index 25a5f773639..b25e89c24a7 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -2192,6 +2192,7 @@ Schema.prototype.indexes = function() { * @param {Boolean|Function} [options.justOne=false] Only works with populate virtuals. If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), will be a single doc or `null`. Otherwise, the populate virtual will be an array. * @param {Boolean} [options.count=false] Only works with populate virtuals. If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), this populate virtual will contain the number of documents rather than the documents themselves when you `populate()`. * @param {Function|null} [options.get=null] Adds a [getter](https://mongoosejs.com/docs/tutorials/getters-setters.html) to this virtual to transform the populated doc. + * @param {Object|Function} [options.match=null] Apply a default [`match` option to populate](https://mongoosejs.com/docs/populate.html#match), adding an additional filter to the populate query. * @return {VirtualType} */ diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 452561f8f2a..d16e0513a19 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -10454,4 +10454,54 @@ describe('model: populate:', function() { assert.equal(normal.test.length, 0); }); }); + + it('calls match function with virtual as parameter (gh-12443)', async function() { + const parentSchema = mongoose.Schema({ name: String }); + parentSchema.virtual('children', { + ref: 'Child', + localField: '_id', + foreignField: 'parentId', + justOne: false, + match: { + isDeleted: false + } + }); + const Parent = db.model('Parent', parentSchema); + + const childSchema = mongoose.Schema({ + name: String, + parentId: 'ObjectId', + isDeleted: Boolean + }); + const Child = db.model('Child', childSchema); + + const { _id } = await Parent.create({ name: 'Darth Vader' }); + await Child.create([ + { name: 'Luke', parentId: _id, isDeleted: false }, + { name: 'Leia', parentId: _id, isDeleted: false }, + { name: 'Chad', parentId: _id, isDeleted: true } + ]); + + let doc = await Parent.findById(_id).populate({ + path: 'children' + }); + assert.deepStrictEqual(doc.children.map(c => c.name).sort(), ['Leia', 'Luke']); + + doc = await Parent.findById(_id).populate({ + path: 'children', + match: (_doc, virtual) => ({ + ...virtual.options.match, + name: /(Luke|Chad)/ + }) + }); + assert.deepStrictEqual(doc.children.map(c => c.name).sort(), ['Luke']); + + doc = await Parent.findById(_id).populate({ + path: 'children', + match: () => ({ + name: /(Luke|Chad)/ + }) + }); + assert.deepStrictEqual(doc.children.map(c => c.name).sort(), ['Chad', 'Luke']); + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index 55e2aa2360b..cfed86c51c3 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -512,7 +512,7 @@ declare module 'mongoose' { count?: boolean; /** Add an extra match condition to `populate()`. */ - match?: FilterQuery | Function; + match?: FilterQuery | ((doc: Record, virtual?: this) => Record | null); /** Add a default `limit` to the `populate()` query. */ limit?: number;