Skip to content

Commit 2241823

Browse files
authored
Merge pull request Automattic#15082 from Automattic/vkarpov15/Automatticgh-15026
fix(query): clone PopulateOptions when setting _localModel to avoid state leaking between subpopulate instances
2 parents 2607904 + 506d125 commit 2241823

File tree

3 files changed

+60
-132
lines changed

3 files changed

+60
-132
lines changed

lib/model.js

Lines changed: 43 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -4245,67 +4245,34 @@ Model.populate = async function populate(docs, paths) {
42454245
if (typeof paths === 'function' || typeof arguments[2] === 'function') {
42464246
throw new MongooseError('Model.populate() no longer accepts a callback');
42474247
}
4248-
const _this = this;
42494248
// normalized paths
42504249
paths = utils.populate(paths);
4251-
// data that should persist across subPopulate calls
4252-
const cache = {};
4253-
4254-
return new Promise((resolve, reject) => {
4255-
_populate(_this, docs, paths, cache, (err, res) => {
4256-
if (err) {
4257-
return reject(err);
4258-
}
4259-
resolve(res);
4260-
});
4261-
});
4262-
};
4263-
4264-
/**
4265-
* Populate helper
4266-
*
4267-
* @param {Model} model the model to use
4268-
* @param {Document|Array} docs Either a single document or array of documents to populate.
4269-
* @param {Object} paths
4270-
* @param {never} cache Unused
4271-
* @param {Function} [callback] Optional callback, executed upon completion. Receives `err` and the `doc(s)`.
4272-
* @return {Function}
4273-
* @api private
4274-
*/
42754250

4276-
function _populate(model, docs, paths, cache, callback) {
4277-
let pending = paths.length;
42784251
if (paths.length === 0) {
4279-
return callback(null, docs);
4252+
return docs;
42804253
}
4254+
42814255
// each path has its own query options and must be executed separately
4256+
const promises = [];
42824257
for (const path of paths) {
4283-
populate(model, docs, path, next);
4258+
promises.push(_populatePath(this, docs, path));
42844259
}
4260+
await Promise.all(promises);
42854261

4286-
function next(err) {
4287-
if (err) {
4288-
return callback(err, null);
4289-
}
4290-
if (--pending) {
4291-
return;
4292-
}
4293-
callback(null, docs);
4294-
}
4295-
}
4262+
return docs;
4263+
};
42964264

42974265
/*!
4298-
* Populates `docs`
4266+
* Populates `docs` for a single `populateOptions` instance.
42994267
*/
43004268
const excludeIdReg = /\s?-_id\s?/;
43014269
const excludeIdRegGlobal = /\s?-_id\s?/g;
43024270

