mongoose 8.7.3 → 8.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/lib/cast.js CHANGED
@@ -28,6 +28,7 @@ const ALLOWED_GEOWITHIN_GEOJSON_TYPES = ['Polygon', 'MultiPolygon'];
28
28
  * @param {Object} [options] the query options
29
29
  * @param {Boolean|"throw"} [options.strict] Wheter to enable all strict options
30
30
  * @param {Boolean|"throw"} [options.strictQuery] Enable strict Queries
31
+ * @param {Boolean} [options.sanitizeFilter] avoid adding implict query selectors ($in)
31
32
  * @param {Boolean} [options.upsert]
32
33
  * @param {Query} [context] passed to setters
33
34
  * @api private
@@ -372,7 +373,7 @@ module.exports = function cast(schema, obj, options, context) {
372
373
 
373
374
  }
374
375
  }
375
- } else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1) {
376
+ } else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1 && !options.sanitizeFilter) {
376
377
  const casted = [];
377
378
  const valuesArray = val;
378
379
 
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ const handleTimestampOption = require('../schema/handleTimestampOption');
4
+ const mpath = require('mpath');
5
+
6
+ module.exports = applyTimestamps;
7
+
8
+ /**
9
+ * Apply a given schema's timestamps to the given POJO
10
+ *
11
+ * @param {Schema} schema
12
+ * @param {Object} obj
13
+ * @param {Object} [options]
14
+ * @param {Boolean} [options.isUpdate=false] if true, treat this as an update: just set updatedAt, skip setting createdAt. If false, set both createdAt and updatedAt
15
+ * @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
16
+ */
17
+
18
+ function applyTimestamps(schema, obj, options) {
19
+ if (obj == null) {
20
+ return obj;
21
+ }
22
+
23
+ applyTimestampsToChildren(schema, obj, options);
24
+ return applyTimestampsToDoc(schema, obj, options);
25
+ }
26
+
27
+ /**
28
+ * Apply timestamps to any subdocuments
29
+ *
30
+ * @param {Schema} schema subdocument schema
31
+ * @param {Object} res subdocument
32
+ * @param {Object} [options]
33
+ * @param {Boolean} [options.isUpdate=false] if true, treat this as an update: just set updatedAt, skip setting createdAt. If false, set both createdAt and updatedAt
34
+ * @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
35
+ */
36
+
37
+ function applyTimestampsToChildren(schema, res, options) {
38
+ for (const childSchema of schema.childSchemas) {
39
+ const _path = childSchema.model.path;
40
+ const _schema = childSchema.schema;
41
+ if (!_path) {
42
+ continue;
43
+ }
44
+ const _obj = mpath.get(_path, res);
45
+ if (_obj == null || (Array.isArray(_obj) && _obj.flat(Infinity).length === 0)) {
46
+ continue;
47
+ }
48
+
49
+ applyTimestamps(_schema, _obj, options);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Apply timestamps to a given document. Does not apply timestamps to subdocuments: use `applyTimestampsToChildren` instead
55
+ *
56
+ * @param {Schema} schema
57
+ * @param {Object} obj
58
+ * @param {Object} [options]
59
+ * @param {Boolean} [options.isUpdate=false] if true, treat this as an update: just set updatedAt, skip setting createdAt. If false, set both createdAt and updatedAt
60
+ * @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
61
+ */
62
+
63
+ function applyTimestampsToDoc(schema, obj, options) {
64
+ if (obj == null || typeof obj !== 'object') {
65
+ return;
66
+ }
67
+ if (Array.isArray(obj)) {
68
+ for (const el of obj) {
69
+ applyTimestampsToDoc(schema, el, options);
70
+ }
71
+ return;
72
+ }
73
+
74
+ if (schema.discriminators && Object.keys(schema.discriminators).length > 0) {
75
+ for (const discriminatorKey of Object.keys(schema.discriminators)) {
76
+ const discriminator = schema.discriminators[discriminatorKey];
77
+ const key = discriminator.discriminatorMapping.key;
78
+ const value = discriminator.discriminatorMapping.value;
79
+ if (obj[key] == value) {
80
+ schema = discriminator;
81
+ break;
82
+ }
83
+ }
84
+ }
85
+
86
+ const createdAt = handleTimestampOption(schema.options.timestamps, 'createdAt');
87
+ const updatedAt = handleTimestampOption(schema.options.timestamps, 'updatedAt');
88
+ const currentTime = options?.currentTime;
89
+
90
+ let ts = null;
91
+ if (currentTime != null) {
92
+ ts = currentTime();
93
+ } else if (schema.base?.now) {
94
+ ts = schema.base.now();
95
+ } else {
96
+ ts = new Date();
97
+ }
98
+
99
+ if (createdAt && obj[createdAt] == null && !options?.isUpdate) {
100
+ obj[createdAt] = ts;
101
+ }
102
+ if (updatedAt) {
103
+ obj[updatedAt] = ts;
104
+ }
105
+ }
@@ -7,8 +7,7 @@
7
7
 
8
8
  function isBsonType(obj, typename) {
9
9
  return (
10
- typeof obj === 'object' &&
11
- obj !== null &&
10
+ obj != null &&
12
11
  obj._bsontype === typename
13
12
  );
14
13
  }
@@ -245,7 +245,7 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
245
245
  }
246
246
 
247
247
  if (op !== '$setOnInsert' &&
248
- handleImmutable(schematype, strict, obj, key, prefix + key, context)) {
248
+ handleImmutable(schematype, strict, obj, key, prefix + key, options, context)) {
249
249
  continue;
250
250
  }
251
251
 
@@ -353,7 +353,7 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
353
353
 
354
354
  // You can use `$setOnInsert` with immutable keys
355
355
  if (op !== '$setOnInsert' &&
356
- handleImmutable(schematype, strict, obj, key, prefix + key, context)) {
356
+ handleImmutable(schematype, strict, obj, key, prefix + key, options, context)) {
357
357
  continue;
358
358
  }
359
359
 
@@ -2,7 +2,20 @@
2
2
 
3
3
  const StrictModeError = require('../../error/strict');
4
4
 
5
- module.exports = function handleImmutable(schematype, strict, obj, key, fullPath, ctx) {
5
+ /**
6
+ * Handle immutable option for a given path when casting updates based on options
7
+ *
8
+ * @param {SchemaType} schematype the resolved schematype for this path
9
+ * @param {Boolean | 'throw' | null} strict whether strict mode is set for this query
10
+ * @param {Object} obj the object containing the value being checked so we can delete
11
+ * @param {String} key the key in `obj` which we are checking for immutability
12
+ * @param {String} fullPath the full path being checked
13
+ * @param {Object} options the query options
14
+ * @param {Query} ctx the query. Passed as `this` and first param to the `immutable` option, if `immutable` is a function
15
+ * @returns true if field was removed, false otherwise
16
+ */
17
+
18
+ module.exports = function handleImmutable(schematype, strict, obj, key, fullPath, options, ctx) {
6
19
  if (schematype == null || !schematype.options || !schematype.options.immutable) {
7
20
  return false;
8
21
  }
@@ -15,6 +28,9 @@ module.exports = function handleImmutable(schematype, strict, obj, key, fullPath
15
28
  return false;
16
29
  }
17
30
 
31
+ if (options && options.overwriteImmutable) {
32
+ return false;
33
+ }
18
34
  if (strict === false) {
19
35
  return false;
20
36
  }
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const get = require('../get');
4
-
5
3
  module.exports = function applyReadConcern(schema, options) {
6
4
  if (options.readConcern !== undefined) {
7
5
  return;
@@ -15,7 +13,7 @@ module.exports = function applyReadConcern(schema, options) {
15
13
  return;
16
14
  }
17
15
 
18
- const level = get(schema, 'options.readConcern.level', null);
16
+ const level = schema.options?.readConcern?.level;
19
17
  if (level != null) {
20
18
  options.readConcern = { level };
21
19
  }
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const get = require('../get');
4
-
5
3
  module.exports = function applyWriteConcern(schema, options) {
6
4
  if (options.writeConcern != null) {
7
5
  return;
@@ -12,7 +10,7 @@ module.exports = function applyWriteConcern(schema, options) {
12
10
  if (options && options.session && options.session.transaction) {
13
11
  return;
14
12
  }
15
- const writeConcern = get(schema, 'options.writeConcern', {});
13
+ const writeConcern = schema.options.writeConcern ?? {};
16
14
  if (Object.keys(writeConcern).length != 0) {
17
15
  options.writeConcern = {};
18
16
  if (!('w' in options) && writeConcern.w != null) {
package/lib/model.js CHANGED
@@ -10,6 +10,7 @@ const Document = require('./document');
10
10
  const DocumentNotFoundError = require('./error/notFound');
11
11
  const EventEmitter = require('events').EventEmitter;
12
12
  const Kareem = require('kareem');
13
+ const { MongoBulkWriteError } = require('mongodb');
13
14
  const MongooseBulkWriteError = require('./error/bulkWriteError');
14
15
  const MongooseError = require('./error/index');
15
16
  const ObjectParameterError = require('./error/objectParameter');
@@ -30,6 +31,7 @@ const applyReadConcern = require('./helpers/schema/applyReadConcern');
30
31
  const applySchemaCollation = require('./helpers/indexes/applySchemaCollation');
31
32
  const applyStaticHooks = require('./helpers/model/applyStaticHooks');
32
33
  const applyStatics = require('./helpers/model/applyStatics');
34
+ const applyTimestampsHelper = require('./helpers/document/applyTimestamps');
33
35
  const applyWriteConcern = require('./helpers/schema/applyWriteConcern');
34
36
  const applyVirtualsHelper = require('./helpers/document/applyVirtuals');
35
37
  const assignVals = require('./helpers/populate/assignVals');
@@ -1219,6 +1221,7 @@ Model.createCollection = async function createCollection(options) {
1219
1221
  *
1220
1222
  * @param {Object} [options] options to pass to `ensureIndexes()`
1221
1223
  * @param {Boolean} [options.background=null] if specified, overrides each index's `background` property
1224
+ * @param {Boolean} [options.hideIndexes=false] set to `true` to hide indexes instead of dropping. Requires MongoDB server 4.4 or higher
1222
1225
  * @return {Promise}
1223
1226
  * @api public
1224
1227
  */
@@ -1439,8 +1442,10 @@ function getIndexesToDrop(schema, schemaIndexes, dbIndexes) {
1439
1442
  *
1440
1443
  * The returned promise resolves to a list of the dropped indexes' names as an array
1441
1444
  *
1442
- * @param {Function} [callback] optional callback
1443
- * @return {Promise|undefined} Returns `undefined` if callback is specified, returns a promise if no callback.
1445
+ * @param {Object} [options]
1446
+ * @param {Array<String>} [options.toDrop] if specified, contains a list of index names to drop
1447
+ * @param {Boolean} [options.hideIndexes=false] set to `true` to hide indexes instead of dropping. Requires MongoDB server 4.4 or higher
1448
+ * @return {Promise<String>} list of dropped or hidden index names
1444
1449
  * @api public
1445
1450
  */
1446
1451
 
@@ -1451,23 +1456,32 @@ Model.cleanIndexes = async function cleanIndexes(options) {
1451
1456
  }
1452
1457
  const model = this;
1453
1458
 
1454
- const collection = model.$__collection;
1455
-
1456
1459
  if (Array.isArray(options && options.toDrop)) {
1457
- const res = await _dropIndexes(options.toDrop, collection);
1460
+ const res = await _dropIndexes(options.toDrop, model, options);
1458
1461
  return res;
1459
1462
  }
1460
1463
 
1461
1464
  const res = await model.diffIndexes();
1462
- return await _dropIndexes(res.toDrop, collection);
1465
+ return await _dropIndexes(res.toDrop, model, options);
1463
1466
  };
1464
1467
 
1465
- async function _dropIndexes(toDrop, collection) {
1468
+ async function _dropIndexes(toDrop, model, options) {
1466
1469
  if (toDrop.length === 0) {
1467
1470
  return [];
1468
1471
  }
1469
1472
 
1470
- await Promise.all(toDrop.map(indexName => collection.dropIndex(indexName)));
1473
+ const collection = model.$__collection;
1474
+ if (options && options.hideIndexes) {
1475
+ await Promise.all(toDrop.map(indexName => {
1476
+ return model.db.db.command({
1477
+ collMod: collection.collectionName,
1478
+ index: { name: indexName, hidden: true }
1479
+ });
1480
+ }));
1481
+ } else {
1482
+ await Promise.all(toDrop.map(indexName => collection.dropIndex(indexName)));
1483
+ }
1484
+
1471
1485
  return toDrop;
1472
1486
  }
1473
1487
 
@@ -1653,7 +1667,24 @@ function _ensureIndexes(model, options, callback) {
1653
1667
  }
1654
1668
  }
1655
1669
 
1656
- model.collection.createIndex(indexFields, indexOptions).then(
1670
+ // Just in case `createIndex()` throws a sync error
1671
+ let promise = null;
1672
+ try {
1673
+ promise = model.collection.createIndex(indexFields, indexOptions);
1674
+ } catch (err) {
1675
+ if (!indexError) {
1676
+ indexError = err;
1677
+ }
1678
+ if (!model.$caught) {
1679
+ model.emit('error', err);
1680
+ }
1681
+
1682
+ indexSingleDone(err, indexFields, indexOptions);
1683
+ create();
1684
+ return;
1685
+ }
1686
+
1687
+ promise.then(
1657
1688
  name => {
1658
1689
  indexSingleDone(null, indexFields, indexOptions, name);
1659
1690
  create();
@@ -3417,6 +3448,11 @@ Model.bulkSave = async function bulkSave(documents, options) {
3417
3448
  (err) => ({ bulkWriteResult: null, bulkWriteError: err })
3418
3449
  );
3419
3450
 
3451
+ // If not a MongoBulkWriteError, treat this as all documents failed to save.
3452
+ if (bulkWriteError != null && !(bulkWriteError instanceof MongoBulkWriteError)) {
3453
+ throw bulkWriteError;
3454
+ }
3455
+
3420
3456
  const matchedCount = bulkWriteResult?.matchedCount ?? 0;
3421
3457
  const insertedCount = bulkWriteResult?.insertedCount ?? 0;
3422
3458
  if (writeOperations.length > 0 && matchedCount + insertedCount < writeOperations.length && !bulkWriteError) {
@@ -3540,6 +3576,41 @@ Model.applyVirtuals = function applyVirtuals(obj, virtualsToApply) {
3540
3576
  return obj;
3541
3577
  };
3542
3578
 
3579
+ /**
3580
+ * Apply this model's timestamps to a given POJO, including subdocument timestamps
3581
+ *
3582
+ * #### Example:
3583
+ *
3584
+ * const userSchema = new Schema({ name: String }, { timestamps: true });
3585
+ * const User = mongoose.model('User', userSchema);
3586
+ *
3587
+ * const obj = { name: 'John' };
3588
+ * User.applyTimestamps(obj);
3589
+ * obj.createdAt; // 2024-06-01T18:00:00.000Z
3590
+ * obj.updatedAt; // 2024-06-01T18:00:00.000Z
3591
+ *
3592
+ * @param {Object} obj object or document to apply virtuals on
3593
+ * @param {Object} [options]
3594
+ * @param {Boolean} [options.isUpdate=false] if true, treat this as an update: just set updatedAt, skip setting createdAt. If false, set both createdAt and updatedAt
3595
+ * @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
3596
+ * @returns {Object} obj
3597
+ * @api public
3598
+ */
3599
+
3600
+ Model.applyTimestamps = function applyTimestamps(obj, options) {
3601
+ if (obj == null) {
3602
+ return obj;
3603
+ }
3604
+ // Nothing to do if this is already a hydrated document - it should already have timestamps
3605
+ if (obj.$__ != null) {
3606
+ return obj;
3607
+ }
3608
+
3609
+ applyTimestampsHelper(this.schema, obj, options);
3610
+
3611
+ return obj;
3612
+ };
3613
+
3543
3614
  /**
3544
3615
  * Cast the given POJO to the model's schema
3545
3616
  *
package/lib/mongoose.js CHANGED
@@ -661,6 +661,8 @@ Mongoose.prototype._model = function(name, schema, collection, options) {
661
661
  utils.toCollectionName(name, _mongoose.pluralize());
662
662
  }
663
663
 
664
+ applyEmbeddedDiscriminators(schema);
665
+
664
666
  const connection = options.connection || _mongoose.connection;
665
667
  model = _mongoose.Model.compile(model || name, schema, collection, connection, _mongoose);
666
668
  // Errors handled internally, so safe to ignore error
@@ -678,8 +680,6 @@ Mongoose.prototype._model = function(name, schema, collection, options) {
678
680
  }
679
681
  }
680
682
 
681
- applyEmbeddedDiscriminators(schema);
682
-
683
683
  return model;
684
684
  };
685
685
 
package/lib/query.js CHANGED
@@ -22,7 +22,6 @@ const castUpdate = require('./helpers/query/castUpdate');
22
22
  const clone = require('./helpers/clone');
23
23
  const getDiscriminatorByValue = require('./helpers/discriminator/getDiscriminatorByValue');
24
24
  const helpers = require('./queryHelpers');
25
- const immediate = require('./helpers/immediate');
26
25
  const internalToObjectOptions = require('./options').internalToObjectOptions;
27
26
  const isExclusive = require('./helpers/projection/isExclusive');
28
27
  const isInclusive = require('./helpers/projection/isInclusive');
@@ -1142,6 +1141,38 @@ Query.prototype.select = function select() {
1142
1141
  throw new TypeError('Invalid select() argument. Must be string or object.');
1143
1142
  };
1144
1143
 
1144
+ /**
1145
+ * Enable or disable schema level projections for this query. Enabled by default.
1146
+ * Set to `false` to include fields with `select: false` in the query result by default.
1147
+ *
1148
+ * #### Example:
1149
+ *
1150
+ * const userSchema = new Schema({
1151
+ * email: { type: String, required: true },
1152
+ * passwordHash: { type: String, select: false, required: true }
1153
+ * });
1154
+ * const UserModel = mongoose.model('User', userSchema);
1155
+ *
1156
+ * const doc = await UserModel.findOne().orFail().schemaLevelProjections(false);
1157
+ *
1158
+ * // Contains password hash, because `schemaLevelProjections()` overrides `select: false`
1159
+ * doc.passwordHash;
1160
+ *
1161
+ * @method schemaLevelProjections
1162
+ * @memberOf Query
1163
+ * @instance
1164
+ * @param {Boolean} value
1165
+ * @return {Query} this
1166
+ * @see SchemaTypeOptions https://mongoosejs.com/docs/schematypes.html#all-schema-types
1167
+ * @api public
1168
+ */
1169
+
1170
+ Query.prototype.schemaLevelProjections = function schemaLevelProjections(value) {
1171
+ this._mongooseOptions.schemaLevelProjections = value;
1172
+
1173
+ return this;
1174
+ };
1175
+
1145
1176
  /**
1146
1177
  * Sets this query's `sanitizeProjection` option. If set, `sanitizeProjection` does
1147
1178
  * two things:
@@ -1594,6 +1625,7 @@ Query.prototype.getOptions = function() {
1594
1625
  * - [writeConcern](https://www.mongodb.com/docs/manual/reference/method/db.collection.update/)
1595
1626
  * - [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.
1596
1627
  * - overwriteDiscriminatorKey: allow setting the discriminator key in the update. Will use the correct discriminator schema if the update changes the discriminator key.
1628
+ * - overwriteImmutable: allow overwriting properties that are set to `immutable` in the schema. Defaults to false.
1597
1629
  *
1598
1630
  * The following options are only for `find()`, `findOne()`, `findById()`, `findOneAndUpdate()`, `findOneAndReplace()`, `findOneAndDelete()`, and `findByIdAndUpdate()`:
1599
1631
  *
@@ -1665,6 +1697,10 @@ Query.prototype.setOptions = function(options, overwrite) {
1665
1697
  this._mongooseOptions.overwriteDiscriminatorKey = options.overwriteDiscriminatorKey;
1666
1698
  delete options.overwriteDiscriminatorKey;
1667
1699
  }
1700
+ if ('overwriteImmutable' in options) {
1701
+ this._mongooseOptions.overwriteImmutable = options.overwriteImmutable;
1702
+ delete options.overwriteImmutable;
1703
+ }
1668
1704
  if ('sanitizeProjection' in options) {
1669
1705
  if (options.sanitizeProjection && !this._mongooseOptions.sanitizeProjection) {
1670
1706
  sanitizeProjection(this._fields);
@@ -1689,6 +1725,10 @@ Query.prototype.setOptions = function(options, overwrite) {
1689
1725
  this._mongooseOptions.translateAliases = options.translateAliases;
1690
1726
  delete options.translateAliases;
1691
1727
  }
1728
+ if ('schemaLevelProjections' in options) {
1729
+ this._mongooseOptions.schemaLevelProjections = options.schemaLevelProjections;
1730
+ delete options.schemaLevelProjections;
1731
+ }
1692
1732
 
1693
1733
  if (options.lean == null && this.schema && 'lean' in this.schema.options) {
1694
1734
  this._mongooseOptions.lean = this.schema.options.lean;
@@ -2207,6 +2247,9 @@ Query.prototype.error = function error(err) {
2207
2247
  */
2208
2248
 
2209
2249
  Query.prototype._unsetCastError = function _unsetCastError() {
2250
+ if (this._error == null) {
2251
+ return;
2252
+ }
2210
2253
  if (this._error != null && !(this._error instanceof CastError)) {
2211
2254
  return;
2212
2255
  }
@@ -2222,6 +2265,7 @@ Query.prototype._unsetCastError = function _unsetCastError() {
2222
2265
  * - `strict`: controls how Mongoose handles keys that aren't in the schema for updates. This option is `true` by default, which means Mongoose will silently strip any paths in the update that aren't in the schema. See the [`strict` mode docs](https://mongoosejs.com/docs/guide.html#strict) for more information.
2223
2266
  * - `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.
2224
2267
  * - `nearSphere`: use `$nearSphere` instead of `near()`. See the [`Query.prototype.nearSphere()` docs](https://mongoosejs.com/docs/api/query.html#Query.prototype.nearSphere())
2268
+ * - `schemaLevelProjections`: if `false`, Mongoose will not apply schema-level `select: false` or `select: true` for this query
2225
2269
  *
2226
2270
  * Mongoose maintains a separate object for internal options because
2227
2271
  * Mongoose sends `Query.prototype.options` to the MongoDB server, and the
@@ -2249,9 +2293,9 @@ Query.prototype.mongooseOptions = function(v) {
2249
2293
 
2250
2294
  Query.prototype._castConditions = function() {
2251
2295
  let sanitizeFilterOpt = undefined;
2252
- if (this.model != null && utils.hasUserDefinedProperty(this.model.db.options, 'sanitizeFilter')) {
2296
+ if (this.model?.db.options?.sanitizeFilter != null) {
2253
2297
  sanitizeFilterOpt = this.model.db.options.sanitizeFilter;
2254
- } else if (this.model != null && utils.hasUserDefinedProperty(this.model.base.options, 'sanitizeFilter')) {
2298
+ } else if (this.model?.base.options?.sanitizeFilter != null) {
2255
2299
  sanitizeFilterOpt = this.model.base.options.sanitizeFilter;
2256
2300
  } else {
2257
2301
  sanitizeFilterOpt = this._mongooseOptions.sanitizeFilter;
@@ -2494,13 +2538,12 @@ Query.prototype.collation = function(value) {
2494
2538
  * @api private
2495
2539
  */
2496
2540
 
2497
- Query.prototype._completeOne = function(doc, res, callback) {
2541
+ Query.prototype._completeOne = function(doc, res, projection, callback) {
2498
2542
  if (!doc && !this.options.includeResultMetadata) {
2499
2543
  return callback(null, null);
2500
2544
  }
2501
2545
 
2502
2546
  const model = this.model;
2503
- const projection = clone(this._fields);
2504
2547
  const userProvidedFields = this._userProvidedFields || {};
2505
2548
  // `populate`, `lean`
2506
2549
  const mongooseOptions = this._mongooseOptions;
@@ -2601,7 +2644,7 @@ Query.prototype._findOne = async function _findOne() {
2601
2644
  // don't pass in the conditions because we already merged them in
2602
2645
  const doc = await this.mongooseCollection.findOne(this._conditions, options);
2603
2646
  return new Promise((resolve, reject) => {
2604
- this._completeOne(doc, null, (err, res) => {
2647
+ this._completeOne(doc, null, options.projection, (err, res) => {
2605
2648
  if (err) {
2606
2649
  return reject(err);
2607
2650
  }
@@ -3196,7 +3239,7 @@ function completeOne(model, doc, res, options, fields, userProvidedFields, pop,
3196
3239
 
3197
3240
  function _init(err, casted) {
3198
3241
  if (err) {
3199
- return immediate(() => callback(err));
3242
+ return callback(err);
3200
3243
  }
3201
3244
 
3202
3245
 
@@ -3209,12 +3252,12 @@ function completeOne(model, doc, res, options, fields, userProvidedFields, pop,
3209
3252
  } else {
3210
3253
  res.value = null;
3211
3254
  }
3212
- return immediate(() => callback(null, res));
3255
+ return callback(null, res);
3213
3256
  }
3214
3257
  if (options.session != null) {
3215
3258
  casted.$session(options.session);
3216
3259
  }
3217
- immediate(() => callback(null, casted));
3260
+ callback(null, casted);
3218
3261
  }
3219
3262
  }
3220
3263
 
@@ -3281,6 +3324,7 @@ function prepareDiscriminatorCriteria(query) {
3281
3324
  * @param {Boolean} [options.returnOriginal=null] An alias for the `new` option. `returnOriginal: false` is equivalent to `new: true`.
3282
3325
  * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
3283
3326
  * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
3327
+ * @param {Boolean} [options.overwriteImmutable=false] Mongoose removes updated immutable properties from `update` by default (excluding $setOnInsert). Set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators.
3284
3328
  * @see Tutorial https://mongoosejs.com/docs/tutorials/findoneandupdate.html
3285
3329
  * @see findAndModify command https://www.mongodb.com/docs/manual/reference/command/findAndModify/
3286
3330
  * @see ModifyResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html
@@ -3422,7 +3466,7 @@ Query.prototype._findOneAndUpdate = async function _findOneAndUpdate() {
3422
3466
  const doc = !options.includeResultMetadata ? res : res.value;
3423
3467
 
3424
3468
  return new Promise((resolve, reject) => {
3425
- this._completeOne(doc, res, (err, res) => {
3469
+ this._completeOne(doc, res, options.projection, (err, res) => {
3426
3470
  if (err) {
3427
3471
  return reject(err);
3428
3472
  }
@@ -3518,7 +3562,7 @@ Query.prototype._findOneAndDelete = async function _findOneAndDelete() {
3518
3562
  const doc = !includeResultMetadata ? res : res.value;
3519
3563
 
3520
3564
  return new Promise((resolve, reject) => {
3521
- this._completeOne(doc, res, (err, res) => {
3565
+ this._completeOne(doc, res, options.projection, (err, res) => {
3522
3566
  if (err) {
3523
3567
  return reject(err);
3524
3568
  }
@@ -3672,7 +3716,7 @@ Query.prototype._findOneAndReplace = async function _findOneAndReplace() {
3672
3716
 
3673
3717
  const doc = !includeResultMetadata ? res : res.value;
3674
3718
  return new Promise((resolve, reject) => {
3675
- this._completeOne(doc, res, (err, res) => {
3719
+ this._completeOne(doc, res, options.projection, (err, res) => {
3676
3720
  if (err) {
3677
3721
  return reject(err);
3678
3722
  }
@@ -3979,6 +4023,7 @@ Query.prototype._replaceOne = async function _replaceOne() {
3979
4023
  * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
3980
4024
  * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
3981
4025
  * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
4026
+ * @param {Boolean} [options.overwriteImmutable=false] Mongoose removes updated immutable properties from `update` by default (excluding $setOnInsert). Set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators.
3982
4027
  * @param {Function} [callback] params are (error, writeOpResult)
3983
4028
  * @return {Query} this
3984
4029
  * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update()
@@ -4049,6 +4094,7 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) {
4049
4094
  * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
4050
4095
  * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
4051
4096
  * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
4097
+ * @param {Boolean} [options.overwriteImmutable=false] Mongoose removes updated immutable properties from `update` by default (excluding $setOnInsert). Set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators.
4052
4098
  * @param {Function} [callback] params are (error, writeOpResult)
4053
4099
  * @return {Query} this
4054
4100
  * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update()
@@ -4670,7 +4716,8 @@ Query.prototype._castUpdate = function _castUpdate(obj) {
4670
4716
  strict: this._mongooseOptions.strict,
4671
4717
  upsert: upsert,
4672
4718
  arrayFilters: this.options.arrayFilters,
4673
- overwriteDiscriminatorKey: this._mongooseOptions.overwriteDiscriminatorKey
4719
+ overwriteDiscriminatorKey: this._mongooseOptions.overwriteDiscriminatorKey,
4720
+ overwriteImmutable: this._mongooseOptions.overwriteImmutable
4674
4721
  }, this, this._conditions);
4675
4722
  };
4676
4723
 
@@ -4863,6 +4910,9 @@ Query.prototype.cast = function(model, obj) {
4863
4910
  opts.strictQuery = this.options.strictQuery;
4864
4911
  }
4865
4912
  }
4913
+ if ('sanitizeFilter' in this._mongooseOptions) {
4914
+ opts.sanitizeFilter = this._mongooseOptions.sanitizeFilter;
4915
+ }
4866
4916
 
4867
4917
  try {
4868
4918
  return cast(model.schema, obj, opts, this);
@@ -4946,7 +4996,11 @@ Query.prototype._applyPaths = function applyPaths() {
4946
4996
  sanitizeProjection = this._mongooseOptions.sanitizeProjection;
4947
4997
  }
4948
4998
 
4949
- helpers.applyPaths(this._fields, this.model.schema, sanitizeProjection);
4999
+ const schemaLevelProjections = this._mongooseOptions.schemaLevelProjections ?? true;
5000
+
5001
+ if (schemaLevelProjections) {
5002
+ helpers.applyPaths(this._fields, this.model.schema, sanitizeProjection);
5003
+ }
4950
5004
 
4951
5005
  let _selectPopulatedPaths = true;
4952
5006