Skip to content

Commit f9c7c3b

Browse files
authored
Merge pull request Automattic#15044 from Automattic/vkarpov15/Automatticgh-14979
feat: add forceRepopulate option for populate() to allow avoiding repopulating already populated docs
2 parents 7aba322 + e93ed6f commit f9c7c3b

File tree

5 files changed

+97
-0
lines changed

5 files changed

+97
-0
lines changed

lib/helpers/populate/getModelsMapForPopulate.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
5454
doc = docs[i];
5555
let justOne = null;
5656

57+
if (doc.$__ != null && doc.populated(options.path)) {
58+
const forceRepopulate = options.forceRepopulate != null ? options.forceRepopulate : doc.constructor.base.options.forceRepopulate;
59+
if (forceRepopulate === false) {
60+
continue;
61+
}
62+
}
63+
5764
const docSchema = doc != null && doc.$__ != null ? doc.$__schema : modelSchema;
5865
schema = getSchemaTypes(model, docSchema, doc, options.path);
5966

lib/model.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4199,6 +4199,7 @@ Model.validate = async function validate(obj, pathsOrOptions, context) {
41994199
* - options: optional query options like sort, limit, etc
42004200
* - justOne: optional boolean, if true Mongoose will always set `path` to a document, or `null` if no document was found. If false, Mongoose will always set `path` to an array, which will be empty if no documents are found. Inferred from schema by default.
42014201
* - strictPopulate: optional boolean, set to `false` to allow populating paths that aren't in the schema.
4202+
* - forceRepopulate: optional boolean, defaults to `true`. Set to `false` to prevent Mongoose from repopulating paths that are already populated
42024203
*
42034204
* #### Example:
42044205
*
@@ -4235,6 +4236,7 @@ Model.validate = async function validate(obj, pathsOrOptions, context) {
42354236
* @param {Boolean} [options.strictPopulate=true] Set to false to allow populating paths that aren't defined in the given model's schema.
42364237
* @param {Object} [options.options=null] Additional options like `limit` and `lean`.
42374238
* @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
4239+
* @param {Boolean} [options.forceRepopulate=true] Set to `false` to prevent Mongoose from repopulating paths that are already populated
42384240
* @param {Function} [callback(err,doc)] Optional callback, executed upon completion. Receives `err` and the `doc(s)`.
42394241
* @return {Promise}
42404242
* @api public

lib/validOptions.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const VALID_OPTIONS = Object.freeze([
1717
'cloneSchemas',
1818
'createInitialConnection',
1919
'debug',
20+
'forceRepopulate',
2021
'id',
2122
'timestamps.createdAt.immutable',
2223
'maxTimeMS',

test/model.populate.test.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11279,4 +11279,89 @@ describe('model: populate:', function() {
1127911279
assert.strictEqual(doc.node.length, 1);
1128011280
assert.strictEqual(doc.node[0]._id, '65c7953e-c6e9-4c2f-8328-fe2de7df560d');
1128111281
});
11282+
11283+
it('avoids repopulating if forceRepopulate is disabled (gh-14979)', async function() {
11284+
const ChildSchema = new Schema({ name: String });
11285+
const ParentSchema = new Schema({
11286+
children: [{ type: Schema.Types.ObjectId, ref: 'Child' }],
11287+
child: { type: 'ObjectId', ref: 'Child' }
11288+
});
11289+
11290+
const Child = db.model('Child', ChildSchema);
11291+
const Parent = db.model('Parent', ParentSchema);
11292+
11293+
const child = await Child.create({ name: 'Child test' });
11294+
let parent = await Parent.create({ child: child._id, children: [child._id] });
11295+
11296+
parent = await Parent.findOne({ _id: parent._id }).populate(['child', 'children']).orFail();
11297+
child.name = 'Child test updated 1';
11298+
await child.save();
11299+
11300+
await parent.populate({ path: 'child', forceRepopulate: false });
11301+
await parent.populate({ path: 'children', forceRepopulate: false });
11302+
assert.equal(parent.child.name, 'Child test');
11303+
assert.equal(parent.children[0].name, 'Child test');
11304+
11305+
await Parent.populate([parent], { path: 'child', forceRepopulate: false });
11306+
await Parent.populate([parent], { path: 'children', forceRepopulate: false });
11307+
assert.equal(parent.child.name, 'Child test');
11308+
assert.equal(parent.children[0].name, 'Child test');
11309+
11310+
parent.depopulate('child');
11311+
parent.depopulate('children');
11312+
await parent.populate({ path: 'child', forceRepopulate: false });
11313+
await parent.populate({ path: 'children', forceRepopulate: false });
11314+
assert.equal(parent.child.name, 'Child test updated 1');
11315+
assert.equal(parent.children[0].name, 'Child test updated 1');
11316+
});
11317+
11318+
it('handles forceRepopulate as a global option (gh-14979)', async function() {
11319+
const m = new mongoose.Mongoose();
11320+
m.set('forceRepopulate', false);
11321+
await m.connect(start.uri);
11322+
const ChildSchema = new m.Schema({ name: String });
11323+
const ParentSchema = new m.Schema({
11324+
children: [{ type: Schema.Types.ObjectId, ref: 'Child' }],
11325+
child: { type: 'ObjectId', ref: 'Child' }
11326+
});
11327+
11328+
const Child = m.model('Child', ChildSchema);
11329+
const Parent = m.model('Parent', ParentSchema);
11330+
11331+
const child = await Child.create({ name: 'Child test' });
11332+
let parent = await Parent.create({ child: child._id, children: [child._id] });
11333+
11334+
parent = await Parent.findOne({ _id: parent._id }).populate(['child', 'children']).orFail();
11335+
child.name = 'Child test updated 1';
11336+
await child.save();
11337+
11338+
await parent.populate({ path: 'child' });
11339+
await parent.populate({ path: 'children' });
11340+
assert.equal(parent.child.name, 'Child test');
11341+
assert.equal(parent.children[0].name, 'Child test');
11342+
11343+
await Parent.populate([parent], { path: 'child' });
11344+
await Parent.populate([parent], { path: 'children' });
11345+
assert.equal(parent.child.name, 'Child test');
11346+
assert.equal(parent.children[0].name, 'Child test');
11347+
11348+
parent.depopulate('child');
11349+
parent.depopulate('children');
11350+
await parent.populate({ path: 'child' });
11351+
await parent.populate({ path: 'children' });
11352+
assert.equal(parent.child.name, 'Child test updated 1');
11353+
assert.equal(parent.children[0].name, 'Child test updated 1');
11354+
11355+
child.name = 'Child test updated 2';
11356+
await child.save();
11357+
11358+
parent.depopulate('child');
11359+
parent.depopulate('children');
11360+
await parent.populate({ path: 'child', forceRepopulate: true });
11361+
await parent.populate({ path: 'children', forceRepopulate: true });
11362+
assert.equal(parent.child.name, 'Child test updated 2');
11363+
assert.equal(parent.children[0].name, 'Child test updated 2');
11364+
11365+
await m.disconnect();
11366+
});
1128211367
});

types/populate.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ declare module 'mongoose' {
3737
localField?: string;
3838
/** Overwrite the schema-level foreign field to populate on if this is a populated virtual. */
3939
foreignField?: string;
40+
/** Set to `false` to prevent Mongoose from repopulating paths that are already populated */
41+
forceRepopulate?: boolean;
4042
}
4143

4244
interface PopulateOption {

0 commit comments

Comments
 (0)