mongoose 9.7.4 → 9.8.1

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/README.md CHANGED
@@ -32,7 +32,7 @@ Check out the [plugins search site](https://plugins.mongoosejs.io/) to see hundr
32
32
  Pull requests are always welcome! Please base pull requests against the `master`
33
33
  branch and follow the [contributing guide](https://github.com/Automattic/mongoose/blob/master/CONTRIBUTING.md).
34
34
 
35
- If your pull requests makes documentation changes, please do **not**
35
+ If your pull request makes documentation changes, please do **not**
36
36
  modify any `.html` files. The `.html` files are compiled code, so please make
37
37
  your changes in `docs/*.pug`, `lib/*.js`, or `test/docs/*.js`.
38
38
 
@@ -341,7 +341,7 @@ new Schema({
341
341
  ### Driver Access
342
342
 
343
343
  Mongoose is built on top of the [official MongoDB Node.js driver](https://github.com/mongodb/node-mongodb-native). Each mongoose model keeps a reference to a [native MongoDB driver collection](http://mongodb.github.io/node-mongodb-native/2.1/api/Collection.html). The collection object can be accessed using `YourModel.collection`. However, using the collection object directly bypasses all mongoose features, including hooks, validation, etc. The one
344
- notable exception that `YourModel.collection` still buffers
344
+ notable exception is that `YourModel.collection` still buffers
345
345
  commands. As such, `YourModel.collection.find()` will **not**
346
346
  return a cursor.
347
347
 
package/lib/aggregate.js CHANGED
@@ -615,6 +615,9 @@ Aggregate.prototype.graphLookup = function(options) {
615
615
  *
616
616
  * aggregate.sample(3); // Add a pipeline that picks 3 random documents
617
617
  *
618
+ * // `$match` before `$sample` samples from the filtered subset
619
+ * aggregate.match({ difficulty: 'easy' }).sample(3);
620
+ *
618
621
  * @see $sample https://www.mongodb.com/docs/manual/reference/operator/aggregation/sample/#pipe._S_sample
619
622
  * @param {number} size number of random documents to pick
620
623
  * @return {Aggregate}
package/lib/cast.js CHANGED
@@ -413,7 +413,7 @@ function _cast(val, numbertype, context) {
413
413
  _cast(item, numbertype, context);
414
414
  val[nkey] = item;
415
415
  } else {
416
- val[nkey] = numbertype.castForQuery({ val: item, context: context });
416
+ val[nkey] = numbertype.castForQuery(null, item, context);
417
417
  }
418
418
  }
419
419
  }
package/lib/document.js CHANGED
@@ -143,7 +143,7 @@ function Document(obj, fields, options) {
143
143
  this.$__.strictMode = schema.options.strict;
144
144
  }
145
145
 
146
- const requiredPaths = schema.requiredPaths(true);
146
+ const requiredPaths = schema.requiredPaths();
147
147
  for (const path of requiredPaths) {
148
148
  this.$__.activePaths.require(path);
149
149
  }
@@ -756,6 +756,7 @@ function init(self, obj, doc, opts, prefix) {
756
756
  let i;
757
757
  const strict = self.$__.strictMode;
758
758
  const docSchema = self.$__schema;
759
+ const strictRead = docSchema.options.strictRead;
759
760
 
760
761
  for (let index = 0; index < len; ++index) {
761
762
  i = keys[index];
@@ -773,6 +774,14 @@ function init(self, obj, doc, opts, prefix) {
773
774
  }
774
775
 
775
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
+
776
785
  if (!schemaType && utils.isPOJO(value)) {
777
786
  // assume nested object
778
787
  if (!doc[i]) {
@@ -785,6 +794,13 @@ function init(self, obj, doc, opts, prefix) {
785
794
  }
786
795
  init(self, value, doc[i], opts, path + '.');
787
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
+ }
788
804
  doc[i] = value;
789
805
  if (!strict && !prefix) {
790
806
  self[i] = value;
@@ -2959,9 +2975,13 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate
2959
2975
  const doValidateOptions = {};
2960
2976
 
2961
2977
  _evaluateRequiredFunctions(doc);
2978
+ // gh-16379: compute the modified-path set at most once and reuse it below, so
2979
+ // checking many required paths doesn't rebuild it per-path (O(required x modified)).
2980
+ let _modifiedPaths;
2981
+ const getModifiedPaths = () => (_modifiedPaths ??= doc.modifiedPaths());
2962
2982
  // only validate required fields when necessary
2963
2983
  let paths = new Set(Object.keys(doc.$__.activePaths.getStatePaths('require')).filter(function(path) {
2964
- if (!doc.$__isSelected(path) && !doc.$isModified(path)) {
2984
+ if (!doc.$__isSelected(path) && !doc.$isModified(path, null, getModifiedPaths())) {
2965
2985
  return false;
2966
2986
  }
2967
2987
  if (path.endsWith('.$*')) {
@@ -3040,7 +3060,7 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate
3040
3060
  }
3041
3061
  }
3042
3062
  }
3043
- const modifiedPaths = doc.modifiedPaths();
3063
+ const modifiedPaths = getModifiedPaths();
3044
3064
  for (const subdoc of topLevelSubdocs) {
3045
3065
  if (subdoc.$basePath) {
3046
3066
  const fullPathToSubdoc = subdoc.$__pathRelativeToParent();
@@ -3567,6 +3587,10 @@ function _checkImmutableSubpaths(subdoc, schematype, priorVal) {
3567
3587
  * @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).
3568
3588
  * @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)
3569
3589
  * @param {boolean} [options.timestamps=true] if `false` and [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`.
3590
+ * @param {Array} [options.pathsToSave] An array of paths that tell mongoose to only validate and save the paths in `pathsToSave`.
3591
+ * @param {boolean|object} [options.middleware=true] set to `false` to skip all user-defined middleware
3592
+ * @param {boolean} [options.middleware.pre=true] set to `false` to skip only pre hooks
3593
+ * @param {boolean} [options.middleware.post=true] set to `false` to skip only post hooks
3570
3594
  * @method save
3571
3595
  * @memberOf Document
3572
3596
  * @instance
@@ -4718,6 +4742,9 @@ Document.prototype.equals = function(doc) {
4718
4742
  * @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).
4719
4743
  * @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.
4720
4744
  * @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.
4745
+ * @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.
4746
+ * @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`.
4747
+ * @param {boolean} [options.strictPopulate=true] Set to false to allow populating paths that aren't defined in the given model's schema.
4721
4748
  * @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
4722
4749
  * @param {object} [options.options=null] Additional options like `limit` and `lean`.
4723
4750
  * @param {boolean} [options.forceRepopulate=true] Set to `false` to prevent Mongoose from repopulating paths that are already populated
@@ -5147,8 +5174,8 @@ Document.prototype.$__delta = function $__delta(pathsToSave, pathsToSaveSet) {
5147
5174
  const optimisticConcurrencyExcludeSet = this.$__schema.options._optimisticConcurrencyExcludeSet;
5148
5175
  const modPaths = this.directModifiedPaths();
5149
5176
  const hasRelevantModPaths = pathsToSave == null ?
5150
- modPaths.find(path => !_pathOverlapsSet(path, optimisticConcurrencyExcludeSet)) :
5151
- modPaths.find(path => !_pathOverlapsSet(path, optimisticConcurrencyExcludeSet) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5177
+ modPaths.find(path => !_pathOrAncestorInSet(path, optimisticConcurrencyExcludeSet)) :
5178
+ modPaths.find(path => !_pathOrAncestorInSet(path, optimisticConcurrencyExcludeSet) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5152
5179
  if (hasRelevantModPaths) {
5153
5180
  this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
5154
5181
  }
@@ -5661,34 +5688,17 @@ Document.prototype._applyVersionIncrement = function _applyVersionIncrement() {
5661
5688
  }
5662
5689
  };
5663
5690
 
5664
- /*!
5665
- * Increment this document's version if necessary.
5666
- */
5667
-
5668
- Document.prototype._applyVersionIncrement = function _applyVersionIncrement() {
5669
- if (!this.$__.version) return;
5670
- const doIncrement = VERSION_INC === (VERSION_INC & this.$__.version);
5671
-
5672
- this.$__.version = undefined;
5673
- if (doIncrement) {
5674
- const key = this.$__schema.options.versionKey;
5675
- const version = this.$__getValue(key) || 0;
5676
- this.$__setValue(key, version + 1); // increment version if was successful
5677
- }
5678
- };
5679
-
5680
5691
  /*!
5681
5692
  * Module exports.
5682
5693
  */
5683
5694
 
5684
5695
  /*!
5685
- * Check if `path`, any of its ancestor paths, or any of its descendant paths
5686
- * exist in `pathSet`.
5696
+ * Check if `path` or any of its ancestor paths exist in `pathSet`.
5687
5697
  * For example:
5688
- * _pathOverlapsSet('profile.firstName', Set(['profile'])) === true
5689
- * _pathOverlapsSet('profile', Set(['profile.firstName'])) === true
5698
+ * _pathOrAncestorInSet('profile.firstName', Set(['profile'])) === true
5699
+ * _pathOrAncestorInSet('profile', Set(['profile.firstName'])) === false
5690
5700
  */
5691
- function _pathOverlapsSet(path, pathSet) {
5701
+ function _pathOrAncestorInSet(path, pathSet) {
5692
5702
  if (pathSet.has(path)) {
5693
5703
  return true;
5694
5704
  }
@@ -5699,6 +5709,20 @@ function _pathOverlapsSet(path, pathSet) {
5699
5709
  }
5700
5710
  idx = path.indexOf('.', idx + 1);
5701
5711
  }
5712
+ return false;
5713
+ }
5714
+
5715
+ /*!
5716
+ * Check if `path`, any of its ancestor paths, or any of its descendant paths
5717
+ * exist in `pathSet`.
5718
+ * For example:
5719
+ * _pathOverlapsSet('profile.firstName', Set(['profile'])) === true
5720
+ * _pathOverlapsSet('profile', Set(['profile.firstName'])) === true
5721
+ */
5722
+ function _pathOverlapsSet(path, pathSet) {
5723
+ if (_pathOrAncestorInSet(path, pathSet)) {
5724
+ return true;
5725
+ }
5702
5726
  for (const p of pathSet) {
5703
5727
  if (p.length > path.length + 1 && p[path.length] === '.' && p.slice(0, path.length) === path) {
5704
5728
  return true;
@@ -11,6 +11,7 @@ const getConstructorName = require('../getConstructorName');
11
11
  const getDiscriminatorByValue = require('../discriminator/getDiscriminatorByValue');
12
12
  const getEmbeddedDiscriminatorPath = require('./getEmbeddedDiscriminatorPath');
13
13
  const handleImmutable = require('./handleImmutable');
14
+ const isOperator = require('./isOperator');
14
15
  const moveImmutableProperties = require('../update/moveImmutableProperties');
15
16
  const schemaMixedSymbol = require('../../schema/symbols').schemaMixedSymbol;
16
17
  const setDottedPath = require('../path/setDottedPath');
@@ -235,10 +236,25 @@ function walkUpdatePath(schema, obj, op, options, context, filter, prefix) {
235
236
  key = keys[i];
236
237
  val = obj[key];
237
238
 
239
+ const fullPath = prefix + key;
240
+ const isTopLevelOperator = !prefix && isOperator(key);
241
+ let fullPathSchema = isTopLevelOperator ? schema._getSchema(fullPath) : null;
242
+ const isTopLevelNestedDollarPath = isTopLevelOperator && Object.hasOwn(schema.nested, key);
243
+ if (isTopLevelOperator &&
244
+ fullPathSchema == null &&
245
+ !isTopLevelNestedDollarPath) {
246
+ throw new MongooseError('Invalid update: Unexpected modifier "' + key + '" as a key in operator "' + op + '". '
247
+ + 'Did you mean something like { ' + op + ': { fieldName: { ' + key + ': [...] } } }? '
248
+ + 'Modifiers must appear under a valid field path.');
249
+ }
250
+
238
251
  // `$pull` is special because we need to cast the RHS as a query, not as
239
252
  // an update.
240
253
  if (op === '$pull') {
241
- schematype = schema._getSchema(prefix + key);
254
+ if (!isTopLevelOperator) {
255
+ fullPathSchema = schema._getSchema(fullPath);
256
+ }
257
+ schematype = fullPathSchema;
242
258
  if (schematype == null) {
243
259
  const _res = getEmbeddedDiscriminatorPath(schema, obj, filter, prefix + key, options);
244
260
  if (_res.schematype != null) {
@@ -269,10 +285,13 @@ function walkUpdatePath(schema, obj, op, options, context, filter, prefix) {
269
285
  }
270
286
  }
271
287
 
288
+ if (!isTopLevelOperator && op !== '$pull') {
289
+ fullPathSchema = schema._getSchema(fullPath);
290
+ }
291
+ schematype = fullPathSchema;
292
+
272
293
  if (getConstructorName(val) === 'Object') {
273
294
  // watch for embedded doc schemas
274
- schematype = schema._getSchema(prefix + key);
275
-
276
295
  if (schematype == null) {
277
296
  const _res = getEmbeddedDiscriminatorPath(schema, obj, filter, prefix + key, options);
278
297
  if (_res.schematype != null) {
@@ -383,14 +402,12 @@ function walkUpdatePath(schema, obj, op, options, context, filter, prefix) {
383
402
  (utils.isObject(val) && utils.hasOwnKeys(val) === false);
384
403
  }
385
404
  } else {
386
- const isModifier = (key === '$each' || key === '$or' || key === '$and' || key === '$in');
387
- if (isModifier && !prefix) {
388
- throw new MongooseError('Invalid update: Unexpected modifier "' + key + '" as a key in operator. '
389
- + 'Did you mean something like { $addToSet: { fieldName: { $each: [...] } } }? '
390
- + 'Modifiers such as "$each", "$or", "$and", "$in" must appear under a valid field path.');
405
+ const isModifier = !isTopLevelNestedDollarPath && schematype == null &&
406
+ (key === '$each' || key === '$or' || key === '$and' || key === '$in');
407
+ const checkPath = isModifier ? prefix : fullPath;
408
+ if (isModifier) {
409
+ schematype = schema._getSchema(checkPath);
391
410
  }
392
- const checkPath = isModifier ? prefix : prefix + key;
393
- schematype = schema._getSchema(checkPath);
394
411
 
395
412
  // You can use `$setOnInsert` with immutable keys
396
413
  if (op !== '$setOnInsert' &&
package/lib/model.js CHANGED
@@ -3656,12 +3656,16 @@ async function buildPreSavePromise(document, options) {
3656
3656
  }
3657
3657
 
3658
3658
  async function handleSuccessfulWrite(document, options) {
3659
- if (document.$isNew) {
3659
+ const wasNew = document.$isNew;
3660
+ if (wasNew) {
3660
3661
  _setIsNew(document, false);
3661
3662
  }
3662
3663
 
3663
3664
  document.$__reset();
3664
- document._applyVersionIncrement();
3665
+ // version key gets initialized to 0 when inserting a new document so avoid incrementing version key here
3666
+ if (!wasNew) {
3667
+ document._applyVersionIncrement();
3668
+ }
3665
3669
  const postFilter = buildMiddlewareFilter(options, 'post');
3666
3670
  return document.schema.s.hooks.execPost('save', document, [document], { filter: postFilter });
3667
3671
  }
@@ -3911,6 +3915,12 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
3911
3915
 
3912
3916
  const isANewDocument = document.isNew;
3913
3917
  if (isANewDocument) {
3918
+ // Like `$__version()` on insert, initialize the version key so
3919
+ // inserted documents get the same shape as `save()` gives them
3920
+ const versionKey = document.$__schema.options.versionKey;
3921
+ if (versionKey) {
3922
+ document.$__setValue(versionKey, 0);
3923
+ }
3914
3924
  const writeOperation = { insertOne: { document } };
3915
3925
  utils.injectTimestampsOption(writeOperation.insertOne, options.timestamps);
3916
3926
  return writeOperation;
package/lib/query.js CHANGED
@@ -5088,12 +5088,17 @@ Query.prototype._castUpdate = function _castUpdate(obj) {
5088
5088
  * @param {object} [match] Conditions for the population query
5089
5089
  * @param {object} [options] Options for the population query (sort, etc)
5090
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).
5091
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.
5092
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).
5093
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.
5094
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.
5095
5099
  * @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
5096
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.
5097
5102
  * @see population https://mongoosejs.com/docs/populate.html
5098
5103
  * @see Query#select https://mongoosejs.com/docs/api/query.html#Query.prototype.select()
5099
5104
  * @see Model.populate https://mongoosejs.com/docs/api/model.html#Model.populate()
@@ -510,9 +510,9 @@ SchemaArray.prototype._castForQuery = function(val, context) {
510
510
  }
511
511
 
512
512
  if (Array.isArray(val)) {
513
- this.setters.reverse().forEach(setter => {
514
- val = setter.call(this, val, this);
515
- });
513
+ for (let i = this.setters.length - 1; i >= 0; i--) {
514
+ val = this.setters[i].call(this, val, this);
515
+ }
516
516
  val = val.map(function(v) {
517
517
  if (utils.isObject(v) && v.$elemMatch) {
518
518
  return v;
package/lib/schema.js CHANGED
@@ -72,6 +72,7 @@ const numberRE = /^\d+$/;
72
72
  * - [shardKey](https://mongoosejs.com/docs/guide.html#shardKey): object - defaults to `null`
73
73
  * - [strict](https://mongoosejs.com/docs/guide.html#strict): bool - defaults to true
74
74
  * - [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery): bool - defaults to false
75
+ * - [strictRead](https://mongoosejs.com/docs/guide.html#strictRead): bool or 'throw' - defaults to false. If set to `true`, fields not in the schema will be stripped when hydrating documents from MongoDB. If set to `'throw'`, an error will be thrown when a field not in the schema is encountered during document hydration.
75
76
  * - [toJSON](https://mongoosejs.com/docs/guide.html#toJSON) - object - no default
76
77
  * - [toObject](https://mongoosejs.com/docs/guide.html#toObject) - object - no default
77
78
  * - [typeKey](https://mongoosejs.com/docs/guide.html#typeKey) - string - defaults to 'type'
@@ -606,10 +607,12 @@ Schema.prototype.defaultOptions = function(options) {
606
607
  const baseOptions = this.base?.options || {};
607
608
  const defaultStrict = baseOptions.strict ?? true;
608
609
  const defaultStrictQuery = baseOptions.strictQuery ?? false;
610
+ const defaultStrictRead = baseOptions.strictRead ?? false;
609
611
  const defaultId = baseOptions.id ?? true;
610
612
  options = {
611
613
  strict: defaultStrict,
612
614
  strictQuery: defaultStrictQuery,
615
+ strictRead: defaultStrictRead,
613
616
  bufferCommands: true,
614
617
  capped: false, // { size, max, autoIndexId }
615
618
  versionKey: '__v',
@@ -403,7 +403,9 @@ if (util.inspect.custom) {
403
403
 
404
404
  /**
405
405
  * Override `$toObject()` to handle minimizing the whole path. Should not minimize if schematype-level minimize
406
- * is set to false re: gh-11247, gh-14058, gh-14151
406
+ * is set to false re: gh-11247, gh-14058, gh-14151. Should not minimize document array elements: an array
407
+ * element cannot be removed without shifting the array, so minimizing it to `undefined` makes the BSON
408
+ * serializer store `null` re: gh-7322.
407
409
  */
408
410
 
409
411
  Subdocument.prototype.$toObject = function $toObject(options, json) {
@@ -413,7 +415,7 @@ Subdocument.prototype.$toObject = function $toObject(options, json) {
413
415
  // If minimize is set, then we can minimize out the whole object.
414
416
  if (utils.hasOwnKeys(ret) === false && options?._calledWithOptions != null) {
415
417
  const minimize = options._calledWithOptions?.minimize ?? this?.$__schemaTypeOptions?.minimize ?? options.minimize;
416
- if (minimize && !this.constructor.$__required) {
418
+ if (minimize && !this.constructor.$__required && !this.$isDocumentArrayElement) {
417
419
  return undefined;
418
420
  }
419
421
  }
package/lib/utils.js CHANGED
@@ -858,11 +858,12 @@ exports.array.unique = function(arr) {
858
858
  ret.push(item);
859
859
  primitives.add(item);
860
860
  } else if (isBsonType(item, 'ObjectId')) {
861
- if (ids.has(item.toString())) {
861
+ const idStr = item.toString();
862
+ if (ids.has(idStr)) {
862
863
  continue;
863
864
  }
864
865
  ret.push(item);
865
- ids.add(item.toString());
866
+ ids.add(idStr);
866
867
  } else {
867
868
  ret.push(item);
868
869
  }
@@ -32,6 +32,7 @@ const VALID_OPTIONS = Object.freeze([
32
32
  'strict',
33
33
  'strictPopulate',
34
34
  'strictQuery',
35
+ 'strictRead',
35
36
  'timestamps.createdAt.immutable',
36
37
  'toJSON',
37
38
  'toObject',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongoose",
3
3
  "description": "Mongoose MongoDB ODM",
4
- "version": "9.7.4",
4
+ "version": "9.8.1",
5
5
  "author": "Guillermo Rauch <guillermo@learnboost.com>",
6
6
  "keywords": [
7
7
  "mongodb",
@@ -22,7 +22,7 @@
22
22
  "dependencies": {
23
23
  "@standard-schema/spec": "^1.1.0",
24
24
  "kareem": "3.3.0",
25
- "mongodb": "~7.2",
25
+ "mongodb": "~7.5",
26
26
  "mpath": "0.9.0",
27
27
  "mquery": "6.0.0",
28
28
  "ms": "2.1.3",
@@ -55,7 +55,7 @@
55
55
  "mkdirp": "^3.0.1",
56
56
  "mocha": "12.0.0-beta-10",
57
57
  "moment": "2.30.1",
58
- "mongodb-client-encryption": "~7.0",
58
+ "mongodb-client-encryption": "^7.2.0",
59
59
  "mongodb-memory-server": "11.2.0",
60
60
  "mongodb-runner": "^6.0.0",
61
61
  "ncp": "^2.0.0",
@@ -99,17 +99,17 @@
99
99
  "mongo": "node ./tools/repl.js",
100
100
  "publish-7x": "npm publish --tag 7x",
101
101
  "create-separate-require-instance": "rm -rf ./node_modules/mongoose-separate-require-instance && node ./scripts/create-tarball && tar -xzf mongoose.tgz -C ./node_modules && mv ./node_modules/package ./node_modules/mongoose-separate-require-instance",
102
- "test": "mocha --exit ./test/*.test.js",
102
+ "test": "mocha --exit --ignore \"test/encryption/**/*.test.js\" \"./test/**/*.test.js\"",
103
103
  "test:ci": "npm run test -- --reporter min",
104
104
  "test-deno": "deno run --allow-env --allow-read --allow-net --allow-run --allow-sys --allow-write ./test/deno.mjs",
105
105
  "test-deno:ci": "npm run test-deno -- --reporter min",
106
- "test-rs": "START_REPLICA_SET=1 mocha --timeout 30000 --exit ./test/*.test.js",
106
+ "test-rs": "START_REPLICA_SET=1 mocha --timeout 30000 --exit --ignore \"test/encryption/**/*.test.js\" \"./test/**/*.test.js\"",
107
107
  "test-rs:ci": "npm run test-rs -- --reporter min",
108
108
  "test:types": "tstyche",
109
109
  "setup-test-encryption": "node scripts/setup-encryption-tests.js",
110
110
  "test-encryption": "mocha --exit ./test/encryption/*.test.js",
111
111
  "test-encryption:ci": "npm run test-encryption -- --reporter min",
112
- "tdd": "mocha --watch --inspect --recursive ./test/*.test.js --watch-files lib/**/*.js test/**/*.js",
112
+ "tdd": "mocha --watch --inspect --ignore \"test/encryption/**/*.test.js\" \"./test/**/*.test.js\" --watch-files lib/**/*.js test/**/*.js",
113
113
  "test-coverage": "c8 --reporter=html --reporter=text npm test",
114
114
  "test-coverage:ci": "c8 --reporter=html --reporter=text npm run test:ci",
115
115
  "ts-benchmark": "cd ./benchmarks/typescript/simple && npm install && npm run benchmark | node ../../../scripts/tsc-diagnostics-check",
package/types/index.d.ts CHANGED
@@ -150,6 +150,8 @@ declare module 'mongoose' {
150
150
  ? IfAny<U, T & { _id: Types.ObjectId }, T & Required<{ _id: U }>>
151
151
  : T & { _id: Types.ObjectId };
152
152
 
153
+ export type Default_id<T, TSchemaOptions = {}> = TSchemaOptions extends { _id: false } ? T : Require_id<T>;
154
+
153
155
  export type Default__v<T, TSchemaOptions = {}> = TSchemaOptions extends { versionKey: false }
154
156
  ? T
155
157
  : TSchemaOptions extends { versionKey: infer VK }
package/types/models.d.ts CHANGED
@@ -116,6 +116,8 @@ declare module 'mongoose' {
116
116
  SessionOption {
117
117
  checkKeys?: boolean;
118
118
  j?: boolean;
119
+ /** An array of paths that tell mongoose to only validate and save the paths in `pathsToSave`. */
120
+ pathsToSave?: string[];
119
121
  safe?: boolean | WriteConcern;
120
122
  timestamps?: boolean | QueryTimestampsConfig;
121
123
  validateBeforeSave?: boolean;
@@ -239,7 +241,12 @@ declare module 'mongoose' {
239
241
  base: Mongoose;
240
242
 
241
243
  /** Standard Schema adapter for validating input with this model's schema. */
242
- readonly '~standard': StandardSchemaV1.Props<Default__v<Require_id<TRawDocType>, ObtainSchemaGeneric<TSchema, 'TSchemaOptions'>>>;
244
+ readonly '~standard': StandardSchemaV1.Props<
245
+ Default__v<
246
+ Default_id<TRawDocType, ObtainSchemaGeneric<TSchema, 'TSchemaOptions'>>,
247
+ ObtainSchemaGeneric<TSchema, 'TSchemaOptions'>
248
+ >
249
+ >;
243
250
 
244
251
  /**
245
252
  * If this is a discriminator model, `baseModelName` is the name of
@@ -516,8 +523,16 @@ declare module 'mongoose' {
516
523
  /**
517
524
  * Shortcut for creating a new Document from existing raw data, pre-saved in the DB.
518
525
  * The document returned has no paths marked as modified initially.
526
+ * With `strict: false`, fields not in the schema are kept on the document; pass
527
+ * `ExtraFields` to describe their types, e.g.
528
+ * `Model.hydrate<{ totalOrders: number }>(obj, null, { strict: false })`.
519
529
  */
520
- hydrate(obj: any, projection?: ProjectionType<TRawDocType>, options?: HydrateOptions): THydratedDocumentType;
530
+ hydrate<ExtraFields = unknown>(
531
+ obj: any,
532
+ projection: ProjectionType<TRawDocType> | null | undefined,
533
+ options: HydrateOptions & { strict: false }
534
+ ): THydratedDocumentType & ExtraFields;
535
+ hydrate(obj: any, projection?: ProjectionType<TRawDocType> | null | undefined, options?: HydrateOptions): THydratedDocumentType;
521
536
 
522
537
  /**
523
538
  * This function is responsible for building [indexes](https://www.mongodb.com/docs/manual/indexes/),
@@ -1215,7 +1230,11 @@ declare module 'mongoose' {
1215
1230
  recompileSchema(): void;
1216
1231
 
1217
1232
  /** Schema the model uses. */
1218
- schema: TSchema;
1233
+ schema: IfAny<
1234
+ TSchema,
1235
+ Schema<TRawDocType, Model<TRawDocType, TQueryHelpers, TInstanceMethods, TVirtuals>, TInstanceMethods, TQueryHelpers, TVirtuals>,
1236
+ TSchema
1237
+ >;
1219
1238
 
1220
1239
  /** Creates a `updateMany` query: updates all documents that match `filter` with `update`. */
1221
1240
  updateMany(
@@ -192,6 +192,14 @@ declare module 'mongoose' {
192
192
  */
193
193
  strictQuery?: boolean | 'throw';
194
194
 
195
+ /**
196
+ * Sets the default [strictRead](https://mongoosejs.com/docs/guide.html#strictRead) mode for schemas.
197
+ * May be `false`, `true`, or `'throw'`.
198
+ *
199
+ * @default false
200
+ */
201
+ strictRead?: boolean | 'throw';
202
+
195
203
  /**
196
204
  * Overwrites default objects to `toJSON()`, for determining how Mongoose
197
205
  * documents get serialized by `JSON.stringify()`
@@ -61,6 +61,14 @@ declare module 'mongoose' {
61
61
  match?: any;
62
62
  /** optional model to use for population */
63
63
  model?: string | Model<any>;
64
+ /** by default, Mongoose removes null and undefined values from populated arrays. Use this option to make `populate()` retain `null` and `undefined` array entries. */
65
+ retainNullValues?: boolean;
66
+ /** if true, Mongoose will call any getters defined on the `localField`. By default, Mongoose gets the raw value of `localField`. */
67
+ getters?: boolean;
68
+ /** if true, Mongoose will clone populated docs before assigning them, so docs that are populated onto multiple parents don't share 1 copy. */
69
+ clone?: boolean;
70
+ /** 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. */
71
+ skipInvalidIds?: boolean;
64
72
  /** optional query options like sort, limit, etc */
65
73
  options?: QueryOptions;
66
74
  /** correct limit on populated array */
@@ -158,6 +158,11 @@ declare module 'mongoose' {
158
158
  * [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery) mode for schemas.
159
159
  */
160
160
  strictQuery?: boolean | 'throw';
161
+ /**
162
+ * May be `false`, `true`, or `'throw'`. Sets the default
163
+ * [strictRead](https://mongoosejs.com/docs/guide.html#strictRead) mode for schemas.
164
+ */
165
+ strictRead?: boolean | 'throw';
161
166
  /** Exactly the same as the toObject option but only applies when the document's toJSON method is called. */
162
167
  toJSON?: ToObjectOptions<DocType, THydratedDocumentType>;
163
168
  /**