mongoose 9.4.1 → 9.6.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.
package/lib/document.js CHANGED
@@ -5112,20 +5112,26 @@ Document.prototype.$__delta = function $__delta(pathsToSave, pathsToSaveSet) {
5112
5112
  const optimisticConcurrency = this.$__schema.options.optimisticConcurrency;
5113
5113
  if (optimisticConcurrency) {
5114
5114
  if (Array.isArray(optimisticConcurrency)) {
5115
- const optCon = new Set(optimisticConcurrency);
5116
- const modPaths = this.modifiedPaths();
5115
+ if (!this.$__schema.options._optimisticConcurrencySet) {
5116
+ this.$__schema.options._optimisticConcurrencySet = new Set(optimisticConcurrency);
5117
+ }
5118
+ const optimisticConcurrencySet = this.$__schema.options._optimisticConcurrencySet;
5119
+ const modPaths = this.directModifiedPaths();
5117
5120
  const hasRelevantModPaths = pathsToSave == null ?
5118
- modPaths.find(path => optCon.has(path)) :
5119
- modPaths.find(path => optCon.has(path) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5121
+ modPaths.find(path => _pathOverlapsSet(path, optimisticConcurrencySet)) :
5122
+ modPaths.find(path => _pathOverlapsSet(path, optimisticConcurrencySet) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5120
5123
  if (hasRelevantModPaths) {
5121
5124
  this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
5122
5125
  }
5123
5126
  } else if (Array.isArray(optimisticConcurrency?.exclude)) {
5124
- const excluded = new Set(optimisticConcurrency.exclude);
5125
- const modPaths = this.modifiedPaths();
5127
+ if (!this.$__schema.options._optimisticConcurrencyExcludeSet) {
5128
+ this.$__schema.options._optimisticConcurrencyExcludeSet = new Set(optimisticConcurrency.exclude);
5129
+ }
5130
+ const optimisticConcurrencyExcludeSet = this.$__schema.options._optimisticConcurrencyExcludeSet;
5131
+ const modPaths = this.directModifiedPaths();
5126
5132
  const hasRelevantModPaths = pathsToSave == null ?
5127
- modPaths.find(path => !excluded.has(path)) :
5128
- modPaths.find(path => !excluded.has(path) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5133
+ modPaths.find(path => !_pathOverlapsSet(path, optimisticConcurrencyExcludeSet)) :
5134
+ modPaths.find(path => !_pathOverlapsSet(path, optimisticConcurrencyExcludeSet) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5129
5135
  if (hasRelevantModPaths) {
5130
5136
  this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
5131
5137
  }
@@ -5652,6 +5658,32 @@ Document.prototype._applyVersionIncrement = function _applyVersionIncrement() {
5652
5658
  * Module exports.
5653
5659
  */
5654
5660
 
5661
+ /*!
5662
+ * Check if `path`, any of its ancestor paths, or any of its descendant paths
5663
+ * exist in `pathSet`.
5664
+ * For example:
5665
+ * _pathOverlapsSet('profile.firstName', Set(['profile'])) === true
5666
+ * _pathOverlapsSet('profile', Set(['profile.firstName'])) === true
5667
+ */
5668
+ function _pathOverlapsSet(path, pathSet) {
5669
+ if (pathSet.has(path)) {
5670
+ return true;
5671
+ }
5672
+ let idx = path.indexOf('.');
5673
+ while (idx !== -1) {
5674
+ if (pathSet.has(path.substring(0, idx))) {
5675
+ return true;
5676
+ }
5677
+ idx = path.indexOf('.', idx + 1);
5678
+ }
5679
+ for (const p of pathSet) {
5680
+ if (p.length > path.length + 1 && p[path.length] === '.' && p.slice(0, path.length) === path) {
5681
+ return true;
5682
+ }
5683
+ }
5684
+ return false;
5685
+ }
5686
+
5655
5687
  Document.VERSION_WHERE = VERSION_WHERE;
5656
5688
  Document.VERSION_INC = VERSION_INC;
5657
5689
  Document.VERSION_ALL = VERSION_ALL;
@@ -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
  }
@@ -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}).';
@@ -98,6 +98,10 @@ module.exports = function castUpdate(schema, obj, options, context, filter) {
98
98
  moveImmutableProperties(schema, obj, context);
99
99
  }
100
100
 
101
+ if (obj?.$__ && typeof obj.toObject === 'function') {
102
+ obj = obj.toObject(internalToObjectOptions);
103
+ }
104
+
101
105
  const ops = Object.keys(obj);
102
106
  let i = ops.length;
103
107
  const ret = {};
package/lib/model.js CHANGED
@@ -2418,13 +2418,6 @@ Model.findOneAndUpdate = function(conditions, update, options) {
2418
2418
  fields = options.fields || options.projection;
2419
2419
  }
2420
2420
 
2421
- update = clone(update, {
2422
- depopulate: true,
2423
- _isNested: true
2424
- });
2425
-
2426
- decorateUpdateWithVersionKey(update, options, this.schema.options.versionKey);
2427
-
2428
2421
  const mq = new this.Query({}, {}, this, this.$__collection);
2429
2422
  mq.select(fields);
2430
2423
 
@@ -2864,6 +2857,10 @@ Model.create = async function create(doc, options) {
2864
2857
  Model.insertOne = async function insertOne(doc, options) {
2865
2858
  _checkContext(this, 'insertOne');
2866
2859
 
2860
+ if (doc == null || typeof doc !== 'object') {
2861
+ throw new ObjectParameterError(doc, 'doc', 'insertOne');
2862
+ }
2863
+
2867
2864
  const discriminatorKey = this.schema.options.discriminatorKey;
2868
2865
  const Model = this.discriminators && doc[discriminatorKey] != null ?
2869
2866
  this.discriminators[doc[discriminatorKey]] || getDiscriminatorByValue(this.discriminators, doc[discriminatorKey]) :
package/lib/mongoose.js CHANGED
@@ -228,7 +228,7 @@ Mongoose.prototype.setDriver = function setDriver(driver) {
228
228
  * - `bufferCommands`: enable/disable mongoose's buffering mechanism for all connections and models
229
229
  * - `bufferTimeoutMS`: If bufferCommands is on, this option sets the maximum amount of time Mongoose buffering will wait before throwing an error. If not specified, Mongoose will use 10000 (10 seconds).
230
230
  * - `cloneSchemas`: `false` by default. Set to `true` to `clone()` all schemas before compiling into a model.
231
- * - `debug`: If `true`, prints the operations mongoose sends to MongoDB to the console. If a writable stream is passed, it will log to that stream, without colorization. If a callback function is passed, it will receive the collection name, the method name, then all arguments passed to the method. For example, if you wanted to replicate the default logging, you could output from the callback `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`.
231
+ * - `debug`: If `true`, prints the operations mongoose sends to MongoDB to the console. If an object is passed, you can set `color`, `shell`, and `timestamp` options. If `timestamp` is `true`, Mongoose prefixes console debug output with an ISO timestamp in brackets. If a writable stream is passed, it will log to that stream, without colorization. If a callback function is passed, it will receive the collection name, the method name, then all arguments passed to the method. For example, if you wanted to replicate the default logging, you could output from the callback `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`.
232
232
  * - `id`: If `true`, adds a `id` virtual to all schemas unless overwritten on a per-schema basis.
233
233
  * - `maxTimeMS`: If set, attaches [maxTimeMS](https://www.mongodb.com/docs/manual/reference/operator/meta/maxTimeMS/) to every query
234
234
  * - `objectIdGetter`: `true` by default. Mongoose adds a getter to MongoDB ObjectId's called `_id` that returns `this` for convenience with populate. Set this to false to remove the getter.
@@ -94,6 +94,20 @@ Object.defineProperty(SchemaTypeOptions.prototype, 'cast', opts);
94
94
 
95
95
  Object.defineProperty(SchemaTypeOptions.prototype, 'required', opts);
96
96
 
97
+ /**
98
+ * Controls whether this path may be set to `null`. By default, Mongoose allows
99
+ * `null` for non-required paths. Set `allowNull: false` to allow `undefined`
100
+ * but disallow `null`.
101
+ *
102
+ * @api public
103
+ * @property allowNull
104
+ * @memberOf SchemaTypeOptions
105
+ * @type {boolean}
106
+ * @instance
107
+ */
108
+
109
+ Object.defineProperty(SchemaTypeOptions.prototype, 'allowNull', opts);
110
+
97
111
  /**
98
112
  * The default value for this path. If a function, Mongoose executes the function
99
113
  * and uses the return value as the default.
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;
@@ -85,6 +86,8 @@ const opToThunk = new Map([
85
86
  ['findOneAndDelete', '_findOneAndDelete']
86
87
  ]);
87
88
 
89
+ const queryUpdateSymbol = Symbol('mongoose#Query#update');
90
+
88
91
  /**
89
92
  * Query constructor used for building queries. You do not need
90
93
  * to instantiate a `Query` directly. Instead use Model functions like
@@ -196,6 +199,18 @@ function checkRequireFilter(filter, options) {
196
199
  Query.prototype = new mquery();
197
200
  Query.prototype.constructor = Query;
198
201
 
202
+ Object.defineProperty(Query.prototype, '_update', {
203
+ configurable: true,
204
+ enumerable: true,
205
+ get: function() {
206
+ _cloneUpdateIfShared(this);
207
+ return this[queryUpdateSymbol];
208
+ },
209
+ set: function(v) {
210
+ this[queryUpdateSymbol] = v;
211
+ }
212
+ });
213
+
199
214
  // Remove some legacy methods that we removed in Mongoose 8, but
200
215
  // are still in mquery 5.
201
216
  Query.prototype.count = undefined;
@@ -300,7 +315,7 @@ Query.prototype.toConstructor = function toConstructor() {
300
315
  p.op = this.op;
301
316
  p._conditions = clone(this._conditions);
302
317
  p._fields = clone(this._fields);
303
- p._update = clone(this._update, {
318
+ p[queryUpdateSymbol] = clone(this[queryUpdateSymbol], {
304
319
  flattenDecimals: false
305
320
  });
306
321
  p._path = this._path;
@@ -348,7 +363,7 @@ Query.prototype.clone = function() {
348
363
  q.op = this.op;
349
364
  q._conditions = clone(this._conditions);
350
365
  q._fields = clone(this._fields);
351
- q._update = clone(this._update, {
366
+ q[queryUpdateSymbol] = clone(this[queryUpdateSymbol], {
352
367
  flattenDecimals: false
353
368
  });
354
369
  q._path = this._path;
@@ -1365,7 +1380,7 @@ Query.prototype.toString = function toString() {
1365
1380
  this.op === 'update' ||
1366
1381
  this.op === 'updateMany' ||
1367
1382
  this.op === 'updateOne') {
1368
- return `${this.model.modelName}.${this.op}(${util.inspect(this._conditions)}, ${util.inspect(this._update)})`;
1383
+ return `${this.model.modelName}.${this.op}(${util.inspect(this._conditions)}, ${util.inspect(this[queryUpdateSymbol])})`;
1369
1384
  }
1370
1385
 
1371
1386
  // 'estimatedDocumentCount' or any others
@@ -1669,6 +1684,7 @@ Query.prototype.getOptions = function() {
1669
1684
  * - [upsert](https://www.mongodb.com/docs/manual/reference/method/db.collection.update/)
1670
1685
  * - [writeConcern](https://www.mongodb.com/docs/manual/reference/method/db.collection.update/)
1671
1686
  * - [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.
1687
+ * - cloneUpdate: set to `false` to skip cloning the update before executing the query.
1672
1688
  * - overwriteDiscriminatorKey: allow setting the discriminator key in the update. Will use the correct discriminator schema if the update changes the discriminator key.
1673
1689
  * - overwriteImmutable: allow overwriting properties that are set to `immutable` in the schema. Defaults to false.
1674
1690
  *
@@ -1679,6 +1695,7 @@ Query.prototype.getOptions = function() {
1679
1695
  * - [projection](https://mongoosejs.com/docs/api/query.html#Query.prototype.projection())
1680
1696
  * - sanitizeProjection
1681
1697
  * - useBigInt64
1698
+ * - defaults: if `false`, skip applying default values to the returned document(s). Defaults to true.
1682
1699
  *
1683
1700
  * The following options are only for all operations **except** `updateOne()`, `updateMany()`, `deleteOne()`, and `deleteMany()`:
1684
1701
  *
@@ -1751,6 +1768,10 @@ Query.prototype.setOptions = function(options, overwrite) {
1751
1768
  this._mongooseOptions.updatePipeline = options.updatePipeline;
1752
1769
  delete options.updatePipeline;
1753
1770
  }
1771
+ if ('cloneUpdate' in options) {
1772
+ this._mongooseOptions.cloneUpdate = options.cloneUpdate;
1773
+ delete options.cloneUpdate;
1774
+ }
1754
1775
  if ('sanitizeProjection' in options) {
1755
1776
  if (options.sanitizeProjection && !this._mongooseOptions.sanitizeProjection) {
1756
1777
  sanitizeProjection(this._fields);
@@ -1984,12 +2005,17 @@ Query.prototype.getUpdate = function() {
1984
2005
  * query.getUpdate(); // { $set: { b: 6 } }
1985
2006
  *
1986
2007
  * @param {object} new update operation
2008
+ * @param {boolean} [cloneUpdate=true] if `false`, Mongoose will not clone the update
1987
2009
  * @return {undefined}
1988
2010
  * @api public
1989
2011
  */
1990
2012
 
1991
- Query.prototype.setUpdate = function(val) {
1992
- this._update = clone(val);
2013
+ Query.prototype.setUpdate = function(val, cloneUpdate) {
2014
+ this[queryUpdateSymbol] = cloneUpdate === false ? val : clone(val);
2015
+ if (cloneUpdate != null) {
2016
+ this._mongooseOptions.cloneUpdate = cloneUpdate;
2017
+ }
2018
+ this._updateIsShared = false;
1993
2019
  };
1994
2020
 
1995
2021
  /**
@@ -2022,7 +2048,7 @@ Query.prototype._fieldsForExec = function() {
2022
2048
  */
2023
2049
 
2024
2050
  Query.prototype._updateForExec = function() {
2025
- const update = clone(this._update, {
2051
+ const update = clone(this[queryUpdateSymbol], {
2026
2052
  transform: false,
2027
2053
  depopulate: true
2028
2054
  });
@@ -2216,6 +2242,8 @@ Query.prototype.lean = function(v) {
2216
2242
  */
2217
2243
 
2218
2244
  Query.prototype.set = function(path, val) {
2245
+ _cloneUpdateIfShared(this);
2246
+
2219
2247
  if (typeof path === 'object') {
2220
2248
  const keys = Object.keys(path);
2221
2249
  for (const key of keys) {
@@ -2224,12 +2252,16 @@ Query.prototype.set = function(path, val) {
2224
2252
  return this;
2225
2253
  }
2226
2254
 
2227
- this._update = this._update || {};
2228
- if (path in this._update) {
2229
- delete this._update[path];
2255
+ let update = this[queryUpdateSymbol];
2256
+ if (update == null) {
2257
+ update = {};
2258
+ this[queryUpdateSymbol] = update;
2259
+ }
2260
+ if (path in update) {
2261
+ delete update[path];
2230
2262
  }
2231
- this._update.$set = this._update.$set || {};
2232
- this._update.$set[path] = val;
2263
+ update.$set = update.$set || {};
2264
+ update.$set[path] = val;
2233
2265
  return this;
2234
2266
  };
2235
2267
 
@@ -2249,7 +2281,7 @@ Query.prototype.set = function(path, val) {
2249
2281
  */
2250
2282
 
2251
2283
  Query.prototype.get = function get(path) {
2252
- const update = this._update;
2284
+ const update = this[queryUpdateSymbol];
2253
2285
  if (update == null) {
2254
2286
  return void 0;
2255
2287
  }
@@ -2333,6 +2365,7 @@ Query.prototype._unsetCastError = function _unsetCastError() {
2333
2365
  * - `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
2366
  * - `nearSphere`: use `$nearSphere` instead of `near()`. See the [`Query.prototype.nearSphere()` docs](https://mongoosejs.com/docs/api/query.html#Query.prototype.nearSphere())
2335
2367
  * - `schemaLevelProjections`: if `false`, Mongoose will not apply schema-level `select: false` or `select: true` for this query
2368
+ * - `cloneUpdate`: if `false`, Mongoose will not clone updates before executing the query
2336
2369
  *
2337
2370
  * Mongoose maintains a separate object for internal options because
2338
2371
  * Mongoose sends `Query.prototype.options` to the MongoDB server, and the
@@ -2421,6 +2454,12 @@ Query.prototype._find = async function _find() {
2421
2454
  lean: mongooseOptions.lean || null
2422
2455
  };
2423
2456
 
2457
+ // Only pass `defaults` through when it is non-nullish; `null` and
2458
+ // `undefined` are treated as "not set" and omitted from `createModel()`.
2459
+ if (mongooseOptions.defaults != null) {
2460
+ completeManyOptions.defaults = mongooseOptions.defaults;
2461
+ }
2462
+
2424
2463
  const options = this._optionsForExec();
2425
2464
 
2426
2465
  const filter = this._conditions;
@@ -2553,9 +2592,10 @@ Query.prototype.merge = function(source) {
2553
2592
  utils.merge(this.options, source.options, opts);
2554
2593
  }
2555
2594
 
2556
- if (source._update) {
2557
- this._update || (this._update = {});
2558
- utils.mergeClone(this._update, source._update);
2595
+ if (source[queryUpdateSymbol] != null) {
2596
+ _cloneUpdateIfShared(this);
2597
+ this[queryUpdateSymbol] || (this[queryUpdateSymbol] = {});
2598
+ utils.mergeClone(this[queryUpdateSymbol], source[queryUpdateSymbol]);
2559
2599
  }
2560
2600
 
2561
2601
  if (source._distinct) {
@@ -2691,7 +2731,7 @@ Query.prototype._completeMany = async function _completeMany(docs, fields, userP
2691
2731
  const model = this.model;
2692
2732
  return Promise.all(docs.map(doc => new Promise((resolve, reject) => {
2693
2733
  const rawDoc = doc;
2694
- doc = helpers.createModel(model, doc, fields, userProvidedFields);
2734
+ doc = helpers.createModel(model, doc, fields, userProvidedFields, opts);
2695
2735
  if (opts.session != null) {
2696
2736
  doc.$session(opts.session);
2697
2737
  }
@@ -2849,9 +2889,10 @@ Query.prototype._applyTranslateAliases = function _applyTranslateAliases() {
2849
2889
  }
2850
2890
 
2851
2891
  if (this.model?.schema?.aliases && utils.hasOwnKeys(this.model.schema.aliases)) {
2892
+ _cloneUpdateIfShared(this);
2852
2893
  this.model.translateAliases(this._conditions, true);
2853
2894
  this.model.translateAliases(this._fields, true);
2854
- this.model.translateAliases(this._update, true);
2895
+ this.model.translateAliases(this[queryUpdateSymbol], true);
2855
2896
  if (this._distinct != null && this.model.schema.aliases[this._distinct] != null) {
2856
2897
  this._distinct = this.model.schema.aliases[this._distinct];
2857
2898
  }
@@ -3405,6 +3446,7 @@ function prepareDiscriminatorCriteria(query) {
3405
3446
  * @param {object|Query} [filter]
3406
3447
  * @param {object} [update]
3407
3448
  * @param {object} [options]
3449
+ * @param {boolean} [options.cloneUpdate=true] if `false`, Mongoose will not clone the update before executing the query
3408
3450
  * @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
3451
  * @param {boolean|'throw'} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
3410
3452
  * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
@@ -3481,11 +3523,20 @@ Query.prototype.findOneAndUpdate = function(filter, update, options) {
3481
3523
  options.updatePipeline = updatePipeline;
3482
3524
  }
3483
3525
 
3526
+ if (!options.updatePipeline && Array.isArray(update)) {
3527
+ throw new MongooseError('Cannot pass an array to query updates unless the `updatePipeline` option is set.');
3528
+ }
3529
+
3484
3530
  this.setOptions(options);
3485
3531
 
3486
3532
  // apply doc
3487
3533
  if (update) {
3488
- this._mergeUpdate(update);
3534
+ if (this[queryUpdateSymbol] == null || utils.isEmptyObject(this[queryUpdateSymbol])) {
3535
+ this[queryUpdateSymbol] = update;
3536
+ this._updateIsShared = true;
3537
+ } else {
3538
+ this._mergeUpdate(update);
3539
+ }
3489
3540
  }
3490
3541
 
3491
3542
  return this;
@@ -3523,49 +3574,51 @@ Query.prototype._findOneAndUpdate = async function _findOneAndUpdate() {
3523
3574
  const options = this._optionsForExec(this.model);
3524
3575
  convertNewToReturnDocument(options);
3525
3576
 
3526
- this._update = this._castUpdate(this._update);
3577
+ this[queryUpdateSymbol] = this._castUpdate(this[queryUpdateSymbol]);
3578
+ this._updateIsShared = false;
3579
+ decorateUpdateWithVersionKey(this[queryUpdateSymbol], options, this.schema.options.versionKey);
3527
3580
 
3528
- this._update = setDefaultsOnInsert(
3581
+ this[queryUpdateSymbol] = setDefaultsOnInsert(
3529
3582
  this._conditions,
3530
3583
  this.model.schema,
3531
- this._update,
3584
+ this[queryUpdateSymbol],
3532
3585
  options,
3533
3586
  this._mongooseOptions,
3534
3587
  this
3535
3588
  );
3536
3589
 
3537
- if (!this._update || utils.hasOwnKeys(this._update) === false) {
3590
+ if (!this[queryUpdateSymbol] || utils.hasOwnKeys(this[queryUpdateSymbol]) === false) {
3538
3591
  if (options.upsert) {
3539
3592
  // still need to do the upsert to empty doc
3540
- const $set = clone(this._update);
3593
+ const $set = clone(this[queryUpdateSymbol]);
3541
3594
  delete $set._id;
3542
- this._update = { $set };
3595
+ this[queryUpdateSymbol] = { $set };
3543
3596
  } else {
3544
3597
  this._execCount = 0;
3545
3598
  const res = await this._findOne();
3546
3599
  return res;
3547
3600
  }
3548
- } else if (this._update instanceof Error) {
3549
- throw this._update;
3601
+ } else if (this[queryUpdateSymbol] instanceof Error) {
3602
+ throw this[queryUpdateSymbol];
3550
3603
  } else {
3551
3604
  // In order to make MongoDB 2.6 happy (see
3552
3605
  // https://jira.mongodb.org/browse/SERVER-12266 and related issues)
3553
3606
  // 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;
3607
+ if (this[queryUpdateSymbol].$set && utils.hasOwnKeys(this[queryUpdateSymbol].$set) === false) {
3608
+ delete this[queryUpdateSymbol].$set;
3556
3609
  }
3557
3610
  }
3558
3611
 
3559
3612
  const runValidators = _getOption(this, 'runValidators', false);
3560
3613
  if (runValidators) {
3561
- await this.validate(this._update, options, false);
3614
+ await this.validate(this[queryUpdateSymbol], options, false);
3562
3615
  }
3563
3616
 
3564
- if (typeof this._update.toBSON === 'function') {
3565
- this._update = this._update.toBSON();
3617
+ if (typeof this[queryUpdateSymbol].toBSON === 'function') {
3618
+ this[queryUpdateSymbol] = this[queryUpdateSymbol].toBSON();
3566
3619
  }
3567
3620
 
3568
- let res = await this.mongooseCollection.findOneAndUpdate(this._conditions, this._update, options);
3621
+ let res = await this.mongooseCollection.findOneAndUpdate(this._conditions, this[queryUpdateSymbol], options);
3569
3622
  for (const fn of this._transforms) {
3570
3623
  res = fn(res);
3571
3624
  }
@@ -3701,6 +3754,7 @@ Query.prototype._findOneAndDelete = async function _findOneAndDelete() {
3701
3754
  * @param {object} [filter]
3702
3755
  * @param {object} [replacement]
3703
3756
  * @param {object} [options]
3757
+ * @param {boolean} [options.cloneUpdate=true] if `false`, Mongoose will not clone the update before executing the query
3704
3758
  * @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
3759
  * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
3706
3760
  * @param {boolean|'throw'} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
@@ -3796,13 +3850,13 @@ Query.prototype._findOneAndReplace = async function _findOneAndReplace() {
3796
3850
  const runValidators = _getOption(this, 'runValidators', false);
3797
3851
 
3798
3852
  try {
3799
- const update = new this.model(this._update, null, modelOpts);
3853
+ const update = new this.model(this[queryUpdateSymbol], null, modelOpts);
3800
3854
  if (runValidators) {
3801
3855
  await update.validate();
3802
3856
  } else if (update.$__.validationError) {
3803
3857
  throw update.$__.validationError;
3804
3858
  }
3805
- this._update = update.toBSON();
3859
+ this[queryUpdateSymbol] = update.toBSON();
3806
3860
  } catch (err) {
3807
3861
  if (err instanceof ValidationError) {
3808
3862
  throw err;
@@ -3812,7 +3866,7 @@ Query.prototype._findOneAndReplace = async function _findOneAndReplace() {
3812
3866
  throw validationError;
3813
3867
  }
3814
3868
 
3815
- let res = await this.mongooseCollection.findOneAndReplace(filter, this._update, options);
3869
+ let res = await this.mongooseCollection.findOneAndReplace(filter, this[queryUpdateSymbol], options);
3816
3870
 
3817
3871
  for (const fn of this._transforms) {
3818
3872
  res = fn(res);
@@ -3874,6 +3928,7 @@ Query.prototype.findById = function(id, projection, options) {
3874
3928
  * @param {any} id value of `_id` to query by
3875
3929
  * @param {object} [doc]
3876
3930
  * @param {object} [options]
3931
+ * @param {boolean} [options.cloneUpdate=true] if `false`, Mongoose will not clone the update before executing the query
3877
3932
  * @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
3933
  * @param {boolean|'throw'} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
3879
3934
  * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
@@ -4049,12 +4104,14 @@ function _completeManyLean(schema, docs, path, opts) {
4049
4104
  */
4050
4105
 
4051
4106
  Query.prototype._mergeUpdate = function(update) {
4107
+ _cloneUpdateIfShared(this);
4108
+
4052
4109
  const updatePipeline = this._mongooseOptions.updatePipeline;
4053
4110
  if (!updatePipeline && Array.isArray(update)) {
4054
4111
  throw new MongooseError('Cannot pass an array to query updates unless the `updatePipeline` option is set.');
4055
4112
  }
4056
- if (!this._update) {
4057
- this._update = Array.isArray(update) ? [] : {};
4113
+ if (!this[queryUpdateSymbol]) {
4114
+ this[queryUpdateSymbol] = Array.isArray(update) ? [] : {};
4058
4115
  }
4059
4116
 
4060
4117
  if (update == null || (typeof update === 'object' && utils.hasOwnKeys(update) === false)) {
@@ -4062,28 +4119,28 @@ Query.prototype._mergeUpdate = function(update) {
4062
4119
  }
4063
4120
 
4064
4121
  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)})`);
4122
+ if (Array.isArray(this[queryUpdateSymbol])) {
4123
+ throw new MongooseError(`Cannot mix array and object updates (current: ${_previewUpdate(this[queryUpdateSymbol])}, incoming: ${_previewUpdate(update[queryUpdateSymbol])})`);
4067
4124
  }
4068
- if (update._update) {
4069
- utils.mergeClone(this._update, update._update);
4125
+ if (update[queryUpdateSymbol]) {
4126
+ utils.mergeClone(this[queryUpdateSymbol], update[queryUpdateSymbol]);
4070
4127
  }
4071
4128
  } else if (Array.isArray(update)) {
4072
- if (!Array.isArray(this._update)) {
4129
+ if (!Array.isArray(this[queryUpdateSymbol])) {
4073
4130
  // `_update` may be empty object by default, like in `doc.updateOne()`
4074
4131
  // 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 = [];
4132
+ if (this[queryUpdateSymbol] == null || utils.isEmptyObject(this[queryUpdateSymbol])) {
4133
+ this[queryUpdateSymbol] = [];
4077
4134
  } else {
4078
- throw new MongooseError(`Cannot mix array and object updates (current: ${_previewUpdate(this._update)}, incoming: ${_previewUpdate(update)})`);
4135
+ throw new MongooseError(`Cannot mix array and object updates (current: ${_previewUpdate(this[queryUpdateSymbol])}, incoming: ${_previewUpdate(update)})`);
4079
4136
  }
4080
4137
  }
4081
- this._update = this._update.concat(update);
4138
+ this[queryUpdateSymbol] = this[queryUpdateSymbol].concat(update);
4082
4139
  } else {
4083
- if (Array.isArray(this._update)) {
4084
- throw new MongooseError(`Cannot mix array and object updates (current: ${_previewUpdate(this._update)}, incoming: ${_previewUpdate(update)})`);
4140
+ if (Array.isArray(this[queryUpdateSymbol])) {
4141
+ throw new MongooseError(`Cannot mix array and object updates (current: ${_previewUpdate(this[queryUpdateSymbol])}, incoming: ${_previewUpdate(update)})`);
4085
4142
  }
4086
- utils.mergeClone(this._update, update);
4143
+ utils.mergeClone(this[queryUpdateSymbol], update);
4087
4144
  }
4088
4145
  };
4089
4146
 
@@ -4157,30 +4214,31 @@ Query.prototype._updateMany = async function _updateMany() {
4157
4214
  }
4158
4215
 
4159
4216
  const options = this._optionsForExec(this.model);
4160
- this._update = this._castUpdate(this._update);
4161
- if (this._update == null || utils.hasOwnKeys(this._update) === false) {
4217
+ _cloneUpdateIfShared(this);
4218
+ this[queryUpdateSymbol] = this._castUpdate(this[queryUpdateSymbol]);
4219
+ if (this[queryUpdateSymbol] == null || utils.hasOwnKeys(this[queryUpdateSymbol]) === false) {
4162
4220
  return { acknowledged: false };
4163
4221
  }
4164
- removeUnusedArrayFilters(this._update, options);
4222
+ removeUnusedArrayFilters(this[queryUpdateSymbol], options);
4165
4223
 
4166
- this._update = setDefaultsOnInsert(
4224
+ this[queryUpdateSymbol] = setDefaultsOnInsert(
4167
4225
  this._conditions,
4168
4226
  this.model.schema,
4169
- this._update,
4227
+ this[queryUpdateSymbol],
4170
4228
  options,
4171
4229
  this._mongooseOptions,
4172
4230
  this
4173
4231
  );
4174
4232
 
4175
4233
  if (_getOption(this, 'runValidators', false)) {
4176
- await this.validate(this._update, options, false);
4234
+ await this.validate(this[queryUpdateSymbol], options, false);
4177
4235
  }
4178
4236
 
4179
- if (typeof this._update.toBSON === 'function') {
4180
- this._update = this._update.toBSON();
4237
+ if (typeof this[queryUpdateSymbol].toBSON === 'function') {
4238
+ this[queryUpdateSymbol] = this[queryUpdateSymbol].toBSON();
4181
4239
  }
4182
4240
 
4183
- return this.mongooseCollection.updateMany(this._conditions, this._update, options);
4241
+ return this.mongooseCollection.updateMany(this._conditions, this[queryUpdateSymbol], options);
4184
4242
  };
4185
4243
 
4186
4244
  /**
@@ -4202,30 +4260,31 @@ Query.prototype._updateOne = async function _updateOne() {
4202
4260
  }
4203
4261
 
4204
4262
  const options = this._optionsForExec(this.model);
4205
- this._update = this._castUpdate(this._update);
4206
- if (this._update == null || utils.hasOwnKeys(this._update) === false) {
4263
+ _cloneUpdateIfShared(this);
4264
+ this[queryUpdateSymbol] = this._castUpdate(this[queryUpdateSymbol]);
4265
+ if (this[queryUpdateSymbol] == null || utils.hasOwnKeys(this[queryUpdateSymbol]) === false) {
4207
4266
  return { acknowledged: false };
4208
4267
  }
4209
- removeUnusedArrayFilters(this._update, options);
4268
+ removeUnusedArrayFilters(this[queryUpdateSymbol], options);
4210
4269
 
4211
- this._update = setDefaultsOnInsert(
4270
+ this[queryUpdateSymbol] = setDefaultsOnInsert(
4212
4271
  this._conditions,
4213
4272
  this.model.schema,
4214
- this._update,
4273
+ this[queryUpdateSymbol],
4215
4274
  options,
4216
4275
  this._mongooseOptions,
4217
4276
  this
4218
4277
  );
4219
4278
 
4220
4279
  if (_getOption(this, 'runValidators', false)) {
4221
- await this.validate(this._update, options, false);
4280
+ await this.validate(this[queryUpdateSymbol], options, false);
4222
4281
  }
4223
4282
 
4224
- if (typeof this._update.toBSON === 'function') {
4225
- this._update = this._update.toBSON();
4283
+ if (typeof this[queryUpdateSymbol].toBSON === 'function') {
4284
+ this[queryUpdateSymbol] = this[queryUpdateSymbol].toBSON();
4226
4285
  }
4227
4286
 
4228
- return this.mongooseCollection.updateOne(this._conditions, this._update, options);
4287
+ return this.mongooseCollection.updateOne(this._conditions, this[queryUpdateSymbol], options);
4229
4288
  };
4230
4289
 
4231
4290
  /**
@@ -4247,18 +4306,18 @@ Query.prototype._replaceOne = async function _replaceOne() {
4247
4306
  }
4248
4307
 
4249
4308
  const options = this._optionsForExec(this.model);
4250
- this._update = new this.model(this._update, null, { skipId: true });
4251
- removeUnusedArrayFilters(this._update, options);
4309
+ this[queryUpdateSymbol] = new this.model(this[queryUpdateSymbol], null, { skipId: true });
4310
+ removeUnusedArrayFilters(this[queryUpdateSymbol], options);
4252
4311
 
4253
4312
  if (_getOption(this, 'runValidators', false)) {
4254
- await this.validate(this._update, options, true);
4313
+ await this.validate(this[queryUpdateSymbol], options, true);
4255
4314
  }
4256
4315
 
4257
- if (typeof this._update.toBSON === 'function') {
4258
- this._update = this._update.toBSON();
4316
+ if (typeof this[queryUpdateSymbol].toBSON === 'function') {
4317
+ this[queryUpdateSymbol] = this[queryUpdateSymbol].toBSON();
4259
4318
  }
4260
4319
 
4261
- return this.mongooseCollection.replaceOne(this._conditions, this._update, options);
4320
+ return this.mongooseCollection.replaceOne(this._conditions, this[queryUpdateSymbol], options);
4262
4321
  };
4263
4322
 
4264
4323
  /**
@@ -4285,6 +4344,7 @@ Query.prototype._replaceOne = async function _replaceOne() {
4285
4344
  * @param {object} [filter]
4286
4345
  * @param {object|Array} [update] the update command. If array, this update will be treated as an update pipeline and not casted.
4287
4346
  * @param {object} [options]
4347
+ * @param {boolean} [options.cloneUpdate=true] if `false`, Mongoose will not clone the update before executing the query
4288
4348
  * @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
4349
  * @param {boolean|'throw'} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
4290
4350
  * @param {boolean} [options.upsert=false] if true, and no documents found, insert a new document
@@ -4360,6 +4420,7 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) {
4360
4420
  * @param {object} [filter]
4361
4421
  * @param {object|Array} [update] the update command. If array, this update will be treated as an update pipeline and not casted.
4362
4422
  * @param {object} [options]
4423
+ * @param {boolean} [options.cloneUpdate=true] if `false`, Mongoose will not clone the update before executing the query
4363
4424
  * @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
4425
  * @param {boolean|'throw'} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
4365
4426
  * @param {boolean} [options.upsert=false] if true, and no documents found, insert a new document
@@ -4429,6 +4490,7 @@ Query.prototype.updateOne = function(conditions, doc, options, callback) {
4429
4490
  * @param {object} [filter]
4430
4491
  * @param {object} [doc] the update command
4431
4492
  * @param {object} [options]
4493
+ * @param {boolean} [options.cloneUpdate=true] if `false`, Mongoose will not clone the update before executing the query
4432
4494
  * @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
4495
  * @param {boolean|'throw'} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
4434
4496
  * @param {boolean} [options.upsert=false] if true, and no documents found, insert a new document
@@ -4510,11 +4572,20 @@ function _update(query, op, filter, doc, options, callback) {
4510
4572
  options.updatePipeline = updatePipeline;
4511
4573
  }
4512
4574
 
4575
+ if (!options?.updatePipeline && Array.isArray(doc)) {
4576
+ throw new MongooseError('Cannot pass an array to query updates unless the `updatePipeline` option is set.');
4577
+ }
4578
+
4513
4579
  if (utils.isObject(options)) {
4514
4580
  query.setOptions(options);
4515
4581
  }
4516
4582
 
4517
- query._mergeUpdate(doc);
4583
+ if (query[queryUpdateSymbol] == null || utils.isEmptyObject(query[queryUpdateSymbol])) {
4584
+ query[queryUpdateSymbol] = doc;
4585
+ query._updateIsShared = true;
4586
+ } else {
4587
+ query._mergeUpdate(doc);
4588
+ }
4518
4589
 
4519
4590
  // Hooks
4520
4591
  if (callback) {
@@ -4789,6 +4860,20 @@ function _executePreHooks(query, op) {
4789
4860
  );
4790
4861
  }
4791
4862
 
4863
+ function _cloneUpdateIfShared(query) {
4864
+ if (!query._updateIsShared) {
4865
+ return;
4866
+ }
4867
+ if (query.mongooseOptions().cloneUpdate === false) {
4868
+ return;
4869
+ }
4870
+
4871
+ query[queryUpdateSymbol] = clone(query[queryUpdateSymbol], {
4872
+ flattenDecimals: false
4873
+ });
4874
+ query._updateIsShared = false;
4875
+ }
4876
+
4792
4877
  /**
4793
4878
  * Executes the query returning a `Promise` which will be
4794
4879
  * resolved with either the doc(s) or rejected with the error.
@@ -84,28 +84,29 @@ exports.createModel = function createModel(model, doc, fields, userProvidedField
84
84
  discriminatorMapping.key :
85
85
  null;
86
86
 
87
+ const _opts = {
88
+ skipId: true,
89
+ isNew: false,
90
+ willInit: true
91
+ };
92
+ if (options?.defaults != null) {
93
+ _opts.defaults = options.defaults;
94
+ }
95
+ if (options?.strict != null) {
96
+ _opts.strict = options.strict;
97
+ }
98
+
87
99
  const value = doc[key];
88
100
  if (key && value && model.discriminators) {
89
101
  const discriminator = model.discriminators[value] || getDiscriminatorByValue(model.discriminators, value);
90
102
  if (discriminator) {
91
103
  const _fields = clone(userProvidedFields);
92
104
  exports.applyPaths(_fields, discriminator.schema);
93
- const _opts = { strict: options?.strict };
105
+
94
106
  return new discriminator(undefined, _fields, _opts);
95
107
  }
96
108
  }
97
109
 
98
- const _opts = {
99
- skipId: true,
100
- isNew: false,
101
- willInit: true
102
- };
103
- if (options != null && 'defaults' in options) {
104
- _opts.defaults = options.defaults;
105
- }
106
- if (options != null && 'strict' in options) {
107
- _opts.strict = options.strict;
108
- }
109
110
  return new model(undefined, fields, _opts);
110
111
  };
111
112
 
package/lib/schema.js CHANGED
@@ -3089,8 +3089,27 @@ function isArrayFilter(piece) {
3089
3089
 
3090
3090
  Schema.prototype._preCompile = function _preCompile() {
3091
3091
  this.plugin(idGetter, { deduplicate: true });
3092
+ _precomputeOptimisticConcurrency(this);
3092
3093
  };
3093
3094
 
3095
+ /*!
3096
+ * Build precomputed sets for optimisticConcurrency include/exclude,
3097
+ * expanding user-specified paths to include all schema subpaths so that
3098
+ * lookups at save time are a simple `Set.has()`.
3099
+ */
3100
+
3101
+ function _precomputeOptimisticConcurrency(schema) {
3102
+ const opt = schema.options.optimisticConcurrency;
3103
+ if (!opt || opt === true) {
3104
+ return;
3105
+ }
3106
+ if (Array.isArray(opt)) {
3107
+ schema.options._optimisticConcurrencySet = new Set(opt);
3108
+ } else if (Array.isArray(opt.exclude)) {
3109
+ schema.options._optimisticConcurrencyExcludeSet = new Set(opt.exclude);
3110
+ }
3111
+ }
3112
+
3094
3113
  /**
3095
3114
  * Returns a JSON schema representation of this Schema.
3096
3115
  *
package/lib/schemaType.js CHANGED
@@ -200,15 +200,16 @@ SchemaType.prototype._createJSONSchemaTypeDefinition = function _createJSONSchem
200
200
  (this.options.required == null ?
201
201
  (options?._defaultRequired === true || this.path === '_id') :
202
202
  this.options.required && typeof this.options.required !== 'function');
203
+ const allowNull = this.options.allowNull !== false;
203
204
 
204
205
  if (useBsonType) {
205
- if (isRequired) {
206
+ if (isRequired || !allowNull) {
206
207
  return { bsonType };
207
208
  }
208
209
  return { bsonType: [bsonType, 'null'] };
209
210
  }
210
211
 
211
- if (isRequired) {
212
+ if (isRequired || !allowNull) {
212
213
  return { type };
213
214
  }
214
215
  return { type: [type, 'null'] };
@@ -1142,6 +1143,12 @@ SchemaType.prototype.required = function(required, message) {
1142
1143
 
1143
1144
  const _this = this;
1144
1145
  this.isRequired = true;
1146
+ this.originalRequiredValue = required;
1147
+
1148
+ if (typeof this.originalRequiredValue !== 'function' &&
1149
+ (utils.hasUserDefinedProperty(this.options, 'allowNull') || this.allowNullValidator != null)) {
1150
+ throw new MongooseError('Path "' + this.path + '" may not have `allowNull` specified when `required` is true');
1151
+ }
1145
1152
 
1146
1153
  this.requiredValidator = function(v) {
1147
1154
  const cachedRequired = this?.$__?.cachedRequired;
@@ -1166,8 +1173,6 @@ SchemaType.prototype.required = function(required, message) {
1166
1173
 
1167
1174
  return _this.checkRequired(v, this);
1168
1175
  };
1169
- this.originalRequiredValue = required;
1170
-
1171
1176
  if (typeof required === 'string') {
1172
1177
  message = required;
1173
1178
  required = undefined;
@@ -1183,6 +1188,53 @@ SchemaType.prototype.required = function(required, message) {
1183
1188
  return this;
1184
1189
  };
1185
1190
 
1191
+ /**
1192
+ * Adds a validator that disallows `null` for this path without making the path
1193
+ * required. `undefined` values still pass validation.
1194
+ *
1195
+ * #### Example:
1196
+ *
1197
+ * const schema = new Schema({
1198
+ * name: { type: String, allowNull: false }
1199
+ * });
1200
+ *
1201
+ * new Model({ name: undefined }).validateSync(); // OK
1202
+ * new Model({ name: null }).validateSync(); // ValidationError
1203
+ *
1204
+ * @param {boolean} allowNull
1205
+ * @return {SchemaType} this
1206
+ * @api public
1207
+ */
1208
+
1209
+ SchemaType.prototype.allowNull = function(allowNull) {
1210
+ if (arguments.length > 0 && this.isRequired && typeof this.originalRequiredValue !== 'function') {
1211
+ throw new MongooseError('Path "' + this.path + '" may not have `allowNull` specified when `required` is true');
1212
+ }
1213
+
1214
+ this.validators = this.validators.filter(function(v) {
1215
+ return v.validator !== this.allowNullValidator;
1216
+ }, this);
1217
+
1218
+ if (allowNull !== false) {
1219
+ delete this.options.allowNull;
1220
+ delete this.allowNullValidator;
1221
+ return this;
1222
+ }
1223
+
1224
+ this.options.allowNull = false;
1225
+ this.allowNullValidator = function(v) {
1226
+ return v !== null;
1227
+ };
1228
+
1229
+ this.validators.push({
1230
+ validator: this.allowNullValidator,
1231
+ message: MongooseError.messages.general.allowNull,
1232
+ type: 'allowNull'
1233
+ });
1234
+
1235
+ return this;
1236
+ };
1237
+
1186
1238
  /**
1187
1239
  * Set the model that this path refers to. This is the option that [populate](https://mongoosejs.com/docs/populate.html)
1188
1240
  * looks at to determine the foreign collection it should query.
@@ -1802,6 +1854,7 @@ SchemaType.prototype.clone = function() {
1802
1854
  const schematype = new this.constructor(this.path, options, this.instance, this.parentSchema);
1803
1855
  schematype.validators = this.validators.slice();
1804
1856
  if (this.requiredValidator !== undefined) schematype.requiredValidator = this.requiredValidator;
1857
+ if (this.allowNullValidator !== undefined) schematype.allowNullValidator = this.allowNullValidator;
1805
1858
  if (this.defaultValue !== undefined) schematype.defaultValue = this.defaultValue;
1806
1859
  if (this.$immutable !== undefined && this.options.immutable === undefined) {
1807
1860
  schematype.$immutable = this.$immutable;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongoose",
3
3
  "description": "Mongoose MongoDB ODM",
4
- "version": "9.4.1",
4
+ "version": "9.6.0",
5
5
  "author": "Guillermo Rauch <guillermo@learnboost.com>",
6
6
  "keywords": [
7
7
  "mongodb",
@@ -20,8 +20,8 @@
20
20
  "type": "commonjs",
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
- "kareem": "3.2.0",
24
- "mongodb": "~7.1",
23
+ "kareem": "3.3.0",
24
+ "mongodb": "~7.2",
25
25
  "mpath": "0.9.0",
26
26
  "mquery": "6.0.0",
27
27
  "ms": "2.1.3",
@@ -49,21 +49,21 @@
49
49
  "linkinator": "7.x",
50
50
  "lodash.isequal": "4.5.0",
51
51
  "lodash.isequalwith": "4.4.0",
52
- "markdownlint-cli2": "^0.21.0",
53
- "marked": "15.x",
52
+ "markdownlint-cli2": "0.22.0",
53
+ "marked": "17.0.5",
54
54
  "mkdirp": "^3.0.1",
55
- "mocha": "11.7.5",
55
+ "mocha": "12.0.0-beta-9.2",
56
56
  "moment": "2.30.1",
57
57
  "mongodb-client-encryption": "~7.0",
58
58
  "mongodb-memory-server": "11.0.1",
59
59
  "mongodb-runner": "^6.0.0",
60
60
  "ncp": "^2.0.0",
61
- "pug": "3.0.3",
62
- "sinon": "21.0.1",
63
- "tstyche": "^7.0.0-rc.0",
61
+ "pug": "3.0.4",
62
+ "sinon": "21.0.3",
63
+ "tstyche": "^7.0.0",
64
64
  "typescript": "5.9.3",
65
65
  "typescript-eslint": "^8.31.1",
66
- "uuid": "11.1.0"
66
+ "uuid": "14.0.0"
67
67
  },
68
68
  "directories": {
69
69
  "lib": "./lib/mongoose"
package/types/index.d.ts CHANGED
@@ -819,12 +819,12 @@ declare module 'mongoose' {
819
819
 
820
820
  export type ReturnsNewDoc = { new: true } | { returnOriginal: false } | { returnDocument: 'after' };
821
821
 
822
- type ArrayOperators = { $slice: number | [number, number]; $elemMatch?: never } | { $elemMatch: Record<string, any>; $slice?: never };
822
+ export type ArrayProjectionOperators = { $slice: number | [number, number]; $elemMatch?: never } | { $elemMatch: Record<string, any>; $slice?: never };
823
823
  /**
824
824
  * This Type Assigns `Element | undefined` recursively to the `T` type.
825
825
  * if it is an array it will do this to the element of the array, if it is an object it will do this for the properties of the object.
826
826
  * `Element` is the truthy or falsy values that are going to be used as the value of the projection.(1 | true or 0 | false)
827
- * For the elements of the array we will use: `Element | `undefined` | `ArrayOperators`
827
+ * For the elements of the array we will use: `Element | `undefined` | `ArrayProjectionOperators`
828
828
  * @example
829
829
  * type CalculatedType = Projector<{ a: string, b: number, c: { d: string }, d: string[] }, true>
830
830
  * type CalculatedType = {
@@ -833,11 +833,11 @@ declare module 'mongoose' {
833
833
  c?: true | {
834
834
  d?: true | undefined;
835
835
  } | undefined;
836
- d?: true | ArrayOperators | undefined;
836
+ d?: true | ArrayProjectionOperators | undefined;
837
837
  }
838
838
  */
839
- type Projector<T, Element> = T extends Array<infer U>
840
- ? Projector<U, Element> | ArrayOperators
839
+ export type Projector<T, Element> = T extends Array<infer U>
840
+ ? Projector<U, Element> | ArrayProjectionOperators
841
841
  : T extends TreatAsPrimitives
842
842
  ? Element
843
843
  : T extends Record<string, any>
@@ -3,7 +3,8 @@ import {
3
3
  PathEnumOrString,
4
4
  OptionalPaths,
5
5
  RequiredPaths,
6
- IsPathRequired
6
+ IsPathRequired,
7
+ PathAllowsNull
7
8
  } from './inferschematype';
8
9
  import { Binary, UUID } from 'mongodb';
9
10
 
@@ -24,7 +25,9 @@ declare module 'mongoose' {
24
25
  OptionalPaths<SchemaDefinition, TSchemaOptions['typeKey']>)
25
26
  ]: IsPathRequired<SchemaDefinition[K], TSchemaOptions['typeKey']> extends true
26
27
  ? ObtainRawDocumentPathType<SchemaDefinition[K], TSchemaOptions['typeKey'], TTransformOptions>
27
- : ObtainRawDocumentPathType<SchemaDefinition[K], TSchemaOptions['typeKey'], TTransformOptions> | null;
28
+ : PathAllowsNull<SchemaDefinition[K]> extends true
29
+ ? ObtainRawDocumentPathType<SchemaDefinition[K], TSchemaOptions['typeKey'], TTransformOptions> | null
30
+ : ObtainRawDocumentPathType<SchemaDefinition[K], TSchemaOptions['typeKey'], TTransformOptions>;
28
31
  }, TSchemaOptions>;
29
32
 
30
33
  export type InferRawDocType<
@@ -47,7 +47,9 @@ declare module 'mongoose' {
47
47
  TSchemaOptions['typeKey']
48
48
  > extends true ?
49
49
  ObtainDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']>
50
- : ObtainDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']> | null;
50
+ : PathAllowsNull<DocDefinition[K]> extends true ?
51
+ ObtainDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']> | null
52
+ : ObtainDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']>;
51
53
  };
52
54
 
53
55
  /**
@@ -222,6 +224,12 @@ type OptionalPaths<T, TypeKey extends string = DefaultTypeKey> = Pick<
222
224
  OptionalPathKeys<T, TypeKey>
223
225
  >;
224
226
 
227
+ /**
228
+ * @summary Checks if a document path allows `null` values.
229
+ * @param {P} P Document path.
230
+ */
231
+ export type PathAllowsNull<P> = P extends { allowNull: false } ? false : true;
232
+
225
233
  /**
226
234
  * @summary Allows users to optionally choose their own type for a schema field for stronger typing.
227
235
  */
@@ -82,7 +82,7 @@ declare module 'mongoose' {
82
82
  */
83
83
  debug?:
84
84
  | boolean
85
- | { color?: boolean; shell?: boolean; }
85
+ | { color?: boolean; shell?: boolean; timestamp?: boolean; }
86
86
  | stream.Writable
87
87
  | ((collectionName: string, methodName: string, ...methodArgs: any[]) => void);
88
88
 
package/types/query.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  declare module 'mongoose' {
2
2
  import mongodb = require('mongodb');
3
3
 
4
- type StringQueryTypeCasting = string | RegExp;
4
+ type StringQueryTypeCasting<T> = string extends T ? string | RegExp : T | RegExp;
5
5
  type ObjectIdQueryTypeCasting = Types.ObjectId | string;
6
6
  type DateQueryTypeCasting = string | number | NativeDate;
7
7
  type UUIDQueryTypeCasting = Types.UUID | string;
8
8
  type BufferQueryCasting = Buffer | mongodb.Binary | number[] | string | { $binary: string | mongodb.Binary };
9
9
  type QueryTypeCasting<T> = T extends string
10
- ? StringQueryTypeCasting
10
+ ? StringQueryTypeCasting<T>
11
11
  : T extends Types.ObjectId
12
12
  ? ObjectIdQueryTypeCasting
13
13
  : T extends Types.UUID
@@ -51,6 +51,7 @@ declare module 'mongoose' {
51
51
  type QueryFilter<T> = IsItRecordAndNotAny<T> extends true ? _QueryFilter<WithLevel1NestedPaths<T>> : _QueryFilterLooseId<Record<string, any>>;
52
52
 
53
53
  type MongooseBaseQueryOptionKeys =
54
+ | 'cloneUpdate'
54
55
  | 'context'
55
56
  | 'middleware'
56
57
  | 'multipleCastError'
@@ -108,6 +109,10 @@ declare module 'mongoose' {
108
109
  collation?: mongodb.CollationOptions;
109
110
  comment?: any;
110
111
  context?: string;
112
+ /**
113
+ * If `false`, Mongoose will not clone the update before executing the query.
114
+ */
115
+ cloneUpdate?: boolean;
111
116
  explain?: mongodb.ExplainVerbosityLike;
112
117
  fields?: any | string;
113
118
  hint?: mongodb.Hint;
@@ -129,6 +134,11 @@ declare module 'mongoose' {
129
134
  */
130
135
  new?: boolean;
131
136
 
137
+ /**
138
+ * If `false`, skip applying default schema values to the returned document(s).
139
+ * @default true
140
+ */
141
+ defaults?: boolean;
132
142
  overwriteDiscriminatorKey?: boolean;
133
143
  /**
134
144
  * Mongoose removes updated immutable properties from `update` by default (excluding $setOnInsert).
@@ -884,7 +894,7 @@ declare module 'mongoose' {
884
894
  setQuery(val: QueryFilter<RawDocType> | null): void;
885
895
  setQuery(val: Query<any, any> | null): void;
886
896
 
887
- setUpdate(update: UpdateQuery<RawDocType> | UpdateWithAggregationPipeline): void;
897
+ setUpdate(update: UpdateQuery<RawDocType> | UpdateWithAggregationPipeline, cloneUpdate?: boolean): void;
888
898
 
889
899
  /** Specifies an `$size` query condition. When called with one argument, the most recent path passed to `where()` is used. */
890
900
  size(path: string, val: number): this;
@@ -104,6 +104,13 @@ declare module 'mongoose' {
104
104
  | [boolean, string]
105
105
  | [(this: THydratedDocumentType) => boolean, string];
106
106
 
107
+ /**
108
+ * Controls whether this path may be set to `null`. By default, Mongoose allows
109
+ * `null` for non-required paths. Set `allowNull: false` to allow `undefined`
110
+ * but disallow `null`.
111
+ */
112
+ allowNull?: boolean;
113
+
107
114
  /**
108
115
  * The default value for this path. If a function, Mongoose executes the function
109
116
  * and uses the return value as the default.
@@ -355,6 +362,9 @@ declare module 'mongoose' {
355
362
  */
356
363
  required(required: boolean, message?: string): this;
357
364
 
365
+ /** Adds or removes a validator that disallows `null` without making this path required. */
366
+ allowNull(allowNull: boolean): this;
367
+
358
368
  /** If the SchemaType is a subdocument or document array, this is the schema of that subdocument */
359
369
  schema?: Schema<any>;
360
370