mongoose 9.9.4 → 9.10.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/eslint.config.mjs CHANGED
@@ -43,6 +43,7 @@ export default defineConfig([
43
43
  'no-constant-condition': 'off',
44
44
  'no-multi-spaces': 'error',
45
45
  'func-call-spacing': 'error',
46
+ 'no-unused-vars': ['error', { ignoreRestSiblings: true }],
46
47
  'no-trailing-spaces': 'error',
47
48
  'no-undef': 'error',
48
49
  'no-unneeded-ternary': 'error',
package/lib/aggregate.js CHANGED
@@ -810,8 +810,7 @@ Aggregate.prototype.explain = async function explain(verbosity) {
810
810
  const postFilter = buildMiddlewareFilter(this.options, 'post');
811
811
 
812
812
  // Remove middleware option before passing to MongoDB
813
- const options = this.options != null ? { ...this.options } : {};
814
- delete options.middleware;
813
+ const { middleware, ...options } = this.options ?? {};
815
814
 
816
815
  try {
817
816
  await model.hooks.execPre('aggregate', this, [], { filter: preFilter });
@@ -925,6 +924,8 @@ Aggregate.prototype.option = function(value) {
925
924
  * Sets the `cursor` option and executes this aggregation, returning an aggregation cursor.
926
925
  * Cursors are useful if you want to process the results of the aggregation one-at-a-time
927
926
  * because the aggregation result is too big to fit into memory.
927
+ * Creating an aggregation cursor runs pre aggregate hooks, but not post aggregate hooks.
928
+ * To skip pre aggregate hooks, pass `middleware: false` to `.cursor()`.
928
929
  *
929
930
  * #### Example:
930
931
  *
@@ -935,6 +936,8 @@ Aggregate.prototype.option = function(value) {
935
936
  *
936
937
  * @param {object} options
937
938
  * @param {number} [options.batchSize] set the cursor batch size
939
+ * @param {boolean|object} [options.middleware=true] set to `false` to skip user-defined pre aggregate middleware
940
+ * @param {boolean} [options.middleware.pre=true] set to `false` to skip pre aggregate middleware
938
941
  * @param {boolean} [options.useMongooseAggCursor] use experimental mongoose-specific aggregation cursor (for `eachAsync()` and other query cursor semantics)
939
942
  * @return {AggregationCursor} cursor representing this aggregation
940
943
  * @api public
@@ -943,6 +946,11 @@ Aggregate.prototype.option = function(value) {
943
946
 
944
947
  Aggregate.prototype.cursor = function(options) {
945
948
  this._optionsForExec();
949
+ if (utils.hasUserDefinedProperty(options, 'middleware')) {
950
+ const { middleware, ...cursorOptions } = options;
951
+ this.options.middleware = middleware;
952
+ options = cursorOptions;
953
+ }
946
954
  this.options.cursor = options || {};
947
955
  return new AggregationCursor(this); // return this;
948
956
  };
@@ -1122,8 +1130,7 @@ Aggregate.prototype.exec = async function exec() {
1122
1130
  throw new MongooseError('Aggregate has empty pipeline');
1123
1131
  }
1124
1132
 
1125
- const options = clone(_this.options || {});
1126
- delete options.middleware;
1133
+ const { middleware, ...options } = clone(_this.options || {});
1127
1134
 
1128
1135
  let result;
1129
1136
  try {
@@ -11,6 +11,7 @@ const immediate = require('../helpers/immediate');
11
11
  const kareem = require('kareem');
12
12
  const util = require('util');
13
13
  const { cursorNextChannel } = require('../tracing');
14
+ const { buildMiddlewareFilter } = require('../helpers/buildMiddlewareFilter');
14
15
 
15
16
  /**
16
17
  * An AggregationCursor is a concurrency primitive for processing aggregation
@@ -49,7 +50,14 @@ function AggregationCursor(agg) {
49
50
  this._mongooseOptions = {};
50
51
 
51
52
  if (connection) {
52
- this.cursor = connection.db.aggregate(agg._pipeline, agg.options || {});
53
+ let options = agg.options || {};
54
+ if (Object.hasOwn(options, 'middleware')) {
55
+ // `middleware` is a Mongoose-only option used to skip user hooks; strip
56
+ // it from the clone passed through to the MongoDB driver.
57
+ const { middleware, ...driverOptions } = options;
58
+ options = driverOptions;
59
+ }
60
+ this.cursor = connection.db.aggregate(agg._pipeline, options);
53
61
  setImmediate(() => this.emit('cursor', this.cursor));
54
62
  } else {
55
63
  _init(model, this, agg);
@@ -63,7 +71,8 @@ util.inherits(AggregationCursor, Readable);
63
71
  */
64
72
 
65
73
  function _init(model, c, agg) {
66
- model.hooks.execPre('aggregate', agg).then(() => onPreComplete(null), err => onPreComplete(err));
74
+ const preFilter = buildMiddlewareFilter(agg.options, 'pre');
75
+ model.hooks.execPre('aggregate', agg, [], { filter: preFilter }).then(() => onPreComplete(null), err => onPreComplete(err));
67
76
 
68
77
  function onPreComplete(err) {
69
78
  if (err != null) {
@@ -90,7 +99,14 @@ function _init(model, c, agg) {
90
99
 
91
100
  function _getRawCursor(model, aggregationCursor, agg) {
92
101
  try {
93
- const cursor = model.collection.aggregate(agg._pipeline, agg.options || {});
102
+ let options = agg.options;
103
+ if (Object.hasOwn(options, 'middleware')) {
104
+ // `middleware` is a Mongoose-only option used to skip user hooks; strip
105
+ // it from the clone passed through to the MongoDB driver.
106
+ const { middleware, ...driverOptions } = options;
107
+ options = driverOptions;
108
+ }
109
+ const cursor = model.collection.aggregate(agg._pipeline, options);
94
110
  aggregationCursor.cursor = cursor;
95
111
  aggregationCursor.emit('cursor', cursor);
96
112
  } catch (err) {
@@ -13,6 +13,7 @@ const immediate = require('../helpers/immediate');
13
13
  const { once } = require('events');
14
14
  const util = require('util');
15
15
  const { cursorNextChannel } = require('../tracing');
16
+ const { buildMiddlewareFilter } = require('../helpers/buildMiddlewareFilter');
16
17
 
17
18
  /**
18
19
  * A QueryCursor is a concurrency primitive for processing query results
@@ -21,7 +22,8 @@ const { cursorNextChannel } = require('../tracing');
21
22
  * one at a time.
22
23
  *
23
24
  * QueryCursors execute the model's pre `find` hooks before loading any documents
24
- * from MongoDB, and the model's post `find` hooks after loading each document.
25
+ * from MongoDB, and the model's post `find` hooks once per document,
26
+ * with an array containing that document.
25
27
  *
26
28
  * Unless you're an advanced user, do **not** instantiate this class directly.
27
29
  * Use [`Query#cursor()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.cursor()) instead.
@@ -50,6 +52,7 @@ function QueryCursor(query) {
50
52
  this._transforms = [];
51
53
  this.model = model;
52
54
  this.options = {};
55
+ this._postMiddlewareFilter = null;
53
56
 
54
57
  const onPreComplete = (err) => {
55
58
  if (err != null) {
@@ -74,7 +77,15 @@ function QueryCursor(query) {
74
77
  this.listeners('error').length > 0 && this.emit('error', err);
75
78
  return;
76
79
  }
77
- Object.assign(this.options, query._optionsForExec());
80
+ const { middleware, ...options } = query._optionsForExec();
81
+ Object.assign(this.options, options);
82
+ // `middleware` is a Mongoose-only option used to skip user hooks; strip it
83
+ // so it isn't passed through to the MongoDB driver. `this.options` is public
84
+ // and may be modified while pre hooks run, so check the target after assign.
85
+ if (Object.hasOwn(this.options, 'middleware')) {
86
+ delete this.options.middleware;
87
+ }
88
+ this._postMiddlewareFilter = buildMiddlewareFilter(query.options, 'post');
78
89
  this._transforms = this._transforms.concat(query._transforms.slice());
79
90
  if (this.options.transform) {
80
91
  this._transforms.push(this.options.transform);
@@ -98,7 +109,8 @@ function QueryCursor(query) {
98
109
  }
99
110
  };
100
111
 
101
- model.hooks.execPre('find', query).then(() => onPreComplete(null), err => onPreComplete(err));
112
+ const preFilter = buildMiddlewareFilter(query.options, 'pre');
113
+ model.hooks.execPre('find', query, [], { filter: preFilter }).then(() => onPreComplete(null), err => onPreComplete(err));
102
114
  }
103
115
 
104
116
  util.inherits(QueryCursor, Readable);
@@ -607,7 +619,7 @@ function _populateBatch() {
607
619
 
608
620
  function _nextDoc(ctx, doc, pop, callback) {
609
621
  if (ctx.query._mongooseOptions.lean) {
610
- return ctx.model.hooks.execPost('find', ctx.query, [[doc]]).then(() => callback(null, doc), err => callback(err));
622
+ return ctx.model.hooks.execPost('find', ctx.query, [[doc]], { filter: ctx._postMiddlewareFilter }).then(() => callback(null, doc), err => callback(err));
611
623
  }
612
624
 
613
625
  const { model, _fields, _userProvidedFields, options } = ctx.query;
@@ -618,7 +630,7 @@ function _nextDoc(ctx, doc, pop, callback) {
618
630
  if (options.session != null) {
619
631
  doc.$session(options.session);
620
632
  }
621
- ctx.model.hooks.execPost('find', ctx.query, [[doc]]).then(() => callback(null, doc), err => callback(err));
633
+ ctx.model.hooks.execPost('find', ctx.query, [[doc]], { filter: ctx._postMiddlewareFilter }).then(() => callback(null, doc), err => callback(err));
622
634
  });
623
635
  }
624
636
 
package/lib/document.js CHANGED
@@ -28,6 +28,7 @@ const compile = require('./helpers/document/compile').compile;
28
28
  const defineKey = require('./helpers/document/compile').defineKey;
29
29
  const firstKey = require('./helpers/firstKey');
30
30
  const flatten = require('./helpers/common').flatten;
31
+ const getConstructorName = require('./helpers/getConstructorName');
31
32
  const getEmbeddedDiscriminatorPath = require('./helpers/document/getEmbeddedDiscriminatorPath');
32
33
  const getKeysInSchemaOrder = require('./helpers/schema/getKeysInSchemaOrder');
33
34
  const getSubdocumentStrictValue = require('./helpers/schema/getSubdocumentStrictValue');
@@ -3589,6 +3590,15 @@ Document.prototype.invalidate = function(path, err, val, kind) {
3589
3590
  return this.$__.validationError;
3590
3591
  }
3591
3592
 
3593
+ // Set the model on cast errors with a custom message so `{MODEL}` gets
3594
+ // replaced for document validation, not just query casting (gh-8300)
3595
+ if (typeof err.setModel === 'function') {
3596
+ const owner = this.ownerDocument();
3597
+ if (getConstructorName(owner) === 'model') {
3598
+ err.setModel(owner.constructor);
3599
+ }
3600
+ }
3601
+
3592
3602
  this.$__.validationError.addError(path, err);
3593
3603
  return this.$__.validationError;
3594
3604
  };
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const isPOJO = require('./isPOJO');
3
4
  const symbols = require('../schema/symbols');
4
5
 
5
6
  /**
@@ -15,10 +16,27 @@ const isBuiltInMiddleware = hook => hook.fn[symbols.builtInMiddleware];
15
16
  * @returns {Function|null} - null runs all middleware, isBuiltInMiddleware skips user middleware
16
17
  */
17
18
  function buildMiddlewareFilter(options, phase) {
18
- const shouldRun = options?.middleware?.[phase] ?? options?.middleware ?? true;
19
- return shouldRun ? null : isBuiltInMiddleware;
19
+ const shouldSkip = options?.middleware === false || options?.middleware?.[phase] === false;
20
+ return shouldSkip ? isBuiltInMiddleware : null;
21
+ }
22
+
23
+ /**
24
+ * kareem `getOptions` implementation for custom statics and methods: reads the
25
+ * `middleware` option from the call's last argument if it is a plain object.
26
+ *
27
+ * @param {Array} args - Arguments the custom static or method was called with
28
+ * @returns {object} - Per-call kareem options with pre/post filters
29
+ */
30
+ function middlewareFiltersFromLastArg(args) {
31
+ const lastArg = args[args.length - 1];
32
+ const options = isPOJO(lastArg) ? lastArg : null;
33
+ return {
34
+ pre: { filter: buildMiddlewareFilter(options, 'pre') },
35
+ post: { filter: buildMiddlewareFilter(options, 'post') }
36
+ };
20
37
  }
21
38
 
22
39
  module.exports = {
23
- buildMiddlewareFilter
40
+ buildMiddlewareFilter,
41
+ middlewareFiltersFromLastArg
24
42
  };
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { buildMiddlewareFilter } = require('../buildMiddlewareFilter');
3
+ const { buildMiddlewareFilter, middlewareFiltersFromLastArg } = require('../buildMiddlewareFilter');
4
4
 
5
5
  /*!
6
6
  * ignore
@@ -118,8 +118,15 @@ function applyHooks(model, schema, options) {
118
118
  }
119
119
  const originalMethod = objToDecorate[method];
120
120
  objToDecorate[`$__${method}`] = objToDecorate[method];
121
+ // Only read the `middleware` option from the method's last argument if
122
+ // the method opts in via `supportsMiddlewareOption`. Custom methods have
123
+ // arbitrary signatures, so the last argument may be an object with an
124
+ // unrelated `middleware` property.
125
+ const methodOptions = originalMethod.supportsMiddlewareOption === true ?
126
+ { ...customMethodOptions, getOptions: middlewareFiltersFromLastArg } :
127
+ customMethodOptions;
121
128
  objToDecorate[method] = middleware.
122
- createWrapper(method, originalMethod, null, customMethodOptions);
129
+ createWrapper(method, originalMethod, null, methodOptions);
123
130
  }
124
131
  }
125
132
 
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const { middlewareFiltersFromLastArg } = require('../buildMiddlewareFilter');
3
4
  const { queryMiddlewareFunctions, aggregateMiddlewareFunctions, modelMiddlewareFunctions, documentMiddlewareFunctions } = require('../../constants');
4
5
 
5
6
  const middlewareFunctions = Array.from(
@@ -27,7 +28,14 @@ module.exports = function applyStaticHooks(model, hooks, statics) {
27
28
  if (hooks.hasHooks(key)) {
28
29
  const original = model[key];
29
30
 
30
- model[key] = hooks.createWrapper(key, original);
31
+ // Only read the `middleware` option from the static's last argument if
32
+ // the static opts in via `supportsMiddlewareOption`. Custom statics have
33
+ // arbitrary signatures, so the last argument may be an object with an
34
+ // unrelated `middleware` property.
35
+ const wrapperOptions = original.supportsMiddlewareOption === true ?
36
+ { getOptions: middlewareFiltersFromLastArg } :
37
+ undefined;
38
+ model[key] = hooks.createWrapper(key, original, null, wrapperOptions);
31
39
  }
32
40
  }
33
41
  };
@@ -7,19 +7,9 @@
7
7
  module.exports = function isPathSelectedInclusive(fields, path) {
8
8
  const chunks = path.split('.');
9
9
  let cur = '';
10
- let j;
11
- let keys;
12
- let numKeys;
13
10
  for (let i = 0; i < chunks.length; ++i) {
14
- cur += cur.length ? '.' : '' + chunks[i];
11
+ cur += cur.length ? '.' + chunks[i] : chunks[i];
15
12
  if (fields[cur]) {
16
- keys = Object.keys(fields);
17
- numKeys = keys.length;
18
- for (j = 0; j < numKeys; ++j) {
19
- if (keys[i].indexOf(cur + '.') === 0 && keys[i].indexOf(path) !== 0) {
20
- continue;
21
- }
22
- }
23
13
  return true;
24
14
  }
25
15
  }
@@ -264,7 +264,7 @@ function castComparison(val, schema, strictQuery) {
264
264
  throw new StrictModeError(path);
265
265
  }
266
266
  } else {
267
- val[1] = _castExpression(val[1]);
267
+ val[1] = _castExpression(val[1], schema, strictQuery);
268
268
  }
269
269
 
270
270
  return val;
package/lib/model.js CHANGED
@@ -1229,8 +1229,8 @@ Model.createCollection = async function createCollection(options) {
1229
1229
 
1230
1230
  // Remove middleware option before passing to MongoDB
1231
1231
  if (options?.middleware != null) {
1232
- options = { ...options };
1233
- delete options.middleware;
1232
+ const { middleware, ...driverOptions } = options;
1233
+ options = driverOptions;
1234
1234
  }
1235
1235
 
1236
1236
  [options] = await this.hooks.execPre('createCollection', this, [options], { filter: preFilter }).catch(err => {
@@ -1431,7 +1431,7 @@ Model.dropSearchIndex = async function dropSearchIndex(name) {
1431
1431
  * const Customer = mongoose.model('Customer', schema);
1432
1432
  *
1433
1433
  * await Customer.createSearchIndex({ name: 'test', definition: { mappings: { dynamic: true } } });
1434
- * const res = await Customer.listSearchIndexes(); // Includes `[{ name: 'test' }]`
1434
+ * const res = await Customer.listSearchIndexes(); // Includes `[{ id: '...', name: 'test', status: 'READY', queryable: true, latestDefinition: { ... } }]`
1435
1435
  *
1436
1436
  * @param {object} [options]
1437
1437
  * @return {Promise<Array>}
@@ -2134,6 +2134,48 @@ Model.find = function find(conditions, projection, options) {
2134
2134
  return mq.find(conditions);
2135
2135
  };
2136
2136
 
2137
+ /**
2138
+ * Finds documents and counts the number of documents matching the filter.
2139
+ *
2140
+ * This function fires both `find` and `countDocuments` middleware.
2141
+ *
2142
+ * #### Example:
2143
+ *
2144
+ * const [adventures, total] = await Adventure.findAndCount(
2145
+ * { type: 'jungle' },
2146
+ * null,
2147
+ * { sort: { name: 1 }, skip: 10, limit: 10 }
2148
+ * );
2149
+ *
2150
+ * adventures; // Array of at most 10 documents matching type 'jungle'
2151
+ * total; // Number of documents matching type 'jungle'
2152
+ *
2153
+ * @param {object|ObjectId} filter
2154
+ * @param {object|string|string[]} [projection] optional fields to return
2155
+ * @param {object} [options] optional query options
2156
+ * @return {Promise<Array>} a promise that resolves to `[documents, total]`
2157
+ * @api public
2158
+ */
2159
+
2160
+ Model.findAndCount = async function findAndCount(conditions, projection, options) {
2161
+ _checkContext(this, 'findAndCount');
2162
+
2163
+ if (options == null) {
2164
+ throw new MongooseError('Model.findAndCount() requires an `options` argument with `sort` and `limit`');
2165
+ }
2166
+ if (options.sort == null) {
2167
+ throw new MongooseError('Model.findAndCount() requires `options.sort`');
2168
+ }
2169
+ if (options.limit == null) {
2170
+ throw new MongooseError('Model.findAndCount() requires `options.limit`');
2171
+ }
2172
+
2173
+ const findQuery = this.find(conditions, projection, options);
2174
+ const countQuery = findQuery.clone().countDocuments().skip(0).limit(null);
2175
+
2176
+ return Promise.all([findQuery.exec(), countQuery.exec()]);
2177
+ };
2178
+
2137
2179
  /**
2138
2180
  * Finds a single document by its _id field. `findById(id)` is equivalent to `findOne({ _id: id })`.
2139
2181
  *
@@ -4597,8 +4639,8 @@ async function _populatePath(model, docs, populateOptions) {
4597
4639
  select = select.filter(field => field !== '-_id');
4598
4640
  } else {
4599
4641
  // preserve original select conditions by copying
4600
- select = { ...select };
4601
- delete select._id;
4642
+ const { _id, ...selectWithoutId } = select;
4643
+ select = selectWithoutId;
4602
4644
  }
4603
4645
  }
4604
4646
 
package/lib/query.js CHANGED
@@ -5329,7 +5329,8 @@ Query.prototype._applyPaths = function applyPaths() {
5329
5329
  * Returns a wrapper around a [mongodb driver cursor](https://mongodb.github.io/node-mongodb-native/7.0/classes/FindCursor.html).
5330
5330
  * A QueryCursor exposes a Streams3 interface, as well as a `.next()` function.
5331
5331
  *
5332
- * The `.cursor()` function triggers pre find hooks, but **not** post find hooks.
5332
+ * The `.cursor()` function triggers pre find hooks before opening the cursor
5333
+ * and post find hooks once per document, with an array containing that document.
5333
5334
  *
5334
5335
  * #### Example:
5335
5336
  *
@@ -5358,9 +5359,14 @@ Query.prototype._applyPaths = function applyPaths() {
5358
5359
  * #### Valid options
5359
5360
  *
5360
5361
  * - `transform`: optional function which accepts a mongoose document. The return value of the function will be emitted on `data` and returned by `.next()`.
5362
+ * - [`middleware`](https://mongoosejs.com/docs/middleware.html#skipping): set to `false` to skip all user-defined middleware, or `{ pre: false }` / `{ post: false }` to skip only pre or post find hooks
5361
5363
  *
5362
5364
  * @return {QueryCursor}
5363
5365
  * @param {object} [options]
5366
+ * @param {Function} [options.transform] optional function which accepts a mongoose document. The return value of the function will be emitted on `data` and returned by `.next()`.
5367
+ * @param {boolean|object} [options.middleware=true] set to `false` to skip all user-defined middleware
5368
+ * @param {boolean} [options.middleware.pre=true] set to `false` to skip only pre hooks
5369
+ * @param {boolean} [options.middleware.post=true] set to `false` to skip only post hooks
5364
5370
  * @see QueryCursor https://mongoosejs.com/docs/api/querycursor.html
5365
5371
  * @api public
5366
5372
  */
@@ -400,7 +400,15 @@ SchemaArray.prototype.cast = function(value, doc, init, prev, options) {
400
400
  if (options.virtuals) {
401
401
  opts.virtuals = options.virtuals;
402
402
  }
403
- rawValue[i] = caster.applySetters(rawValue[i], doc, init, void 0, opts);
403
+ if (rawValue[i] === undefined && caster.defaultValue !== undefined) {
404
+ // No need to add _skipMarkModified to opts here (fallback for `getDefault()` opts parameter)
405
+ // because array setters don't rely on Document.prototype.set() - subdocuments do rely on
406
+ // Document.prototype.set() when applying setters, which is why _skipMarkModified is necessary
407
+ // for subdocuments. But here we know that `caster` is an array not a subdocument.
408
+ rawValue[i] = caster.getDefault(doc, init, null, opts);
409
+ } else {
410
+ rawValue[i] = caster.applySetters(rawValue[i], doc, init, void 0, opts);
411
+ }
404
412
  }
405
413
  } catch (e) {
406
414
  // rethrow
@@ -466,8 +466,8 @@ SchemaDocumentArray.prototype.cast = function(value, doc, init, prev, options) {
466
466
  // Don't pass `path` to $init - it's only for this DocumentArray itself, not its element fields.
467
467
  // Element subdocuments use relative paths internally for change tracking.
468
468
  if (options.path != null) {
469
- options = { ...options };
470
- delete options.path;
469
+ const { path, ...initOptions } = options;
470
+ options = initOptions;
471
471
  }
472
472
  rawArray[i] = subdoc.$init(rawArray[i], options);
473
473
  } else {
@@ -206,8 +206,8 @@ SchemaSubdocument.prototype.cast = function(val, doc, init, priorVal, options) {
206
206
  // For change tracking, subdocuments use relative paths internally.
207
207
  // Here, `options.path` contains the absolute path and is only used by the subdocument constructor, not by $init.
208
208
  if (options.path != null) {
209
- options = { ...options };
210
- delete options.path;
209
+ const { path, ...initOptions } = options;
210
+ options = initOptions;
211
211
  }
212
212
  subdoc.$init(val, options);
213
213
  const exclude = isExclusive(selected);
package/lib/schema.js CHANGED
@@ -2280,6 +2280,37 @@ Schema.prototype.plugin = function(fn, opts) {
2280
2280
  return this;
2281
2281
  };
2282
2282
 
2283
+ /**
2284
+ * Adds a query helper to this schema. Equivalent to `schema.query[name] = fn`.
2285
+ *
2286
+ * #### Example:
2287
+ *
2288
+ * schema.queryHelper('byName', function(name) {
2289
+ * return this.where({ name });
2290
+ * });
2291
+ * const Test = mongoose.model('Test', schema);
2292
+ * await Test.find().byName('John'); // Equivalent to `Test.find({ name: 'John' })`
2293
+ *
2294
+ * @param {string} name The query helper name.
2295
+ * @param {Function} fn The query helper function.
2296
+ * @return {Schema} this
2297
+ * @api public
2298
+ */
2299
+
2300
+ Schema.prototype.queryHelper = function queryHelper(name, fn) {
2301
+ if (typeof name !== 'string') {
2302
+ throw new MongooseError('First param to `schema.queryHelper()` must be a string, ' +
2303
+ 'got "' + (typeof name) + '"');
2304
+ }
2305
+ if (typeof fn !== 'function') {
2306
+ throw new MongooseError('Second param to `schema.queryHelper()` must be a function, ' +
2307
+ 'got "' + (typeof fn) + '"');
2308
+ }
2309
+
2310
+ this.query[name] = fn;
2311
+ return this;
2312
+ };
2313
+
2283
2314
  /**
2284
2315
  * Adds an instance method to documents constructed from Models compiled from this schema.
2285
2316
  *
@@ -2310,9 +2341,21 @@ Schema.prototype.plugin = function(fn, opts) {
2310
2341
  *
2311
2342
  * NOTE: `Schema.method()` adds instance methods to the `Schema.methods` object. You can also add instance methods directly to the `Schema.methods` object as seen in the [guide](https://mongoosejs.com/docs/guide.html#methods)
2312
2343
  *
2344
+ * If the method has hooks registered via `schema.pre('meow')` or `schema.post('meow')`, you can allow callers
2345
+ * to skip them for a single call by setting `supportsMiddlewareOption = true` on the function:
2346
+ *
2347
+ * schema.method('meow', function(options = {}) {
2348
+ * console.log('meeeeeoooooooooooow');
2349
+ * });
2350
+ * schema.methods.meow.supportsMiddlewareOption = true;
2351
+ *
2352
+ * // Skips `pre('meow')` and `post('meow')` hooks
2353
+ * await fizz.meow({ middleware: false });
2354
+ *
2313
2355
  * @param {string|object} name The Method Name for a single function, or an Object of "string-function" pairs.
2314
2356
  * @param {Function} [fn] The Function in a single-function definition.
2315
2357
  * @api public
2358
+ * @see Skip Middleware https://mongoosejs.com/docs/middleware.html#skip-custom-statics-and-methods
2316
2359
  */
2317
2360
 
2318
2361
  Schema.prototype.method = function(name, fn, options) {
@@ -2355,10 +2398,22 @@ Schema.prototype.method = function(name, fn, options) {
2355
2398
  *
2356
2399
  * If a hash of name/fn pairs is passed as the only argument, each name/fn pair will be added as statics.
2357
2400
  *
2401
+ * If the static has hooks registered via `schema.pre('findByName')` or `schema.post('findByName')`, you can allow
2402
+ * callers to skip them for a single call by setting `supportsMiddlewareOption = true` on the function:
2403
+ *
2404
+ * schema.static('findByName', function(name, options = {}) {
2405
+ * return this.find({ name: name });
2406
+ * });
2407
+ * schema.statics.findByName.supportsMiddlewareOption = true;
2408
+ *
2409
+ * // Skips `pre('findByName')` and `post('findByName')` hooks
2410
+ * await Drink.findByName('LaCroix', { middleware: false });
2411
+ *
2358
2412
  * @param {string|object} name The Method Name for a single function, or an Object of "string-function" pairs.
2359
2413
  * @param {Function} [fn] The Function in a single-function definition.
2360
2414
  * @api public
2361
2415
  * @see Statics https://mongoosejs.com/docs/guide.html#statics
2416
+ * @see Skip Middleware https://mongoosejs.com/docs/middleware.html#skip-custom-statics-and-methods
2362
2417
  */
2363
2418
 
2364
2419
  Schema.prototype.static = function(name, fn) {
package/lib/schemaType.js CHANGED
@@ -176,8 +176,7 @@ SchemaType.prototype.path;
176
176
  */
177
177
 
178
178
  SchemaType.prototype.toJSON = function toJSON() {
179
- const res = { ...this };
180
- delete res.parentSchema;
179
+ const { parentSchema, ...res } = this;
181
180
  return res;
182
181
  };
183
182
 
@@ -1299,11 +1298,12 @@ SchemaType.prototype.ref = function(ref) {
1299
1298
  * @param {object} options
1300
1299
  * @param {object} [options.context]
1301
1300
  * @param {boolean} [options.skipCast]
1301
+ * @param {object} [applySettersOptions] passed to applySetters
1302
1302
  * @return {any} The Stored default value.
1303
1303
  * @api private
1304
1304
  */
1305
1305
 
1306
- SchemaType.prototype.getDefault = function getDefault(parentDoc, init, options) {
1306
+ SchemaType.prototype.getDefault = function getDefault(parentDoc, init, options, applySettersOptions) {
1307
1307
  let ret;
1308
1308
  if (this.defaultValue == null) {
1309
1309
  return this.defaultValue;
@@ -1337,7 +1337,13 @@ SchemaType.prototype.getDefault = function getDefault(parentDoc, init, options)
1337
1337
  return this._applySetters(ret, parentDoc);
1338
1338
  }
1339
1339
 
1340
- const casted = this.applySetters(ret, parentDoc, init, undefined, setOptionsForDefaults);
1340
+ const casted = this.applySetters(
1341
+ ret,
1342
+ parentDoc,
1343
+ init,
1344
+ undefined,
1345
+ applySettersOptions ?? setOptionsForDefaults
1346
+ );
1341
1347
  if (casted && !Array.isArray(casted) && casted.$isSingleNested) {
1342
1348
  casted.$__parent = parentDoc;
1343
1349
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongoose",
3
3
  "description": "Mongoose MongoDB ODM",
4
- "version": "9.9.4",
4
+ "version": "9.10.0",
5
5
  "author": "Guillermo Rauch <guillermo@learnboost.com>",
6
6
  "keywords": [
7
7
  "mongodb",
@@ -21,8 +21,8 @@
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
23
  "@standard-schema/spec": "^1.1.0",
24
- "kareem": "3.3.0",
25
- "mongodb": "~7.5",
24
+ "kareem": "3.4.0",
25
+ "mongodb": "~7.6",
26
26
  "mpath": "0.9.0",
27
27
  "mquery": "6.0.0",
28
28
  "ms": "2.1.3",
@@ -40,20 +40,20 @@
40
40
  "c8": "12.0.0",
41
41
  "cheerio": "1.2.0",
42
42
  "dox": "1.0.0",
43
- "eslint": "10.8.0",
43
+ "eslint": "10.9.1",
44
44
  "eslint-plugin-mocha-no-only": "1.2.0",
45
45
  "express": "5.2.1",
46
46
  "fs-extra": "~11.4.0",
47
47
  "glob": "^13.0.6",
48
48
  "globals": "^17.4.0",
49
- "highlight.js": "11.11.1",
49
+ "highlight.js": "11.12.0",
50
50
  "linkinator": "8.x",
51
51
  "lodash.isequal": "4.5.0",
52
52
  "lodash.isequalwith": "4.4.0",
53
53
  "markdownlint-cli2": "0.23.2",
54
- "marked": "18.0.7",
54
+ "marked": "18.0.11",
55
55
  "mkdirp": "^3.0.1",
56
- "mocha": "12.0.0-rc.5",
56
+ "mocha": "12.0.0-rc.6",
57
57
  "moment": "2.30.1",
58
58
  "mongodb-client-encryption": "^7.2.0",
59
59
  "mongodb-memory-server": "11.2.0",
@@ -64,7 +64,7 @@
64
64
  "tstyche": "^7.0.0",
65
65
  "typescript": "5.9.3",
66
66
  "typescript-eslint": "^8.31.1",
67
- "uuid": "14.0.1",
67
+ "uuid": "14.0.2",
68
68
  "xss": "1.0.15"
69
69
  },
70
70
  "directories": {