@depup/mongoose 9.4.1-depup.0 → 9.8.0-depup.0

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.
Files changed (59) hide show
  1. package/README.md +3 -9
  2. package/changes.json +3 -8
  3. package/eslint.config.mjs +4 -1
  4. package/lib/aggregate.js +54 -24
  5. package/lib/cast.js +1 -1
  6. package/lib/connection.js +1 -1
  7. package/lib/cursor/aggregationCursor.js +47 -16
  8. package/lib/cursor/queryCursor.js +23 -7
  9. package/lib/document.js +133 -58
  10. package/lib/drivers/node-mongodb-native/collection.js +10 -4
  11. package/lib/error/cast.js +18 -9
  12. package/lib/error/divergentArray.js +1 -1
  13. package/lib/error/messages.js +1 -0
  14. package/lib/helpers/clone.js +22 -5
  15. package/lib/helpers/document/applyDefaults.js +5 -0
  16. package/lib/helpers/populate/createPopulateQueryFilter.js +9 -1
  17. package/lib/helpers/populate/getModelsMapForPopulate.js +8 -6
  18. package/lib/helpers/populate/splitPopulateQuery.js +81 -0
  19. package/lib/helpers/query/castUpdate.js +4 -0
  20. package/lib/helpers/schema/getKeysInSchemaOrder.js +13 -16
  21. package/lib/helpers/update/applyTimestampsToUpdate.js +4 -2
  22. package/lib/model.js +153 -38
  23. package/lib/mongoose.js +3 -3
  24. package/lib/options/schemaTypeOptions.js +14 -0
  25. package/lib/plugins/saveSubdocs.js +16 -13
  26. package/lib/query.js +208 -102
  27. package/lib/queryHelpers.js +13 -12
  28. package/lib/schema/array.js +3 -3
  29. package/lib/schema/date.js +7 -5
  30. package/lib/schema/documentArray.js +1 -1
  31. package/lib/schema/objectId.js +7 -1
  32. package/lib/schema/string.js +19 -2
  33. package/lib/schema/subdocument.js +1 -1
  34. package/lib/schema/union.js +2 -2
  35. package/lib/schema.js +38 -12
  36. package/lib/schemaType.js +58 -10
  37. package/lib/standardSchema/convertErrorToIssues.js +20 -0
  38. package/lib/stateMachine.js +11 -6
  39. package/lib/tracing.js +38 -0
  40. package/lib/types/documentArray/methods/index.js +117 -3
  41. package/lib/types/subdocument.js +4 -2
  42. package/lib/validOptions.js +1 -0
  43. package/package.json +27 -30
  44. package/tstyche.json +2 -1
  45. package/types/augmentations.d.ts +2 -2
  46. package/types/document.d.ts +21 -4
  47. package/types/expressions.d.ts +16 -0
  48. package/types/index.d.ts +13 -10
  49. package/types/inferhydrateddoctype.d.ts +1 -1
  50. package/types/inferrawdoctype.d.ts +6 -3
  51. package/types/inferschematype.d.ts +10 -2
  52. package/types/models.d.ts +28 -11
  53. package/types/mongooseoptions.d.ts +9 -1
  54. package/types/populate.d.ts +8 -0
  55. package/types/query.d.ts +13 -3
  56. package/types/schemaoptions.d.ts +5 -0
  57. package/types/schematypes.d.ts +10 -0
  58. package/types/tracing.d.ts +10 -0
  59. package/types/utility.d.ts +9 -0
package/lib/query.js CHANGED
@@ -20,6 +20,7 @@ const castArrayFilters = require('./helpers/update/castArrayFilters');
20
20
  const castNumber = require('./cast/number');
21
21
  const castUpdate = require('./helpers/query/castUpdate');
22
22
  const clone = require('./helpers/clone');
23
+ const decorateUpdateWithVersionKey = require('./helpers/update/decorateUpdateWithVersionKey');
23
24
  const getDiscriminatorByValue = require('./helpers/discriminator/getDiscriminatorByValue');
24
25
  const helpers = require('./queryHelpers');
25
26
  const internalToObjectOptions = require('./options').internalToObjectOptions;
@@ -41,6 +42,8 @@ const updateValidators = require('./helpers/updateValidators');
41
42
  const util = require('util');
42
43
  const utils = require('./utils');
43
44
  const queryMiddlewareFunctions = require('./constants').queryMiddlewareFunctions;
45
+ const { createTracedChannel } = require('./tracing');
46
+ const { trace: traceQuery } = createTracedChannel('mongoose:query');
44
47
 
