mongoose 7.8.9 → 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.
- package/16282.diff +643 -0
- package/dist/browser.umd.js +1 -1
- package/gh-16033.ts +35 -0
- package/lib/document.js +10 -3
- package/lib/schema/bigint.js +1 -1
- package/lib/schema/buffer.js +4 -4
- package/lib/schema/date.js +1 -1
- package/lib/schema/decimal128.js +1 -1
- package/lib/schema/number.js +1 -1
- package/lib/schema/objectid.js +3 -2
- package/lib/schema/string.js +3 -2
- package/lib/schema.js +10 -6
- package/package.json +1 -1
- package/step1.mjs +44 -0
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,
|