mongoose 7.8.8 → 7.8.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "mcp__acp__Bash"
5
+ ]
6
+ }
7
+ }
package/16282.diff ADDED
@@ -0,0 +1,643 @@
1
+ diff --git a/lib/types/documentArray/methods/index.js b/lib/types/documentArray/methods/index.js
2
+ index d5cca29563..ccb34506ad 100644
3
+ --- a/lib/types/documentArray/methods/index.js
4
+ +++ b/lib/types/documentArray/methods/index.js
5
+ @@ -12,6 +12,7 @@ const arrayPathSymbol = require('../../../helpers/symbols').arrayPathSymbol;
6
+ const arraySchemaSymbol = require('../../../helpers/symbols').arraySchemaSymbol;
7
+ const documentArrayParent = require('../../../helpers/symbols').documentArrayParent;
8
+
9
+ +const _baseReverse = Array.prototype.reverse;
10
+ const _baseToString = Array.prototype.toString;
11
+
12
+ const methods = {
13
+ @@ -211,7 +212,7 @@ const methods = {
14
+ },
15
+
16
+ /**
17
+ - * Wraps [`Array#push`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/push) with proper change tracking.
18
+ + * Wraps [`Array#push`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push) with proper change tracking.
19
+ *
20
+ * @param {...object} [args]
21
+ * @api public
22
+ @@ -220,8 +221,37 @@ const methods = {
23
+ */
24
+
25
+ push() {
26
+ + const shouldReindex = _shouldReindexAfterPush(arguments);
27
+ const ret = ArrayMethods.push.apply(this, arguments);
28
+
29
+ + if (shouldReindex) {
30
+ + _updateSubdocIndexes(this);
31
+ + }
32
+ + _updateParentPopulated(this);
33
+ +
34
+ + return ret;
35
+ + },
36
+ +
37
+ + /**
38
+ + * Wraps [`Array#pop`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/pop) with proper change tracking.
39
+ + * @api private
40
+ + */
41
+ +
42
+ + pop() {
43
+ + const ret = ArrayMethods.pop.apply(this, arguments);
44
+ +
45
+ + _updateParentPopulated(this);
46
+ +
47
+ + return ret;
48
+ + },
49
+ +
50
+ + /*!
51
+ + * ignore
52
+ + */
53
+ +
54
+ + $pop() {
55
+ + const ret = ArrayMethods.$pop.apply(this, arguments);
56
+ +
57
+ _updateParentPopulated(this);
58
+
59
+ return ret;
60
+ @@ -239,32 +269,96 @@ const methods = {
61
+ pull() {
62
+ const ret = ArrayMethods.pull.apply(this, arguments);
63
+
64
+ + _updateSubdocIndexes(this);
65
+ _updateParentPopulated(this);
66
+
67
+ return ret;
68
+ },
69
+
70
+ /**
71
+ - * Wraps [`Array#shift`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/unshift) with proper change tracking.
72
+ + * Wraps [`Array#shift`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift) with proper change tracking.
73
+ * @api private
74
+ */
75
+
76
+ shift() {
77
+ const ret = ArrayMethods.shift.apply(this, arguments);
78
+
79
+ + _updateSubdocIndexes(this);
80
+ + _updateParentPopulated(this);
81
+ +
82
+ + return ret;
83
+ + },
84
+ +
85
+ + /*!
86
+ + * ignore
87
+ + */
88
+ +
89
+ + $shift() {
90
+ + const ret = ArrayMethods.$shift.apply(this, arguments);
91
+ +
92
+ + _updateSubdocIndexes(this);
93
+ _updateParentPopulated(this);
94
+
95
+ return ret;
96
+ },
97
+
98
+ /**
99
+ - * Wraps [`Array#splice`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/splice) with proper change tracking and casting.
100
+ + * Wraps [`Array#splice`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) with proper change tracking and casting.
101
+ * @api private
102
+ */
103
+
104
+ splice() {
105
+ const ret = ArrayMethods.splice.apply(this, arguments);
106
+
107
+ + _updateSubdocIndexes(this);
108
+ + _updateParentPopulated(this);
109
+ +
110
+ + return ret;
111
+ + },
112
+ +
113
+ + /**
114
+ + * Wraps [`Array#reverse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse)
115
+ + * with proper change tracking.
116
+ + * @api private
117
+ + */
118
+ +
119
+ + reverse() {
120
+ + _baseReverse.call(this.__array);
121
+ + this._registerAtomic('$set', this);
122
+ +
123
+ + _updateSubdocIndexes(this);
124
+ + _updateParentPopulated(this);
125
+ +
126
+ + return this;
127
+ + },
128
+ +
129
+ + /**
130
+ + * Wraps [`Array#sort`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)
131
+ + * with proper change tracking.
132
+ + *
133
+ + * @param {Function} [compareFunction]
134
+ + * @api private
135
+ + */
136
+ +
137
+ + sort() {
138
+ + const ret = ArrayMethods.sort.apply(this, arguments);
139
+ +
140
+ + _updateSubdocIndexes(this);
141
+ + _updateParentPopulated(this);
142
+ +
143
+ + return ret;
144
+ + },
145
+ +
146
+ + /**
147
+ + * Wraps [`Array#unshift`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift)
148
+ + * with proper change tracking.
149
+ + * @api private
150
+ + */
151
+ +
152
+ + unshift() {
153
+ + const ret = ArrayMethods.unshift.apply(this, arguments);
154
+ +
155
+ + _updateSubdocIndexes(this);
156
+ _updateParentPopulated(this);
157
+
158
+ return ret;
159
+ @@ -389,6 +483,26 @@ const methods = {
160
+
161
+ module.exports = methods;
162
+
163
+ +/*!
164
+ + * ignore
165
+ + */
166
+ +
167
+ +function _updateSubdocIndexes(arr) {
168
+ + const rawArray = utils.isMongooseArray(arr) ? arr.__array : arr;
169
+ +
170
+ + for (let i = 0; i < rawArray.length; ++i) {
171
+ + if (typeof rawArray[i]?.$setIndex === 'function') {
172
+ + rawArray[i].$setIndex(i);
173
+ + }
174
+ + }
175
+ +}
176
+ +
177
+ +function _shouldReindexAfterPush(args) {
178
+ + return args[0] != null &&
179
+ + utils.hasUserDefinedProperty(args[0], '$each') &&
180
+ + args[0].$position != null;
181
+ +}
182
+ +
183
+ /**
184
+ * If this is a document array, each element may contain single
185
+ * populated paths, so we need to modify the top-level document's
186
+ diff --git a/test/types.documentarray.test.js b/test/types.documentarray.test.js
187
+ index 4392057698..d3d98b15f4 100644
188
+ --- a/test/types.documentarray.test.js
189
+ +++ b/test/types.documentarray.test.js
190
+ @@ -847,6 +847,453 @@ describe('types.documentarray', function() {
191
+ assert.strictEqual(fetched.array[1].v, 0);
192
+ });
193
+
194
+ + describe('document array indexes after removal', function() {
195
+ + it('reindexes subdocs after pull() so subsequent nested changes save correctly', async function() {
196
+ + // Arrange
197
+ + const { User, user } = await createTestContext();
198
+ + user.addresses.pull(user.addresses[0]._id);
199
+ + await user.save();
200
+ + assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
201
+ +
202
+ + // Act
203
+ + user.addresses[0].city = 'New York';
204
+ +
205
+ + // Assert
206
+ + assert.strictEqual(
207
+ + user.addresses[0].$__fullPath('city'),
208
+ + 'addresses.0.city',
209
+ + 'first remaining subdoc should use its current array index'
210
+ + );
211
+ + assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
212
+ + assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.0.city': 'New York' } });
213
+ +
214
+ + await user.save();
215
+ + const fetched = await User.findById(user._id).orFail().lean();
216
+ + assert.strictEqual(fetched.addresses[0].city, 'New York');
217
+ + assert.strictEqual(fetched.addresses[1].city, 'Denver');
218
+ + });
219
+ +
220
+ + it('reindexes subdocs after splice() so subsequent nested changes save correctly', async function() {
221
+ + // Arrange
222
+ + const { User, user } = await createTestContext();
223
+ + user.addresses.splice(0, 1);
224
+ + await user.save();
225
+ + assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
226
+ +
227
+ + // Act
228
+ + user.addresses[0].city = 'New York';
229
+ +
230
+ + // Assert
231
+ + assert.strictEqual(
232
+ + user.addresses[0].$__fullPath('city'),
233
+ + 'addresses.0.city',
234
+ + 'first remaining subdoc should use its current array index'
235
+ + );
236
+ + assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
237
+ + assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.0.city': 'New York' } });
238
+ +
239
+ + await user.save();
240
+ + const fetched = await User.findById(user._id).orFail().lean();
241
+ + assert.strictEqual(fetched.addresses[0].city, 'New York');
242
+ + assert.strictEqual(fetched.addresses[1].city, 'Denver');
243
+ + });
244
+ +
245
+ + it('reindexes subdocs after shift() so subsequent nested changes save correctly', async function() {
246
+ + // Arrange
247
+ + const { User, user } = await createTestContext();
248
+ + user.addresses.shift();
249
+ + await user.save();
250
+ + assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
251
+ +
252
+ + // Act
253
+ + user.addresses[0].city = 'New York';
254
+ +
255
+ + // Assert
256
+ + assert.strictEqual(
257
+ + user.addresses[0].$__fullPath('city'),
258
+ + 'addresses.0.city',
259
+ + 'first remaining subdoc should use its current array index'
260
+ + );
261
+ + assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
262
+ + assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.0.city': 'New York' } });
263
+ +
264
+ + await user.save();
265
+ + const fetched = await User.findById(user._id).orFail().lean();
266
+ + assert.strictEqual(fetched.addresses[0].city, 'New York');
267
+ + assert.strictEqual(fetched.addresses[1].city, 'Denver');
268
+ + });
269
+ +
270
+ + it('reindexes subdocs after $shift() so subsequent nested changes save correctly', async function() {
271
+ + // Arrange
272
+ + const { User, user } = await createTestContext();
273
+ + user.addresses.$shift();
274
+ + await user.save();
275
+ + assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
276
+ +
277
+ + // Act
278
+ + user.addresses[0].city = 'New York';
279
+ +
280
+ + // Assert
281
+ + assert.strictEqual(
282
+ + user.addresses[0].$__fullPath('city'),
283
+ + 'addresses.0.city',
284
+ + 'first remaining subdoc should use its current array index'
285
+ + );
286
+ + assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
287
+ + assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.0.city': 'New York' } });
288
+ +
289
+ + await user.save();
290
+ + const fetched = await User.findById(user._id).orFail().lean();
291
+ + assert.strictEqual(fetched.addresses[0].city, 'New York');
292
+ + assert.strictEqual(fetched.addresses[1].city, 'Denver');
293
+ + });
294
+ +
295
+ + async function createTestContext() {
296
+ + const addressSchema = new mongoose.Schema({
297
+ + street: String,
298
+ + city: String
299
+ + });
300
+ + const userSchema = new mongoose.Schema({
301
+ + name: String,
302
+ + addresses: [addressSchema]
303
+ + });
304
+ + const User = db.model('UserDocumentArrayIndex', userSchema);
305
+ + const user = new User({
306
+ + name: 'John',
307
+ + addresses: [
308
+ + { street: '1 Main', city: 'Boston' },
309
+ + { street: '2 Main', city: 'Chicago' },
310
+ + { street: '3 Main', city: 'Denver' }
311
+ + ]
312
+ + });
313
+ + await user.save();
314
+ +
315
+ + return { User, user };
316
+ + }
317
+ + });
318
+ +
319
+ + describe('document array indexes after reordering', function() {
320
+ + it('reindexes subdocs after unshift() so subsequent nested changes save correctly', async function() {
321
+ + // Arrange
322
+ + const { User, user } = await createTestContext();
323
+ + user.addresses.unshift({ street: '0 Main', city: 'Amsterdam' });
324
+ + await user.save();
325
+ + assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
326
+ +
327
+ + // Act
328
+ + user.addresses[1].city = 'New York';
329
+ +
330
+ + // Assert
331
+ + assert.strictEqual(
332
+ + user.addresses[1].$__fullPath('city'),
333
+ + 'addresses.1.city',
334
+ + 'shifted subdoc should use its current array index'
335
+ + );
336
+ + assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.1', 'addresses.1.city']);
337
+ + assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.1.city': 'New York' } });
338
+ +
339
+ + await user.save();
340
+ + const fetched = await User.findById(user._id).orFail().lean();
341
+ + assert.deepStrictEqual(
342
+ + fetched.addresses.map(address => address.city),
343
+ + ['Amsterdam', 'New York', 'Chicago', 'Denver']
344
+ + );
345
+ + });
346
+ +
347
+ + it('reindexes subdocs after positioned push() so subsequent nested changes save correctly', async function() {
348
+ + // Arrange
349
+ + const { User, user } = await createTestContext();
350
+ + user.addresses.push({
351
+ + $each: [
352
+ + { street: '0 Main', city: 'Amsterdam' },
353
+ + { street: '0 Main', city: 'Rotterdam' }
354
+ + ],
355
+ + $position: 0
356
+ + });
357
+ + await user.save();
358
+ + assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
359
+ +
360
+ + // Act
361
+ + user.addresses[1].city = 'New York';
362
+ +
363
+ + // Assert
364
+ + assert.strictEqual(
365
+ + user.addresses[0].$__fullPath('city'),
366
+ + 'addresses.0.city',
367
+ + 'first positioned pushed subdoc should use its current array index'
368
+ + );
369
+ + assert.strictEqual(
370
+ + user.addresses[1].$__fullPath('city'),
371
+ + 'addresses.1.city',
372
+ + 'second positioned pushed subdoc should use its current array index'
373
+ + );
374
+ + assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.1', 'addresses.1.city']);
375
+ + assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.1.city': 'New York' } });
376
+ +
377
+ + await user.save();
378
+ + const fetched = await User.findById(user._id).orFail().lean();
379
+ + assert.deepStrictEqual(
380
+ + fetched.addresses.map(address => address.city),
381
+ + ['Amsterdam', 'New York', 'Boston', 'Chicago', 'Denver']
382
+ + );
383
+ + });
384
+ +
385
+ + it('does not restamp existing subdocs after append-only push()', async function() {
386
+ + // Arrange
387
+ + const { user } = await createTestContext();
388
+ + const originalSetIndex = user.addresses[0].$setIndex;
389
+ + let setIndexCalls = 0;
390
+ + user.addresses[0].$setIndex = function(index) {
391
+ + ++setIndexCalls;
392
+ + return originalSetIndex.call(this, index);
393
+ + };
394
+ +
395
+ + // Act
396
+ + user.addresses.push({ street: '4 Main', city: 'Austin' });
397
+ +
398
+ + // Assert
399
+ + assert.strictEqual(setIndexCalls, 0);
400
+ + assert.strictEqual(
401
+ + user.addresses[3].$__fullPath('city'),
402
+ + 'addresses.3.city',
403
+ + 'appended subdoc should use its appended array index'
404
+ + );
405
+ + assert.deepStrictEqual(user.$getChanges(), {
406
+ + $push: {
407
+ + addresses: {
408
+ + $each: [{ street: '4 Main', city: 'Austin', _id: user.addresses[3]._id }]
409
+ + }
410
+ + },
411
+ + $inc: { __v: 1 }
412
+ + });
413
+ + });
414
+ +
415
+ + it('reindexes subdocs after sort() so subsequent nested changes save correctly', async function() {
416
+ + // Arrange
417
+ + const { User, user } = await createTestContext();
418
+ + user.addresses.sort((a, b) => b.city.localeCompare(a.city));
419
+ + await user.save();
420
+ + assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
421
+ +
422
+ + // Act
423
+ + user.addresses[0].city = 'New York';
424
+ +
425
+ + // Assert
426
+ + assert.strictEqual(
427
+ + user.addresses[0].$__fullPath('city'),
428
+ + 'addresses.0.city',
429
+ + 'sorted subdoc should use its current array index'
430
+ + );
431
+ + assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
432
+ + assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.0.city': 'New York' } });
433
+ +
434
+ + await user.save();
435
+ + const fetched = await User.findById(user._id).orFail().lean();
436
+ + assert.deepStrictEqual(
437
+ + fetched.addresses.map(address => address.city),
438
+ + ['New York', 'Chicago', 'Boston']
439
+ + );
440
+ + });
441
+ +
442
+ + it('keeps subdoc indexes correct after reverse() so subsequent nested changes save correctly', async function() {
443
+ + // Arrange
444
+ + const { User, user } = await createTestContext();
445
+ + user.addresses.reverse();
446
+ + await user.save();
447
+ + assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
448
+ +
449
+ + // Act
450
+ + user.addresses[0].city = 'New York';
451
+ +
452
+ + // Assert
453
+ + assert.strictEqual(
454
+ + user.addresses[0].$__fullPath('city'),
455
+ + 'addresses.0.city',
456
+ + 'reversed subdoc should use its current array index'
457
+ + );
458
+ + assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
459
+ + assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.0.city': 'New York' } });
460
+ +
461
+ + await user.save();
462
+ + const fetched = await User.findById(user._id).orFail().lean();
463
+ + assert.deepStrictEqual(
464
+ + fetched.addresses.map(address => address.city),
465
+ + ['New York', 'Chicago', 'Boston']
466
+ + );
467
+ + });
468
+ +
469
+ + it('registers full array atomics after reverse() follows append-only push()', async function() {
470
+ + // Arrange
471
+ + const { user } = await createTestContext();
472
+ + user.addresses.push({ street: '4 Main', city: 'Austin' });
473
+ + assert.deepStrictEqual(Object.keys(user.addresses.$atomics()), ['$push'], 'sanity: append-only push starts as $push');
474
+ +
475
+ + // Act
476
+ + const ret = user.addresses.reverse();
477
+ +
478
+ + // Assert
479
+ + assert.strictEqual(ret, user.addresses);
480
+ + assert.deepStrictEqual(Object.keys(user.addresses.$atomics()), ['$set']);
481
+ + assert.deepStrictEqual(
482
+ + user.addresses.$atomics().$set.map(address => address.city),
483
+ + ['Austin', 'Denver', 'Chicago', 'Boston']
484
+ + );
485
+ + assert.deepStrictEqual(user.$getChanges(), {
486
+ + $set: {
487
+ + addresses: [
488
+ + { street: '4 Main', city: 'Austin', _id: user.addresses[0]._id },
489
+ + { street: '3 Main', city: 'Denver', _id: user.addresses[1]._id },
490
+ + { street: '2 Main', city: 'Chicago', _id: user.addresses[2]._id },
491
+ + { street: '1 Main', city: 'Boston', _id: user.addresses[3]._id }
492
+ + ]
493
+ + },
494
+ + $inc: { __v: 1 }
495
+ + });
496
+ + });
497
+ +
498
+ + async function createTestContext() {
499
+ + const addressSchema = new mongoose.Schema({
500
+ + street: String,
501
+ + city: String
502
+ + });
503
+ + const userSchema = new mongoose.Schema({
504
+ + name: String,
505
+ + addresses: [addressSchema]
506
+ + });
507
+ + const User = db.model('UserDocumentArrayReorderIndex', userSchema);
508
+ + const user = new User({
509
+ + name: 'John',
510
+ + addresses: [
511
+ + { street: '1 Main', city: 'Boston' },
512
+ + { street: '2 Main', city: 'Chicago' },
513
+ + { street: '3 Main', city: 'Denver' }
514
+ + ]
515
+ + });
516
+ + await user.save();
517
+ +
518
+ + return { User, user };
519
+ + }
520
+ + });
521
+ +
522
+ + describe('document array populated paths', function() {
523
+ + it('updates top-level populated() after sort() reorders a document array', async function() {
524
+ + // Arrange
525
+ + const { Trip, locations, trip, getPopulatedLocationIds } = await createTestContext();
526
+ + const fromDb = await Trip.findById(trip._id).orFail().populate('stops.location');
527
+ + assert.deepStrictEqual(
528
+ + getPopulatedLocationIds(fromDb),
529
+ + locations.map(location => location._id.toString())
530
+ + );
531
+ +
532
+ + // Act
533
+ + fromDb.stops.sort((left, right) => left.sequence - right.sequence);
534
+ +
535
+ + // Assert
536
+ + assert.deepStrictEqual(
537
+ + getPopulatedLocationIds(fromDb),
538
+ + [
539
+ + locations[1]._id.toString(),
540
+ + locations[0]._id.toString(),
541
+ + locations[2]._id.toString()
542
+ + ]
543
+ + );
544
+ + });
545
+ +
546
+ + it('updates top-level populated() after reverse() reorders a document array', async function() {
547
+ + // Arrange
548
+ + const { Trip, locations, trip, getPopulatedLocationIds } = await createTestContext();
549
+ + const fromDb = await Trip.findById(trip._id).orFail().populate('stops.location');
550
+ + assert.deepStrictEqual(
551
+ + getPopulatedLocationIds(fromDb),
552
+ + locations.map(location => location._id.toString())
553
+ + );
554
+ +
555
+ + // Act
556
+ + fromDb.stops.reverse();
557
+ +
558
+ + // Assert
559
+ + assert.deepStrictEqual(
560
+ + getPopulatedLocationIds(fromDb),
561
+ + [
562
+ + locations[2]._id.toString(),
563
+ + locations[1]._id.toString(),
564
+ + locations[0]._id.toString()
565
+ + ]
566
+ + );
567
+ + });
568
+ +
569
+ + it('updates top-level populated() after pop() removes the last subdoc', async function() {
570
+ + // Arrange
571
+ + const { Trip, locations, trip, getPopulatedLocationIds } = await createTestContext();
572
+ + const fromDb = await Trip.findById(trip._id).orFail().populate('stops.location');
573
+ + assert.deepStrictEqual(
574
+ + getPopulatedLocationIds(fromDb),
575
+ + locations.map(location => location._id.toString())
576
+ + );
577
+ +
578
+ + // Act
579
+ + fromDb.stops.pop();
580
+ +
581
+ + // Assert
582
+ + assert.deepStrictEqual(
583
+ + getPopulatedLocationIds(fromDb),
584
+ + [locations[0]._id.toString(), locations[1]._id.toString()]
585
+ + );
586
+ + });
587
+ +
588
+ + it('updates top-level populated() after $pop() removes the last subdoc', async function() {
589
+ + // Arrange
590
+ + const { Trip, locations, trip, getPopulatedLocationIds } = await createTestContext();
591
+ + const fromDb = await Trip.findById(trip._id).orFail().populate('stops.location');
592
+ + assert.deepStrictEqual(
593
+ + getPopulatedLocationIds(fromDb),
594
+ + locations.map(location => location._id.toString())
595
+ + );
596
+ +
597
+ + // Act
598
+ + fromDb.stops.$pop();
599
+ +
600
+ + // Assert
601
+ + assert.deepStrictEqual(
602
+ + getPopulatedLocationIds(fromDb),
603
+ + [locations[0]._id.toString(), locations[1]._id.toString()]
604
+ + );
605
+ + });
606
+ +
607
+ + async function createTestContext() {
608
+ + const locationSchema = new Schema({ name: String });
609
+ + const stopSchema = new Schema({
610
+ + location: { type: Schema.Types.ObjectId, ref: 'Location' },
611
+ + sequence: Number
612
+ + });
613
+ + const tripSchema = new Schema({
614
+ + name: String,
615
+ + stops: [stopSchema]
616
+ + });
617
+ + const Location = db.model('Location', locationSchema);
618
+ + const Trip = db.model('Trip', tripSchema);
619
+ + const locations = await Location.create([
620
+ + { name: 'Boston' },
621
+ + { name: 'Chicago' },
622
+ + { name: 'Denver' }
623
+ + ]);
624
+ + const trip = await Trip.create({
625
+ + name: 'Route planning',
626
+ + stops: [
627
+ + { location: locations[0]._id, sequence: 2 },
628
+ + { location: locations[1]._id, sequence: 1 },
629
+ + { location: locations[2]._id, sequence: 3 }
630
+ + ]
631
+ + });
632
+ +
633
+ + return { Trip, locations, trip, getPopulatedLocationIds };
634
+ + }
635
+ +
636
+ + function getPopulatedLocationIds(trip) {
637
+ + return trip.populated('stops.location').map(id => id.toString());
638
+ + }
639
+ + });
640
+ +
641
+ it('clones subdoc when same subdoc reference appears multiple times in array (gh-15973)', async function() {
642
+ const childSchema = new mongoose.Schema({
643
+ id: Number,