45
48
  const queryOptionMethods = new Set([
46
49
  'allowDiskUse',
@@ -85,6 +88,8 @@ const opToThunk = new Map([
85
88
  ['findOneAndDelete', '_findOneAndDelete']
86
89
  ]);
87
90
 
91
+ const queryUpdateSymbol = Symbol('mongoose#Query#update');
92
+
88
93
  /**
89
94
  * Query constructor used for building queries. You do not need
90
95
  * to instantiate a `Query` directly. Instead use Model functions like
@@ -196,6 +201,18 @@ function checkRequireFilter(filter, options) {
196
201
  Query.prototype = new mquery();
197
202
  Query.prototype.constructor = Query;
198
203
 
204
+ Object.defineProperty(Query.prototype, '_update', {
205
+ configurable: true,
206
+ enumerable: true,
207
+ get: function() {
208
+ _cloneUpdateIfShared(this);
209
+ return this[queryUpdateSymbol];
210
+ },
211
+ set: function(v) {
212
+ this[queryUpdateSymbol] = v;
213
+ }
214
+ });
215
+
199
216
  // Remove some legacy methods that we removed in Mongoose 8, but
200
217
  // are still in mquery 5.
201
218
  Query.prototype.count = undefined;
@@ -300,7 +317,7 @@ Query.prototype.toConstructor = function toConstructor() {
300
317
  p.op = this.op;
301
318
  p._conditions = clone(this._conditions);
302
319
  p._fields = clone(this._fields);
303
- p._update = clone(this._update, {
320
+ p[queryUpdateSymbol] = clone(this[queryUpdateSymbol], {
304
321
  flattenDecimals: false
305
322
  });
306
323
  p._path = this._path;
@@ -348,7 +365,7 @@ Query.prototype.clone = function() {
348
365
  q.op = this.op;
349
366
  q._conditions = clone(this._conditions);
350
367
  q._fields = clone(this._fields);
351
- q._update = clone(this._update, {
368
+ q[queryUpdateSymbol] = clone(this[queryUpdateSymbol], {
352
369
  flattenDecimals: false
353
370
  });
354
371
  q._path = this._path;
@@ -1365,7 +1382,7 @@ Query.prototype.toString = function toString() {
1365
1382
  this.op === 'update' ||
1366
1383
  this.op === 'updateMany' ||
1367
1384
  this.op === 'updateOne') {
1368
- return `${this.model.modelName}.${this.op}(${util.inspect(this._conditions)}, ${util.inspect(this._update)})`;
1385
+ return `${this.model.modelName}.${this.op}(${util.inspect(this._conditions)}, ${util.inspect(this[queryUpdateSymbol])})`;
1369
1386
  }
1370
1387
 
1371
1388
  // 'estimatedDocumentCount' or any others
@@ -1669,6 +1686,7 @@ Query.prototype.getOptions = function() {
1669
1686
  * - [upsert](https://www.mongodb.com/docs/manual/reference/method/db.collection.update/)
1670
1687
  * - [writeConcern](https://www.mongodb.com/docs/manual/reference/method/db.collection.update/)
1671
1688
  * - [timestamps](https://mongoosejs.com/docs/guide.html#timestamps): If `timestamps` is set in the schema, set this option to `false` to skip timestamps for that particular update. Has no effect if `timestamps` is not enabled in the schema options.
1689
+ * - cloneUpdate: set to `false` to skip cloning the update before executing the query.
1672
1690
  * - overwriteDiscriminatorKey: allow setting the discriminator key in the update. Will use the correct discriminator schema if the update changes the discriminator key.
1673
1691
  * - overwriteImmutable: allow overwriting properties that are set to `immutable` in the schema. Defaults to false.
1674
1692
  *
@@ -1679,6 +1697,7 @@ Query.prototype.getOptions = function() {
1679
1697
  * - [projection](https://mongoosejs.com/docs/api/query.html#Query.prototype.projection())
1680
1698
  * - sanitizeProjection
1681
1699
  * - useBigInt64
1700
+ * - defaults: if `false`, skip applying default values to the returned document(s). Defaults to true.
1682
1701
  *
1683
1702
  * The following options are only for all operations **except** `updateOne()`, `updateMany()`, `deleteOne()`, and `deleteMany()`:
1684
1703
  *
@@ -1751,6 +1770,10 @@ Query.prototype.setOptions = function(options, overwrite) {
1751
1770
  this._mongooseOptions.updatePipeline = options.updatePipeline;
1752
1771
  delete options.updatePipeline;
1753
1772
  }
1773
+ if ('cloneUpdate' in options) {
1774
+ this._mongooseOptions.cloneUpdate = options.cloneUpdate;
1775
+ delete options.cloneUpdate;
1776
+ }
1754
1777
  if ('sanitizeProjection' in options) {
1755
1778
  if (options.sanitizeProjection && !this._mongooseOptions.sanitizeProjection) {
1756
1779
  sanitizeProjection(this._fields);
@@ -1984,12 +2007,17 @@ Query.prototype.getUpdate = function() {
1984
2007
  * query.getUpdate(); // { $set: { b: 6 } }
1985
2008
  *
1986
2009
  * @param {object} new update operation
2010
+ * @param {boolean} [cloneUpdate=true] if `false`, Mongoose will not clone the update
1987
2011
  * @return {undefined}
1988
2012
  * @api public
1989
2013
  */
1990
2014
 
1991
- Query.prototype.setUpdate = function(val) {
1992
- this._update = clone(val);
2015
+ Query.prototype.setUpdate = function(val, cloneUpdate) {
2016
+ this[queryUpdateSymbol] = cloneUpdate === false ? val : clone(val);
2017
+ if (cloneUpdate != null) {
2018
+ this._mongooseOptions.cloneUpdate = cloneUpdate;
2019
+ }
2020
+ this._updateIsShared = false;
1993
2021
  };
1994
2022
 
1995
2023
  /**
@@ -2022,7 +2050,7 @@ Query.prototype._fieldsForExec = function() {
2022
2050
  */
2023
2051
 
2024
2052
  Query.prototype._updateForExec = function() {
2025
- const update = clone(this._update, {
2053
+ const update = clone(this[queryUpdateSymbol], {
2026
2054
  transform: false,
2027
2055
  depopulate: true
2028
2056
  });
@@ -2216,6 +2244,8 @@ Query.prototype.lean = function(v) {
2216
2244
  */
2217
2245
 
2218
2246
  Query.prototype.set = function(path, val) {
2247
+ _cloneUpdateIfShared(this);
2248
+
2219
2249
  if (typeof path === 'object') {
2220
2250
  const keys = Object.keys(path);
2221
2251
  for (const key of keys) {
@@ -2224,12 +2254,16 @@ Query.prototype.set = function(path, val) {
2224
2254
  return this;
2225
2255
  }
2226
2256
 
2227
- this._update = this._update || {};
2228
- if (path in this._update) {
2229
- delete this._update[path];
2257
+ let update = this[queryUpdateSymbol];
2258
+ if (update == null) {
2259
+ update = {};
2260
+ this[queryUpdateSymbol] = update;
2261
+ }
2262
+ if (path in update) {
2263
+ delete update[path];
2230
2264
  }
2231
- this._update.$set = this._update.$set || {};
2232
- this._update.$set[path] = val;
2265
+ update.$set = update.$set || {};
2266
+ update.$set[path] = val;
2233
2267
  return this;
2234
2268
  };
2235
2269
 
@@ -2249,7 +2283,7 @@ Query.prototype.set = function(path, val) {
2249
2283
  */
2250
2284
 
2251
2285
  Query.prototype.get = function get(path) {
2252
- const update = this._update;
2286
+ const update = this[queryUpdateSymbol];
2253
2287
  if (update == null) {
2254
2288
  return void 0;
2255
2289
  }
@@ -2333,6 +2367,7 @@ Query.prototype._unsetCastError = function _unsetCastError() {
2333
2367
  * - `strictQuery`: controls how Mongoose handles keys that aren't in the schema for the query `filter`. This option is `false` by default, which means Mongoose will allow `Model.find({ foo: 'bar' })` even if `foo` is not in the schema. See the [`strictQuery` docs](https://mongoosejs.com/docs/guide.html#strictQuery) for more information.
2334
2368
  * - `nearSphere`: use `$nearSphere` instead of `near()`. See the [`Query.prototype.nearSphere()` docs](https://mongoosejs.com/docs/api/query.html#Query.prototype.nearSphere())
2335
2369
  * - `schemaLevelProjections`: if `false`, Mongoose will not apply schema-level `select: false` or `select: true` for this query
2370
+ * - `cloneUpdate`: if `false`, Mongoose will not clone updates before executing the query
2336
2371
  *
2337
2372
  * Mongoose maintains a separate object for internal options because
2338
2373
  * Mongoose sends `Query.prototype.options` to the MongoDB server, and the
@@ -2421,6 +2456,12 @@ Query.prototype._find = async function _find() {
2421
2456
  lean: mongooseOptions.lean || null
2422
2457
  };
2423
2458
 
2459
+ // Only pass `defaults` through when it is non-nullish; `null` and
2460
+ // `undefined` are treated as "not set" and omitted from `createModel()`.
2461
+ if (mongooseOptions.defaults != null) {
2462
+ completeManyOptions.defaults = mongooseOptions.defaults;
2463
+ }
2464
+
2424
2465
  const options = this._optionsForExec();
2425
2466
 
2426
2467
  const filter = this._conditions;
@@ -2553,9 +2594,10 @@ Query.prototype.merge = function(source) {
2553
2594
  utils.merge(this.options, source.options, opts);
2554
2595
  }
2555
2596
 
2556
- if (source._update) {
2557
- this._update || (this._update = {});
2558
- utils.mergeClone(this._update, source._update);
2597
+ if (source[queryUpdateSymbol] != null) {
2598
+ _cloneUpdateIfShared(this);
2599
+ this[queryUpdateSymbol] || (this[queryUpdateSymbol] = {});
2600
+ utils.mergeClone(this[queryUpdateSymbol], source[queryUpdateSymbol]);
2559
2601
  }
2560
2602
 
2561
2603
  if (source._distinct) {
@@ -2691,7 +2733,7 @@ Query.prototype._completeMany = async function _completeMany(docs, fields, userP
2691
2733
  const model = this.model;
2692
2734
  return Promise.all(docs.map(doc => new Promise((resolve, reject) => {
2693
2735
  const rawDoc = doc;
2694
- doc = helpers.createModel(model, doc, fields, userProvidedFields);
2736
+ doc = helpers.createModel(model, doc, fields, userProvidedFields, opts);
2695
2737
  if (opts.session != null) {
2696
2738
  doc.$session(opts.session);
2697
2739
  }
@@ -2849,9 +2891,10 @@ Query.prototype._applyTranslateAliases = function _applyTranslateAliases() {
2849
2891
  }
2850
2892
 
2851
2893
  if (this.model?.schema?.aliases && utils.hasOwnKeys(this.model.schema.aliases)) {
2894
+ _cloneUpdateIfShared(this);
2852
2895
  this.model.translateAliases(this._conditions, true);
2853
2896
  this.model.translateAliases(this._fields, true);
2854
- this.model.translateAliases(this._update, true);
2897
+ this.model.translateAliases(this[queryUpdateSymbol], true);
2855
2898
  if (this._distinct != null && this.model.schema.aliases[this._distinct] != null) {
2856
2899
  this._distinct = this.model.schema.aliases[this._distinct];
2857
2900
  }
@@ -3405,6 +3448,7 @@ function prepareDiscriminatorCriteria(query) {
3405
3448
  * @param {object|Query} [filter]
3406
3449
  * @param {object} [update]
3407
3450
  * @param {object} [options]
3451
+ * @param {boolean} [options.cloneUpdate=true] if `false`, Mongoose will not clone the update before executing the query
3408
3452
  * @param {boolean} [options.includeResultMetadata] if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/7.0/interfaces/ModifyResult.html) rather than just the document
3409
3453
  * @param {boolean|'throw'} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
3410
3454
  * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
@@ -3481,11 +3525,20 @@ Query.prototype.findOneAndUpdate = function(filter, update, options) {
3481
3525
  options.updatePipeline = updatePipeline;
3482
3526
  }
3483
3527
 
3528
+ if (!options.updatePipeline && Array.isArray(update)) {
3529
+ throw new MongooseError('Cannot pass an array to query updates unless the `updatePipeline` option is set.');
3530
+ }
3531
+
3484
3532
  this.setOptions(options);
3485
3533
 
3486
3534
  // apply doc
3487
3535
  if (update) {
3488
- this._mergeUpdate(update);
3536
+ if (this[queryUpdateSymbol] == null || utils.isEmptyObject(this[queryUpdateSymbol])) {
3537
+ this[queryUpdateSymbol] = update;
3538
+ this._updateIsShared = true;
3539
+ } else {
3540
+ this._mergeUpdate(update);
3541
+ }
3489
3542
  }
3490
3543
 
3491
3544
  return this;
@@ -3523,49 +3576,51 @@ Query.prototype._findOneAndUpdate = async function _findOneAndUpdate() {
3523
3576
  const options = this._optionsForExec(this.model);
3524
3577
  convertNewToReturnDocument(options);
3525
3578
 
3526
- this._update = this._castUpdate(this._update);
3579
+ this[queryUpdateSymbol] = this._castUpdate(this[queryUpdateSymbol]);
3580
+ this._updateIsShared = false;
3581
+ decorateUpdateWithVersionKey(this[queryUpdateSymbol], options, this.schema.options.versionKey);
3527
3582
 
3528
- this._update = setDefaultsOnInsert(
3583
+ this[queryUpdateSymbol] = setDefaultsOnInsert(
3529
3584
  this._conditions,
3530
3585
  this.model.schema,
3531
- this._update,
3586
+ this[queryUpdateSymbol],
3532
3587
  options,
3533
3588
  this._mongooseOptions,
3534
3589
  this
3535
3590
  );
3536
3591
 
3537
- if (!this._update || utils.hasOwnKeys(this._update) === false) {
3592
+ if (!this[queryUpdateSymbol] || utils.hasOwnKeys(this[queryUpdateSymbol]) === false) {
3538
3593
  if (options.upsert) {
3539
3594
  // still need to do the upsert to empty doc
3540
- const $set = clone(this._update);
3595
+ const $set = clone(this[queryUpdateSymbol]);
3541
3596
  delete $set._id;
3542
- this._update = { $set };
3597
+ this[queryUpdateSymbol] = { $set };
3543
3598
  } else {
3544
3599
  this._execCount = 0;
3545
3600
  const res = await this._findOne();
3546
3601
  return res;
3547
3602
  }
3548
- } else if (this._update instanceof Error) {
3549
- throw this._update;
3603
+ } else if (this[queryUpdateSymbol] instanceof Error) {
3604
+ throw this[queryUpdateSymbol];
3550
3605
  } else {
3551
3606
  // In order to make MongoDB 2.6 happy (see
3552
3607
  // https://jira.mongodb.org/browse/SERVER-12266 and related issues)
3553
3608
  // if we have an actual update document but $set is empty, junk the $set.
3554
- if (this._update.$set && utils.hasOwnKeys(this._update.$set) === false) {
3555
- delete this._update.$set;
3609
+ if (this[queryUpdateSymbol].$set && utils.hasOwnKeys(this[queryUpdateSymbol].$set) === false) {
3610
+ delete this[queryUpdateSymbol].$set;
3556
3611
  }
3557
3612
  }
3558
3613
 
3559
3614
  const runValidators = _getOption(this, 'runValidators', false);
3560
3615
  if (runValidators) {
3561
- await this.validate(this._update, options, false);
3616
+ await this.validate(this[queryUpdateSymbol], options, false);
3562
3617
  }
3563
3618
 
3564
- if (typeof this._update.toBSON === 'function') {
3565
- this._update = this._update.toBSON();
3619
+ if (typeof this[queryUpdateSymbol].toBSON === 'function') {
3620
+ this[queryUpdateSymbol] = this[queryUpdateSymbol].toBSON();
3566
3621
  }
3567
3622
 
3568
- let res = await this.mongooseCollection.findOneAndUpdate(this._conditions, this._update, options);
3623
+ let res = await this.mongooseCollection.findOneAndUpdate(this._conditions, this[queryUpdateSymbol], options);
3569
3624
  for (const fn of this._transforms) {
3570
3625
  res = fn(res);
3571
3626
  }
@@ -3701,6 +3756,7 @@ Query.prototype._findOneAndDelete = async function _findOneAndDelete() {
3701
3756
  * @param {object} [filter]
3702
3757
  * @param {object} [replacement]
3703
3758
  * @param {object} [options]
3759
+ * @param {boolean} [options.cloneUpdate=true] if `false`, Mongoose will not clone the update before executing the query
3704
3760
  * @param {boolean} [options.includeResultMetadata] if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/7.0/interfaces/ModifyResult.html) rather than just the document
3705
3761
  * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
3706
3762
  * @param {boolean|'throw'} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
@@ -3796,13 +3852,13 @@ Query.prototype._findOneAndReplace = async function _findOneAndReplace() {
3796
3852
  const runValidators = _getOption(this, 'runValidators', false);
3797
3853
 
3798
3854
  try {
3799
- const update = new this.model(this._update, null, modelOpts);
3855
+ const update = new this.model(this[queryUpdateSymbol], null, modelOpts);
3800
3856
  if (runValidators) {
3801
3857
  await update.validate();
3802
3858
  } else if (update.$__.validationError) {
3803
3859
  throw update.$__.validationError;
3804
3860
  }
3805
- this._update = update.toBSON();
3861
+ this[queryUpdateSymbol] = update.toBSON();
3806
3862
  } catch (err) {
3807
3863
  if (err instanceof ValidationError) {
3808
3864
  throw err;
@@ -3812,7 +3868,7 @@ Query.prototype._findOneAndReplace = async function _findOneAndReplace() {
3812
3868
  throw validationError;
3813
3869
  }
3814
3870
 
3815
- let res = await this.mongooseCollection.findOneAndReplace(filter, this._update, options);
3871
+ let res = await this.mongooseCollection.findOneAndReplace(filter, this[queryUpdateSymbol], options);
3816
3872
 
3817
3873
  for (const fn of this._transforms) {
3818
3874
  res = fn(res);
@@ -3874,6 +3930,7 @@ Query.prototype.findById = function(id, projection, options) {
3874
3930
  * @param {any} id value of `_id` to query by
3875
3931
  * @param {object} [doc]
3876
3932
  * @param {object} [options]
3933
+ * @param {boolean} [options.cloneUpdate=true] if `false`, Mongoose will not clone the update before executing the query
3877
3934
  * @param {boolean} [options.includeResultMetadata] if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/7.0/interfaces/ModifyResult.html) rather than just the document
3878
3935
  * @param {boolean|'throw'} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
3879
3936
  * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
@@ -4049,12 +4106,14 @@ function _completeManyLean(schema, docs, path, opts) {
4049
4106
  */
4050
4107
 
4051
4108
  Query.prototype._mergeUpdate = function(update) {
4109
+ _cloneUpdateIfShared(this);
4110
+
4052
4111
  const updatePipeline = this._mongooseOptions.updatePipeline;
4053
4112
  if (!updatePipeline && Array.isArray(update)) {
4054
4113
  throw new MongooseError('Cannot pass an array to query updates unless the `updatePipeline` option is set.');
4055
4114
  }
4056
- if (!this._update) {
4057
- this._update = Array.isArray(update) ? [] : {};
4115
+ if (!this[queryUpdateSymbol]) {
4116
+ this[queryUpdateSymbol] = Array.isArray(update) ? [] : {};
4058
4117
  }
4059
4118
 
4060
4119
  if (update == null || (typeof update === 'object' && utils.hasOwnKeys(update) === false)) {
@@ -4062,28 +4121,28 @@ Query.prototype._mergeUpdate = function(update) {
4062
4121
  }
4063
4122
 
4064
4123
  if (update instanceof Query) {
4065
- if (Array.isArray(this._update)) {
4066
- throw new MongooseError(`Cannot mix array and object updates (current: ${_previewUpdate(this._update)}, incoming: ${_previewUpdate(update._update)})`);
4124
+ if (Array.isArray(this[queryUpdateSymbol])) {
4125
+ throw new MongooseError(`Cannot mix array and object updates (current: ${_previewUpdate(this[queryUpdateSymbol])}, incoming: ${_previewUpdate(update[queryUpdateSymbol])})`);
4067
4126
  }
4068
- if (update._update) {
4069
- utils.mergeClone(this._update, update._update);
4127
+ if (update[queryUpdateSymbol]) {
4128
+ utils.mergeClone(this[queryUpdateSymbol], update[queryUpdateSymbol]);
4070
4129
  }
4071
4130
  } else if (Array.isArray(update)) {
4072
- if (!Array.isArray(this._update)) {
4131
+ if (!Array.isArray(this[queryUpdateSymbol])) {
4073
4132
  // `_update` may be empty object by default, like in `doc.updateOne()`
4074
4133
  // because we create the query first, then run hooks, then apply the update.
4075
- if (this._update == null || utils.isEmptyObject(this._update)) {
4076
- this._update = [];
4134
+ if (this[queryUpdateSymbol] == null || utils.isEmptyObject(this[queryUpdateSymbol])) {
4135
+ this[queryUpdateSymbol] = [];
4077
4136
  } else {
4078
- throw new MongooseError(`Cannot mix array and object updates (current: ${_previewUpdate(this._update)}, incoming: ${_previewUpdate(update)})`);
4137
+ throw new MongooseError(`Cannot mix array and object updates (current: ${_previewUpdate(this[queryUpdateSymbol])}, incoming: ${_previewUpdate(update)})`);
4079
4138
  }
4080
4139
  }
4081
- this._update = this._update.concat(update);
4140
+ this[queryUpdateSymbol] = this[queryUpdateSymbol].concat(update);
4082
4141
  } else {
4083
- if (Array.isArray(this._update)) {
4084
- throw new MongooseError(`Cannot mix array and object updates (current: ${_previewUpdate(this._update)}, incoming: ${_previewUpdate(update)})`);
4142
+ if (Array.isArray(this[queryUpdateSymbol])) {
4143
+ throw new MongooseError(`Cannot mix array and object updates (current: ${_previewUpdate(this[queryUpdateSymbol])}, incoming: ${_previewUpdate(update)})`);
4085
4144
  }
4086
- utils.mergeClone(this._update, update);
4145
+ utils.mergeClone(this[queryUpdateSymbol], update);
4087
4146
  }
4088
4147
  };
4089
4148
 
@@ -4157,30 +4216,31 @@ Query.prototype._updateMany = async function _updateMany() {
4157
4216
  }
4158
4217
 
4159
4218
  const options = this._optionsForExec(this.model);
4160
- this._update = this._castUpdate(this._update);
4161
- if (this._update == null || utils.hasOwnKeys(this._update) === false) {
4219
+ _cloneUpdateIfShared(this);
4220
+ this[queryUpdateSymbol] = this._castUpdate(this[queryUpdateSymbol]);
4221
+ if (this[queryUpdateSymbol] == null || utils.hasOwnKeys(this[queryUpdateSymbol]) === false) {
4162
4222
  return { acknowledged: false };
4163
4223
  }
4164
- removeUnusedArrayFilters(this._update, options);
4224
+ removeUnusedArrayFilters(this[queryUpdateSymbol], options);
4165
4225
 
4166
- this._update = setDefaultsOnInsert(
4226
+ this[queryUpdateSymbol] = setDefaultsOnInsert(
4167
4227
  this._conditions,
4168
4228
  this.model.schema,
4169
- this._update,
4229
+ this[queryUpdateSymbol],
4170
4230
  options,
4171
4231
  this._mongooseOptions,
4172
4232
  this
4173
4233
  );
4174
4234
 
4175
4235
  if (_getOption(this, 'runValidators', false)) {
4176
- await this.validate(this._update, options, false);
4236
+ await this.validate(this[queryUpdateSymbol], options, false);
4177
4237
  }
4178
4238
 
4179
- if (typeof this._update.toBSON === 'function') {
4180
- this._update = this._update.toBSON();
4239
+ if (typeof this[queryUpdateSymbol].toBSON === 'function') {
4240
+ this[queryUpdateSymbol] = this[queryUpdateSymbol].toBSON();
4181
4241
  }
4182
4242
 
4183
- return this.mongooseCollection.updateMany(this._conditions, this._update, options);
4243
+ return this.mongooseCollection.updateMany(this._conditions, this[queryUpdateSymbol], options);
4184
4244
  };
4185
4245
 
4186
4246
  /**
@@ -4202,30 +4262,31 @@ Query.prototype._updateOne = async function _updateOne() {
4202
4262
  }
4203
4263
 
4204
4264
  const options = this._optionsForExec(this.model);
4205
- this._update = this._castUpdate(this._update);
4206
- if (this._update == null || utils.hasOwnKeys(this._update) === false) {
4265
+ _cloneUpdateIfShared(this);
4266
+ this[queryUpdateSymbol] = this._castUpdate(this[queryUpdateSymbol]);
4267
+ if (this[queryUpdateSymbol] == null || utils.hasOwnKeys(this[queryUpdateSymbol]) === false) {
4207
4268
  return { acknowledged: false };
4208
4269
  }
4209
- removeUnusedArrayFilters(this._update, options);
4270
+ removeUnusedArrayFilters(this[queryUpdateSymbol], options);
4210
4271
 
4211
- this._update = setDefaultsOnInsert(
4272
+ this[queryUpdateSymbol] = setDefaultsOnInsert(
4212
4273
  this._conditions,
4213
4274
  this.model.schema,
4214
- this._update,
4275
+ this[queryUpdateSymbol],
4215
4276
  options,
4216
4277
  this._mongooseOptions,
4217
4278
  this
4218
4279
  );
4219
4280
 
4220
4281
  if (_getOption(this, 'runValidators', false)) {
4221
- await this.validate(this._update, options, false);
4282
+ await this.validate(this[queryUpdateSymbol], options, false);
4222
4283
  }
4223
4284
 
4224
- if (typeof this._update.toBSON === 'function') {
4225
- this._update = this._update.toBSON();
4285
+ if (typeof this[queryUpdateSymbol].toBSON === 'function') {
4286
+ this[queryUpdateSymbol] = this[queryUpdateSymbol].toBSON();
4226
4287
  }
4227
4288
 
4228
- return this.mongooseCollection.updateOne(this._conditions, this._update, options);
4289
+ return this.mongooseCollection.updateOne(this._conditions, this[queryUpdateSymbol], options);
4229
4290
  };
4230
4291
 
4231
4292
  /**
@@ -4247,18 +4308,18 @@ Query.prototype._replaceOne = async function _replaceOne() {
4247
4308
  }
4248
4309
 
4249
4310
  const options = this._optionsForExec(this.model);
4250
- this._update = new this.model(this._update, null, { skipId: true });
4251
- removeUnusedArrayFilters(this._update, options);
4311
+ this[queryUpdateSymbol] = new this.model(this[queryUpdateSymbol], null, { skipId: true });
4312
+ removeUnusedArrayFilters(this[queryUpdateSymbol], options);
4252
4313
 
4253
4314
  if (_getOption(this, 'runValidators', false)) {
4254
- await this.validate(this._update, options, true);
4315
+ await this.validate(this[queryUpdateSymbol], options, true);
4255
4316
  }
4256
4317
 
4257
- if (typeof this._update.toBSON === 'function') {
4258
- this._update = this._update.toBSON();
4318
+ if (typeof this[queryUpdateSymbol].toBSON === 'function') {
4319
+ this[queryUpdateSymbol] = this[queryUpdateSymbol].toBSON();
4259
4320
  }
4260
4321
 
4261
- return this.mongooseCollection.replaceOne(this._conditions, this._update, options);
4322
+ return this.mongooseCollection.replaceOne(this._conditions, this[queryUpdateSymbol], options);
4262
4323
  };
4263
4324
 
4264
4325
  /**
@@ -4285,6 +4346,7 @@ Query.prototype._replaceOne = async function _replaceOne() {
4285
4346
  * @param {object} [filter]
4286
4347
  * @param {object|Array} [update] the update command. If array, this update will be treated as an update pipeline and not casted.
4287
4348
  * @param {object} [options]
4349
+ * @param {boolean} [options.cloneUpdate=true] if `false`, Mongoose will not clone the update before executing the query
4288
4350
  * @param {boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors.
4289
4351
  * @param {boolean|'throw'} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
4290
4352
  * @param {boolean} [options.upsert=false] if true, and no documents found, insert a new document
@@ -4360,6 +4422,7 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) {
4360
4422
  * @param {object} [filter]
4361
4423
  * @param {object|Array} [update] the update command. If array, this update will be treated as an update pipeline and not casted.
4362
4424
  * @param {object} [options]
4425
+ * @param {boolean} [options.cloneUpdate=true] if `false`, Mongoose will not clone the update before executing the query
4363
4426
  * @param {boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors.
4364
4427
  * @param {boolean|'throw'} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
4365
4428
  * @param {boolean} [options.upsert=false] if true, and no documents found, insert a new document
@@ -4429,6 +4492,7 @@ Query.prototype.updateOne = function(conditions, doc, options, callback) {
4429
4492
  * @param {object} [filter]
4430
4493
  * @param {object} [doc] the update command
4431
4494
  * @param {object} [options]
4495
+ * @param {boolean} [options.cloneUpdate=true] if `false`, Mongoose will not clone the update before executing the query
4432
4496
  * @param {boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors.
4433
4497
  * @param {boolean|'throw'} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
4434
4498
  * @param {boolean} [options.upsert=false] if true, and no documents found, insert a new document
@@ -4510,11 +4574,20 @@ function _update(query, op, filter, doc, options, callback) {
4510
4574
  options.updatePipeline = updatePipeline;
4511
4575
  }
4512
4576
 
4577
+ if (!options?.updatePipeline && Array.isArray(doc)) {
4578
+ throw new MongooseError('Cannot pass an array to query updates unless the `updatePipeline` option is set.');
4579
+ }
4580
+
4513
4581
  if (utils.isObject(options)) {
4514
4582
  query.setOptions(options);
4515
4583
  }
4516
4584
 
4517
- query._mergeUpdate(doc);
4585
+ if (query[queryUpdateSymbol] == null || utils.isEmptyObject(query[queryUpdateSymbol])) {
4586
+ query[queryUpdateSymbol] = doc;
4587
+ query._updateIsShared = true;
4588
+ } else {
4589
+ query._mergeUpdate(doc);
4590
+ }
4518
4591
 
4519
4592
  // Hooks
4520
4593
  if (callback) {
@@ -4707,42 +4780,56 @@ Query.prototype.exec = async function exec(op) {
4707
4780
  }
4708
4781
  this._execCount++;
4709
4782
 
4710
- let skipWrappedFunction = null;
4711
- try {
4712
- await this._hooks.execPre('exec', this, []);
4713
- } catch (err) {
4714
- if (err instanceof Kareem.skipWrappedFunction) {
4715
- skipWrappedFunction = err;
4716
- } else {
4717
- throw err;
4783
+ const _this = this;
4784
+ return traceQuery(async function maybeTracedQueryExec() {
4785
+ let skipWrappedFunction = null;
4786
+ try {
4787
+ await _this._hooks.execPre('exec', _this, []);
4788
+ } catch (err) {
4789
+ if (err instanceof Kareem.skipWrappedFunction) {
4790
+ skipWrappedFunction = err;
4791
+ } else {
4792
+ throw err;
4793
+ }
4718
4794
  }
4719
- }
4720
4795
 
4721
- let res;
4796
+ let res;
4722
4797
 
4723
- let error = null;
4724
- try {
4725
- await _executePreHooks(this);
4726
- res = skipWrappedFunction ? skipWrappedFunction.args[0] : await this[thunk]();
4798
+ let error = null;
4799
+ try {
4800
+ await _executePreHooks(_this);
4801
+ res = skipWrappedFunction ? skipWrappedFunction.args[0] : await _this[thunk]();
4727
4802
 
4728
- for (const fn of this._transforms) {
4729
- res = fn(res);
4730
- }
4731
- } catch (err) {
4732
- if (err instanceof Kareem.skipWrappedFunction) {
4733
- res = err.args[0];
4734
- } else {
4735
- error = err;
4736
- }
4803
+ for (const fn of _this._transforms) {
4804
+ res = fn(res);
4805
+ }
4806
+ } catch (err) {
4807
+ if (err instanceof Kareem.skipWrappedFunction) {
4808
+ res = err.args[0];
4809
+ } else {
4810
+ error = err;
4811
+ }
4737
4812
 
4738
- error = this.model.schema._transformDuplicateKeyError(error);
4739
- }
4813
+ error = _this.model.schema._transformDuplicateKeyError(error);
4814
+ }
4740
4815
 
4741
- res = await _executePostHooks(this, res, error);
4816
+ res = await _executePostHooks(_this, res, error);
4742
4817
 
4743
- await this._hooks.execPost('exec', this, []);
4818
+ await _this._hooks.execPost('exec', _this, []);
4744
4819
 
4745
- return res;
4820
+ return res;
4821
+ }, () => ({
4822
+ operation: _this.op,
4823
+ collection: _this.mongooseCollection.name,
4824
+ database: _this.model.db?.name,
4825
+ serverAddress: _this.model.db?.host,
4826
+ serverPort: _this.model.db?.port,
4827
+ args: {
4828
+ filter: _this.getFilter(),
4829
+ fields: _this._fields,
4830
+ options: _this._mongooseOptions
4831
+ }
4832
+ }));
4746
4833
  };
4747
4834
 
4748
4835
  /*!
@@ -4789,6 +4876,20 @@ function _executePreHooks(query, op) {
4789
4876
  );
4790
4877
  }
4791
4878
 
4879
+ function _cloneUpdateIfShared(query) {
4880
+ if (!query._updateIsShared) {
4881
+ return;
4882
+ }
4883
+ if (query.mongooseOptions().cloneUpdate === false) {
4884
+ return;
4885
+ }
4886
+
4887
+ query[queryUpdateSymbol] = clone(query[queryUpdateSymbol], {
4888
+ flattenDecimals: false
4889
+ });
4890
+ query._updateIsShared = false;
4891
+ }
4892
+
4792
4893
  /**
4793
4894
  * Executes the query returning a `Promise` which will be
4794
4895
  * resolved with either the doc(s) or rejected with the error.
@@ -4987,12 +5088,17 @@ Query.prototype._castUpdate = function _castUpdate(obj) {
4987
5088
  * @param {object} [match] Conditions for the population query
4988
5089
  * @param {object} [options] Options for the population query (sort, etc)
4989
5090
  * @param {string} [options.path=null] The path to populate.
5091
+ * @param {string|PopulateOptions} [options.populate=null] Recursively populate paths in the populated documents. See [deep populate docs](https://mongoosejs.com/docs/populate.html#deep-populate).
4990
5092
  * @param {boolean} [options.retainNullValues=false] by default, Mongoose removes null and undefined values from populated arrays. Use this option to make `populate()` retain `null` and `undefined` array entries.
4991
5093
  * @param {boolean} [options.getters=false] if true, Mongoose will call any getters defined on the `localField`. By default, Mongoose gets the raw value of `localField`. For example, you would need to set this option to `true` if you wanted to [add a `lowercase` getter to your `localField`](https://mongoosejs.com/docs/schematypes.html#schematype-options).
4992
5094
  * @param {boolean} [options.clone=false] When you do `BlogPost.find().populate('author')`, blog posts with the same author will share 1 copy of an `author` doc. Enable this option to make Mongoose clone populated docs before assigning them.
4993
5095
  * @param {object|Function} [options.match=null] Add an additional filter to the populate query. Can be a filter object containing [MongoDB query syntax](https://www.mongodb.com/docs/manual/tutorial/query-documents/), or a function that returns a filter object.
5096
+ * @param {boolean} [options.skipInvalidIds=false] By default, Mongoose throws a cast error if `localField` and `foreignField` schemas don't line up. If you enable this option, Mongoose will instead filter out any `localField` properties that cannot be casted to `foreignField`'s schema type.
5097
+ * @param {number} [options.perDocumentLimit=null] For legacy reasons, `limit` with `populate()` may give incorrect results because it only executes a single query for every document being populated. If you set `perDocumentLimit`, Mongoose will ensure correct `limit` per document by executing a separate query for each document to `populate()`. For example, `.find().populate({ path: 'test', perDocumentLimit: 2 })` will execute 2 additional queries if `.find()` returns 2 documents.
5098
+ * @param {boolean} [options.strictPopulate=true] Set to false to allow populating paths that aren't defined in the given model's schema.
4994
5099
  * @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
4995
5100
  * @param {object} [options.options=null] Additional options like `limit` and `lean`.
5101
+ * @param {boolean} [options.ordered=false] Set to `true` to execute any populate queries one at a time, as opposed to in parallel. Set this option to `true` if populating multiple paths or paths with multiple models in transactions.
4996
5102
  * @see population https://mongoosejs.com/docs/populate.html
4997
5103
  * @see Query#select https://mongoosejs.com/docs/api/query.html#Query.prototype.select()
4998
5104
  * @see Model.populate https://mongoosejs.com/docs/api/model.html#Model.populate()