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
+ })
+});
+```
+
[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;