mongoose 9.7.0 → 9.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/document.js CHANGED
@@ -562,27 +562,26 @@ function $applyDefaultsToNested(val, path, doc) {
562
562
  Document.prototype.$__buildDoc = function(obj, fields, skipId, exclude, hasIncludedChildren) {
563
563
  const doc = {};
564
564
 
565
- const paths = Object.keys(this.$__schema.paths).
566
- // Don't build up any paths that are underneath a map, we don't know
567
- // what the keys will be
568
- filter(p => !p.includes('$*'));
565
+ const paths = Object.keys(this.$__schema.paths);
569
566
  const plen = paths.length;
570
567
  let ii = 0;
571
568
 
572
569
  for (; ii < plen; ++ii) {
573
570
  const p = paths[ii];
574
571
 
575
- if (p === '_id') {
576
- if (skipId) {
577
- continue;
578
- }
579
- if (obj && '_id' in obj) {
580
- continue;
581
- }
572
+ // Don't build up any paths that are underneath a map, we don't know
573
+ // what the keys will be
574
+ if (p.includes('$*')) {
575
+ continue;
582
576
  }
583
577
 
584
578
  const path = this.$__schema.paths[p].splitPath();
585
579
  const len = path.length;
580
+ // This loop only creates intermediate objects for nested paths: it never
581
+ // sets any leaf values, so single-segment paths are no-ops.
582
+ if (len === 1) {
583
+ continue;
584
+ }
586
585
  const last = len - 1;
587
586
  let curPath = '';
588
587
  let doc_ = doc;
@@ -1133,7 +1132,11 @@ Document.prototype.$set = function $set(path, val, type, options) {
1133
1132
  return this;
1134
1133
  }
1135
1134
 
1136
- options = Object.assign({}, options, { _skipMinimizeTopLevel: false });
1135
+ // Only need to clone if we're overwriting `_skipMinimizeTopLevel`, recursive
1136
+ // calls treat a missing `_skipMinimizeTopLevel` as `false`
1137
+ if (_skipMinimizeTopLevel) {
1138
+ options = Object.assign({}, options, { _skipMinimizeTopLevel: false });
1139
+ }
1137
1140
 
1138
1141
  for (let i = 0; i < len; ++i) {
1139
1142
  key = keys[i];
@@ -1220,15 +1223,9 @@ Document.prototype.$set = function $set(path, val, type, options) {
1220
1223
  val = handleSpreadDoc(val, true);
1221
1224
 
1222
1225
  // if this doc is being constructed we should not trigger getters
1223
- const priorVal = (() => {
1224
- if (this.$__.priorDoc != null) {
1225
- return this.$__.priorDoc.$__getValue(path);
1226
- }
1227
- if (constructing) {
1228
- return void 0;
1229
- }
1230
- return this.$__getValue(path);
1231
- })();
1226
+ const priorVal = this.$__.priorDoc != null ?
1227
+ this.$__.priorDoc.$__getValue(path) :
1228
+ (constructing ? void 0 : this.$__getValue(path));
1232
1229
 
1233
1230
  if (pathType === 'nested' && val) {
1234
1231
  if (typeof val === 'object' && val != null) {
@@ -1410,13 +1407,10 @@ Document.prototype.$set = function $set(path, val, type, options) {
1410
1407
  try {
1411
1408
  // If the user is trying to set a ref path to a document with
1412
1409
  // the correct model name, treat it as populated
1413
- const refMatches = (() => {
1410
+ const refMatches = !(val instanceof Document) ? false : (() => {
1414
1411
  if (schema.options == null) {
1415
1412
  return false;
1416
1413
  }
1417
- if (!(val instanceof Document)) {
1418
- return false;
1419
- }
1420
1414
  const model = val.constructor;
1421
1415
 
1422
1416
  // Check ref
@@ -2870,7 +2864,8 @@ Document.prototype.validate = async function validate(pathsToValidate, options)
2870
2864
 
2871
2865
  await this._execDocumentPostHooks('validate', options, error);
2872
2866
  } finally {
2873
- delete this.$__.validateModifiedOnly;
2867
+ // Assign rather than `delete` to avoid putting `$__` in dictionary mode
2868
+ this.$__.validateModifiedOnly = undefined;
2874
2869
  this.$op = null;
2875
2870
  this.$__.validating = null;
2876
2871
  }
@@ -2913,7 +2908,7 @@ function _completeValidate(doc) {
2913
2908
  }
2914
2909
  }
2915
2910
 
2916
- doc.$__.cachedRequired = {};
2911
+ doc.$__.cachedRequired = null;
2917
2912
  doc.$emit('validate', doc);
2918
2913
  doc.constructor.emit('validate', doc);
2919
2914
 
@@ -3204,7 +3199,7 @@ function _pushNestedArrayPaths(val, paths, path) {
3204
3199
  * ignore
3205
3200
  */
3206
3201
 
3207
- Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks(opName, options, argsForHooks) {
3202
+ Document.prototype._execDocumentPreHooks = function _execDocumentPreHooks(opName, options, argsForHooks) {
3208
3203
  const filter = buildMiddlewareFilter(options, 'pre');
3209
3204
  return this.$__middleware.execPre(opName, this, argsForHooks || [], { filter });
3210
3205
  };
@@ -3213,7 +3208,7 @@ Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks(
3213
3208
  * ignore
3214
3209
  */
3215
3210
 
3216
- Document.prototype._execDocumentPostHooks = async function _execDocumentPostHooks(opName, options, error) {
3211
+ Document.prototype._execDocumentPostHooks = function _execDocumentPostHooks(opName, options, error) {
3217
3212
  const filter = buildMiddlewareFilter(options, 'post');
3218
3213
  return this.$__middleware.execPost(opName, this, [this], { error, filter });
3219
3214
  };
@@ -3630,7 +3625,11 @@ Document.prototype.$__reset = function reset() {
3630
3625
  // Clear atomics on dirty paths. Walk the modified and default paths
3631
3626
  // directly instead of calling $__dirty(), which builds intermediate
3632
3627
  // arrays, a Map, and does parent-path deduplication we don't need here.
3633
- this.$__resetAtomics();
3628
+ // Skip entirely if the doc only has primitive values, because only arrays
3629
+ // and maps have atomics.
3630
+ if (!onlyPrimitiveValues) {
3631
+ this.$__resetAtomics();
3632
+ }
3634
3633
 
3635
3634
  this.$__.backup = {};
3636
3635
  this.$__.backup.activePaths = {
@@ -17,6 +17,11 @@ module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildre
17
17
  }
18
18
 
19
19
  const type = doc.$__schema.paths[p];
20
+ // `getDefault()` returns undefined when `defaultValue` is undefined, in which
21
+ // case this loop never modifies the doc, so skip traversing the path entirely.
22
+ if (type.defaultValue === undefined) {
23
+ continue;
24
+ }
20
25
  const path = type.splitPath();
21
26
  const len = path.length;
22
27
  if (path[len - 1] === '$*') {
@@ -25,7 +25,15 @@ module.exports = function createPopulateQueryFilter(ids, _match, _foreignField,
25
25
  for (let i = 0; i < _parentPaths.length - 1; ++i) {
26
26
  const cur = _parentPaths[i];
27
27
  if (match[cur] != null && match[cur].$elemMatch != null) {
28
- match[cur].$elemMatch[foreignField.slice(cur.length + 1)] = trusted({ $in: ids });
28
+ // Copy rather than mutate so the user's `match` stays unchanged and split populate
29
+ // queries (gh-5890) each get their own `$in` rather than sharing one object
30
+ match[cur] = {
31
+ ...match[cur],
32
+ $elemMatch: {
33
+ ...match[cur].$elemMatch,
34
+ [foreignField.slice(cur.length + 1)]: trusted({ $in: ids })
35
+ }
36
+ };
29
37
  delete match[foreignField];
30
38
  break;
31
39
  }
@@ -649,11 +649,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
649
649
  ids = matchIdsToRefPaths(ret, modelNamesForRefPath, modelName);
650
650
  }
651
651
 
652
- const perDocumentLimit = options.perDocumentLimit == null ?
653
- get(options, 'options.perDocumentLimit', null) :
654
- options.perDocumentLimit;
655
-
656
- if (!available[modelName] || perDocumentLimit != null) {
652
+ if (!available[modelName]) {
657
653
  const currentOptions = {
658
654
  model: Model
659
655
  };
@@ -682,6 +678,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
682
678
  isVirtual: data.isVirtual,
683
679
  virtual: data.virtual,
684
680
  count: data.count,
681
+ isRefPath: !!data.isRefPath,
685
682
  [populateModelSymbol]: Model
686
683
  };
687
684
  map.push(available[modelName]);
@@ -692,6 +689,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
692
689
  available[modelName].ids.push(ids);
693
690
  available[modelName].allIds.push(ret);
694
691
  available[modelName].unpopulatedValues.push(unpopulatedValue);
692
+ available[modelName].isRefPath = available[modelName].isRefPath || !!data.isRefPath;
695
693
  if (data.hasMatchFunction) {
696
694
  available[modelName].match.push(data.match);
697
695
  }
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const createPopulateQueryFilter = require('./createPopulateQueryFilter');
4
+ const get = require('../get');
5
+ const utils = require('../../utils');
6
+
7
+ module.exports = splitPopulateQuery;
8
+
9
+ /*!
10
+ * If a single populate query would have more than this many elements in its `$in` filter,
11
+ * Mongoose splits the populate into a separate query per document to avoid going over
12
+ * MongoDB's 16 MB BSON size limit on queries. Overwritable for testing purposes. See gh-5890.
13
+ */
14
+
15
+ splitPopulateQuery.maxInFilterLength = 50000;
16
+
17
+ /*!
18
+ * Split a populate models-map entry into a separate query per document if either:
19
+ *
20
+ * 1. The `perDocumentLimit` option is set, so each document needs its own query with its
21
+ * own `limit` (gh-7318), or
22
+ * 2. A single populate query for `mod` would have too many elements in its `$in` filter
23
+ * (gh-5890). With multiple foreign fields, `createPopulateQueryFilter()` repeats the ids
24
+ * under `$or` once per foreign field, so the threshold counts one copy of `ids` per
25
+ * foreign field.
26
+ *
27
+ * Returns a list of `[mod, match, select, assignmentOpts]` params, one per document, for
28
+ * `_execPopulateQuery()`. Returns `null` if the populate query doesn't need to be split.
29
+ * A `null` `match` means the document has no ids to query: `_execPopulateQuery()` skips
30
+ * executing a query, and `_assign()` just sets the document's populated path to the
31
+ * default value.
32
+ *
33
+ * Splitting on document boundaries means each document's populated value is the result of
34
+ * exactly one query, so split entries can typically be assigned from only their own query's
35
+ * results (`_assignFromOwnResults`) rather than scanning every populate query's results.
36
+ * refPath is the exception: a single document's array can contain ids for multiple models,
37
+ * so assigning refPath populate results relies on every query's results being available for
38
+ * every document. refPath entries are therefore only split when `perDocumentLimit` requires
39
+ * it, not to keep the `$in` filter small. A single document whose ids alone overflow the
40
+ * BSON size limit cannot be split.
41
+ */
42
+
43
+ function splitPopulateQuery(mod, ids, select, assignmentOpts) {
44
+ if (mod.docs.length <= 1) {
45
+ return null;
46
+ }
47
+ const perDocumentLimit = mod.options.perDocumentLimit == null ?
48
+ get(mod.options, 'options.perDocumentLimit', null) :
49
+ mod.options.perDocumentLimit;
50
+ const numInFilterElements = ids.length * mod.foreignField.size;
51
+ if (perDocumentLimit == null && (numInFilterElements <= splitPopulateQuery.maxInFilterLength || mod.isRefPath)) {
52
+ return null;
53
+ }
54
+
55
+ return mod.docs.map((doc, i) => {
56
+ const subMod = {
57
+ ...mod,
58
+ docs: [doc],
59
+ ids: [mod.ids[i]],
60
+ allIds: [mod.allIds[i]],
61
+ unpopulatedValues: [mod.unpopulatedValues[i]],
62
+ match: Array.isArray(mod.match) ? [mod.match[i]] : mod.match,
63
+ _assignFromOwnResults: !mod.isRefPath
64
+ };
65
+ let subIds = utils.array.flatten(subMod.ids, flatten);
66
+ subIds = utils.array.unique(subIds);
67
+ const match = subIds.length === 0 || subIds.every(utils.isNullOrUndefined) ?
68
+ null :
69
+ createPopulateQueryFilter(subIds, subMod.match, subMod.foreignField, subMod.model, subMod.options.skipInvalidIds);
70
+ return [subMod, match, select, assignmentOpts];
71
+ });
72
+ }
73
+
74
+ /*!
75
+ * ignore
76
+ */
77
+
78
+ function flatten(item) {
79
+ // no need to include undefined values in our query
80
+ return undefined !== item;
81
+ }
@@ -3,25 +3,22 @@
3
3
  const get = require('../get');
4
4
 
5
5
  module.exports = function getKeysInSchemaOrder(schema, val, path) {
6
+ const valKeys = Object.keys(val);
7
+ if (valKeys.length <= 1) {
8
+ return valKeys;
9
+ }
10
+
6
11
  const schemaKeys = path != null ? Object.keys(get(schema.tree, path, {})) : Object.keys(schema.tree);
7
- const valKeys = new Set(Object.keys(val));
12
+ const remaining = new Set(valKeys);
8
13
 
9
- let keys;
10
- if (valKeys.size > 1) {
11
- keys = new Set();
12
- for (const key of schemaKeys) {
13
- if (valKeys.has(key)) {
14
- keys.add(key);
15
- }
14
+ const keys = [];
15
+ for (const key of schemaKeys) {
16
+ if (remaining.delete(key)) {
17
+ keys.push(key);
16
18
  }
17
- for (const key of valKeys) {
18
- if (!keys.has(key)) {
19
- keys.add(key);
20
- }
21
- }
22
- keys = Array.from(keys);
23
- } else {
24
- keys = Array.from(valKeys);
19
+ }
20
+ for (const key of remaining) {
21
+ keys.push(key);
25
22
  }
26
23
 
27
24
  return keys;
package/lib/model.js CHANGED
@@ -71,6 +71,7 @@ const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscrim
71
71
  const pushNestedArrayPaths = require('./helpers/model/pushNestedArrayPaths');
72
72
  const removeDeselectedForeignField = require('./helpers/populate/removeDeselectedForeignField');
73
73
  const setDottedPath = require('./helpers/path/setDottedPath');
74
+ const splitPopulateQuery = require('./helpers/populate/splitPopulateQuery');
74
75
  const { buildMiddlewareFilter } = require('./helpers/buildMiddlewareFilter');
75
76
  const util = require('util');
76
77
  const utils = require('./utils');
@@ -4543,7 +4544,6 @@ async function _populatePath(model, docs, populateOptions) {
4543
4544
  mod.foreignField.clear();
4544
4545
  mod.foreignField.add(populateOptions.foreignField);
4545
4546
  }
4546
- const match = createPopulateQueryFilter(ids, mod.match, mod.foreignField, mod.model, mod.options.skipInvalidIds);
4547
4547
  if (assignmentOpts.excludeId) {
4548
4548
  // override the exclusion from the query so we can use the _id
4549
4549
  // for document matching during assignment. we'll delete the
@@ -4564,6 +4564,17 @@ async function _populatePath(model, docs, populateOptions) {
4564
4564
  } else if (mod.options.limit != null) {
4565
4565
  assignmentOpts.originalLimit = mod.options.limit;
4566
4566
  }
4567
+
4568
+ // Execute a separate query per document if `perDocumentLimit` is set (gh-7318), or if
4569
+ // there are so many ids that the `$in` filter may overflow MongoDB's 16 MB BSON size
4570
+ // limit on queries (gh-5890).
4571
+ const splitParams = splitPopulateQuery(mod, ids, select, assignmentOpts);
4572
+ if (splitParams != null) {
4573
+ params.push(...splitParams);
4574
+ continue;
4575
+ }
4576
+
4577
+ const match = createPopulateQueryFilter(ids, mod.match, mod.foreignField, mod.model, mod.options.skipInvalidIds);
4567
4578
  params.push([mod, match, select, assignmentOpts]);
4568
4579
  }
4569
4580
  if (!hasOne) {
@@ -4584,6 +4595,9 @@ async function _populatePath(model, docs, populateOptions) {
4584
4595
 
4585
4596
  // Track deferred populates per-param (per model) to avoid mixing them
4586
4597
  const deferredPopulatesPerParam = new Map();
4598
+ // Query results per param. Batches that were split off of a large populate query (gh-5890)
4599
+ // are assigned from only their own query's results rather than the combined `vals`.
4600
+ const valsByParam = [];
4587
4601
 
4588
4602
  if (populateOptions.ordered) {
4589
4603
  // Populate in series, primarily for transactions because MongoDB doesn't support multiple operations on
@@ -4591,7 +4605,10 @@ async function _populatePath(model, docs, populateOptions) {
4591
4605
  for (let i = 0; i < params.length; i++) {
4592
4606
  const arr = params[i];
4593
4607
  const { docs, deferredPopulates } = await _execPopulateQuery.apply(null, arr);
4594
- vals = vals.concat(docs);
4608
+ valsByParam.push(docs);
4609
+ if (!arr[0]._assignFromOwnResults) {
4610
+ vals = vals.concat(docs);
4611
+ }
4595
4612
  if (deferredPopulates.length > 0) {
4596
4613
  deferredPopulatesPerParam.set(i, deferredPopulates);
4597
4614
  }
@@ -4606,7 +4623,10 @@ async function _populatePath(model, docs, populateOptions) {
4606
4623
  const results = await Promise.all(promises);
4607
4624
  for (let i = 0; i < results.length; i++) {
4608
4625
  const { docs, deferredPopulates } = results[i];
4609
- vals = vals.concat(docs);
4626
+ valsByParam.push(docs);
4627
+ if (!params[i][0]._assignFromOwnResults) {
4628
+ vals = vals.concat(docs);
4629
+ }
4610
4630
  if (deferredPopulates.length > 0) {
4611
4631
  deferredPopulatesPerParam.set(i, deferredPopulates);
4612
4632
  }
@@ -4614,13 +4634,15 @@ async function _populatePath(model, docs, populateOptions) {
4614
4634
  }
4615
4635
 
4616
4636
 
4617
- for (const arr of params) {
4637
+ for (let i = 0; i < params.length; i++) {
4638
+ const arr = params[i];
4618
4639
  const mod = arr[0];
4619
4640
  const assignmentOpts = arr[3];
4620
- for (const val of vals) {
4641
+ const valsForMod = mod._assignFromOwnResults ? valsByParam[i] : vals;
4642
+ for (const val of valsForMod) {
4621
4643
  mod.options._childDocs.push(val);
4622
4644
  }
4623
- _assign(model, vals, mod, assignmentOpts);
4645
+ _assign(model, valsForMod, mod, assignmentOpts);
4624
4646
  }
4625
4647
 
4626
4648
  // Handle deferred populate for cases with per-document match functions.
@@ -4660,13 +4682,15 @@ async function _populatePath(model, docs, populateOptions) {
4660
4682
  }
4661
4683
  }
4662
4684
 
4663
- for (const arr of params) {
4664
- removeDeselectedForeignField(arr[0].foreignField, arr[0].options, vals);
4685
+ for (let i = 0; i < params.length; i++) {
4686
+ const mod = params[i][0];
4687
+ removeDeselectedForeignField(mod.foreignField, mod.options, mod._assignFromOwnResults ? valsByParam[i] : vals);
4665
4688
  }
4666
- for (const arr of params) {
4667
- const mod = arr[0];
4689
+ for (let i = 0; i < params.length; i++) {
4690
+ const mod = params[i][0];
4668
4691
  if (mod.options?.options?._leanTransform) {
4669
- for (const doc of vals) {
4692
+ const valsForMod = mod._assignFromOwnResults ? valsByParam[i] : vals;
4693
+ for (const doc of valsForMod) {
4670
4694
  mod.options.options._leanTransform(doc);
4671
4695
  }
4672
4696
  }
@@ -4678,6 +4702,12 @@ async function _populatePath(model, docs, populateOptions) {
4678
4702
  */
4679
4703
 
4680
4704
  function _execPopulateQuery(mod, match, select) {
4705
+ // `null` match means `mod`'s documents have no ids to query, possible if a large populate
4706
+ // was split into a query per document (gh-5890). Skip executing a query: `_assign()` still
4707
+ // sets the documents' populated paths to the appropriate default value.
4708
+ if (match == null) {
4709
+ return Promise.resolve({ docs: [], deferredPopulates: [] });
4710
+ }
4681
4711
  let subPopulate = clone(mod.options.populate);
4682
4712
  const queryOptions = {};
4683
4713
  if (mod.options.skip !== undefined) {
@@ -16,7 +16,10 @@ module.exports = function saveSubdocs(schema) {
16
16
  };
17
17
 
18
18
 
19
- async function saveSubdocsPreSave() {
19
+ // These hooks are deliberately not `async` so that they don't allocate a
20
+ // promise and force an extra microtask hop on every `save()` when there are
21
+ // no subdocuments. kareem only awaits hooks that return a promise.
22
+ function saveSubdocsPreSave() {
20
23
  if (this.$isSubdocument) {
21
24
  return;
22
25
  }
@@ -28,15 +31,15 @@ async function saveSubdocsPreSave() {
28
31
  }
29
32
 
30
33
  const options = this.$__.saveOptions;
31
- await Promise.all(subdocs.map(subdoc => subdoc._execDocumentPreHooks('save', options, [options])));
32
-
33
- // Invalidate subdocs cache because subdoc pre hooks can add new subdocuments
34
- if (this.$__.saveOptions) {
35
- this.$__.saveOptions.__subdocs = null;
36
- }
34
+ return Promise.all(subdocs.map(subdoc => subdoc._execDocumentPreHooks('save', options, [options]))).then(() => {
35
+ // Invalidate subdocs cache because subdoc pre hooks can add new subdocuments
36
+ if (this.$__.saveOptions) {
37
+ this.$__.saveOptions.__subdocs = null;
38
+ }
39
+ });
37
40
  }
38
41
 
39
- async function saveSubdocsPostSave() {
42
+ function saveSubdocsPostSave() {
40
43
  if (this.$isSubdocument) {
41
44
  return;
42
45
  }
@@ -53,10 +56,10 @@ async function saveSubdocsPostSave() {
53
56
  promises.push(subdoc._execDocumentPostHooks('save', options));
54
57
  }
55
58
 
56
- await Promise.all(promises);
59
+ return Promise.all(promises);
57
60
  }
58
61
 
59
- async function saveSubdocsPreDeleteOne() {
62
+ function saveSubdocsPreDeleteOne() {
60
63
  const removedSubdocs = this.$__.removedSubdocs;
61
64
  if (!removedSubdocs?.length) {
62
65
  return;
@@ -68,10 +71,10 @@ async function saveSubdocsPreDeleteOne() {
68
71
  promises.push(subdoc._execDocumentPreHooks('deleteOne', options));
69
72
  }
70
73
 
71
- await Promise.all(promises);
74
+ return Promise.all(promises);
72
75
  }
73
76
 
74
- async function saveSubdocsPostDeleteOne() {
77
+ function saveSubdocsPostDeleteOne() {
75
78
  const removedSubdocs = this.$__.removedSubdocs;
76
79
  if (!removedSubdocs?.length) {
77
80
  return;
@@ -84,7 +87,7 @@ async function saveSubdocsPostDeleteOne() {
84
87
  }
85
88
 
86
89
  this.$__.removedSubdocs = null;
87
- await Promise.all(promises);
90
+ return Promise.all(promises);
88
91
  }
89
92
 
90
93
 
@@ -317,7 +317,13 @@ function resetId(v) {
317
317
  */
318
318
 
319
319
  SchemaObjectId.prototype.toJSONSchema = function toJSONSchema(options) {
320
- return this._createJSONSchemaTypeDefinition('string', 'objectId', options);
320
+ const jsonSchema = this._createJSONSchemaTypeDefinition('string', 'objectId', options);
321
+ // `bsonType: 'objectId'` already validates ObjectIds, so the regex is only useful
322
+ // for the portable `type: 'string'` representation (e.g. for ajv, zod, etc.)
323
+ if (!options?.useBsonType) {
324
+ jsonSchema.pattern = '^[A-Fa-f0-9]{24}$';
325
+ }
326
+ return jsonSchema;
321
327
  };
322
328
 
323
329
  SchemaObjectId.prototype.autoEncryptionType = function autoEncryptionType() {
@@ -69,8 +69,10 @@ StateMachine.prototype._changeState = function _changeState(path, nextState) {
69
69
  if (prevState === nextState) {
70
70
  return;
71
71
  }
72
- const prevBucket = this.states[prevState];
73
- if (prevBucket) delete prevBucket[path];
72
+ if (prevState !== undefined) {
73
+ const prevBucket = this.states[prevState];
74
+ if (prevBucket) delete prevBucket[path];
75
+ }
74
76
 
75
77
  this.paths[path] = nextState;
76
78
  this.states[nextState] = this.states[nextState] || {};
@@ -86,13 +88,16 @@ StateMachine.prototype.clear = function clear(state) {
86
88
  return;
87
89
  }
88
90
  const keys = Object.keys(this.states[state]);
91
+ if (keys.length === 0) {
92
+ return;
93
+ }
94
+ // Replace the bucket rather than deleting each key: repeated `delete` puts
95
+ // the object in dictionary mode, which is significantly slower.
96
+ this.states[state] = {};
89
97
  let i = keys.length;
90
- let path;
91
98
 
92
99
  while (i--) {
93
- path = keys[i];
94
- delete this.states[state][path];
95
- delete this.paths[path];
100
+ delete this.paths[keys[i]];
96
101
  }
97
102
  };
98
103
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongoose",
3
3
  "description": "Mongoose MongoDB ODM",
4
- "version": "9.7.0",
4
+ "version": "9.7.1",
5
5
  "author": "Guillermo Rauch <guillermo@learnboost.com>",
6
6
  "keywords": [
7
7
  "mongodb",
@@ -63,7 +63,8 @@
63
63
  "tstyche": "^7.0.0",
64
64
  "typescript": "5.9.3",
65
65
  "typescript-eslint": "^8.31.1",
66
- "uuid": "14.0.0"
66
+ "uuid": "14.0.0",
67
+ "xss": "1.0.15"
67
68
  },
68
69
  "directories": {
69
70
  "lib": "./lib/mongoose"