4303-
function populate(model, docs, options, callback) {
4304-
const populateOptions = options;
4305-
if (options.strictPopulate == null) {
4306-
if (options._localModel != null && options._localModel.schema._userProvidedOptions.strictPopulate != null) {
4307-
populateOptions.strictPopulate = options._localModel.schema._userProvidedOptions.strictPopulate;
4308-
} else if (options._localModel != null && model.base.options.strictPopulate != null) {
4271+
async function _populatePath(model, docs, populateOptions) {
4272+
if (populateOptions.strictPopulate == null) {
4273+
if (populateOptions._localModel != null && populateOptions._localModel.schema._userProvidedOptions.strictPopulate != null) {
4274+
populateOptions.strictPopulate = populateOptions._localModel.schema._userProvidedOptions.strictPopulate;
4275+
} else if (populateOptions._localModel != null && model.base.options.strictPopulate != null) {
43094276
populateOptions.strictPopulate = model.base.options.strictPopulate;
43104277
} else if (model.base.options.strictPopulate != null) {
43114278
populateOptions.strictPopulate = model.base.options.strictPopulate;
@@ -4317,15 +4284,12 @@ function populate(model, docs, options, callback) {
43174284
docs = [docs];
43184285
}
43194286
if (docs.length === 0 || docs.every(utils.isNullOrUndefined)) {
4320-
return callback();
4287+
return;
43214288
}
43224289

43234290
const modelsMap = getModelsMapForPopulate(model, docs, populateOptions);
4324-
43254291
if (modelsMap instanceof MongooseError) {
4326-
return immediate(function() {
4327-
callback(modelsMap);
4328-
});
4292+
throw modelsMap;
43294293
}
43304294
const len = modelsMap.length;
43314295
let vals = [];
@@ -4335,7 +4299,6 @@ function populate(model, docs, options, callback) {
43354299
return undefined !== item;
43364300
}
43374301

4338-
let _remaining = len;
43394302
let hasOne = false;
43404303
const params = [];
43414304
for (let i = 0; i < len; ++i) {
@@ -4366,7 +4329,6 @@ function populate(model, docs, options, callback) {
43664329
// Ensure that we set to 0 or empty array even
43674330
// if we don't actually execute a query to make sure there's a value
43684331
// and we know this path was populated for future sets. See gh-7731, gh-8230
4369-
--_remaining;
43704332
_assign(model, [], mod, assignmentOpts);
43714333
continue;
43724334
}
@@ -4397,72 +4359,58 @@ function populate(model, docs, options, callback) {
43974359
} else if (mod.options.limit != null) {
43984360
assignmentOpts.originalLimit = mod.options.limit;
43994361
}
4400-
params.push([mod, match, select, assignmentOpts, _next]);
4362+
params.push([mod, match, select, assignmentOpts]);
44014363
}
44024364
if (!hasOne) {
44034365
// If models but no docs, skip further deep populate.
44044366
if (modelsMap.length !== 0) {
4405-
return callback();
4367+
return;
44064368
}
4407-
// If no models to populate but we have a nested populate,
4408-
// keep trying, re: gh-8946
4369+
// If no models and no docs to populate but we have a nested populate,
4370+
// probably a case of unnecessarily populating a non-ref path re: gh-8946
44094371
if (populateOptions.populate != null) {
44104372
const opts = utils.populate(populateOptions.populate).map(pop => Object.assign({}, pop, {
44114373
path: populateOptions.path + '.' + pop.path
44124374
}));
4413-
model.populate(docs, opts).then(res => { callback(null, res); }, err => { callback(err); });
4414-
return;
4375+
return model.populate(docs, opts);
44154376
}
4416-
return callback();
4377+
return;
44174378
}
44184379

4380+
const promises = [];
44194381
for (const arr of params) {
4420-
_execPopulateQuery.apply(null, arr);
4421-
}
4422-
function _next(err, valsFromDb) {
4423-
if (err != null) {
4424-
return callback(err, null);
4425-
}
4426-
vals = vals.concat(valsFromDb);
4427-
if (--_remaining === 0) {
4428-
_done();
4429-
}
4382+
promises.push(_execPopulateQuery.apply(null, arr).then(valsFromDb => { vals = vals.concat(valsFromDb); }));
44304383
}
44314384

4432-
function _done() {
4433-
for (const arr of params) {
4434-
const mod = arr[0];
4435-
const assignmentOpts = arr[3];
4436-
for (const val of vals) {
4437-
mod.options._childDocs.push(val);
4438-
}
4439-
try {
4440-
_assign(model, vals, mod, assignmentOpts);
4441-
} catch (err) {
4442-
return callback(err);
4443-
}
4444-
}
4385+
await Promise.all(promises);
44454386

4446-
for (const arr of params) {
4447-
removeDeselectedForeignField(arr[0].foreignField, arr[0].options, vals);
4387+
for (const arr of params) {
4388+
const mod = arr[0];
4389+
const assignmentOpts = arr[3];
4390+
for (const val of vals) {
4391+
mod.options._childDocs.push(val);
44484392
}
4449-
for (const arr of params) {
4450-
const mod = arr[0];
4451-
if (mod.options && mod.options.options && mod.options.options._leanTransform) {
4452-
for (const doc of vals) {
4453-
mod.options.options._leanTransform(doc);
4454-
}
4393+
_assign(model, vals, mod, assignmentOpts);
4394+
}
4395+
4396+
for (const arr of params) {
4397+
removeDeselectedForeignField(arr[0].foreignField, arr[0].options, vals);
4398+
}
4399+
for (const arr of params) {
4400+
const mod = arr[0];
4401+
if (mod.options && mod.options.options && mod.options.options._leanTransform) {
4402+
for (const doc of vals) {
4403+
mod.options.options._leanTransform(doc);
44554404
}
44564405
}
4457-
callback();
44584406
}
44594407
}
44604408

44614409
/*!
44624410
* ignore
44634411
*/
44644412

4465-
function _execPopulateQuery(mod, match, select, assignmentOpts, callback) {
4413+
function _execPopulateQuery(mod, match, select) {
44664414
let subPopulate = clone(mod.options.populate);
44674415
const queryOptions = Object.assign({
44684416
skip: mod.options.skip,
@@ -4528,15 +4476,12 @@ function _execPopulateQuery(mod, match, select, assignmentOpts, callback) {
45284476
query.populate(subPopulate);
45294477
}
45304478

4531-
query.exec().then(
4479+
return query.exec().then(
45324480
docs => {
45334481
for (const val of docs) {
45344482
leanPopulateMap.set(val, mod.model);
45354483
}
4536-
callback(null, docs);
4537-
},
4538-
err => {
4539-
callback(err);
4484+
return docs;
45404485
}
45414486
);
45424487
}

lib/query.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2381,6 +2381,7 @@ Query.prototype._find = async function _find() {
23812381
_completeManyLean(_this.model.schema, docs, null, completeManyOptions) :
23822382
_this._completeMany(docs, fields, userProvidedFields, completeManyOptions);
23832383
}
2384+
23842385
const pop = helpers.preparePopulationOptionsMQ(_this, mongooseOptions);
23852386

23862387
if (mongooseOptions.lean) {
@@ -4762,12 +4763,13 @@ Query.prototype._castUpdate = function _castUpdate(obj) {
47624763
*/
47634764

47644765
Query.prototype.populate = function() {
4766+
const args = Array.from(arguments);
47654767
// Bail when given no truthy arguments
4766-
if (!Array.from(arguments).some(Boolean)) {
4768+
if (!args.some(Boolean)) {
47674769
return this;
47684770
}
47694771

4770-
const res = utils.populate.apply(null, arguments);
4772+
const res = utils.populate.apply(null, args);
47714773

47724774
// Propagate readConcern and readPreference and lean from parent query,
47734775
// unless one already specified

lib/queryHelpers.js

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Module dependencies
55
*/
66

7+
const PopulateOptions = require('./options/populateOptions');
78
const checkEmbeddedDiscriminatorKeyProjection =
89
require('./helpers/discriminator/checkEmbeddedDiscriminatorKeyProjection');
910
const get = require('./helpers/get');
@@ -13,32 +14,6 @@ const isDefiningProjection = require('./helpers/projection/isDefiningProjection'
1314
const clone = require('./helpers/clone');
1415
const isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive');
1516

16-
/**
17-
* Prepare a set of path options for query population.
18-
*
19-
* @param {Query} query
20-
* @param {Object} options
21-
* @return {Array}
22-
*/
23-
24-
exports.preparePopulationOptions = function preparePopulationOptions(query, options) {
25-
const _populate = query.options.populate;
26-
const pop = Object.keys(_populate).reduce((vals, key) => vals.concat([_populate[key]]), []);
27-
28-
// lean options should trickle through all queries
29-
if (options.lean != null) {
30-
pop
31-
.filter(p => (p && p.options && p.options.lean) == null)
32-
.forEach(makeLean(options.lean));
33-
}
34-
35-
pop.forEach(opts => {
36-
opts._localModel = query.model;
37-
});
38-
39-
return pop;
40-
};
41-
4217
/**
4318
* Prepare a set of path options for query population. This is the MongooseQuery
4419
* version
@@ -73,12 +48,18 @@ exports.preparePopulationOptionsMQ = function preparePopulationOptionsMQ(query,
7348
}
7449

7550
const projection = query._fieldsForExec();
76-
pop.forEach(p => {
77-
p._queryProjection = projection;
78-
});
79-
pop.forEach(opts => {
80-
opts._localModel = query.model;
81-
});
51+
for (let i = 0; i < pop.length; ++i) {
52+
if (pop[i] instanceof PopulateOptions) {
53+
pop[i] = new PopulateOptions({
54+
...pop[i],
55+
_queryProjection: projection,
56+
_localModel: query.model
57+
});
58+
} else {
59+
pop[i]._queryProjection = projection;
60+
pop[i]._localModel = query.model;
61+
}
62+
}
8263

8364
return pop;
8465
};

0 commit comments

Comments
 (0)