mongoose 9.10.2 → 9.10.4

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) {
@@ -2,6 +2,9 @@
2
2
 
3
3
  const get = require('../get');
4
4
  const utils = require('../../utils');
5
+
6
+ const simpleCollation = Object.freeze({ locale: 'simple' });
7
+
5
8
  /**
6
9
  * Given a Mongoose index definition (key + options objects) and a MongoDB server
7
10
  * index definition, determine if the two indexes are equal.
@@ -61,14 +64,13 @@ module.exports = function isIndexEqual(schemaIndexKeysObject, options, dbIndex)
61
64
  continue;
62
65
  }
63
66
  if (key === 'collation') {
64
- if (options[key] == null || dbIndex[key] == null) {
65
- return options[key] == null && dbIndex[key] == null;
66
- }
67
- const definedKeys = Object.keys(options.collation);
68
- const schemaCollation = options.collation;
69
- const dbCollation = dbIndex.collation;
70
- for (const opt of definedKeys) {
71
- if (get(schemaCollation, opt) !== get(dbCollation, opt)) {
67
+ // No collation is equivalent to { locale: 'simple' }. MongoDB server 9
68
+ // reports { locale: 'simple' } for indexes created with no collation,
69
+ // older versions omit it.
70
+ const schemaCollation = options.collation ?? simpleCollation;
71
+ const dbCollation = dbIndex.collation ?? simpleCollation;
72
+ for (const opt of Object.keys(schemaCollation)) {
73
+ if (schemaCollation[opt] !== dbCollation[opt]) {
72
74
  return false;
73
75
  }
74
76
  }
@@ -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;
@@ -97,7 +97,15 @@ function _castExpression(val, schema, strictQuery) {
97
97
  val.$ifNull.map(v => _castExpression(v, schema, strictQuery));
98
98
  } else if (val.$switch != null) {
99
99
  if (Array.isArray(val.$switch.branches)) {
100
- val.$switch.branches = val.$switch.branches.map(v => _castExpression(v, schema, strictQuery));
100
+ for (const branch of val.$switch.branches) {
101
+ if (branch?.case != null) {
102
+ branch.case = _castExpression(branch.case, schema, strictQuery);
103
+ }
104
+ if (branch?.then != null) {
105
+ branch.then = _castExpression(branch.then, schema, strictQuery);
106
+ }
107
+ omitUndefined(branch);
108
+ }
101
109
  }
102
110
  if ('default' in val.$switch) {
103
111
  val.$switch.default = _castExpression(val.$switch.default, schema, strictQuery);
@@ -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)));
@@ -3976,11 +3979,12 @@ Model.castObject = function castObject(obj, options) {
3976
3979
  * @param {object} options
3977
3980
  * @param {boolean} options.skipValidation defaults to `false`, when set to true, building the write operations will bypass validating the documents.
3978
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.
3979
3983
  * @return {Promise[]} Returns a array of all Promises the function executes to be awaited.
3980
3984
  * @api private
3981
3985
  */
3982
3986
 
3983
- Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, options) {
3987
+ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, options, documentIndexes) {
3984
3988
  if (!Array.isArray(documents)) {
3985
3989
  throw new MongooseError(`bulkSave expects an array of documents to be passed, received \`${documents}\` instead`);
3986
3990
  }
@@ -4010,6 +4014,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
4010
4014
  }
4011
4015
  const writeOperation = { insertOne: { document } };
4012
4016
  utils.injectTimestampsOption(writeOperation.insertOne, options.timestamps);
4017
+ documentIndexes?.push(i);
4013
4018
  return writeOperation;
4014
4019
  }
4015
4020
 
@@ -4036,6 +4041,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
4036
4041
  document.$__version(where, delta);
4037
4042
  const writeOperation = { updateOne: { filter: where, update: changes } };
4038
4043
  utils.injectTimestampsOption(writeOperation.updateOne, options.timestamps);
4044
+ documentIndexes?.push(i);
4039
4045
  return writeOperation;
4040
4046
  }
4041
4047
 
