mongoose 9.10.1 → 9.10.3

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/lib/collection.js CHANGED
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  const EventEmitter = require('events').EventEmitter;
8
- const STATES = require('./connectionState');
9
8
  const immediate = require('./helpers/immediate');
10
9
 
11
10
  /**
@@ -32,7 +31,7 @@ function Collection(name, conn, opts) {
32
31
  this.buffer = !conn?._hasOpened;
33
32
  this.emitter = new EventEmitter();
34
33
 
35
- if (STATES.connected === this.conn.readyState) {
34
+ if (conn?._hasOpened) {
36
35
  this.onOpen();
37
36
  }
38
37
  }
@@ -495,6 +495,11 @@ function _setClient(conn, client, options, dbName) {
495
495
  otherDb._flushQueue();
496
496
  }
497
497
  }
498
+ for (const collection of Object.values(conn.collections)) {
499
+ if (collection.buffer) {
500
+ collection.onOpen();
501
+ }
502
+ }
498
503
  });
499
504
 
500
505
  if (options.monitorCommands) {
@@ -197,6 +197,11 @@ function cloneObject(obj, options, isArrayChild) {
197
197
  }
198
198
 
199
199
  function cloneArray(arr, options) {
200
+ const seen = options?._seen;
201
+ if (seen && seen.has(arr)) {
202
+ return seen.get(arr);
203
+ }
204
+
200
205
  let i = 0;
201
206
  const len = arr.length;
202
207
 
@@ -218,6 +223,10 @@ function cloneArray(arr, options) {
218
223
  ret = new Array(len);
219
224
  }
220
225
 
226
+ if (seen) {
227
+ seen.set(arr, ret);
228
+ }
229
+
221
230
  arr = isMongooseArray(arr) ? arr.__array : arr;
222
231
  if (ret.isMongooseDocumentArray) {
223
232
  // Create new options object to avoid mutating the shared options.
@@ -149,7 +149,7 @@ module.exports.castUpdateOne = function castUpdateOne(originalModel, updateOne,
149
149
  model.schema.options.versionKey
150
150
  );
151
151
 
152
- updateOne['filter'] = cast(model.schema, updateOne['filter'], {
152
+ updateOne['filter'] = _castFilter(model, updateOne['filter'], {
153
153
  strict: strict,
154
154
  upsert: updateOne.upsert
155
155
  });
@@ -176,11 +176,13 @@ module.exports.castUpdateMany = function castUpdateMany(originalModel, updateMan
176
176
  const schema = model.schema;
177
177
  const strict = options.strict ?? model.schema.options.strict;
178
178
 
179
+ const update = clone(updateMany['update']);
180
+
179
181
  const globalSetDefaultsOnInsert = originalModel.base.options.setDefaultsOnInsert;
180
182
  const shouldSetDefaultsOnInsert = updateMany.setDefaultsOnInsert ?? globalSetDefaultsOnInsert;
181
183
 
182
184
  if (shouldSetDefaultsOnInsert !== false) {
183
- setDefaultsOnInsert(updateMany['filter'], model.schema, updateMany['update'], {
185
+ setDefaultsOnInsert(updateMany['filter'], model.schema, update, {
184
186
  setDefaultsOnInsert: true,
185
187
  upsert: updateMany.upsert
186
188
  });
@@ -191,29 +193,29 @@ module.exports.castUpdateMany = function castUpdateMany(originalModel, updateMan
191
193
  if (model.schema.$timestamps != null && doInitTimestamps) {
192
194
  const createdAt = model.schema.$timestamps.createdAt;
193
195
  const updatedAt = model.schema.$timestamps.updatedAt;
194
- applyTimestampsToUpdate(now, createdAt, updatedAt, updateMany['update'], {
196
+ applyTimestampsToUpdate(now, createdAt, updatedAt, update, {
195
197
  timestamps: updateMany.timestamps,
196
198
  overwriteImmutable: updateMany.overwriteImmutable
197
199
  }, updateMany.upsert);
198
200
  }
199
201
  if (doInitTimestamps) {
200
- applyTimestampsToChildren(now, updateMany['update'], model.schema);
202
+ applyTimestampsToChildren(now, update, model.schema);
201
203
  }
202
204
 
203
205
  _addDiscriminatorToObject(schema, updateMany['filter']);
204
206
 
205
207
  decorateUpdateWithVersionKey(
206
- updateMany['update'],
208
+ update,
207
209
  updateMany,
208
210
  model.schema.options.versionKey
209
211
  );
210
212
 
211
- updateMany['filter'] = cast(model.schema, updateMany['filter'], {
213
+ updateMany['filter'] = _castFilter(model, updateMany['filter'], {
212
214
  strict: strict,
213
215
  upsert: updateMany.upsert
214
216
  });
215
217
 
216
- updateMany['update'] = castUpdate(model.schema, updateMany['update'], {
218
+ updateMany['update'] = castUpdate(model.schema, update, {
217
219
  strict: strict,
218
220
  upsert: updateMany.upsert,
219
221
  arrayFilters: updateMany.arrayFilters,
@@ -228,7 +230,7 @@ module.exports.castReplaceOne = async function castReplaceOne(originalModel, rep
228
230
  const strict = options.strict ?? model.schema.options.strict;
229
231
 
230
232
  _addDiscriminatorToObject(schema, replaceOne['filter']);
231
- replaceOne['filter'] = cast(model.schema, replaceOne['filter'], {
233
+ replaceOne['filter'] = _castFilter(model, replaceOne['filter'], {
232
234
  strict: strict,
233
235
  upsert: replaceOne.upsert
234
236
  });
@@ -262,7 +264,7 @@ module.exports.castDeleteOne = function castDeleteOne(originalModel, deleteOne)
262
264
 
263
265
  _addDiscriminatorToObject(schema, deleteOne['filter']);
264
266
 
265
- deleteOne['filter'] = cast(model.schema, deleteOne['filter']);
267
+ deleteOne['filter'] = _castFilter(model, deleteOne['filter']);
266
268
  };
267
269
 
268
270
  module.exports.castDeleteMany = function castDeleteMany(originalModel, deleteMany) {
@@ -271,7 +273,7 @@ module.exports.castDeleteMany = function castDeleteMany(originalModel, deleteMan
271
273
 
272
274
  _addDiscriminatorToObject(schema, deleteMany['filter']);
273
275
 
274
- deleteMany['filter'] = cast(model.schema, deleteMany['filter']);
276
+ deleteMany['filter'] = _castFilter(model, deleteMany['filter']);
275
277
  };
276
278
 
277
279
  module.exports.cast = {
@@ -283,6 +285,19 @@ module.exports.cast = {
283
285
  deleteMany: module.exports.castDeleteMany
284
286
  };
285
287
 
288
+ function _castFilter(model, filter, options) {
289
+ try {
290
+ return cast(model.schema, filter, options);
291
+ } catch (err) {
292
+ // Set the model on cast errors so `{MODEL}` gets replaced, like it is for
293
+ // query filter casting (gh-8300)
294
+ if (typeof err.setModel === 'function') {
295
+ err.setModel(model);
296
+ }
297
+ throw err;
298
+ }
299
+ }
300
+
286
301
  function _addDiscriminatorToObject(schema, obj) {
287
302
  if (schema == null) {
288
303
  return;
@@ -262,7 +262,16 @@ function walkUpdatePath(schema, obj, op, options, context, filter, prefix) {
262
262
  }
263
263
  }
264
264
  if (schematype?.schema != null) {
265
- obj[key] = cast(schematype.schema, obj[key], options, context);
265
+ try {
266
+ obj[key] = cast(schematype.schema, obj[key], options, context);
267
+ } catch (error) {
268
+ if (error instanceof CastError) {
269
+ aggregatedError = _appendError(error, context, key, aggregatedError);
270
+ continue;
271
+ } else {
272
+ throw error;
273
+ }
274
+ }
266
275
  hasKeys = true;
267
276
  continue;
268
277
  }
@@ -446,11 +455,15 @@ function walkUpdatePath(schema, obj, op, options, context, filter, prefix) {
446
455
  }
447
456
  } else {
448
457
  if (op === '$rename') {
449
- if (obj[key] == null) {
450
- throw new CastError('String', obj[key], `${prefix}${key}.$rename`);
458
+ try {
459
+ if (obj[key] == null) {
460
+ throw new CastError('String', obj[key], `${prefix}${key}.$rename`);
461
+ }
462
+ const schematype = new SchemaString(`${prefix}${key}.$rename`, null, null, schema);
463
+ obj[key] = schematype.castForQuery(null, obj[key], context);
464
+ } catch (error) {
465
+ aggregatedError = _appendError(error, context, key, aggregatedError);
451
466
  }
452
- const schematype = new SchemaString(`${prefix}${key}.$rename`, null, null, schema);
453
- obj[key] = schematype.castForQuery(null, obj[key], context);
454
467
  continue;
455
468
  }
456
469
 
@@ -499,6 +512,14 @@ function walkUpdatePath(schema, obj, op, options, context, filter, prefix) {
499
512
  */
