mongoose 8.7.2 → 8.8.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/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
 
@@ -196,6 +196,37 @@ AggregationCursor.prototype.close = async function close() {
196
196
  this.emit('close');
197
197
  };
198
198
 
199
+ /**
200
+ * Marks this cursor as destroyed. Will stop streaming and subsequent calls to
201
+ * `next()` will error.
202
+ *
203
+ * @return {this}
204
+ * @api private
205
+ * @method _destroy
206
+ */
207
+
208
+ AggregationCursor.prototype._destroy = function _destroy(_err, callback) {
209
+ let waitForCursor = null;
210
+ if (!this.cursor) {
211
+ waitForCursor = new Promise((resolve) => {
212
+ this.once('cursor', resolve);
213
+ });
214
+ } else {
215
+ waitForCursor = Promise.resolve();
216
+ }
217
+
218
+ waitForCursor
219
+ .then(() => this.cursor.close())
220
+ .then(() => {
221
+ this._closed = true;
222
+ callback();
223
+ })
224
+ .catch(error => {
225
+ callback(error);
226
+ });
227
+ return this;
228
+ };
229
+
199
230
  /**
200
231
  * Get the next document from this cursor. Will return `null` when there are
201
232
  * no documents left.
@@ -238,6 +238,39 @@ QueryCursor.prototype.close = async function close() {
238
238
  }
239
239
  };
240
240
 
241
+ /**
242
+ * Marks this cursor as destroyed. Will stop streaming and subsequent calls to
243
+ * `next()` will error.
244
+ *
245
+ * @return {this}
246
+ * @api private
247
+ * @method _destroy
248
+ */
249
+
250
+ QueryCursor.prototype._destroy = function _destroy(_err, callback) {
251
+ let waitForCursor = null;
252
+ if (!this.cursor) {
253
+ waitForCursor = new Promise((resolve) => {
254
+ this.once('cursor', resolve);
255
+ });
256
+ } else {
257
+ waitForCursor = Promise.resolve();
258
+ }
259
+
260
+ waitForCursor
261
+ .then(() => {
262
+ this.cursor.close();
263
+ })
264
+ .then(() => {
265
+ this._closed = true;
266
+ callback();
267
+ })
268
+ .catch(error => {
269
+ callback(error);
270
+ });
271
+ return this;
272
+ };
273
+
241
274
  /**
242
275
  * Rewind this cursor to its uninitialized state. Any options that are present on the cursor will
243
276
  * remain in effect. Iterating this cursor will cause new queries to be sent to the server, even
@@ -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
+ }
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/query.js CHANGED
@@ -1142,6 +1142,38 @@ Query.prototype.select = function select() {
1142
1142
  throw new TypeError('Invalid select() argument. Must be string or object.');
1143
1143
  };
1144
1144
 
1145
+ /**
1146
+ * Enable or disable schema level projections for this query. Enabled by default.
1147
+ * Set to `false` to include fields with `select: false` in the query result by default.
1148
+ *
1149
+ * #### Example:
1150
+ *
1151
+ * const userSchema = new Schema({
1152
+ * email: { type: String, required: true },
1153
+ * passwordHash: { type: String, select: false, required: true }
1154
+ * });
1155
+ * const UserModel = mongoose.model('User', userSchema);
1156
+ *
1157
+ * const doc = await UserModel.findOne().orFail().schemaLevelProjections(false);
1158
+ *
1159
+ * // Contains password hash, because `schemaLevelProjections()` overrides `select: false`
1160
+ * doc.passwordHash;
1161
+ *
1162
+ * @method schemaLevelProjections
1163
+ * @memberOf Query
1164
+ * @instance
1165
+ * @param {Boolean} value
1166
+ * @return {Query} this
1167
+ * @see SchemaTypeOptions https://mongoosejs.com/docs/schematypes.html#all-schema-types
1168
+ * @api public
1169
+ */
1170
+
1171
+ Query.prototype.schemaLevelProjections = function schemaLevelProjections(value) {
1172
+ this._mongooseOptions.schemaLevelProjections = value;
1173
+
1174
+ return this;
1175
+ };
1176
+
1145
1177
  /**
1146
1178
  * Sets this query's `sanitizeProjection` option. If set, `sanitizeProjection` does
1147
1179
  * two things:
@@ -1689,6 +1721,10 @@ Query.prototype.setOptions = function(options, overwrite) {
1689
1721
  this._mongooseOptions.translateAliases = options.translateAliases;
1690
1722
  delete options.translateAliases;
1691
1723
  }
1724
+ if ('schemaLevelProjections' in options) {
1725
+ this._mongooseOptions.schemaLevelProjections = options.schemaLevelProjections;
1726
+ delete options.schemaLevelProjections;
1727
+ }
1692
1728
 
1693
1729
  if (options.lean == null && this.schema && 'lean' in this.schema.options) {
1694
1730
  this._mongooseOptions.lean = this.schema.options.lean;
@@ -2222,6 +2258,7 @@ Query.prototype._unsetCastError = function _unsetCastError() {
2222
2258
  * - `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
2259
  * - `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
2260
  * - `nearSphere`: use `$nearSphere` instead of `near()`. See the [`Query.prototype.nearSphere()` docs](https://mongoosejs.com/docs/api/query.html#Query.prototype.nearSphere())
2261
+ * - `schemaLevelProjections`: if `false`, Mongoose will not apply schema-level `select: false` or `select: true` for this query
2225
2262
  *
2226
2263
  * Mongoose maintains a separate object for internal options because
2227
2264
  * Mongoose sends `Query.prototype.options` to the MongoDB server, and the
@@ -4863,6 +4900,9 @@ Query.prototype.cast = function(model, obj) {
4863
4900
  opts.strictQuery = this.options.strictQuery;
4864
4901
  }
4865
4902
  }
4903
+ if ('sanitizeFilter' in this._mongooseOptions) {
4904
+ opts.sanitizeFilter = this._mongooseOptions.sanitizeFilter;
4905
+ }
4866
4906
 
4867
4907
  try {
4868
4908
  return cast(model.schema, obj, opts, this);
@@ -4946,7 +4986,11 @@ Query.prototype._applyPaths = function applyPaths() {
4946
4986
  sanitizeProjection = this._mongooseOptions.sanitizeProjection;
4947
4987
  }
4948
4988
 
4949
- helpers.applyPaths(this._fields, this.model.schema, sanitizeProjection);
4989
+ const schemaLevelProjections = this._mongooseOptions.schemaLevelProjections ?? true;
4990
+
4991
+ if (schemaLevelProjections) {
4992
+ helpers.applyPaths(this._fields, this.model.schema, sanitizeProjection);
4993
+ }
4950
4994
 
4951
4995
  let _selectPopulatedPaths = true;
4952
4996
 
@@ -11,9 +11,12 @@ const SchemaArrayOptions = require('../options/schemaArrayOptions');
11
11
  const SchemaType = require('../schemaType');
12
12
  const CastError = SchemaType.CastError;
13
13
  const Mixed = require('./mixed');
14
+ const VirtualOptions = require('../options/virtualOptions');
15
+ const VirtualType = require('../virtualType');
14
16
  const arrayDepth = require('../helpers/arrayDepth');
15
17
  const cast = require('../cast');
16
18
  const clone = require('../helpers/clone');
19
+ const getConstructorName = require('../helpers/getConstructorName');
17
20
  const isOperator = require('../helpers/query/isOperator');
18
21
  const util = require('util');
19
22
  const utils = require('../utils');
@@ -217,6 +220,12 @@ SchemaArray._checkRequired = SchemaType.prototype.checkRequired;
217
220
 
218
221
  SchemaArray.checkRequired = SchemaType.checkRequired;
219
222
 
223
+ /*!
224
+ * Virtuals defined on this array itself.
225
+ */
226
+
227
+ SchemaArray.prototype.virtuals = null;
228
+
220
229
  /**
221
230
  * Check if the given value satisfies the `required` validator.
222
231
  *
@@ -575,6 +584,32 @@ SchemaArray.prototype.castForQuery = function($conditional, val, context) {
575
584
  }
576
585
  };
577
586
 
587
+ /**
588
+ * Add a virtual to this array. Specifically to this array, not the individual elements.
589
+ *
590
+ * @param {String} name
591
+ * @param {Object} [options]
592
+ * @api private
593
+ */
594
+
595
+ SchemaArray.prototype.virtual = function virtual(name, options) {
596
+ if (name instanceof VirtualType || getConstructorName(name) === 'VirtualType') {
597
+ return this.virtual(name.path, name.options);
598
+ }
599
+ options = new VirtualOptions(options);
600
+
601
+ if (utils.hasUserDefinedProperty(options, ['ref', 'refPath'])) {
602
+ throw new MongooseError('Cannot set populate virtual as a property of an array');
603
+ }
604
+
605
+ const virtual = new VirtualType(options, name);
606
+ if (this.virtuals === null) {
607
+ this.virtuals = {};
608
+ }
609
+ this.virtuals[name] = virtual;
610
+ return virtual;
611
+ };
612
+
578
613
  function cast$all(val, context) {
579
614
  if (!Array.isArray(val)) {
580
615
  val = [val];
@@ -429,7 +429,7 @@ SchemaDocumentArray.prototype.cast = function(value, doc, init, prev, options) {
429
429
  // We need to create a new array, otherwise change tracking will
430
430
  // update the old doc (gh-4449)
431
431
  if (!options.skipDocumentArrayCast || utils.isMongooseDocumentArray(value)) {
432
- value = new MongooseDocumentArray(value, path, doc);
432
+ value = new MongooseDocumentArray(value, path, doc, this);
433
433
  }
434
434
 
435
435
  if (prev != null) {
package/lib/schema.js CHANGED
@@ -2304,6 +2304,7 @@ Schema.prototype.indexes = function() {
2304
2304
  * @param {Boolean} [options.count=false] Only works with populate virtuals. If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), this populate virtual will contain the number of documents rather than the documents themselves when you `populate()`.
2305
2305
  * @param {Function|null} [options.get=null] Adds a [getter](https://mongoosejs.com/docs/tutorials/getters-setters.html) to this virtual to transform the populated doc.
2306
2306
  * @param {Object|Function} [options.match=null] Apply a default [`match` option to populate](https://mongoosejs.com/docs/populate.html#match), adding an additional filter to the populate query.
2307
+ * @param {Boolean} [options.applyToArray=false] If true and the given `name` is a direct child of an array, apply the virtual to the array rather than the elements.
2307
2308
  * @return {VirtualType}
2308
2309
  */
2309
2310
 
@@ -2416,6 +2417,15 @@ Schema.prototype.virtual = function(name, options) {
2416
2417
  return mem[part];
2417
2418
  }, this.tree);
2418
2419
 
2420
+ if (options && options.applyToArray && parts.length > 1) {
2421
+ const path = this.path(parts.slice(0, -1).join('.'));
2422
+ if (path && path.$isMongooseArray) {
2423
+ return path.virtual(parts[parts.length - 1], options);
2424
+ } else {
2425
+ throw new MongooseError(`Path "${path}" is not an array`);
2426
+ }
2427
+ }
2428
+
2419
2429
  return virtuals[name];
2420
2430
  };
2421
2431
 
@@ -90,6 +90,9 @@ function MongooseArray(values, path, doc, schematype) {
90
90
  if (mongooseArrayMethods.hasOwnProperty(prop)) {
91
91
  return mongooseArrayMethods[prop];
92
92
  }
93
+ if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
94
+ return schematype.virtuals[prop].applyGetters(undefined, target);
95
+ }
93
96
  if (typeof prop === 'string' && numberRE.test(prop) && schematype?.$embeddedSchemaType != null) {
94
97
  return schematype.$embeddedSchemaType.applyGetters(__array[prop], doc);
95
98
  }
@@ -101,6 +104,8 @@ function MongooseArray(values, path, doc, schematype) {
101
104
  mongooseArrayMethods.set.call(proxy, prop, value, false);
102
105
  } else if (internals.hasOwnProperty(prop)) {
103
106
  internals[prop] = value;
107
+ } else if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
108
+ schematype.virtuals[prop].applySetters(value, target);
104
109
  } else {
105
110
  __array[prop] = value;
106
111
  }
@@ -28,7 +28,7 @@ const numberRE = /^\d+$/;
28
28
  * @see https://bit.ly/f6CnZU
29
29
  */
30
30
 
31
- function MongooseDocumentArray(values, path, doc) {
31
+ function MongooseDocumentArray(values, path, doc, schematype) {
32
32
  const __array = [];
33
33
 
34
34
  const internals = {
@@ -84,6 +84,9 @@ function MongooseDocumentArray(values, path, doc) {
84
84
  if (DocumentArrayMethods.hasOwnProperty(prop)) {
85
85
  return DocumentArrayMethods[prop];
86
86
  }
87
+ if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
88
+ return schematype.virtuals[prop].applyGetters(undefined, target);
89
+ }
87
90
  if (ArrayMethods.hasOwnProperty(prop)) {
88
91
  return ArrayMethods[prop];
89
92
  }
@@ -95,6 +98,8 @@ function MongooseDocumentArray(values, path, doc) {
95
98
  DocumentArrayMethods.set.call(proxy, prop, value, false);
96
99
  } else if (internals.hasOwnProperty(prop)) {
97
100
  internals[prop] = value;
101
+ } else if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
102
+ schematype.virtuals[prop].applySetters(value, target);
98
103
  } else {
99
104
  __array[prop] = value;
100
105
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongoose",
3
3
  "description": "Mongoose MongoDB ODM",
4
- "version": "8.7.2",
4
+ "version": "8.8.0",
5
5
  "author": "Guillermo Rauch <guillermo@learnboost.com>",
6
6
  "keywords": [
7
7
  "mongodb",
@@ -21,7 +21,7 @@
21
21
  "dependencies": {
22
22
  "bson": "^6.7.0",
23
23
  "kareem": "2.6.3",
24
- "mongodb": "6.9.0",
24
+ "mongodb": "~6.10.0",
25
25
  "mpath": "0.9.0",
26
26
  "mquery": "5.0.0",
27
27
  "ms": "2.1.3",
@@ -50,6 +50,12 @@ declare module 'mongoose' {
50
50
  autoIndex?: boolean;
51
51
  /** Set to `false` to disable Mongoose automatically calling `createCollection()` on every model created on this connection. */
52
52
  autoCreate?: boolean;
53
+ /**
54
+ * Sanitizes query filters against [query selector injection attacks](
55
+ * https://thecodebarbarian.com/2014/09/04/defending-against-query-selector-injection-attacks.html
56
+ * ) by wrapping any nested objects that have a property whose name starts with $ in a $eq.
57
+ */
58
+ sanitizeFilter?: boolean;
53
59
  }
54
60
 
55
61
  class Connection extends events.EventEmitter implements SessionStarter {
package/types/cursor.d.ts CHANGED
@@ -26,6 +26,12 @@ declare module 'mongoose' {
26
26
  */
27
27
  close(): Promise<void>;
28
28
 
29
+ /**
30
+ * Destroy this cursor, closing the underlying cursor. Will stop streaming
31
+ * and subsequent calls to `next()` will error.
32
+ */
33
+ destroy(): this;
34
+
29
35
  /**
30
36
  * Rewind this cursor to its uninitialized state. Any options that are present on the cursor will
31
37
  * remain in effect. Iterating this cursor will cause new queries to be sent to the server, even
@@ -256,10 +256,17 @@ declare module 'mongoose' {
256
256
  set(value: string | Record<string, any>): this;
257
257
 
258
258
  /** The return value of this method is used in calls to JSON.stringify(doc). */
259
- toJSON(options?: ToObjectOptions & { flattenMaps?: true }): FlattenMaps<Require_id<DocType>>;
259
+ toJSON(options?: ToObjectOptions & { flattenMaps?: true, flattenObjectIds?: false }): FlattenMaps<Require_id<DocType>>;
260
+ toJSON(options: ToObjectOptions & { flattenObjectIds: false }): FlattenMaps<Require_id<DocType>>;
261
+ toJSON(options: ToObjectOptions & { flattenObjectIds: true }): ObjectIdToString<FlattenMaps<Require_id<DocType>>>;
260
262
  toJSON(options: ToObjectOptions & { flattenMaps: false }): Require_id<DocType>;
261
- toJSON<T = Require_id<DocType>>(options?: ToObjectOptions & { flattenMaps?: true }): FlattenMaps<T>;
263
+ toJSON(options: ToObjectOptions & { flattenMaps: false; flattenObjectIds: true }): ObjectIdToString<Require_id<DocType>>;
264
+
265
+ toJSON<T = Require_id<DocType>>(options?: ToObjectOptions & { flattenMaps?: true, flattenObjectIds?: false }): FlattenMaps<T>;
266
+ toJSON<T = Require_id<DocType>>(options: ToObjectOptions & { flattenObjectIds: false }): FlattenMaps<T>;
267
+ toJSON<T = Require_id<DocType>>(options: ToObjectOptions & { flattenObjectIds: true }): ObjectIdToString<FlattenMaps<T>>;
262
268
  toJSON<T = Require_id<DocType>>(options: ToObjectOptions & { flattenMaps: false }): T;
269
+ toJSON<T = Require_id<DocType>>(options: ToObjectOptions & { flattenMaps: false; flattenObjectIds: true }): ObjectIdToString<T>;
263
270
 
264
271
  /** Converts this document into a plain-old JavaScript object ([POJO](https://masteringjs.io/tutorials/fundamentals/pojo)). */
265
272
  toObject(options?: ToObjectOptions): Require_id<DocType>;