@@ -4763,7 +4769,10 @@ async function _populatePath(model, docs, populateOptions) {
4763
4769
  // Get the assigned (filtered) children from parent docs
4764
4770
  const childDocsToPopulate = [];
4765
4771
  for (const doc of mod.docs) {
4766
- const childVal = doc.$__ != null ? doc.get(mod.options.path) : doc[mod.options.path];
4772
+ // `utils.getValue()` rather than a bracket lookup, because `mod.options.path` may be
4773
+ // a dotted path like 'children.child', which a lean result does not store as a
4774
+ // single key.
4775
+ const childVal = doc.$__ != null ? doc.get(mod.options.path) : utils.getValue(mod.options.path, doc);
4767
4776
  if (Array.isArray(childVal)) {
4768
4777
  childDocsToPopulate.push(...childVal);
4769
4778
  } else if (childVal != null) {
@@ -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
@@ -544,11 +544,13 @@ const methods = {
544
544
  if (isBsonType(obj, 'ObjectId')) {
545
545
  obj = obj.toString();
546
546
  }
547
+ // Compare dates by timestamp like `addToSet()` does, so `pull()` works on date arrays
548
+ const isDate = obj instanceof Date;
547
549
 
548
550
  fromIndex = fromIndex == null ? 0 : fromIndex;
549
551
  const len = this.length;
550
552
  for (let i = fromIndex; i < len; ++i) {
551
- if (obj == this[i]) {
553
+ if (obj == this[i] || (isDate && this[i] instanceof Date && +obj === +this[i])) {
552
554
  return i;
553
555
  }
554
556
  }
package/lib/types/map.js CHANGED
@@ -4,6 +4,7 @@ const Mixed = require('../schema/mixed');
4
4
  const MongooseError = require('../error/mongooseError');
5
5
  const clone = require('../helpers/clone');
6
6
  const deepEqual = require('../utils').deepEqual;
7
+ const isMongooseArray = require('../utils').isMongooseArray;
7
8
  const getConstructorName = require('../helpers/getConstructorName');
8
9
  const handleSpreadDoc = require('../helpers/document/handleSpreadDoc');
9
10
  const util = require('util');
@@ -160,6 +161,11 @@ class MongooseMap extends Map {
160
161
  if (parent?.$__ != null && !deepEqual(value, priorVal)) {
161
162
  const path = fullPath.call(this);
162
163
  parent.markModified(path);
164
+ // Replacing the whole array means later push(), pull() or addToSet() calls must not be sent as
165
+ // atomics against the array that was in the database, same as Document#$set re: gh-1126
166
+ if (isMongooseArray(value)) {
167
+ value._registerAtomic('$set', value);
168
+ }
163
169
  // If overwriting the full document array or subdoc, make sure to clean up any paths that were modified
164
170
  // before re: #15108
165
171
  if (this.$__schemaType.$isMongooseDocumentArray || this.$__schemaType.$isSingleNested) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongoose",
3
3
  "description": "Mongoose MongoDB ODM",
4
- "version": "9.10.2",
4
+ "version": "9.10.4",
5
5
  "author": "Guillermo Rauch <guillermo@learnboost.com>",
6
6
  "keywords": [
7
7
  "mongodb",
@@ -29,9 +29,9 @@
29
29
  "sift": "17.1.3"
30
30
  },
31
31
  "devDependencies": {
32
- "@ark/attest": "0.56.3",
32
+ "@ark/attest": "0.56.5",
33
33
  "@eslint/js": "^9.39.3",
34
- "@mongodb-js/mongodb-downloader": "^1.0.0",
34
+ "@mongodb-js/mongodb-downloader": "^2.0.2",
35
35
  "@types/node": "^20.19.0",
36
36
  "acquit": "1.4.0",
37
37
  "acquit-ignore": "0.2.2",
@@ -40,7 +40,7 @@
40
40
  "c8": "12.0.0",
41
41
  "cheerio": "1.2.0",
42
42
  "dox": "1.0.0",
43
- "eslint": "10.9.1",
43
+ "eslint": "10.11.0",
44
44
  "eslint-plugin-mocha-no-only": "1.2.0",
45
45
  "express": "5.2.1",
46
46
  "fs-extra": "~11.4.0",
@@ -50,13 +50,13 @@
50
50
  "linkinator": "8.x",
51
51
  "lodash.isequal": "4.5.0",
52
52
  "lodash.isequalwith": "4.4.0",
53
- "markdownlint-cli2": "0.23.2",
54
- "marked": "18.0.11",
53
+ "markdownlint-cli2": "0.23.3",
54
+ "marked": "18.0.14",
55
55
  "mkdirp": "^3.0.1",
56
- "mocha": "12.0.0-rc.6",
57
- "moment": "2.30.1",
56
+ "mocha": "12.0.2",
57
+ "moment": "2.31.0",
58
58
  "mongodb-client-encryption": "^7.2.0",
59
- "mongodb-memory-server": "11.2.0",
59
+ "mongodb-memory-server": "11.3.0",
60
60
  "mongodb-runner": "^6.0.0",
61
61
  "ncp": "^2.0.0",
62
62
  "pug": "3.0.4",