Skip to content

Commit 3526799

Browse files
authored
Merge pull request #14580 from Automattic/vkarpov15/gh-14567
fix(query): shallow clone $or, $and if merging onto empty query filter
2 parents 11c754c + a15855f commit 3526799

File tree

3 files changed

+56
-6
lines changed

3 files changed

+56
-6
lines changed

lib/query.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -2380,13 +2380,19 @@ Query.prototype.merge = function(source) {
23802380
}
23812381

23822382
opts.omit = {};
2383-
if (this._conditions && this._conditions.$and && source.$and) {
2383+
if (source.$and) {
23842384
opts.omit['$and'] = true;
2385-
this._conditions.$and = this._conditions.$and.concat(source.$and);
2385+
if (!this._conditions) {
2386+
this._conditions = {};
2387+
}
2388+
this._conditions.$and = (this._conditions.$and || []).concat(source.$and);
23862389
}
2387-
if (this._conditions && this._conditions.$or && source.$or) {
2390+
if (source.$or) {
23882391
opts.omit['$or'] = true;
2389-
this._conditions.$or = this._conditions.$or.concat(source.$or);
2392+
if (!this._conditions) {
2393+
this._conditions = {};
2394+
}
2395+
this._conditions.$or = (this._conditions.$or || []).concat(source.$or);
23902396
}
23912397

23922398
// plain object

lib/utils.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const isObject = require('./helpers/isObject');
1515
const isMongooseArray = require('./types/array/isMongooseArray');
1616
const isMongooseDocumentArray = require('./types/documentArray/isMongooseDocumentArray');
1717
const isBsonType = require('./helpers/isBsonType');
18+
const isPOJO = require('./helpers/isPOJO');
1819
const getFunctionName = require('./helpers/getFunctionName');
1920
const isMongooseObject = require('./helpers/isMongooseObject');
2021
const promiseOrCallback = require('./helpers/promiseOrCallback');
@@ -264,7 +265,13 @@ exports.merge = function merge(to, from, options, path) {
264265
continue;
265266
}
266267
if (to[key] == null) {
267-
to[key] = from[key];
268+
if (isPOJO(from[key])) {
269+
to[key] = { ...from[key] };
270+
} else if (Array.isArray(from[key])) {
271+
to[key] = [...from[key]];
272+
} else {
273+
to[key] = from[key];
274+
}
268275
} else if (exports.isObject(from[key])) {
269276
if (!exports.isObject(to[key])) {
270277
to[key] = {};
@@ -501,7 +508,7 @@ exports.populate = function populate(path, select, model, match, options, subPop
501508
if (path instanceof PopulateOptions) {
502509
// If reusing old populate docs, avoid reusing `_docs` because that may
503510
// lead to bugs and memory leaks. See gh-11641
504-
path._docs = [];
511+
path._docs = {};
505512
path._childDocs = [];
506513
return [path];
507514
}

test/query.test.js

+37
Original file line numberDiff line numberDiff line change
@@ -4054,6 +4054,25 @@ describe('Query', function() {
40544054
});
40554055
});
40564056

4057+
it('shallow clones $and, $or if merging with empty filter (gh-14567) (gh-12944)', function() {
4058+
const TestModel = db.model(
4059+
'Test',
4060+
Schema({ name: String, age: Number, active: Boolean })
4061+
);
4062+
4063+
let originalQuery = { $and: [{ active: true }] };
4064+
let q = TestModel.countDocuments(originalQuery)
4065+
.and([{ age: { $gte: 18 } }]);
4066+
assert.deepStrictEqual(originalQuery, { $and: [{ active: true }] });
4067+
assert.deepStrictEqual(q.getFilter(), { $and: [{ active: true }, { age: { $gte: 18 } }] });
4068+
4069+
originalQuery = { $or: [{ active: true }] };
4070+
q = TestModel.countDocuments(originalQuery)
4071+
.or([{ age: { $gte: 18 } }]);
4072+
assert.deepStrictEqual(originalQuery, { $or: [{ active: true }] });
4073+
assert.deepStrictEqual(q.getFilter(), { $or: [{ active: true }, { age: { $gte: 18 } }] });
4074+
});
4075+
40574076
it('should avoid sending empty session to MongoDB server (gh-13052)', async function() {
40584077
const m = new mongoose.Mongoose();
40594078

@@ -4236,4 +4255,22 @@ describe('Query', function() {
42364255
q.sort({}, { override: true });
42374256
assert.deepStrictEqual(q.getOptions().sort, {});
42384257
});
4258+
4259+
it('avoids mutating user-provided query selectors (gh-14567)', async function() {
4260+
const TestModel = db.model(
4261+
'Test',
4262+
Schema({ name: String, age: Number, active: Boolean })
4263+
);
4264+
4265+
await TestModel.create({ name: 'John', age: 21 });
4266+
await TestModel.create({ name: 'Bob', age: 35 });
4267+
4268+
const adultQuery = { age: { $gte: 18 } };
4269+
4270+
const docs = await TestModel.find(adultQuery).where('age').lte(25);
4271+
assert.equal(docs.length, 1);
4272+
assert.equal(docs[0].name, 'John');
4273+
4274+
assert.deepStrictEqual(adultQuery, { age: { $gte: 18 } });
4275+
});
42394276
});

0 commit comments

Comments
 (0)