mongoose 9.9.0 → 9.9.2

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/connection.js CHANGED
@@ -778,6 +778,9 @@ function _resetSessionDocuments(session) {
778
778
  if (Object.hasOwn(state, 'versionKey')) {
779
779
  doc.set(doc.schema.options.versionKey, state.versionKey);
780
780
  }
781
+ if (Object.hasOwn(state, 'isDeleted')) {
782
+ doc.$isDeleted(state.isDeleted);
783
+ }
781
784
 
782
785
  for (const path of state.modifiedPaths) {
783
786
  doc.$__.activePaths.modify(path);
package/lib/document.js CHANGED
@@ -14,6 +14,7 @@ const ModifiedPathsSnapshot = require('./modifiedPathsSnapshot');
14
14
  const ObjectExpectedError = require('./error/objectExpected');
15
15
  const ObjectParameterError = require('./error/objectParameter');
16
16
  const ParallelValidateError = require('./error/parallelValidate');
17
+ const PathTrie = require('./helpers/pathTrie');
17
18
  const Schema = require('./schema');
18
19
  const StrictModeError = require('./error/strict');
19
20
  const ValidationError = require('./error/validation');
@@ -31,7 +32,6 @@ const getEmbeddedDiscriminatorPath = require('./helpers/document/getEmbeddedDisc
31
32
  const getKeysInSchemaOrder = require('./helpers/schema/getKeysInSchemaOrder');
32
33
  const getSubdocumentStrictValue = require('./helpers/schema/getSubdocumentStrictValue');
33
34
  const handleSpreadDoc = require('./helpers/document/handleSpreadDoc');
34
- const isBsonType = require('./helpers/isBsonType');
35
35
  const isDefiningProjection = require('./helpers/projection/isDefiningProjection');
36
36
  const isExclusive = require('./helpers/projection/isExclusive');
37
37
  const isPathExcluded = require('./helpers/projection/isPathExcluded');
@@ -5242,26 +5242,26 @@ Document.prototype.$__delta = function $__delta(pathsToSave, pathsToSaveSet) {
5242
5242
  const optimisticConcurrency = this.$__schema.options.optimisticConcurrency;
5243
5243
  if (optimisticConcurrency) {
5244
5244
  if (Array.isArray(optimisticConcurrency)) {
5245
- if (!this.$__schema.options._optimisticConcurrencySet) {
5246
- this.$__schema.options._optimisticConcurrencySet = new Set(optimisticConcurrency);
5245
+ if (!this.$__schema.options._optimisticConcurrencyTrie) {
5246
+ this.$__schema.options._optimisticConcurrencyTrie = new PathTrie(optimisticConcurrency);
5247
5247
  }
5248
- const optimisticConcurrencySet = this.$__schema.options._optimisticConcurrencySet;
5248
+ const optimisticConcurrencyTrie = this.$__schema.options._optimisticConcurrencyTrie;
5249
5249
  const modPaths = this.directModifiedPaths();
5250
5250
  const hasRelevantModPaths = pathsToSave == null ?
5251
- modPaths.find(path => _pathOverlapsSet(path, optimisticConcurrencySet)) :
5252
- modPaths.find(path => _pathOverlapsSet(path, optimisticConcurrencySet) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5251
+ modPaths.find(path => optimisticConcurrencyTrie.overlapsPath(path.indexOf('.') === -1 ? path : path.split('.'))) :
5252
+ modPaths.find(path => optimisticConcurrencyTrie.overlapsPath(path.indexOf('.') === -1 ? path : path.split('.')) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5253
5253
  if (hasRelevantModPaths) {
5254
5254
  this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
5255
5255
  }
5256
5256
  } else if (Array.isArray(optimisticConcurrency?.exclude)) {
5257
- if (!this.$__schema.options._optimisticConcurrencyExcludeSet) {
5258
- this.$__schema.options._optimisticConcurrencyExcludeSet = new Set(optimisticConcurrency.exclude);
5257
+ if (!this.$__schema.options._optimisticConcurrencyExcludeTrie) {
5258
+ this.$__schema.options._optimisticConcurrencyExcludeTrie = new PathTrie(optimisticConcurrency.exclude);
5259
5259
  }
5260
- const optimisticConcurrencyExcludeSet = this.$__schema.options._optimisticConcurrencyExcludeSet;
5260
+ const optimisticConcurrencyExcludeTrie = this.$__schema.options._optimisticConcurrencyExcludeTrie;
5261
5261
  const modPaths = this.directModifiedPaths();
5262
5262
  const hasRelevantModPaths = pathsToSave == null ?
5263
- modPaths.find(path => !_pathOrAncestorInSet(path, optimisticConcurrencyExcludeSet)) :
5264
- modPaths.find(path => !_pathOrAncestorInSet(path, optimisticConcurrencyExcludeSet) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5263
+ modPaths.find(path => !optimisticConcurrencyExcludeTrie.matchesPathOrAncestor(path.indexOf('.') === -1 ? path : path.split('.'))) :
5264
+ modPaths.find(path => !optimisticConcurrencyExcludeTrie.matchesPathOrAncestor(path.indexOf('.') === -1 ? path : path.split('.')) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5265
5265
  if (hasRelevantModPaths) {
5266
5266
  this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
5267
5267
  }
@@ -5755,11 +5755,21 @@ Document.prototype.$__hasOnlyPrimitiveValues = function $__hasOnlyPrimitiveValue
5755
5755
  const doc = this._doc;
5756
5756
  for (const key in doc) {
5757
5757
  const v = doc[key];
5758
- if (v == null
5759
- || typeof v !== 'object'
5760
- || (utils.isNativeObject(v) && !Array.isArray(v))
5761
- || isBsonType(v, 'ObjectId')
5762
- || isBsonType(v, 'Decimal128')) {
5758
+ if (v == null || typeof v !== 'object') {
5759
+ continue;
5760
+ }
5761
+ // Equivalent to `isNativeObject(v) && !Array.isArray(v)`, plus the
5762
+ // `isBsonType()` checks below, but without the helper calls or the
5763
+ // repeated `Array.isArray()`. This runs for every key of every document
5764
+ // that `toObject()` touches, so the call overhead is measurable.
5765
+ if (Array.isArray(v)) {
5766
+ return false;
5767
+ }
5768
+ if (v instanceof Date || v instanceof Boolean || v instanceof Number || v instanceof String) {
5769
+ continue;
5770
+ }
5771
+ const bsontype = v._bsontype;
5772
+ if (bsontype === 'ObjectId' || bsontype === 'Decimal128') {
5763
5773
  continue;
5764
5774
  }
5765
5775
  return false;
@@ -5787,45 +5797,6 @@ Document.prototype._applyVersionIncrement = function _applyVersionIncrement() {
5787
5797
  * Module exports.
5788
5798
  */
5789
5799
 
5790
- /*!
5791
- * Check if `path` or any of its ancestor paths exist in `pathSet`.
5792
- * For example:
5793
- * _pathOrAncestorInSet('profile.firstName', Set(['profile'])) === true
5794
- * _pathOrAncestorInSet('profile', Set(['profile.firstName'])) === false
5795
- */
5796
- function _pathOrAncestorInSet(path, pathSet) {
5797
- if (pathSet.has(path)) {
5798
- return true;
5799
- }
5800
- let idx = path.indexOf('.');
5801
- while (idx !== -1) {
5802
- if (pathSet.has(path.substring(0, idx))) {
5803
- return true;
5804
- }
5805
- idx = path.indexOf('.', idx + 1);
5806
- }
5807
- return false;
5808
- }
5809
-
5810
- /*!
5811
- * Check if `path`, any of its ancestor paths, or any of its descendant paths
5812
- * exist in `pathSet`.
5813
- * For example:
5814
- * _pathOverlapsSet('profile.firstName', Set(['profile'])) === true
5815
- * _pathOverlapsSet('profile', Set(['profile.firstName'])) === true
5816
- */
5817
- function _pathOverlapsSet(path, pathSet) {
5818
- if (_pathOrAncestorInSet(path, pathSet)) {
5819
- return true;
5820
- }
5821
- for (const p of pathSet) {
5822
- if (p.length > path.length + 1 && p[path.length] === '.' && p.slice(0, path.length) === path) {
5823
- return true;
5824
- }
5825
- }
5826
- return false;
5827
- }
5828
-
5829
5800
  Document.VERSION_WHERE = VERSION_WHERE;
5830
5801
  Document.VERSION_INC = VERSION_INC;
5831
5802
  Document.VERSION_ALL = VERSION_ALL;
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ const wildcardSegment = '$*';
4
+ const numericSegmentRegex = /^\d+$/;
5
+
6
+ class PathTrieNode {
7
+ constructor() {
8
+ this.children = new Map();
9
+ this.terminal = false;
10
+ this.hasWildcard = false;
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Trie of dotted paths for efficient path matching. Supports the map wildcard
16
+ * segment `$*`, which matches any single segment. For example, a trie
17
+ * containing `settings.$*` matches `settings.theme` but not `settings` or
18
+ * `profile.theme`. In array lookups, numeric segments like array indexes are
19
+ * ignorable: a trie containing `comments.text` matches `comments.0.text`.
20
+ * Single-segment string lookups skip the numeric check entirely.
21
+ *
22
+ * @api private
23
+ */
24
+
25
+ class PathTrie {
26
+ /**
27
+ * @param {string[]} [paths] paths to add to the trie
28
+ */
29
+ constructor(paths) {
30
+ this.root = new PathTrieNode();
31
+ if (paths != null) {
32
+ for (const path of paths) {
33
+ this.add(path);
34
+ }
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Add a dotted path, like `settings.$*` or `profile.firstName`, to the trie.
40
+ *
41
+ * @param {string} path
42
+ */
43
+
44
+ add(path) {
45
+ if (path.indexOf('.') === -1) {
46
+ this._addSegment(this.root, path).terminal = true;
47
+ return;
48
+ }
49
+ let node = this.root;
50
+ for (const segment of path.split('.')) {
51
+ node = this._addSegment(node, segment);
52
+ }
53
+ node.terminal = true;
54
+ }
55
+
56
+ /**
57
+ * @param {PathTrieNode} node
58
+ * @param {string} segment
59
+ */
60
+ _addSegment(node, segment) {
61
+ let child = node.children.get(segment);
62
+ if (child == null) {
63
+ child = new PathTrieNode();
64
+ node.children.set(segment, child);
65
+ if (segment === wildcardSegment) {
66
+ node.hasWildcard = true;
67
+ }
68
+ }
69
+ return child;
70
+ }
71
+
72
+ /**
73
+ * Check if `path` or any of its ancestor paths exist in the trie. `path` is
74
+ * either a single segment string or a pre-split array of segments — lookups
75
+ * never split, callers are responsible for splitting dotted paths.
76
+ * For example, with a trie containing 'profile':
77
+ * matchesPathOrAncestor(['profile', 'firstName']) === true
78
+ * matchesPathOrAncestor('profile') === true
79
+ * But with a trie containing 'profile.firstName':
80
+ * matchesPathOrAncestor('profile') === false
81
+ *
82
+ * @param {string|string[]} path
83
+ */
84
+
85
+ matchesPathOrAncestor(path) {
86
+ return this._walk(path) === true;
87
+ }
88
+
89
+ /**
90
+ * Check if `path`, any of its ancestor paths, or any of its descendant paths
91
+ * exist in the trie. `path` is either a single segment string or a pre-split
92
+ * array of segments — lookups never split, callers are responsible for
93
+ * splitting dotted paths. For example:
94
+ * new PathTrie(['profile']).overlapsPath(['profile', 'firstName']) === true
95
+ * new PathTrie(['profile.firstName']).overlapsPath('profile') === true
96
+ *
97
+ * @param {string|string[]} path
98
+ */
99
+
100
+ overlapsPath(path) {
101
+ return this._walk(path) !== false;
102
+ }
103
+
104
+ /**
105
+ * Walk `path` through the trie. `path` is assumed to be pre-split - if the
106
+ * path has multiple segments, pass in an array of strings. Returns `true` if
107
+ * a terminal node was found at `path` or one of its ancestors, `false` if the
108
+ * walk dead-ended, and the array of reached nodes if `path` is a strict prefix
109
+ * of entries in the trie.
110
+ *
111
+ * @param {string|string[]} path
112
+ */
113
+
114
+ _walk(path) {
115
+ if (typeof path === 'string') {
116
+ const next = [];
117
+ const literal = this.root.children.get(path);
118
+ if (literal != null) {
119
+ if (literal.terminal) {
120
+ return true;
121
+ }
122
+ next.push(literal);
123
+ }
124
+ if (this.root.hasWildcard) {
125
+ const wildcard = this.root.children.get(wildcardSegment);
126
+ if (wildcard.terminal) {
127
+ return true;
128
+ }
129
+ next.push(wildcard);
130
+ }
131
+ return next.length === 0 ? false : next;
132
+ }
133
+ let nodes = [this.root];
134
+ for (const segment of path) {
135
+ const next = [];
136
+ const isNumericSegment = numericSegmentRegex.test(segment);
137
+ for (const node of nodes) {
138
+ if (isNumericSegment) {
139
+ // Numeric segments like array indexes are ignorable: `comments.text`
140
+ // in the trie matches the modified path `comments.0.text`.
141
+ next.push(node);
142
+ }
143
+ const literal = node.children.get(segment);
144
+ if (literal != null) {
145
+ if (literal.terminal) {
146
+ return true;
147
+ }
148
+ next.push(literal);
149
+ }
150
+ if (node.hasWildcard) {
151
+ const wildcard = node.children.get(wildcardSegment);
152
+ if (wildcard.terminal) {
153
+ return true;
154
+ }
155
+ next.push(wildcard);
156
+ }
157
+ }
158
+ if (next.length === 0) {
159
+ return false;
160
+ }
161
+ nodes = next;
162
+ }
163
+ return nodes;
164
+ }
165
+ }
166
+
167
+ module.exports = PathTrie;
package/lib/model.js CHANGED
@@ -3622,6 +3622,15 @@ Model.bulkSave = async function bulkSave(documents, options) {
3622
3622
  }
3623
3623
  }
3624
3624
 
3625
+ // Associate the documents with the session like `save()` does, so built-in
3626
+ // `pre('save')` middleware like `trackTransaction` can snapshot document
3627
+ // state and restore it between transaction retries. Re: gh-16432
3628
+ if (Object.hasOwn(options, 'session')) {
3629
+ for (const document of documents) {
3630
+ document.$session(options.session);
3631
+ }
3632
+ }
3633
+
3625
3634
  await Promise.all(documents.map(doc => buildPreSavePromise(doc, options)));
3626
3635
 
3627
3636
  const writeOperations = this.buildBulkWriteOperations(documents, options);
@@ -7,31 +7,45 @@ const utils = require('../utils');
7
7
 
8
8
  module.exports = function trackTransaction(schema) {
9
9
  schema.pre('save', trackTransactionPreSave);
10
+ schema.pre('deleteOne', { document: true, query: false }, trackTransactionPreDeleteOne);
10
11
  };
11
12
 
12
13
  function trackTransactionPreSave() {
13
- const session = this.$session();
14
+ _getInitialState(this);
15
+ }
16
+
17
+ function trackTransactionPreDeleteOne() {
18
+ const initialState = _getInitialState(this);
19
+ if (initialState != null && !Object.hasOwn(initialState, 'isDeleted')) {
20
+ initialState.isDeleted = this.$isDeleted();
21
+ }
22
+ }
23
+
24
+ function _getInitialState(doc) {
25
+ const session = doc.$session();
14
26
  if (session == null) {
15
- return;
27
+ return null;
16
28
  }
17
29
  if (session.transaction == null || session[sessionNewDocuments] == null) {
18
- return;
30
+ return null;
19
31
  }
20
32
 
21
- if (!session[sessionNewDocuments].has(this)) {
33
+ if (!session[sessionNewDocuments].has(doc)) {
22
34
  const initialState = {};
23
- if (this.isNew) {
35
+ if (doc.isNew) {
24
36
  initialState.isNew = true;
25
37
  }
26
- if (this.$__schema.options.versionKey) {
27
- initialState.versionKey = this.get(this.$__schema.options.versionKey);
38
+ if (doc.$__schema.options.versionKey) {
39
+ initialState.versionKey = doc.get(doc.$__schema.options.versionKey);
28
40
  }
29
41
 
30
- initialState.modifiedPaths = new Set(Object.keys(this.$__.activePaths.getStatePaths('modify')));
31
- initialState.atomics = _getAtomics(this);
42
+ initialState.modifiedPaths = new Set(Object.keys(doc.$__.activePaths.getStatePaths('modify')));
43
+ initialState.atomics = _getAtomics(doc);
32
44
 
33
- session[sessionNewDocuments].set(this, initialState);
45
+ session[sessionNewDocuments].set(doc, initialState);
34
46
  }
47
+
48
+ return session[sessionNewDocuments].get(doc);
35
49
  }
36
50
 
37
51
  function _getAtomics(doc, previous) {
@@ -87,3 +101,4 @@ function mergeAtomics(destination, source) {
87
101
  }
88
102
 
89
103
  trackTransactionPreSave[symbols.builtInMiddleware] = true;
104
+ trackTransactionPreDeleteOne[symbols.builtInMiddleware] = true;
package/lib/query.js CHANGED
@@ -2849,12 +2849,7 @@ Query.prototype.findOne = function(conditions, projection, options) {
2849
2849
 
2850
2850
  Query.prototype._countDocuments = async function _countDocuments() {
2851
2851
  this._applyTranslateAliases();
2852
-
2853
- try {
2854
- this.cast(this.model);
2855
- } catch (err) {
2856
- this.error(err);
2857
- }
2852
+ this._castConditions();
2858
2853
 
2859
2854
  if (this.error()) {
2860
2855
  throw this.error();
@@ -5377,11 +5372,17 @@ Query.prototype.cursor = function cursor(opts) {
5377
5372
  }
5378
5373
 
5379
5374
  try {
5380
- this.cast(this.model);
5375
+ this._castConditions();
5381
5376
  } catch (err) {
5377
+ // `_castConditions()` reports cast errors on the query, but `sanitizeFilter`
5378
+ // throws for filters it refuses outright, like `$where`.
5382
5379
  return (new QueryCursor(this))._markError(err);
5383
5380
  }
5384
5381
 
5382
+ if (this.error()) {
5383
+ return (new QueryCursor(this))._markError(this.error());
5384
+ }
5385
+
5385
5386
  return new QueryCursor(this);
5386
5387
  };
5387
5388
 
@@ -209,6 +209,13 @@ exports.applyPaths = function applyPaths(fields, schema, sanitizeProjection) {
209
209
  switch (exclude) {
210
210
  case true:
211
211
  for (const fieldName of excluded) {
212
+ // Skip if an ancestor path is already excluded in `fields` (for
213
+ // example the user did `.select('-subd')` and `subd.raw` has
214
+ // schema-level `select: false`) to avoid MongoDB's "Path collision"
215
+ // error from projecting both `subd` and `subd.raw`. See gh-12798
216
+ if (hasExcludedAncestor(fields, fieldName)) {
217
+ continue;
218
+ }
212
219
  fields[fieldName] = 0;
213
220
  }
214
221
  break;
@@ -372,6 +379,21 @@ exports.applyPaths = function applyPaths(fields, schema, sanitizeProjection) {
372
379
  (type.selected ? selected : excluded).push(path);
373
380
  return path;
374
381
  }
382
+
383
+ function hasExcludedAncestor(fields, path) {
384
+ let i = -1;
385
+ while ((i = path.indexOf('.', i + 1)) !== -1) {
386
+ const ancestor = fields[path.slice(0, i)];
387
+ // Any falsy defining value (`0`, `false`, `''`, `NaN`) excludes a
388
+ // path, matching how `exclude` itself is derived above (`!field`).
389
+ // `ancestor == null` means the key isn't present at all, which is
390
+ // not an exclusion. See gh-12798
391
+ if (ancestor != null && !ancestor) {
392
+ return true;
393
+ }
394
+ }
395
+ return false;
396
+ }
375
397
  };
376
398
 
377
399
  /**
@@ -498,7 +498,7 @@ SchemaNumber.prototype.castForQuery = function($conditional, val, context) {
498
498
  */
499
499
 
500
500
  SchemaNumber.prototype.toJSONSchema = function toJSONSchema(options) {
501
- return this._createJSONSchemaTypeDefinition('number', 'number', options);
501
+ return this._addJSONSchemaEnum(this._createJSONSchemaTypeDefinition('number', 'number', options));
502
502
  };
503
503
 
504
504
  /*!
@@ -719,7 +719,7 @@ SchemaString.prototype.castForQuery = function($conditional, val, context) {
719
719
  */
720
720
 
721
721
  SchemaString.prototype.toJSONSchema = function toJSONSchema(options) {
722
- return this._createJSONSchemaTypeDefinition('string', 'string', options);
722
+ return this._addJSONSchemaEnum(this._createJSONSchemaTypeDefinition('string', 'string', options));
723
723
  };
724
724
 
725
725
  SchemaString.prototype.autoEncryptionType = function autoEncryptionType() {
package/lib/schema.js CHANGED
@@ -7,6 +7,7 @@
7
7
  const EventEmitter = require('events').EventEmitter;
8
8
  const Kareem = require('kareem');
9
9
  const MongooseError = require('./error/mongooseError');
10
+ const PathTrie = require('./helpers/pathTrie');
10
11
  const SchemaType = require('./schemaType');
11
12
  const SchemaTypeOptions = require('./options/schemaTypeOptions');
12
13
  const VirtualOptions = require('./options/virtualOptions');
@@ -79,7 +80,7 @@ const numberRE = /^\d+$/;
79
80
  * - [validateBeforeSave](https://mongoosejs.com/docs/guide.html#validateBeforeSave) - bool - defaults to `true`
80
81
  * - [validateModifiedOnly](https://mongoosejs.com/docs/api/document.html#Document.prototype.validate()) - bool - defaults to `false`
81
82
  * - [versionKey](https://mongoosejs.com/docs/guide.html#versionKey): string or object - defaults to "__v"
82
- * - [optimisticConcurrency](https://mongoosejs.com/docs/guide.html#optimisticConcurrency): bool or string[] or { exclude: string[] } - defaults to false. Set to true to enable [optimistic concurrency](https://thecodebarbarian.com/whats-new-in-mongoose-5-10-optimistic-concurrency.html) for all fields. Set to a string array to enable optimistic concurrency only for the specified fields; note that this **replaces** the default array versioning behavior. Set to `{ exclude: string[] }` to enable optimistic concurrency for all fields except the specified ones; this also replaces the default array versioning.
83
+ * - [optimisticConcurrency](https://mongoosejs.com/docs/guide.html#optimisticConcurrency): bool or string[] or { exclude: string[] } - defaults to false. Set to true to enable [optimistic concurrency](https://thecodebarbarian.com/whats-new-in-mongoose-5-10-optimistic-concurrency.html) for all fields. Set to a string array to enable optimistic concurrency only for the specified fields; note that this **replaces** the default array versioning behavior. Set to `{ exclude: string[] }` to enable optimistic concurrency for all fields except the specified ones; this also replaces the default array versioning. Paths may include the map wildcard `$*`, like `'settings.$*'`, to match all keys of a map.
83
84
  * - [collation](https://mongoosejs.com/docs/guide.html#collation): object - defaults to null (which means use no collation)
84
85
  * - [timeseries](https://mongoosejs.com/docs/guide.html#timeseries): object - defaults to null (which means this schema's collection won't be a timeseries collection)
85
86
  * - [selectPopulatedPaths](https://mongoosejs.com/docs/guide.html#selectPopulatedPaths): boolean - defaults to `true`
@@ -3142,9 +3143,9 @@ Schema.prototype._preCompile = function _preCompile() {
3142
3143
  };
3143
3144
 
3144
3145
  /*!
3145
- * Build precomputed sets for optimisticConcurrency include/exclude,
3146
- * expanding user-specified paths to include all schema subpaths so that
3147
- * lookups at save time are a simple `Set.has()`.
3146
+ * Build precomputed path tries for optimisticConcurrency include/exclude so
3147
+ * that lookups at save time are a single trie walk that handles nested paths
3148
+ * and map wildcards like `settings.$*`.
3148
3149
  */
3149
3150
 
3150
3151
  function _precomputeOptimisticConcurrency(schema) {
@@ -3153,9 +3154,9 @@ function _precomputeOptimisticConcurrency(schema) {
3153
3154
  return;
3154
3155
  }
3155
3156
  if (Array.isArray(opt)) {
3156
- schema.options._optimisticConcurrencySet = new Set(opt);
3157
+ schema.options._optimisticConcurrencyTrie = new PathTrie(opt);
3157
3158
  } else if (Array.isArray(opt.exclude)) {
3158
- schema.options._optimisticConcurrencyExcludeSet = new Set(opt.exclude);
3159
+ schema.options._optimisticConcurrencyExcludeTrie = new PathTrie(opt.exclude);
3159
3160
  }
3160
3161
  }
3161
3162
 
@@ -3215,27 +3216,19 @@ Schema.prototype.toJSONSchema = function toJSONSchema(options) {
3215
3216
  }
3216
3217
 
3217
3218
  const lastSubpath = schemaType._presplitPath[schemaType._presplitPath.length - 1];
3218
- let isRequired = false;
3219
3219
  if (path === '_id') {
3220
3220
  if (!jsonSchemaForPath.required) {
3221
3221
  jsonSchemaForPath.required = [];
3222
3222
  }
3223
3223
  jsonSchemaForPath.required.push('_id');
3224
- isRequired = true;
3225
3224
  } else if (schemaType.options.required && typeof schemaType.options.required !== 'function') {
3226
3225
  if (!jsonSchemaForPath.required) {
3227
3226
  jsonSchemaForPath.required = [];
3228
3227
  }
3229
3228
  // Only `required: true` paths are required, conditional required is not required
3230
3229
  jsonSchemaForPath.required.push(lastSubpath);
3231
- isRequired = true;
3232
3230
  }
3233
3231
  jsonSchemaForPath.properties[lastSubpath] = schemaType.toJSONSchema(options);
3234
- if (schemaType.options.enum) {
3235
- jsonSchemaForPath.properties[lastSubpath].enum = isRequired
3236
- ? schemaType.options.enum
3237
- : [...schemaType.options.enum, null];
3238
- }
3239
3232
  }
3240
3233
 
3241
3234
  // Otherwise MongoDB errors with "$jsonSchema keyword 'required' cannot be an empty array"
package/lib/schemaType.js CHANGED
@@ -215,6 +215,27 @@ SchemaType.prototype._createJSONSchemaTypeDefinition = function _createJSONSchem
215
215
  return { type: [type, 'null'] };
216
216
  };
217
217
 
218
+ /**
219
+ * Helper for adding this SchemaType's enum values to a JSON schema type definition.
220
+ * Reads `enumValues` rather than `options.enum` because `enum` also accepts an object.
221
+ *
222
+ * @param {object} definition the type definition from `_createJSONSchemaTypeDefinition()`
223
+ * @returns {object} the same definition
224
+ * @api private
225
+ */
226
+
227
+ SchemaType.prototype._addJSONSchemaEnum = function _addJSONSchemaEnum(definition) {
228
+ if (!Array.isArray(this.enumValues) || this.enumValues.length === 0) {
229
+ return definition;
230
+ }
231
+
232
+ // The enum validator allows nullish values, so allow `null` wherever the type does.
233
+ const allowsNull = Array.isArray(definition.type ?? definition.bsonType);
234
+ definition.enum = allowsNull ? [...this.enumValues, null] : [...this.enumValues];
235
+
236
+ return definition;
237
+ };
238
+
218
239
  /**
219
240
  * The validators that Mongoose should run to validate properties at this SchemaType's path.
220
241
  *
package/lib/utils.js CHANGED
@@ -421,26 +421,16 @@ exports.isPOJO = require('./helpers/isPOJO');
421
421
 
422
422
  exports.isNonBuiltinObject = function isNonBuiltinObject(val) {
423
423
  return typeof val === 'object' &&
424
- !exports.isNativeObject(val) &&
424
+ !Array.isArray(val) &&
425
+ !(val instanceof Date) &&
426
+ !(val instanceof Boolean) &&
427
+ !(val instanceof Number) &&
428
+ !(val instanceof String) &&
425
429
  !exports.isMongooseType(val) &&
426
430
  !(val instanceof UUID) &&
427
431
  val != null;
428
432
  };
429
433
 
430
- /**
431
- * Determines if `obj` is a built-in object like an array, date, boolean,
432
- * etc.
433
- * @param {any} arg
434
- */
435
-
436
- exports.isNativeObject = function(arg) {
437
- return Array.isArray(arg) ||
438
- arg instanceof Date ||
439
- arg instanceof Boolean ||
440
- arg instanceof Number ||
441
- arg instanceof String;
442
- };
443
-
444
434
  /**
445
435
  * Determines if `val` is an object that has no own keys
446
436
  * @param {any} val
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongoose",
3
3
  "description": "Mongoose MongoDB ODM",
4
- "version": "9.9.0",
4
+ "version": "9.9.2",
5
5
  "author": "Guillermo Rauch <guillermo@learnboost.com>",
6
6
  "keywords": [
7
7
  "mongodb",
@@ -29,7 +29,7 @@
29
29
  "sift": "17.1.3"
30
30
  },
31
31
  "devDependencies": {
32
- "@ark/attest": "0.56.1",
32
+ "@ark/attest": "0.56.3",
33
33
  "@eslint/js": "^9.39.3",
34
34
  "@mongodb-js/mongodb-downloader": "^1.0.0",
35
35
  "@types/node": "^20.19.0",
@@ -37,30 +37,30 @@
37
37
  "acquit-ignore": "0.2.2",
38
38
  "acquit-require": "0.1.1",
39
39
  "ajv": "8.20.0",
40
- "c8": "11.0.0",
40
+ "c8": "12.0.0",
41
41
  "cheerio": "1.2.0",
42
42
  "dox": "1.0.0",
43
- "eslint": "10.6.0",
43
+ "eslint": "10.8.0",
44
44
  "eslint-plugin-mocha-no-only": "1.2.0",
45
45
  "express": "5.2.1",
46
- "fs-extra": "~11.3.0",
46
+ "fs-extra": "~11.4.0",
47
47
  "glob": "^13.0.6",
48
48
  "globals": "^17.4.0",
49
49
  "highlight.js": "11.11.1",
50
- "linkinator": "7.x",
50
+ "linkinator": "8.x",
51
51
  "lodash.isequal": "4.5.0",
52
52
  "lodash.isequalwith": "4.4.0",
53
- "markdownlint-cli2": "0.22.1",
54
- "marked": "18.0.5",
53
+ "markdownlint-cli2": "0.23.2",
54
+ "marked": "18.0.7",
55
55
  "mkdirp": "^3.0.1",
56
- "mocha": "12.0.0-beta-10",
56
+ "mocha": "12.0.0-rc.5",
57
57
  "moment": "2.30.1",
58
58
  "mongodb-client-encryption": "^7.2.0",
59
59
  "mongodb-memory-server": "11.2.0",
60
60
  "mongodb-runner": "^6.0.0",
61
61
  "ncp": "^2.0.0",
62
62
  "pug": "3.0.4",
63
- "sinon": "22.0.0",
63
+ "sinon": "22.1.0",
64
64
  "tstyche": "^7.0.0",
65
65
  "typescript": "5.9.3",
66
66
  "typescript-eslint": "^8.31.1",
@@ -100,7 +100,7 @@
100
100
  "publish-7x": "npm publish --tag 7x",
101
101
  "create-separate-require-instance": "rm -rf ./node_modules/mongoose-separate-require-instance && node ./scripts/create-tarball && tar -xzf mongoose.tgz -C ./node_modules && mv ./node_modules/package ./node_modules/mongoose-separate-require-instance",
102
102
  "test": "mocha --exit --ignore \"test/encryption/**/*.test.js\" \"./test/**/*.test.js\"",
103
- "test:ci": "npm run test -- --reporter min",
103
+ "test:ci": "npm run test -- --reporter min --timeout 10000",
104
104
  "test-deno": "deno run --allow-env --allow-read --allow-net --allow-run --allow-sys --allow-write ./test/deno.mjs",
105
105
  "test-deno:ci": "npm run test-deno -- --reporter min",
106
106
  "test-rs": "START_REPLICA_SET=1 mocha --timeout 30000 --exit --ignore \"test/encryption/**/*.test.js\" \"./test/**/*.test.js\"",
@@ -289,9 +289,9 @@ declare module 'mongoose' {
289
289
  ): ToObjectReturnType<PopulatedRawDocType, TVirtuals, O, TSchemaOptions>;
290
290
  toJSON<PopulatedRawDocType>(
291
291
  this: PopulatedDocumentMarker<PopulatedRawDocType, any>
292
- ): Default__v<Require_id<PopulatedRawDocType>, TSchemaOptions>;
292
+ ): DefaultToObjectReturnType<PopulatedRawDocType, TVirtuals, TSchemaOptions, 'toJSON'>;
293
293
  toJSON<O extends ToObjectOptions>(options: O): ToObjectReturnType<DocType, TVirtuals, O, TSchemaOptions>;
294
- toJSON(options?: ToObjectOptions): Default__v<Require_id<DocType>, TSchemaOptions>;
294
+ toJSON(options?: ToObjectOptions): DefaultToObjectReturnType<DocType, TVirtuals, TSchemaOptions, 'toJSON'>;
295
295
  toJSON<T>(options?: ToObjectOptions): Default__v<Require_id<T>, ResolveSchemaOptions<TSchemaOptions>>;
296
296
 
297
297
  /** Converts this document into a plain-old JavaScript object ([POJO](https://masteringjs.io/tutorials/fundamentals/pojo)). */
@@ -309,9 +309,9 @@ declare module 'mongoose' {
309
309
  ): ToObjectReturnType<PopulatedRawDocType, TVirtuals, O, TSchemaOptions>;
310
310
  toObject<PopulatedRawDocType>(
311
311
  this: PopulatedDocumentMarker<PopulatedRawDocType, any>
312
- ): Default__v<Require_id<PopulatedRawDocType>, TSchemaOptions>;
312
+ ): DefaultToObjectReturnType<PopulatedRawDocType, TVirtuals, TSchemaOptions, 'toObject'>;
313
313
  toObject<O extends ToObjectOptions>(options: O): ToObjectReturnType<DocType, TVirtuals, O, TSchemaOptions>;
314
- toObject(options?: ToObjectOptions): Default__v<Require_id<DocType>, TSchemaOptions>;
314
+ toObject(options?: ToObjectOptions): DefaultToObjectReturnType<DocType, TVirtuals, TSchemaOptions, 'toObject'>;
315
315
  toObject<T>(options?: ToObjectOptions): Default__v<Require_id<T>, ResolveSchemaOptions<TSchemaOptions>>;
316
316
 
317
317
  /** Clears the modified state on the specified path. */
package/types/index.d.ts CHANGED
@@ -1122,6 +1122,31 @@ declare module 'mongoose' {
1122
1122
  ? { [K in keyof T]: ApplyFlattenTransforms<T[K], O> }
1123
1123
  : T;
1124
1124
 
1125
+ /**
1126
+ * Extracts the `toObject` or `toJSON` options declared in the schema options.
1127
+ * The runtime applies these as defaults when the corresponding method is
1128
+ * called without arguments.
1129
+ */
1130
+ export type SchemaDeclaredToObjectOptions<TSchemaOptions, Key extends 'toObject' | 'toJSON'> =
1131
+ Key extends keyof TSchemaOptions
1132
+ ? NonNullable<TSchemaOptions[Key]> extends infer O extends ToObjectOptions
1133
+ ? O
1134
+ : {}
1135
+ : {};
1136
+
1137
+ /**
1138
+ * Computes the return type of toObject/toJSON when called without arguments,
1139
+ * applying the options declared in the schema like the runtime does. When the
1140
+ * schema declares no options for the method, the plain document shape is
1141
+ * returned without running it through the option transforms.
1142
+ */
1143
+ export type DefaultToObjectReturnType<DocType, TVirtuals, TSchemaOptions, Key extends 'toObject' | 'toJSON'> =
1144
+ SchemaDeclaredToObjectOptions<TSchemaOptions, Key> extends infer O extends ToObjectOptions
1145
+ ? keyof O extends never
1146
+ ? Default__v<Require_id<DocType>, TSchemaOptions>
1147
+ : ToObjectReturnType<DocType, TVirtuals, O, TSchemaOptions>
1148
+ : Default__v<Require_id<DocType>, TSchemaOptions>;
1149
+
1125
1150
  /**
1126
1151
  * Computes the return type of toObject/toJSON based on the provided options.
1127
1152
  * Uses a single-pass transform for flatten operations to correctly handle all combinations.
package/types/models.d.ts CHANGED
@@ -457,6 +457,18 @@ declare module 'mongoose' {
457
457
  'findOne',
458
458
  TInstanceMethods & TVirtuals
459
459
  >;
460
+ findById<ResultDoc = THydratedDocumentType>(
461
+ id: any,
462
+ projection: ProjectionType<TRawDocType> | null | undefined,
463
+ options: QueryOptions<TRawDocType> & { lean: false }
464
+ ): QueryWithHelpers<
465
+ ResultDoc | null,
466
+ ResultDoc,
467
+ TQueryHelpers,
468
+ TLeanResultType,
469
+ 'findOne',
470
+ TInstanceMethods & TVirtuals
471
+ >;
460
472
  findById<ResultDoc = THydratedDocumentType>(
461
473
  id?: any,
462
474
  projection?: ProjectionType<TRawDocType> | null | undefined,
@@ -495,6 +507,18 @@ declare module 'mongoose' {
495
507
  'findOne',
496
508
  TInstanceMethods & TVirtuals
497
509
  >;
510
+ findOne<ResultDoc = THydratedDocumentType>(
511
+ filter: QueryFilter<TRawDocType>,
512
+ projection: ProjectionType<TRawDocType> | null | undefined,
513
+ options: QueryOptions<TRawDocType> & { lean: false } & mongodb.Abortable
514
+ ): QueryWithHelpers<
515
+ ResultDoc | null,
516
+ ResultDoc,
517
+ TQueryHelpers,
518
+ TLeanResultType,
519
+ 'findOne',
520
+ TInstanceMethods & TVirtuals
521
+ >;
498
522
  findOne<ResultDoc = THydratedDocumentType>(
499
523
  filter?: QueryFilter<TRawDocType>,
500
524
  projection?: ProjectionType<TRawDocType> | null | undefined,
@@ -780,12 +804,24 @@ declare module 'mongoose' {
780
804
  'find',
781
805
  TInstanceMethods & TVirtuals
782
806
  >;
807
+ find<ResultDoc = THydratedDocumentType>(
808
+ filter: QueryFilter<TRawDocType>,
809
+ projection: ProjectionType<TRawDocType> | null | undefined,
810
+ options: QueryOptions<TRawDocType> & { lean: false } & mongodb.Abortable
811
+ ): QueryWithHelpers<
812
+ ResultDoc[],
813
+ ResultDoc,
814
+ TQueryHelpers,
815
+ TLeanResultType,
816
+ 'find',
817
+ TInstanceMethods & TVirtuals
818
+ >;
783
819
  find<ResultDoc = THydratedDocumentType>(
784
820
  filter?: QueryFilter<TRawDocType>,
785
821
  projection?: ProjectionType<TRawDocType> | null | undefined,
786
822
  options?: QueryOptions<TRawDocType> & mongodb.Abortable
787
823
  ): QueryWithHelpers<
788
- ResultDoc[],
824
+ HasLeanOption<TSchema> extends true ? TLeanResultType[] : ResultDoc[],
789
825
  ResultDoc,
790
826
  TQueryHelpers,
791
827
  TLeanResultType,
@@ -797,7 +833,7 @@ declare module 'mongoose' {
797
833
  projection?: ProjectionType<TRawDocType> | null | undefined,
798
834
  options?: QueryOptions<TRawDocType> & mongodb.Abortable
799
835
  ): QueryWithHelpers<
800
- THydratedDocumentType[],
836
+ HasLeanOption<TSchema> extends true ? TLeanResultType[] : THydratedDocumentType[],
801
837
  THydratedDocumentType,
802
838
  TQueryHelpers,
803
839
  TLeanResultType,
@@ -828,6 +864,17 @@ declare module 'mongoose' {
828
864
  'findOneAndDelete',
829
865
  TInstanceMethods & TVirtuals
830
866
  >;
867
+ findByIdAndDelete<ResultDoc = THydratedDocumentType>(
868
+ id: mongodb.ObjectId | any,
869
+ options: QueryOptions<TRawDocType> & { lean: false }
870
+ ): QueryWithHelpers<
871
+ ResultDoc | null,
872
+ ResultDoc,
873
+ TQueryHelpers,
874
+ TLeanResultType,
875
+ 'findOneAndDelete',
876
+ TInstanceMethods & TVirtuals
877
+ >;
831
878
  findByIdAndDelete<ResultDoc = THydratedDocumentType>(
832
879
  id: mongodb.ObjectId | any,
833
880
  options: QueryOptions<TRawDocType> & { includeResultMetadata: true }
@@ -889,6 +936,18 @@ declare module 'mongoose' {
889
936
  'findOneAndUpdate',
890
937
  TInstanceMethods & TVirtuals
891
938
  >;
939
+ findByIdAndUpdate<ResultDoc = THydratedDocumentType>(
940
+ id: mongodb.ObjectId | any,
941
+ update: UpdateQuery<TRawDocType>,
942
+ options: QueryOptions<TRawDocType> & { lean: false }
943
+ ): QueryWithHelpers<
944
+ ResultDoc | null,
945
+ ResultDoc,
946
+ TQueryHelpers,
947
+ TLeanResultType,
948
+ 'findOneAndUpdate',
949
+ TInstanceMethods & TVirtuals
950
+ >;
892
951
  findByIdAndUpdate<ResultDoc = THydratedDocumentType>(
893
952
  id: mongodb.ObjectId | any,
894
953
  update: UpdateQuery<TRawDocType>,
@@ -949,6 +1008,28 @@ declare module 'mongoose' {
949
1008
  'findOneAndDelete',
950
1009
  TInstanceMethods & TVirtuals
951
1010
  >;
1011
+ findOneAndDelete<ResultDoc = THydratedDocumentType>(
1012
+ filter: QueryFilter<TRawDocType>,
1013
+ options: QueryOptions<TRawDocType> & { lean: false }
1014
+ ): QueryWithHelpers<
1015
+ ResultDoc | null,
1016
+ ResultDoc,
1017
+ TQueryHelpers,
1018
+ TLeanResultType,
1019
+ 'findOneAndDelete',
1020
+ TInstanceMethods & TVirtuals
1021
+ >;
1022
+ findOneAndDelete(
1023
+ filter: Query<any, any>,
1024
+ options: QueryOptions<TRawDocType> & { lean: false }
1025
+ ): QueryWithHelpers<
1026
+ THydratedDocumentType | null,
1027
+ THydratedDocumentType,
1028
+ TQueryHelpers,
1029
+ TLeanResultType,
1030
+ 'findOneAndDelete',
1031
+ TInstanceMethods & TVirtuals
1032
+ >;
952
1033
  findOneAndDelete<ResultDoc = THydratedDocumentType>(
953
1034
  filter: QueryFilter<TRawDocType>,
954
1035
  options: QueryOptions<TRawDocType> & { includeResultMetadata: true }
@@ -975,7 +1056,7 @@ declare module 'mongoose' {
975
1056
  filter?: QueryFilter<TRawDocType> | null,
976
1057
  options?: QueryOptions<TRawDocType> | null
977
1058
  ): QueryWithHelpers<
978
- HasLeanOption<TSchema> extends true ? TRawDocType | null : ResultDoc | null,
1059
+ HasLeanOption<TSchema> extends true ? TLeanResultType | null : ResultDoc | null,
979
1060
  ResultDoc,
980
1061
  TQueryHelpers,
981
1062
  TLeanResultType,
@@ -986,7 +1067,7 @@ declare module 'mongoose' {
986
1067
  filter?: Query<any, any> | null,
987
1068
  options?: QueryOptions<TRawDocType> | null
988
1069
  ): QueryWithHelpers<
989
- HasLeanOption<TSchema> extends true ? TRawDocType | null : THydratedDocumentType | null,
1070
+ HasLeanOption<TSchema> extends true ? TLeanResultType | null : THydratedDocumentType | null,
990
1071
  THydratedDocumentType,
991
1072
  TQueryHelpers,
992
1073
  TLeanResultType,
@@ -1019,6 +1100,30 @@ declare module 'mongoose' {
1019
1100
  'findOneAndReplace',
1020
1101
  TInstanceMethods & TVirtuals
1021
1102
  >;
1103
+ findOneAndReplace<ResultDoc = THydratedDocumentType>(
1104
+ filter: QueryFilter<TRawDocType>,
1105
+ replacement: TRawDocType | AnyObject,
1106
+ options: QueryOptions<TRawDocType> & { lean: false }
1107
+ ): QueryWithHelpers<
1108
+ ResultDoc | null,
1109
+ ResultDoc,
1110
+ TQueryHelpers,
1111
+ TLeanResultType,
1112
+ 'findOneAndReplace',
1113
+ TInstanceMethods & TVirtuals
1114
+ >;
1115
+ findOneAndReplace(
1116
+ filter: Query<any, any>,
1117
+ replacement: TRawDocType | AnyObject,
1118
+ options: QueryOptions<TRawDocType> & { lean: false }
1119
+ ): QueryWithHelpers<
1120
+ THydratedDocumentType | null,
1121
+ THydratedDocumentType,
1122
+ TQueryHelpers,
1123
+ TLeanResultType,
1124
+ 'findOneAndReplace',
1125
+ TInstanceMethods & TVirtuals
1126
+ >;
1022
1127
  findOneAndReplace<ResultDoc = THydratedDocumentType>(
1023
1128
  filter: QueryFilter<TRawDocType>,
1024
1129
  replacement: TRawDocType | AnyObject,
@@ -1141,6 +1246,30 @@ declare module 'mongoose' {
1141
1246
  'findOneAndUpdate',
1142
1247
  TInstanceMethods & TVirtuals
1143
1248
  >;
1249
+ findOneAndUpdate<ResultDoc = THydratedDocumentType>(
1250
+ filter: QueryFilter<TRawDocType>,
1251
+ update: UpdateQuery<TRawDocType>,
1252
+ options: QueryOptions<TRawDocType> & { lean: false }
1253
+ ): QueryWithHelpers<
1254
+ ResultDoc | null,
1255
+ ResultDoc,
1256
+ TQueryHelpers,
1257
+ TLeanResultType,
1258
+ 'findOneAndUpdate',
1259
+ TInstanceMethods & TVirtuals
1260
+ >;
1261
+ findOneAndUpdate(
1262
+ filter: Query<any, any>,
1263
+ update: UpdateQuery<TRawDocType>,
1264
+ options: QueryOptions<TRawDocType> & { lean: false }
1265
+ ): QueryWithHelpers<
1266
+ THydratedDocumentType | null,
1267
+ THydratedDocumentType,
1268
+ TQueryHelpers,
1269
+ TLeanResultType,
1270
+ 'findOneAndUpdate',
1271
+ TInstanceMethods & TVirtuals
1272
+ >;
1144
1273
  findOneAndUpdate<ResultDoc = THydratedDocumentType>(
1145
1274
  filter: QueryFilter<TRawDocType>,
1146
1275
  update: UpdateQuery<TRawDocType>,
@@ -1,4 +1,4 @@
1
- import * as BSON from 'bson';
1
+ import { BSON } from 'mongodb';
2
2
 
3
3
  declare module 'mongoose' {
4
4
  /** The Mongoose Date [SchemaType](/docs/schematypes.html). */