mongoose 9.9.1 → 9.9.3

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.
@@ -30,6 +30,12 @@ module.exports = function castDouble(val) {
30
30
  assert.ok(false);
31
31
  }
32
32
  } else if (typeof val === 'object') {
33
+ if (Array.isArray(val)) {
34
+ // `[5].valueOf()` returns the array itself, so without this guard a
35
+ // single-element or empty array would fall through to `Number(tempVal)`
36
+ // below and silently coerce instead of throwing.
37
+ assert.ok(false);
38
+ }
33
39
  const tempVal = val.valueOf() ?? val.toString();
34
40
  // ex: { a: 'im an object, valueOf: () => 'helloworld' } // throw an error
35
41
  if (typeof tempVal === 'string') {
package/lib/cast/int32.js CHANGED
@@ -20,6 +20,12 @@ module.exports = function castInt32(val) {
20
20
  if (val === '') {
21
21
  return null;
22
22
  }
23
+ if (Array.isArray(val)) {
24
+ // `Number([5])` is `5` and `Number([])` is `0`, so without this guard a
25
+ // single-element or empty array would silently coerce instead of throwing,
26
+ // unlike every other array input and unlike `castNumber()`'s own guard.
27
+ assert.ok(false);
28
+ }
23
29
 
24
30
  const coercedVal = isBsonType(val, 'Long') ? val.toNumber() : Number(val);
25
31
 
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');
@@ -2570,25 +2570,15 @@ Document.prototype.isSelected = function isSelected(path) {
2570
2570
  return path.some(p => this.$__isSelected(p));
2571
2571
  }
2572
2572
 
2573
- const paths = Object.keys(this.$__.selected);
2574
- let inclusive = null;
2573
+ const index = _getProjectionIndex(this);
2574
+ const paths = index.paths;
2575
+ const inclusive = index.inclusive;
2575
2576
 
2576
- if (paths.length === 1 && paths[0] === '_id') {
2577
+ if (index.onlyId) {
2577
2578
  // only _id was selected.
2578
2579
  return this.$__.selected._id === 0;
2579
2580
  }
2580
2581
 
2581
- for (const cur of paths) {
2582
- if (cur === '_id') {
2583
- continue;
2584
- }
2585
- if (!isDefiningProjection(this.$__.selected[cur])) {
2586
- continue;
2587
- }
2588
- inclusive = !!this.$__.selected[cur];
2589
- break;
2590
- }
2591
-
2592
2582
  if (inclusive === null) {
2593
2583
  return true;
2594
2584
  }
@@ -2599,6 +2589,25 @@ Document.prototype.isSelected = function isSelected(path) {
2599
2589
 
2600
2590
  const pathHasDot = path.indexOf('.') !== -1;
2601
2591
 
2592
+ if (!index.hasNestedKey) {
2593
+ // No projection key is nested, so no key can start with `path + '.'` and
2594
+ // only the "is an ancestor of `path` projected?" check below can match.
2595
+ // Every ancestor that matches returns the same value, so we can walk
2596
+ // `path`'s ancestors instead of scanning every projection key. This keeps
2597
+ // `isSelected()` O(depth of path) rather than O(number of projected paths),
2598
+ // which matters because `toObject({ getters: true })` calls this once per
2599
+ // schema path. Re: gh-16373
2600
+ if (pathHasDot) {
2601
+ for (let dot = path.indexOf('.'); dot !== -1; dot = path.indexOf('.', dot + 1)) {
2602
+ const ancestor = path.slice(0, dot);
2603
+ if (ancestor !== '_id' && Object.hasOwn(this.$__.selected, ancestor)) {
2604
+ return inclusive;
2605
+ }
2606
+ }
2607
+ }
2608
+ return !inclusive;
2609
+ }
2610
+
2602
2611
  for (const cur of paths) {
2603
2612
  if (cur === '_id') {
2604
2613
  continue;
@@ -2615,6 +2624,43 @@ Document.prototype.isSelected = function isSelected(path) {
2615
2624
  return !inclusive;
2616
2625
  };
2617
2626
 
2627
+ /*!
2628
+ * Computes and caches the derived properties of `doc.$__.selected` that
2629
+ * `isSelected()` and `isDirectSelected()` would otherwise recompute on every
2630
+ * call. `$__.selected` is assigned once in the Document constructor and never
2631
+ * mutated afterwards, so this can safely be cached for the document's lifetime.
2632
+ */
2633
+
2634
+ function _getProjectionIndex(doc) {
2635
+ if (doc.$__.selectedIndex !== undefined) {
2636
+ return doc.$__.selectedIndex;
2637
+ }
2638
+
2639
+ const selected = doc.$__.selected;
2640
+ const paths = Object.keys(selected);
2641
+ let inclusive = null;
2642
+ let hasNestedKey = false;
2643
+
2644
+ for (const cur of paths) {
2645
+ if (cur === '_id') {
2646
+ continue;
2647
+ }
2648
+ if (cur.indexOf('.') !== -1) {
2649
+ hasNestedKey = true;
2650
+ }
2651
+ if (inclusive === null && isDefiningProjection(selected[cur])) {
2652
+ inclusive = !!selected[cur];
2653
+ }
2654
+ }
2655
+
2656
+ return (doc.$__.selectedIndex = {
2657
+ paths,
2658
+ inclusive,
2659
+ hasNestedKey,
2660
+ onlyId: paths.length === 1 && paths[0] === '_id'
2661
+ });
2662
+ }
2663
+
2618
2664
  Document.prototype.$__isSelected = Document.prototype.isSelected;
2619
2665
 
2620
2666
  /**
@@ -2650,34 +2696,22 @@ Document.prototype.isDirectSelected = function isDirectSelected(path) {
2650
2696
  return path.some(p => this.isDirectSelected(p));
2651
2697
  }
2652
2698
 
2653
- const paths = Object.keys(this.$__.selected);
2654
- let inclusive = null;
2699
+ const index = _getProjectionIndex(this);
2655
2700
 
2656
- if (paths.length === 1 && paths[0] === '_id') {
2701
+ if (index.onlyId) {
2657
2702
  // only _id was selected.
2658
2703
  return this.$__.selected._id === 0;
2659
2704
  }
2660
2705
 
2661
- for (const cur of paths) {
2662
- if (cur === '_id') {
2663
- continue;
2664
- }
2665
- if (!isDefiningProjection(this.$__.selected[cur])) {
2666
- continue;
2667
- }
2668
- inclusive = !!this.$__.selected[cur];
2669
- break;
2670
- }
2671
-
2672
- if (inclusive === null) {
2706
+ if (index.inclusive === null) {
2673
2707
  return true;
2674
2708
  }
2675
2709
 
2676
2710
  if (Object.hasOwn(this.$__.selected, path)) {
2677
- return inclusive;
2711
+ return index.inclusive;
2678
2712
  }
2679
2713
 
2680
- return !inclusive;
2714
+ return !index.inclusive;
2681
2715
  };
2682
2716
 
2683
2717
  /**
@@ -5242,26 +5276,26 @@ Document.prototype.$__delta = function $__delta(pathsToSave, pathsToSaveSet) {
5242
5276
  const optimisticConcurrency = this.$__schema.options.optimisticConcurrency;
5243
5277
  if (optimisticConcurrency) {
5244
5278
  if (Array.isArray(optimisticConcurrency)) {
5245
- if (!this.$__schema.options._optimisticConcurrencySet) {
5246
- this.$__schema.options._optimisticConcurrencySet = new Set(optimisticConcurrency);
5279
+ if (!this.$__schema.options._optimisticConcurrencyTrie) {
5280
+ this.$__schema.options._optimisticConcurrencyTrie = new PathTrie(optimisticConcurrency);
5247
5281
  }
5248
- const optimisticConcurrencySet = this.$__schema.options._optimisticConcurrencySet;
5282
+ const optimisticConcurrencyTrie = this.$__schema.options._optimisticConcurrencyTrie;
5249
5283
  const modPaths = this.directModifiedPaths();
5250
5284
  const hasRelevantModPaths = pathsToSave == null ?
5251
- modPaths.find(path => _pathOverlapsSet(path, optimisticConcurrencySet)) :
5252
- modPaths.find(path => _pathOverlapsSet(path, optimisticConcurrencySet) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5285
+ modPaths.find(path => optimisticConcurrencyTrie.overlapsPath(path.indexOf('.') === -1 ? path : path.split('.'))) :
5286
+ modPaths.find(path => optimisticConcurrencyTrie.overlapsPath(path.indexOf('.') === -1 ? path : path.split('.')) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5253
5287
  if (hasRelevantModPaths) {
5254
5288
  this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
5255
5289
  }
5256
5290
  } else if (Array.isArray(optimisticConcurrency?.exclude)) {
5257
- if (!this.$__schema.options._optimisticConcurrencyExcludeSet) {
5258
- this.$__schema.options._optimisticConcurrencyExcludeSet = new Set(optimisticConcurrency.exclude);
5291
+ if (!this.$__schema.options._optimisticConcurrencyExcludeTrie) {
5292
+ this.$__schema.options._optimisticConcurrencyExcludeTrie = new PathTrie(optimisticConcurrency.exclude);
5259
5293
  }
5260
- const optimisticConcurrencyExcludeSet = this.$__schema.options._optimisticConcurrencyExcludeSet;
5294
+ const optimisticConcurrencyExcludeTrie = this.$__schema.options._optimisticConcurrencyExcludeTrie;
5261
5295
  const modPaths = this.directModifiedPaths();
5262
5296
  const hasRelevantModPaths = pathsToSave == null ?
5263
- modPaths.find(path => !_pathOrAncestorInSet(path, optimisticConcurrencyExcludeSet)) :
5264
- modPaths.find(path => !_pathOrAncestorInSet(path, optimisticConcurrencyExcludeSet) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5297
+ modPaths.find(path => !optimisticConcurrencyExcludeTrie.matchesPathOrAncestor(path.indexOf('.') === -1 ? path : path.split('.'))) :
5298
+ modPaths.find(path => !optimisticConcurrencyExcludeTrie.matchesPathOrAncestor(path.indexOf('.') === -1 ? path : path.split('.')) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5265
5299
  if (hasRelevantModPaths) {
5266
5300
  this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
5267
5301
  }
@@ -5755,11 +5789,21 @@ Document.prototype.$__hasOnlyPrimitiveValues = function $__hasOnlyPrimitiveValue
5755
5789
  const doc = this._doc;
5756
5790
  for (const key in doc) {
5757
5791
  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')) {
5792
+ if (v == null || typeof v !== 'object') {
5793
+ continue;
5794
+ }
5795
+ // Equivalent to `isNativeObject(v) && !Array.isArray(v)`, plus the
5796
+ // `isBsonType()` checks below, but without the helper calls or the
5797
+ // repeated `Array.isArray()`. This runs for every key of every document
5798
+ // that `toObject()` touches, so the call overhead is measurable.
5799
+ if (Array.isArray(v)) {
5800
+ return false;
5801
+ }
5802
+ if (v instanceof Date || v instanceof Boolean || v instanceof Number || v instanceof String) {
5803
+ continue;
5804
+ }
5805
+ const bsontype = v._bsontype;
5806
+ if (bsontype === 'ObjectId' || bsontype === 'Decimal128') {
5763
5807
  continue;
5764
5808
  }
5765
5809
  return false;
@@ -5787,45 +5831,6 @@ Document.prototype._applyVersionIncrement = function _applyVersionIncrement() {
5787
5831
  * Module exports.
5788
5832
  */
5789
5833
 
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
5834
  Document.VERSION_WHERE = VERSION_WHERE;
5830
5835
  Document.VERSION_INC = VERSION_INC;
5831
5836
  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/internal.js CHANGED
@@ -17,6 +17,8 @@ InternalCache.prototype.strictMode = true;
17
17
 
18
18
  InternalCache.prototype.fullPath = undefined;
19
19
  InternalCache.prototype.selected = undefined;
20
+ // Lazily computed index over `selected`, see `_getProjectionIndex()` in document.js
21
+ InternalCache.prototype.selectedIndex = undefined;
20
22
  InternalCache.prototype.shardval = undefined;
21
23
  InternalCache.prototype.saveError = undefined;
22
24
  InternalCache.prototype.validationError = undefined;
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
 
@@ -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.1",
4
+ "version": "9.9.3",
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.
@@ -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). */