@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/document.js CHANGED
@@ -562,27 +562,26 @@ function $applyDefaultsToNested(val, path, doc) {
562
562
  Document.prototype.$__buildDoc = function(obj, fields, skipId, exclude, hasIncludedChildren) {
563
563
  const doc = {};
564
564
 
565
- const paths = Object.keys(this.$__schema.paths).
566
- // Don't build up any paths that are underneath a map, we don't know
567
- // what the keys will be
568
- filter(p => !p.includes('$*'));
565
+ const paths = Object.keys(this.$__schema.paths);
569
566
  const plen = paths.length;
570
567
  let ii = 0;
571
568
 
572
569
  for (; ii < plen; ++ii) {
573
570
  const p = paths[ii];
574
571
 
575
- if (p === '_id') {
576
- if (skipId) {
577
- continue;
578
- }
579
- if (obj && '_id' in obj) {
580
- continue;
581
- }
572
+ // Don't build up any paths that are underneath a map, we don't know
573
+ // what the keys will be
574
+ if (p.includes('$*')) {
575
+ continue;
582
576
  }
583
577
 
584
578
  const path = this.$__schema.paths[p].splitPath();
585
579
  const len = path.length;
580
+ // This loop only creates intermediate objects for nested paths: it never
581
+ // sets any leaf values, so single-segment paths are no-ops.
582
+ if (len === 1) {
583
+ continue;
584
+ }
586
585
  const last = len - 1;
587
586
  let curPath = '';
588
587
  let doc_ = doc;
@@ -757,6 +756,7 @@ function init(self, obj, doc, opts, prefix) {
757
756
  let i;
758
757
  const strict = self.$__.strictMode;
759
758
  const docSchema = self.$__schema;
759
+ const strictRead = docSchema.options.strictRead;
760
760
 
761
761
  for (let index = 0; index < len; ++index) {
762
762
  i = keys[index];
@@ -774,6 +774,14 @@ function init(self, obj, doc, opts, prefix) {
774
774
  }
775
775
 
776
776
  const value = obj[i];
777
+ if (!schemaType && strictRead && docSchema.pathType(path) === 'adhocOrUndefined') {
778
+ if (strictRead === 'throw') {
779
+ throw new StrictModeError(path, 'Field `' + path + '` is not in schema and strictRead is set to throw.');
780
+ } else if (strictRead === true) {
781
+ continue;
782
+ }
783
+ }
784
+
777
785
  if (!schemaType && utils.isPOJO(value)) {
778
786
  // assume nested object
779
787
  if (!doc[i]) {
@@ -786,6 +794,13 @@ function init(self, obj, doc, opts, prefix) {
786
794
  }
787
795
  init(self, value, doc[i], opts, path + '.');
788
796
  } else if (!schemaType) {
797
+ // Handle strictRead: filter unknown fields during document hydration from DB
798
+ if (strictRead === 'throw') {
799
+ throw new StrictModeError(path, 'Field `' + path + '` is not in schema and strictRead is set to throw.');
800
+ } else if (strictRead === true) {
801
+ // Skip this field - do not store in _doc
802
+ continue;
803
+ }
789
804
  doc[i] = value;
790
805
  if (!strict && !prefix) {
791
806
  self[i] = value;
@@ -1133,7 +1148,11 @@ Document.prototype.$set = function $set(path, val, type, options) {
1133
1148
  return this;
1134
1149
  }
1135
1150
 
1136
- options = Object.assign({}, options, { _skipMinimizeTopLevel: false });
1151
+ // Only need to clone if we're overwriting `_skipMinimizeTopLevel`, recursive
1152
+ // calls treat a missing `_skipMinimizeTopLevel` as `false`
1153
+ if (_skipMinimizeTopLevel) {
1154
+ options = Object.assign({}, options, { _skipMinimizeTopLevel: false });
1155
+ }
1137
1156
 
1138
1157
  for (let i = 0; i < len; ++i) {
1139
1158
  key = keys[i];
@@ -1220,15 +1239,9 @@ Document.prototype.$set = function $set(path, val, type, options) {
1220
1239
  val = handleSpreadDoc(val, true);
1221
1240
 
1222
1241
  // if this doc is being constructed we should not trigger getters
1223
- const priorVal = (() => {
1224
- if (this.$__.priorDoc != null) {
1225
- return this.$__.priorDoc.$__getValue(path);
1226
- }
1227
- if (constructing) {
1228
- return void 0;
1229
- }
1230
- return this.$__getValue(path);
1231
- })();
1242
+ const priorVal = this.$__.priorDoc != null ?
1243
+ this.$__.priorDoc.$__getValue(path) :
1244
+ (constructing ? void 0 : this.$__getValue(path));
1232
1245
 
1233
1246
  if (pathType === 'nested' && val) {
1234
1247
  if (typeof val === 'object' && val != null) {
@@ -1410,13 +1423,10 @@ Document.prototype.$set = function $set(path, val, type, options) {
1410
1423
  try {
1411
1424
  // If the user is trying to set a ref path to a document with
1412
1425
  // the correct model name, treat it as populated
1413
- const refMatches = (() => {
1426
+ const refMatches = !(val instanceof Document) ? false : (() => {
1414
1427
  if (schema.options == null) {
1415
1428
  return false;
1416
1429
  }
1417
- if (!(val instanceof Document)) {
1418
- return false;
1419
- }
1420
1430
  const model = val.constructor;
1421
1431
 
1422
1432
  // Check ref
@@ -1954,7 +1964,9 @@ Document.prototype.get = function(path, type, options) {
1954
1964
 
1955
1965
  // Fast path if we know we're just accessing top-level path on the document:
1956
1966
  // just get the schema path, avoid `$__path()` because that does string manipulation
1957
- let schema = noDottedPath ? this.$__schema.paths[path] : this.$__path(path);
1967
+ let schema = noDottedPath ?
1968
+ Object.hasOwn(this.$__schema.paths, path) ? this.$__schema.paths[path] : undefined :
1969
+ this.$__path(path);
1958
1970
  if (schema == null) {
1959
1971
  schema = this.$__schema.virtualpath(path);
1960
1972
 
@@ -1964,7 +1976,7 @@ Document.prototype.get = function(path, type, options) {
1964
1976
  }
1965
1977
 
1966
1978
  if (noDottedPath) {
1967
- let obj = this._doc[path];
1979
+ let obj = Object.hasOwn(this._doc, path) ? this._doc[path] : undefined;
1968
1980
  if (adhoc) {
1969
1981
  obj = adhoc.cast(obj);
1970
1982
  }
@@ -1991,6 +2003,10 @@ Document.prototype.get = function(path, type, options) {
1991
2003
  }
1992
2004
 
1993
2005
  for (let i = 0, l = pieces.length; i < l; i++) {
2006
+ if (specialProperties.has(pieces[i])) {
2007
+ return undefined;
2008
+ }
2009
+
1994
2010
  if (obj?._doc) {
1995
2011
  obj = obj._doc;
1996
2012
  }
@@ -2012,7 +2028,7 @@ Document.prototype.get = function(path, type, options) {
2012
2028
 
2013
2029
  if (schema != null && options.getters !== false) {
2014
2030
  obj = schema.applyGetters(obj, this);
2015
- } else if (this.$__schema.nested[path] && options.virtuals) {
2031
+ } else if (Object.hasOwn(this.$__schema.nested, path) && options.virtuals) {
2016
2032
  // Might need to apply virtuals if this is a nested path
2017
2033
  return applyVirtuals(this, clone(obj) || {}, { path: path });
2018
2034
  }
@@ -2870,7 +2886,8 @@ Document.prototype.validate = async function validate(pathsToValidate, options)
2870
2886
 
2871
2887
  await this._execDocumentPostHooks('validate', options, error);
2872
2888
  } finally {
2873
- delete this.$__.validateModifiedOnly;
2889
+ // Assign rather than `delete` to avoid putting `$__` in dictionary mode
2890
+ this.$__.validateModifiedOnly = undefined;
2874
2891
  this.$op = null;
2875
2892
  this.$__.validating = null;
2876
2893
  }
@@ -2913,7 +2930,7 @@ function _completeValidate(doc) {
2913
2930
  }
2914
2931
  }
2915
2932
 
2916
- doc.$__.cachedRequired = {};
2933
+ doc.$__.cachedRequired = null;
2917
2934
  doc.$emit('validate', doc);
2918
2935
  doc.constructor.emit('validate', doc);
2919
2936
 
@@ -3204,7 +3221,7 @@ function _pushNestedArrayPaths(val, paths, path) {
3204
3221
  * ignore
3205
3222
  */
3206
3223
 
3207
- Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks(opName, options, argsForHooks) {
3224
+ Document.prototype._execDocumentPreHooks = function _execDocumentPreHooks(opName, options, argsForHooks) {
3208
3225
  const filter = buildMiddlewareFilter(options, 'pre');
3209
3226
  return this.$__middleware.execPre(opName, this, argsForHooks || [], { filter });
3210
3227
  };
@@ -3213,7 +3230,7 @@ Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks(
3213
3230
  * ignore
3214
3231
  */
3215
3232
 
3216
- Document.prototype._execDocumentPostHooks = async function _execDocumentPostHooks(opName, options, error) {
3233
+ Document.prototype._execDocumentPostHooks = function _execDocumentPostHooks(opName, options, error) {
3217
3234
  const filter = buildMiddlewareFilter(options, 'post');
3218
3235
  return this.$__middleware.execPost(opName, this, [this], { error, filter });
3219
3236
  };
@@ -3264,6 +3281,9 @@ function _handlePathsToSkip(paths, pathsToSkip) {
3264
3281
  /**
3265
3282
  * Executes registered validation rules (skipping asynchronous validators) for this document.
3266
3283
  *
3284
+ * **Deprecated.** Use [`validate()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.validate()) instead.
3285
+ * `validateSync()` does not run validation middleware and will be removed in Mongoose 10.
3286
+ *
3267
3287
  * #### Note:
3268
3288
  *
3269
3289
  * This method is useful if you need synchronous validation.
@@ -3281,14 +3301,21 @@ function _handlePathsToSkip(paths, pathsToSkip) {
3281
3301
  * @param {object} [options] options for validation
3282
3302
  * @param {boolean} [options.validateModifiedOnly=false] If `true`, Mongoose will only validate modified paths, as opposed to modified paths and `required` paths.
3283
3303
  * @param {Array|string} [options.pathsToSkip] list of paths to skip. If set, Mongoose will validate every modified path that is not in this list.
3284
- * @param {boolean|object} [options.middleware=true] set to `false` to skip all user-defined middleware
3285
- * @param {boolean} [options.middleware.pre=true] set to `false` to skip only pre hooks
3286
- * @param {boolean} [options.middleware.post=true] set to `false` to skip only post hooks
3287
3304
  * @return {ValidationError|undefined} ValidationError if there are errors during validation, or undefined if there is no error.
3305
+ * @deprecated Use `validate()` instead. `validateSync()` does not run middleware and will be removed in Mongoose 10.
3288
3306
  * @api public
3289
3307
  */
3290
3308
 
3291
- Document.prototype.validateSync = function(pathsToValidate, options) {
3309
+ Document.prototype.validateSync = function() {
3310
+ utils.warn('Mongoose: `Document.prototype.validateSync()` is deprecated and will be removed in Mongoose 10. Use `Document.prototype.validate()` instead.');
3311
+ return this.$__validateSync.apply(this, arguments);
3312
+ };
3313
+
3314
+ /*!
3315
+ * ignore
3316
+ */
3317
+
3318
+ Document.prototype.$__validateSync = function(pathsToValidate, options) {
3292
3319
  const _this = this;
3293
3320
 
3294
3321
  if (arguments.length === 1 && typeof arguments[0] === 'object' && !Array.isArray(arguments[0])) {
@@ -3556,6 +3583,10 @@ function _checkImmutableSubpaths(subdoc, schematype, priorVal) {
3556
3583
  * @param {number} [options.wtimeout] sets a [timeout for the write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern).
3557
3584
  * @param {boolean} [options.checkKeys=true] the MongoDB driver prevents you from saving keys that start with '$' or contain '.' by default. Set this option to `false` to skip that check. See [restrictions on field names](https://www.mongodb.com/docs/manual/reference/limits/#Restrictions-on-Field-Names)
3558
3585
  * @param {boolean} [options.timestamps=true] if `false` and [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`.
3586
+ * @param {Array} [options.pathsToSave] An array of paths that tell mongoose to only validate and save the paths in `pathsToSave`.
3587
+ * @param {boolean|object} [options.middleware=true] set to `false` to skip all user-defined middleware
3588
+ * @param {boolean} [options.middleware.pre=true] set to `false` to skip only pre hooks
3589
+ * @param {boolean} [options.middleware.post=true] set to `false` to skip only post hooks
3559
3590
  * @method save
3560
3591
  * @memberOf Document
3561
3592
  * @instance
@@ -3620,7 +3651,11 @@ Document.prototype.$__reset = function reset() {
3620
3651
  // Clear atomics on dirty paths. Walk the modified and default paths
3621
3652
  // directly instead of calling $__dirty(), which builds intermediate
3622
3653
  // arrays, a Map, and does parent-path deduplication we don't need here.
3623
- this.$__resetAtomics();
3654
+ // Skip entirely if the doc only has primitive values, because only arrays
3655
+ // and maps have atomics.
3656
+ if (!onlyPrimitiveValues) {
3657
+ this.$__resetAtomics();
3658
+ }
3624
3659
 
3625
3660
  this.$__.backup = {};
3626
3661
  this.$__.backup.activePaths = {
@@ -3750,15 +3785,17 @@ Document.prototype.$__dirty = function() {
3750
3785
  }
3751
3786
 
3752
3787
  let top = null;
3788
+ let foundParent = false;
3753
3789
 
3754
3790
  const array = parentPaths(item.path);
3755
3791
  for (let i = 0; i < array.length - 1; i++) {
3756
3792
  if (allPaths.has(array[i])) {
3757
3793
  top = allPaths.get(array[i]);
3794
+ foundParent = true;
3758
3795
  break;
3759
3796
  }
3760
3797
  }
3761
- if (top == null) {
3798
+ if (!foundParent) {
3762
3799
  minimal.push(item);
3763
3800
  } else if (top != null &&
3764
3801
  top[arrayAtomicsSymbol] != null &&
@@ -4701,6 +4738,9 @@ Document.prototype.equals = function(doc) {
4701
4738
  * @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).
4702
4739
  * @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.
4703
4740
  * @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.
4741
+ * @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.
4742
+ * @param {number} [options.perDocumentLimit=null] For legacy reasons, `limit` with `populate()` may give incorrect results when populating multiple parent documents because it executes a single query for all parents. If you set `perDocumentLimit`, Mongoose will execute a separate query per parent document to ensure correct per-document `limit`.
4743
+ * @param {boolean} [options.strictPopulate=true] Set to false to allow populating paths that aren't defined in the given model's schema.
4704
4744
  * @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
4705
4745
  * @param {object} [options.options=null] Additional options like `limit` and `lean`.
4706
4746
  * @param {boolean} [options.forceRepopulate=true] Set to `false` to prevent Mongoose from repopulating paths that are already populated
@@ -5112,20 +5152,26 @@ Document.prototype.$__delta = function $__delta(pathsToSave, pathsToSaveSet) {
5112
5152
  const optimisticConcurrency = this.$__schema.options.optimisticConcurrency;
5113
5153
  if (optimisticConcurrency) {
5114
5154
  if (Array.isArray(optimisticConcurrency)) {
5115
- const optCon = new Set(optimisticConcurrency);
5116
- const modPaths = this.modifiedPaths();
5155
+ if (!this.$__schema.options._optimisticConcurrencySet) {
5156
+ this.$__schema.options._optimisticConcurrencySet = new Set(optimisticConcurrency);
5157
+ }
5158
+ const optimisticConcurrencySet = this.$__schema.options._optimisticConcurrencySet;
5159
+ const modPaths = this.directModifiedPaths();
5117
5160
  const hasRelevantModPaths = pathsToSave == null ?
5118
- modPaths.find(path => optCon.has(path)) :
5119
- modPaths.find(path => optCon.has(path) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5161
+ modPaths.find(path => _pathOverlapsSet(path, optimisticConcurrencySet)) :
5162
+ modPaths.find(path => _pathOverlapsSet(path, optimisticConcurrencySet) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5120
5163
  if (hasRelevantModPaths) {
5121
5164
  this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
5122
5165
  }
5123
5166
  } else if (Array.isArray(optimisticConcurrency?.exclude)) {
5124
- const excluded = new Set(optimisticConcurrency.exclude);
5125
- const modPaths = this.modifiedPaths();
5167
+ if (!this.$__schema.options._optimisticConcurrencyExcludeSet) {
5168
+ this.$__schema.options._optimisticConcurrencyExcludeSet = new Set(optimisticConcurrency.exclude);
5169
+ }
5170
+ const optimisticConcurrencyExcludeSet = this.$__schema.options._optimisticConcurrencyExcludeSet;
5171
+ const modPaths = this.directModifiedPaths();
5126
5172
  const hasRelevantModPaths = pathsToSave == null ?
5127
- modPaths.find(path => !excluded.has(path)) :
5128
- modPaths.find(path => !excluded.has(path) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5173
+ modPaths.find(path => !_pathOrAncestorInSet(path, optimisticConcurrencyExcludeSet)) :
5174
+ modPaths.find(path => !_pathOrAncestorInSet(path, optimisticConcurrencyExcludeSet) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5129
5175
  if (hasRelevantModPaths) {
5130
5176
  this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
5131
5177
  }
@@ -5258,6 +5304,12 @@ function checkDivergentArray(doc, path, array) {
5258
5304
  // If any array was selected using an $elemMatch projection, we deny the update.
5259
5305
  // NOTE: MongoDB only supports projected $elemMatch on top level array.
5260
5306
  const top = path.split('.')[0];
5307
+ if (doc.$__.selected[top] && doc.$__.selected[top].$slice != null && utils.isMongooseArray(array)) {
5308
+ const atomics = array[arrayAtomicsSymbol];
5309
+ if (atomics.$set) {
5310
+ return top;
5311
+ }
5312
+ }
5261
5313
  if (doc.$__.selected[top + '.$']) {
5262
5314
  return top;
5263
5315
  }
@@ -5633,24 +5685,47 @@ Document.prototype._applyVersionIncrement = function _applyVersionIncrement() {
5633
5685
  };
5634
5686
 
5635
5687
  /*!
5636
- * Increment this document's version if necessary.
5688
+ * Module exports.
5637
5689
  */
5638
5690
 
5639
- Document.prototype._applyVersionIncrement = function _applyVersionIncrement() {
5640
- if (!this.$__.version) return;
5641
- const doIncrement = VERSION_INC === (VERSION_INC & this.$__.version);
5642
-
5643
- this.$__.version = undefined;
5644
- if (doIncrement) {
5645
- const key = this.$__schema.options.versionKey;
5646
- const version = this.$__getValue(key) || 0;
5647
- this.$__setValue(key, version + 1); // increment version if was successful
5691
+ /*!
5692
+ * Check if `path` or any of its ancestor paths exist in `pathSet`.
5693
+ * For example:
5694
+ * _pathOrAncestorInSet('profile.firstName', Set(['profile'])) === true
5695
+ * _pathOrAncestorInSet('profile', Set(['profile.firstName'])) === false
5696
+ */
5697
+ function _pathOrAncestorInSet(path, pathSet) {
5698
+ if (pathSet.has(path)) {
5699
+ return true;
5648
5700
  }
5649
- };
5701
+ let idx = path.indexOf('.');
5702
+ while (idx !== -1) {
5703
+ if (pathSet.has(path.substring(0, idx))) {
5704
+ return true;
5705
+ }
5706
+ idx = path.indexOf('.', idx + 1);
5707
+ }
5708
+ return false;
5709
+ }
5650
5710
 
5651
5711
  /*!
5652
- * Module exports.
5712
+ * Check if `path`, any of its ancestor paths, or any of its descendant paths
5713
+ * exist in `pathSet`.
5714
+ * For example:
5715
+ * _pathOverlapsSet('profile.firstName', Set(['profile'])) === true
5716
+ * _pathOverlapsSet('profile', Set(['profile.firstName'])) === true
5653
5717
  */
5718
+ function _pathOverlapsSet(path, pathSet) {
5719
+ if (_pathOrAncestorInSet(path, pathSet)) {
5720
+ return true;
5721
+ }
5722
+ for (const p of pathSet) {
5723
+ if (p.length > path.length + 1 && p[path.length] === '.' && p.slice(0, path.length) === path) {
5724
+ return true;
5725
+ }
5726
+ }
5727
+ return false;
5728
+ }
5654
5729
 
5655
5730
  Document.VERSION_WHERE = VERSION_WHERE;
5656
5731
  Document.VERSION_INC = VERSION_INC;
@@ -172,7 +172,8 @@ function iter(i) {
172
172
  } else {
173
173
  const color = debug.color == null ? true : debug.color;
174
174
  const shell = debug.shell == null ? false : debug.shell;
175
- this.$print(_this.name, i, args, color, shell);
175
+ const timestamp = debug.timestamp == null ? false : debug.timestamp;
176
+ this.$print(_this.name, i, args, color, shell, timestamp);
176
177
  }
177
178
  }
178
179
 
@@ -256,7 +257,12 @@ for (const key of Object.getOwnPropertyNames(Collection.prototype)) {
256
257
  * @method $print
257
258
  */
258
259
 
259
- NativeCollection.prototype.$print = function(name, i, args, color, shell) {
260
+ NativeCollection.prototype.$print = function(name, i, args, color, shell, timestamp) {
261
+ let prefix = '';
262
+ if (timestamp) {
263
+ const ts = new Date().toISOString();
264
+ prefix = color ? `\x1B[0;90m[${ts}]\x1B[0m ` : `[${ts}] `;
265
+ }
260
266
  const moduleName = color ? '\x1B[0;36mMongoose:\x1B[0m ' : 'Mongoose: ';
261
267
  const functionCall = [name, i].join('.');
262
268
  const _args = [];
@@ -267,14 +273,14 @@ NativeCollection.prototype.$print = function(name, i, args, color, shell) {
267
273
  }
268
274
  const params = '(' + _args.join(', ') + ')';
269
275
 
270
- console.info(moduleName + functionCall + params);
276
+ console.info(prefix + moduleName + functionCall + params);
271
277
  };
272
278
 
273
279
  /**
274
280
  * Debug print helper
275
281
  *
276
282
  * @api public
277
- * @method $print
283
+ * @method $printToStream
278
284
  */
279
285
 
280
286
  NativeCollection.prototype.$printToStream = function(name, i, args, stream) {
package/lib/error/cast.js CHANGED
@@ -42,6 +42,7 @@ class CastError extends MongooseError {
42
42
  message: this.message
43
43
  };
44
44
  }
45
+
45
46
  /*!
46
47
  * ignore
47
48
  */
@@ -76,7 +77,7 @@ class CastError extends MongooseError {
76
77
  */
77
78
  setModel(model) {
78
79
  this.message = formatMessage(model, this.kind, this.value, this.path,
79
- this.messageFormat, this.valueType);
80
+ this.messageFormat, this.valueType, this.reason);
80
81
  }
81
82
  }
82
83
 
@@ -122,10 +123,11 @@ function getMessageFormat(schemaType) {
122
123
  function formatMessage(model, kind, value, path, messageFormat, valueType, reason) {
123
124
  if (typeof messageFormat === 'string') {
124
125
  const stringValue = getStringValue(value);
125
- let ret = messageFormat.
126
- replace('{KIND}', kind).
127
- replace('{VALUE}', stringValue).
128
- replace('{PATH}', path);
126
+ let ret = messageFormat
127
+ .replace('{KIND}', kind)
128
+ .replace('{VALUE}', stringValue)
129
+ .replace('{PATH}', path);
130
+
129
131
  if (model != null) {
130
132
  ret = ret.replace('{MODEL}', model.modelName);
131
133
  }
@@ -136,17 +138,24 @@ function formatMessage(model, kind, value, path, messageFormat, valueType, reaso
136
138
  } else {
137
139
  const stringValue = getStringValue(value);
138
140
  const valueTypeMsg = valueType ? ' (type ' + valueType + ')' : '';
141
+
139
142
  let ret = 'Cast to ' + kind + ' failed for value ' +
140
143
  stringValue + valueTypeMsg + ' at path "' + path + '"';
144
+
141
145
  if (model != null) {
142
146
  ret += ' for model "' + model.modelName + '"';
143
147
  }
144
- if (reason != null &&
145
- typeof reason.constructor === 'function' &&
146
- reason.constructor.name !== 'AssertionError' &&
147
- reason.constructor.name !== 'Error') {
148
+
149
+ if (
150
+ reason != null &&
151
+ typeof reason.constructor === 'function' &&
152
+ reason.constructor.name !== 'AssertionError' &&
153
+ reason.constructor.name !== 'Error' &&
154
+ reason.constructor.name !== 'BSONError'
155
+ ) {
148
156
  ret += ' because of "' + reason.constructor.name + '"';
149
157
  }
158
+
150
159
  return ret;
151
160
  }
152
161
  }
@@ -17,7 +17,7 @@ class DivergentArrayError extends MongooseError {
17
17
 
18
18
  constructor(paths) {
19
19
  const msg = 'For your own good, using `document.save()` to update an array '
20
- + 'which was selected using an $elemMatch projection OR '
20
+ + 'which was selected using an $elemMatch or $slice projection OR '
21
21
  + 'populated using skip, limit, query conditions, or exclusion of '
22
22
  + 'the _id field when the operation results in a $pop or $set of '
23
23
  + 'the entire array is not supported. The following '
@@ -30,6 +30,7 @@ msg.DocumentNotFoundError = null;
30
30
  msg.general = {};
31
31
  msg.general.default = 'Validator failed for path `{PATH}` with value `{VALUE}`';
32
32
  msg.general.required = 'Path `{PATH}` is required.';
33
+ msg.general.allowNull = 'Path `{PATH}` does not allow null values.';
33
34
 
34
35
  msg.Number = {};
35
36
  msg.Number.min = 'Path `{PATH}` ({VALUE}) is less than minimum allowed value ({MIN}).';
@@ -202,10 +202,15 @@ function cloneArray(arr, options) {
202
202
 
203
203
  let ret = null;
204
204
  if (options?.retainDocuments) {
205
+ // Use the cloned parent doc (options.parentDoc) when available so the new
206
+ // array is wired up to the clone, not the source. Falling back to the
207
+ // source's parent preserves the previous behavior for callers that don't
208
+ // pass parentDoc.
209
+ const newParent = options.parentDoc ?? (arr.isMongooseArray ? arr.$parent() : undefined);
205
210
  if (arr.isMongooseDocumentArray) {
206
- ret = new (arr.$schemaType().schema.base.Types.DocumentArray)([], arr.$path(), arr.$parent(), arr.$schemaType());
211
+ ret = new (arr.$schemaType().schema.base.Types.DocumentArray)([], arr.$path(), newParent, arr.$schemaType());
207
212
  } else if (arr.isMongooseArray) {
208
- ret = new (arr.$parent().schema.base.Types.Array)([], arr.$path(), arr.$parent(), arr.$schemaType());
213
+ ret = new (arr.$parent().schema.base.Types.Array)([], arr.$path(), newParent, arr.$schemaType());
209
214
  } else {
210
215
  ret = new Array(len);
211
216
  }
@@ -218,9 +223,21 @@ function cloneArray(arr, options) {
218
223
  // Create new options object to avoid mutating the shared options.
219
224
  // Subdocs need parentArray to point to their own cloned array.
220
225
  options = { ...options, parentArray: ret };
221
- }
222
- for (i = 0; i < len; ++i) {
223
- ret[i] = clone(arr[i], options, true);
226
+ // Skip change-tracking while populating the freshly cloned array: every
227
+ // index assignment would otherwise call markModified on the cloned parent
228
+ // doc and (before this guard) on the source parent doc.
229
+ for (i = 0; i < len; ++i) {
230
+ const doc = clone(arr[i], options, true);
231
+ // Document arrays can contain null entries, so only restamp actual subdocs.
232
+ if (typeof doc?.$setIndex === 'function') {
233
+ doc.$setIndex(i);
234
+ }
235
+ ret.set(i, doc, true);
236
+ }
237
+ } else {
238
+ for (i = 0; i < len; ++i) {
239
+ ret[i] = clone(arr[i], options, true);
240
+ }
224
241
  }
225
242
 
226
243
  return ret;
@@ -17,6 +17,11 @@ module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildre
17
17
  }
18
18
 
19
19
  const type = doc.$__schema.paths[p];
20
+ // `getDefault()` returns undefined when `defaultValue` is undefined, in which
21
+ // case this loop never modifies the doc, so skip traversing the path entirely.
22
+ if (type.defaultValue === undefined) {
23
+ continue;
24
+ }
20
25
  const path = type.splitPath();
21
26
  const len = path.length;
22
27
  if (path[len - 1] === '$*') {
@@ -25,7 +25,15 @@ module.exports = function createPopulateQueryFilter(ids, _match, _foreignField,
25
25
  for (let i = 0; i < _parentPaths.length - 1; ++i) {
26
26
  const cur = _parentPaths[i];
27
27
  if (match[cur] != null && match[cur].$elemMatch != null) {
28
- match[cur].$elemMatch[foreignField.slice(cur.length + 1)] = trusted({ $in: ids });
28
+ // Copy rather than mutate so the user's `match` stays unchanged and split populate
29
+ // queries (gh-5890) each get their own `$in` rather than sharing one object
30
+ match[cur] = {
31
+ ...match[cur],
32
+ $elemMatch: {
33
+ ...match[cur].$elemMatch,
34
+ [foreignField.slice(cur.length + 1)]: trusted({ $in: ids })
35
+ }
36
+ };
29
37
  delete match[foreignField];
30
38
  break;
31
39
  }
@@ -649,11 +649,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
649
649
  ids = matchIdsToRefPaths(ret, modelNamesForRefPath, modelName);
650
650
  }
651
651
 
652
- const perDocumentLimit = options.perDocumentLimit == null ?
653
- get(options, 'options.perDocumentLimit', null) :
654
- options.perDocumentLimit;
655
-
656
- if (!available[modelName] || perDocumentLimit != null) {
652
+ if (!available[modelName]) {
657
653
  const currentOptions = {
658
654
  model: Model
659
655
  };
@@ -682,6 +678,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
682
678
  isVirtual: data.isVirtual,
683
679
  virtual: data.virtual,
684
680
  count: data.count,
681
+ isRefPath: !!data.isRefPath,
685
682
  [populateModelSymbol]: Model
686
683
  };
687
684
  map.push(available[modelName]);
@@ -692,6 +689,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
692
689
  available[modelName].ids.push(ids);
693
690
  available[modelName].allIds.push(ret);
694
691
  available[modelName].unpopulatedValues.push(unpopulatedValue);
692
+ available[modelName].isRefPath = available[modelName].isRefPath || !!data.isRefPath;
695
693
  if (data.hasMatchFunction) {
696
694
  available[modelName].match.push(data.match);
697
695
  }
@@ -873,7 +871,11 @@ function _findRefPathForDiscriminators(doc, modelSchema, data, options, normaliz
873
871
  }
874
872
 
875
873
  /**
876
- * Throw an error if there are any $where keys
874
+ * Throw an error if there are any $where keys to defend against [CVE-2024-53900](https://nvd.nist.gov/vuln/detail/CVE-2024-53900)
875
+ *
876
+ * Note that this is ONLY for $where because sift executes $where in Node.js memory.
877
+ * Other forms of MongoDB server-side execution, like $expr, are NOT filtered out.
878
+ * This function is not meant to protect against server-side execution in MongoDB.
877
879
  */
878
880
 
879
881
  function throwOn$where(match) {