Skip to content

Commit 41aebad

Browse files
authored
Merge pull request #14883 from Automattic/vkarpov15/gh-1635-2
fix: depopulate if `push()` or `addToSet()` with an ObjectId on a populated array
2 parents faa30a1 + edc7dde commit 41aebad

File tree

4 files changed

+80
-1
lines changed

4 files changed

+80
-1
lines changed

docs/populate.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,33 @@ story.author = author;
119119
console.log(story.author.name); // prints "Ian Fleming"
120120
```
121121

122+
You can also push documents or POJOs onto a populated array, and Mongoose will add those documents if their `ref` matches.
123+
124+
```javascript
125+
const fan1 = await Person.create({ name: 'Sean' });
126+
await Story.updateOne({ title: 'Casino Royale' }, { $push: { fans: { $each: [fan1._id] } } });
127+
128+
const story = await Story.findOne({ title: 'Casino Royale' }).populate('fans');
129+
story.fans[0].name; // 'Sean'
130+
131+
const fan2 = await Person.create({ name: 'George' });
132+
story.fans.push(fan2);
133+
story.fans[1].name; // 'George'
134+
135+
story.fans.push({ name: 'Roger' });
136+
story.fans[2].name; // 'Roger'
137+
```
138+
139+
If you push a non-POJO and non-document value, like an ObjectId, Mongoose `>= 8.7.0` will depopulate the entire array.
140+
141+
```javascript
142+
const fan4 = await Person.create({ name: 'Timothy' });
143+
story.fans.push(fan4._id); // Push the `_id`, not the full document
144+
145+
story.fans[0].name; // undefined, `fans[0]` is now an ObjectId
146+
story.fans[0].toString() === fan1._id.toString(); // true
147+
```
148+
122149
## Checking Whether a Field is Populated {#checking-populated}
123150

124151
You can call the `populated()` function to check whether a field is populated.

lib/types/array/methods/index.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ const methods = {
410410

411411
addToSet() {
412412
_checkManualPopulation(this, arguments);
413+
_depopulateIfNecessary(this, arguments);
413414

414415
const values = [].map.call(arguments, this._mapCast, this);
415416
const added = [];
@@ -691,6 +692,7 @@ const methods = {
691692
}
692693

693694
_checkManualPopulation(this, values);
695+
_depopulateIfNecessary(this, values);
694696

695697
values = [].map.call(values, this._mapCast, this);
696698
let ret;
@@ -1009,6 +1011,30 @@ function _checkManualPopulation(arr, docs) {
10091011
}
10101012
}
10111013

1014+
/*!
1015+
* If `docs` isn't all instances of the right model, depopulate `arr`
1016+
*/
1017+
1018+
function _depopulateIfNecessary(arr, docs) {
1019+
const ref = arr == null ?
1020+
null :
1021+
arr[arraySchemaSymbol] && arr[arraySchemaSymbol].caster && arr[arraySchemaSymbol].caster.options && arr[arraySchemaSymbol].caster.options.ref || null;
1022+
const parentDoc = arr[arrayParentSymbol];
1023+
const path = arr[arrayPathSymbol];
1024+
if (!ref || !parentDoc.populated(path)) {
1025+
return;
1026+
}
1027+
for (const doc of docs) {
1028+
if (doc == null) {
1029+
continue;
1030+
}
1031+
if (typeof doc !== 'object' || doc instanceof String || doc instanceof Number || doc instanceof Buffer || utils.isMongooseType(doc)) {
1032+
parentDoc.depopulate(path);
1033+
break;
1034+
}
1035+
}
1036+
}
1037+
10121038
const returnVanillaArrayMethods = [
10131039
'filter',
10141040
'flat',

test/model.populate.setting.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ describe('model: populate:', function() {
152152
assert.equal(doc.fans[6], null);
153153

154154
const _id = construct[id]();
155-
doc.fans.addToSet(_id);
155+
doc.fans.addToSet({ _id });
156156
if (Buffer.isBuffer(_id)) {
157157
assert.equal(doc.fans[7]._id.toString('utf8'), _id.toString('utf8'));
158158
} else {

test/model.populate.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11131,4 +11131,30 @@ describe('model: populate:', function() {
1113111131
}
1113211132
assert.equal(posts.length, 2);
1113311133
});
11134+
11135+
it('depopulates if pushing ObjectId to a populated array (gh-1635)', async function() {
11136+
const ParentModel = db.model('Test', mongoose.Schema({
11137+
name: String,
11138+
children: [{ type: 'ObjectId', ref: 'Child' }]
11139+
}));
11140+
const ChildModel = db.model('Child', mongoose.Schema({ name: String }));
11141+
11142+
const children = await ChildModel.create([{ name: 'Luke' }, { name: 'Leia' }]);
11143+
const newChild = await ChildModel.create({ name: 'Taco' });
11144+
const { _id } = await ParentModel.create({ name: 'Anakin', children });
11145+
11146+
const doc = await ParentModel.findById(_id).populate('children');
11147+
doc.children.push(newChild._id);
11148+
11149+
assert.ok(doc.children[0] instanceof mongoose.Types.ObjectId);
11150+
assert.ok(doc.children[1] instanceof mongoose.Types.ObjectId);
11151+
assert.ok(doc.children[2] instanceof mongoose.Types.ObjectId);
11152+
11153+
await doc.save();
11154+
11155+
const fromDb = await ParentModel.findById(_id);
11156+
assert.equal(fromDb.children[0].toHexString(), children[0]._id.toHexString());
11157+
assert.equal(fromDb.children[1].toHexString(), children[1]._id.toHexString());
11158+
assert.equal(fromDb.children[2].toHexString(), newChild._id.toHexString());
11159+
});
1113411160
});

0 commit comments

Comments
 (0)