500
513
 
501
514
  function _appendError(error, query, key, aggregatedError) {
515
+ // Set the model on cast errors so `{MODEL}` gets replaced, like it is for
516
+ // query filter casting and document validation (gh-8300). `query` is a
517
+ // Query, or the model itself when casting a `bulkWrite()` operation.
518
+ const model = query?.modelName != null ? query : query?.model;
519
+ if (model != null && typeof error.setModel === 'function') {
520
+ error.setModel(model);
521
+ }
522
+
502
523
  if (typeof query !== 'object' || !query.options.multipleCastError) {
503
524
  throw error;
504
525
  }
package/lib/model.js CHANGED
@@ -3676,7 +3676,8 @@ Model.bulkSave = async function bulkSave(documents, options) {
3676
3676
 
3677
3677
  await Promise.all(documents.map(doc => buildPreSavePromise(doc, options)));
3678
3678
 
3679
- const writeOperations = this.buildBulkWriteOperations(documents, options);
3679
+ const documentIndexes = [];
3680
+ const writeOperations = this.buildBulkWriteOperations(documents, options, documentIndexes);
3680
3681
  const opts = { skipValidation: true, _skipCastBulkWrite: true, ...options };
3681
3682
  const { bulkWriteResult, bulkWriteError } = await this.bulkWrite(writeOperations, opts).then(
3682
3683
  (res) => ({ bulkWriteResult: res, bulkWriteError: null }),
@@ -3697,20 +3698,22 @@ Model.bulkSave = async function bulkSave(documents, options) {
3697
3698
  );
3698
3699
  }
3699
3700
 
3700
- const failedDocumentIds = documents.length >= 25 ? new Set(bulkWriteError?.writeErrors?.map(writeError => {
3701
- const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;
3702
- return writeErrorDocumentId.toString();
3703
- })) : null;
3704
- const successfulDocuments = [];
3705
- for (let i = 0; i < documents.length; i++) {
3706
- const document = documents[i];
3707
- const documentId = document._doc._id.toString();
3708
- const failed = failedDocumentIds == null ? bulkWriteError?.writeErrors.find(writeError => {
3709
- const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;
3710
- return writeErrorDocumentId.toString() === documentId;
3711
- }) != null : failedDocumentIds.has(documentId);
3712
- if (!failed) {
3713
- successfulDocuments.push(document);
3701
+ const writeErrors = bulkWriteError?.writeErrors || [];
3702
+ let successfulDocuments = documents;
3703
+ if (writeErrors.length > 0) {
3704
+ if (options.ordered !== false) {
3705
+ // Unchanged documents don't generate operations, so map back to the input before slicing.
3706
+ successfulDocuments = documents.slice(0, documentIndexes[writeErrors[0].index]);
3707
+ } else {
3708
+ const failedDocumentIndexes = documents.length >= 25 ? {} : null;
3709
+ if (failedDocumentIndexes != null) {
3710
+ for (const writeError of writeErrors) {
3711
+ failedDocumentIndexes[documentIndexes[writeError.index]] = true;
3712
+ }
3713
+ }
3714
+ successfulDocuments = documents.filter((document, i) => failedDocumentIndexes == null ?
3715
+ !writeErrors.some(writeError => documentIndexes[writeError.index] === i) :
3716
+ !failedDocumentIndexes[i]);
3714
3717
  }
3715
3718
  }
3716
3719
  await Promise.all(successfulDocuments.map(document => handleSuccessfulWrite(document, options)));
@@ -3951,6 +3954,18 @@ Model.castObject = function castObject(obj, options) {
3951
3954
  }
3952
3955
 
3953
3956
  if (error != null) {
3957
+ // `{MODEL}` is replaced for query casting and document validation, so do it
3958
+ // here too. Only the top-level call knows the model: the recursive calls for
3959
+ // subdocuments are invoked on the subdocument constructor, which has no
3960
+ // `modelName`, so this runs once over every collected error. (gh-8300)
3961
+ if (this.modelName != null) {
3962
+ for (const key of Object.keys(error.errors)) {
3963
+ const err = error.errors[key];
3964
+ if (typeof err.setModel === 'function') {
3965
+ err.setModel(this);
3966
+ }
3967
+ }
3968
+ }
3954
3969
  throw error;
3955
3970
  }
3956
3971
 
@@ -3964,11 +3979,12 @@ Model.castObject = function castObject(obj, options) {
3964
3979
  * @param {object} options
3965
3980
  * @param {boolean} options.skipValidation defaults to `false`, when set to true, building the write operations will bypass validating the documents.
3966
3981
  * @param {boolean} options.timestamps defaults to `null`, when set to false, mongoose will not add/update timestamps to the documents.
3982
+ * @param {number[]} [documentIndexes] If provided, receives the input document index for each generated operation in order.
3967
3983
  * @return {Promise[]} Returns a array of all Promises the function executes to be awaited.
3968
3984
  * @api private
3969
3985
  */
3970
3986
 
3971
- Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, options) {
3987
+ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, options, documentIndexes) {
3972
3988
  if (!Array.isArray(documents)) {
3973
3989
  throw new MongooseError(`bulkSave expects an array of documents to be passed, received \`${documents}\` instead`);
3974
3990
  }
@@ -3998,6 +4014,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
3998
4014
  }
3999
4015
  const writeOperation = { insertOne: { document } };
4000
4016
  utils.injectTimestampsOption(writeOperation.insertOne, options.timestamps);
4017
+ documentIndexes?.push(i);
4001
4018
  return writeOperation;
4002
4019
  }
4003
4020
 
@@ -4024,6 +4041,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
4024
4041
  document.$__version(where, delta);
4025
4042
  const writeOperation = { updateOne: { filter: where, update: changes } };
4026
4043
  utils.injectTimestampsOption(writeOperation.updateOne, options.timestamps);
4044
+ documentIndexes?.push(i);
4027
4045
  return writeOperation;
4028
4046
  }
4029
4047
 
package/lib/schema/map.js CHANGED
@@ -30,20 +30,6 @@ class SchemaMap extends SchemaType {
30
30
  this._createNestedSchemaType(parentSchema, key, options, schemaOptions);
31
31
  }
32
32
 
33
- /**
34
- * Sets a default option for all Map instances.
35
- *
36
- * @param {string} option The option you'd like to set the value for
37
- * @param {any} value value for option
38
- * @return {undefined}
39
- * @function set
40
- * @api public
41
- */
42
-
43
- set(option, value) {
44
- return SchemaType.set(option, value);
45
- }
46
-
47
33
  /**
48
34
  * Casts to Map
49
35
  *
@@ -143,6 +129,19 @@ class SchemaMap extends SchemaType {
143
129
  }
144
130
  }
145
131
 
132
+ /**
133
+ * Sets a default option for all Map instances.
134
+ *
135
+ * @param {string} option The option you'd like to set the value for
136
+ * @param {any} value value for option
137
+ * @return {undefined}
138
+ * @function set
139
+ * @static
140
+ * @api public
141
+ */
142
+
143
+ SchemaMap.set = SchemaType.set;
144
+
146
145
  /**
147
146
  * This schema type's name, to defend against minifiers that mangle
148
147
  * function names.
@@ -60,6 +60,10 @@ class Union extends SchemaType {
60
60
 
61
61
  // Setters also need to be aware of casting - we need to apply the setters of the entry in the union we choose.
62
62
  applySetters(val, doc, init, prev, options) {
63
+ // Setters on the union path itself run first, so they can change which entry
64
+ // in the union the value resolves to.
65
+ val = this._applySetters(val, doc, init, prev, options);
66
+
63
67
  let firstValue = firstValueSymbol;
64
68
  let lastError;
65
69
  // Loop through each schema type in the union. If one of the schematypes returns a value that is `=== val`, then
package/lib/schema.js CHANGED
@@ -595,12 +595,6 @@ Schema.prototype.omit = function(paths, options) {
595
595
 
596
596
  newSchema.remove(paths);
597
597
 
598
- for (const nested in newSchema.singleNestedPaths) {
599
- if (paths.includes(nested)) {
600
- delete newSchema.singleNestedPaths[nested];
601
- }
602
- }
603
-
604
598
  return newSchema;
605
599
  };
606
600
 
@@ -2831,6 +2825,7 @@ Schema.prototype.remove = function(path) {
2831
2825
  this.mapPaths = this.mapPaths.filter(
2832
2826
  schemaType => !schemaType.path.startsWith(name + '.')
2833
2827
  );
2828
+ _deleteSubpaths(this, name);
2834
2829
 
2835
2830
  delete this.nested[name];
2836
2831
  _deletePath(this, name);
@@ -2849,6 +2844,11 @@ Schema.prototype.remove = function(path) {
2849
2844
  this.mapPaths = this.mapPaths.filter(
2850
2845
  schemaType => !schemaType.path.startsWith(name + '.')
2851
2846
  );
2847
+ // A document array registers its element paths under `subpaths` and a
2848
+ // single nested subdocument registers its paths under `singleNestedPaths`.
2849
+ // Left behind, they keep answering `path()` and `pathType()`, and queries
2850
+ // on the removed path keep being cast.
2851
+ _deleteSubpaths(this, name);
2852
2852
  _deletePath(this, name);
2853
2853
 
2854
2854
  this._removeEncryptedField(name);
@@ -2861,6 +2861,20 @@ Schema.prototype.remove = function(path) {
2861
2861
  * ignore
2862
2862
  */
2863
2863
 
2864
+ function _deleteSubpaths(schema, name) {
2865
+ const prefix = name + '.';
2866
+ for (const subpath of Object.keys(schema.subpaths)) {
2867
+ if (subpath === name || subpath.startsWith(prefix)) {
2868
+ delete schema.subpaths[subpath];
2869
+ }
2870
+ }
2871
+ for (const subpath of Object.keys(schema.singleNestedPaths)) {
2872
+ if (subpath === name || subpath.startsWith(prefix)) {
2873
+ delete schema.singleNestedPaths[subpath];
2874
+ }
2875
+ }
2876
+ }
2877
+
2864
2878
  function _deletePath(schema, name) {
2865
2879
  const pieces = name.split('.');
2866
2880
  const last = pieces.pop();
@@ -211,6 +211,26 @@ const methods = {
211
211
  return this.constructor.prototype.toObject.apply(this, arguments);
212
212
  },
213
213
 
214
+ /**
215
+ * Wraps [`MongooseArray#addToSet`](https://mongoosejs.com/docs/api/array.html#MongooseArray.prototype.addToSet())
216
+ * with proper change tracking.
217
+ *
218
+ * @param {...object} [args]
219
+ * @api public
220
+ * @method addToSet
221
+ * @memberOf MongooseDocumentArray
222
+ */
223
+
224
+ addToSet() {
225
+ // Arguments are cast before duplicates are skipped, so any value added
226
+ // after a skipped duplicate is cast with an index that is too high.
227
+ const ret = ArrayMethods.addToSet.apply(this, arguments);
228
+
229
+ _updateSubdocIndexes(this);
230
+
231
+ return ret;
232
+ },
233
+
214
234
  /**
215
235
  * Wraps [`Array#push`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push) with proper change tracking.
216
236
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongoose",
3
3
  "description": "Mongoose MongoDB ODM",
4
- "version": "9.10.1",
4
+ "version": "9.10.3",
5
5
  "author": "Guillermo Rauch <guillermo@learnboost.com>",
6
6
  "keywords": [
7
7
  "mongodb",