mongoose 9.7.0 → 9.7.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/eslint.config.mjs CHANGED
@@ -15,7 +15,10 @@ export default defineConfig([
15
15
  '!**/.*',
16
16
  '**/node_modules',
17
17
  '**/.git',
18
- '**/data'
18
+ '**/data',
19
+ '**/docs/3.*/**',
20
+ '**/docs/5.*/**',
21
+ '**/docs/vendor/*'
19
22
  ]),
20
23
  js.configs.recommended,
21
24
  // general options
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
@@ -1954,7 +1948,9 @@ Document.prototype.get = function(path, type, options) {
1954
1948
 
1955
1949
  // Fast path if we know we're just accessing top-level path on the document:
1956
1950
  // just get the schema path, avoid `$__path()` because that does string manipulation
1957
- let schema = noDottedPath ? this.$__schema.paths[path] : this.$__path(path);
1951
+ let schema = noDottedPath ?
1952
+ Object.hasOwn(this.$__schema.paths, path) ? this.$__schema.paths[path] : undefined :
1953
+ this.$__path(path);
1958
1954
  if (schema == null) {
1959
1955
  schema = this.$__schema.virtualpath(path);
1960
1956
 
@@ -1964,7 +1960,7 @@ Document.prototype.get = function(path, type, options) {
1964
1960
  }
1965
1961
 
1966
1962
  if (noDottedPath) {
1967
- let obj = this._doc[path];
1963
+ let obj = Object.hasOwn(this._doc, path) ? this._doc[path] : undefined;
1968
1964
  if (adhoc) {
1969
1965
  obj = adhoc.cast(obj);
1970
1966
  }
@@ -1991,6 +1987,10 @@ Document.prototype.get = function(path, type, options) {
1991
1987
  }
1992
1988
 
1993
1989
  for (let i = 0, l = pieces.length; i < l; i++) {
1990
+ if (specialProperties.has(pieces[i])) {
1991
+ return undefined;
1992
+ }
1993
+
1994
1994
  if (obj?._doc) {
1995
1995
  obj = obj._doc;
1996
1996
  }
@@ -2012,7 +2012,7 @@ Document.prototype.get = function(path, type, options) {
2012
2012
 
2013
2013
  if (schema != null && options.getters !== false) {
2014
2014
  obj = schema.applyGetters(obj, this);
2015
- } else if (this.$__schema.nested[path] && options.virtuals) {
2015
+ } else if (Object.hasOwn(this.$__schema.nested, path) && options.virtuals) {
2016
2016
  // Might need to apply virtuals if this is a nested path
2017
2017
  return applyVirtuals(this, clone(obj) || {}, { path: path });
2018
2018
  }
@@ -2870,7 +2870,8 @@ Document.prototype.validate = async function validate(pathsToValidate, options)
2870
2870
 
2871
2871
  await this._execDocumentPostHooks('validate', options, error);
2872
2872
  } finally {
2873
- delete this.$__.validateModifiedOnly;
2873
+ // Assign rather than `delete` to avoid putting `$__` in dictionary mode
2874
+ this.$__.validateModifiedOnly = undefined;
2874
2875
  this.$op = null;
2875
2876
  this.$__.validating = null;
2876
2877
  }
@@ -2913,7 +2914,7 @@ function _completeValidate(doc) {
2913
2914
  }
2914
2915
  }
2915
2916
 
2916
- doc.$__.cachedRequired = {};
2917
+ doc.$__.cachedRequired = null;
2917
2918
  doc.$emit('validate', doc);
2918
2919
  doc.constructor.emit('validate', doc);
2919
2920
 
@@ -3204,7 +3205,7 @@ function _pushNestedArrayPaths(val, paths, path) {
3204
3205
  * ignore
3205
3206
  */
3206
3207
 
3207
- Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks(opName, options, argsForHooks) {
3208
+ Document.prototype._execDocumentPreHooks = function _execDocumentPreHooks(opName, options, argsForHooks) {
3208
3209
  const filter = buildMiddlewareFilter(options, 'pre');
3209
3210
  return this.$__middleware.execPre(opName, this, argsForHooks || [], { filter });
3210
3211
  };
@@ -3213,7 +3214,7 @@ Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks(
3213
3214
  * ignore
3214
3215
  */
3215
3216
 
3216
- Document.prototype._execDocumentPostHooks = async function _execDocumentPostHooks(opName, options, error) {
3217
+ Document.prototype._execDocumentPostHooks = function _execDocumentPostHooks(opName, options, error) {
3217
3218
  const filter = buildMiddlewareFilter(options, 'post');
3218
3219
  return this.$__middleware.execPost(opName, this, [this], { error, filter });
3219
3220
  };
@@ -3630,7 +3631,11 @@ Document.prototype.$__reset = function reset() {
3630
3631
  // Clear atomics on dirty paths. Walk the modified and default paths
3631
3632
  // directly instead of calling $__dirty(), which builds intermediate
3632
3633
  // arrays, a Map, and does parent-path deduplication we don't need here.
3633
- this.$__resetAtomics();
3634
+ // Skip entirely if the doc only has primitive values, because only arrays
3635
+ // and maps have atomics.
3636
+ if (!onlyPrimitiveValues) {
3637
+ this.$__resetAtomics();
3638
+ }
3634
3639
 
3635
3640
  this.$__.backup = {};
3636
3641
  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() {
package/lib/schema.js CHANGED
@@ -1268,7 +1268,7 @@ reserved.collection = 1;
1268
1268
 
1269
1269
  Schema.prototype.path = function(path, obj) {
1270
1270
  if (obj === undefined) {
1271
- if (this.paths[path] != null) {
1271
+ if (Object.hasOwn(this.paths, path)) {
1272
1272
  return this.paths[path];
1273
1273
  }
1274
1274
  // Convert to '.$' to check subpaths re: gh-6405
@@ -1296,9 +1296,15 @@ Schema.prototype.path = function(path, obj) {
1296
1296
  : undefined;
1297
1297
  }
1298
1298
 
1299
+ const subpaths = path.indexOf('.') === -1 ? [path] : path.split('.');
1300
+ const last = subpaths.pop();
1301
+ if (utils.specialProperties.has(last)) {
1302
+ throw new MongooseError('Cannot set special property `' + last + '` on a schema');
1303
+ }
1304
+
1299
1305
  // some path names conflict with document methods
1300
- const firstPieceOfPath = path.split('.')[0];
1301
- if (reserved[firstPieceOfPath] && !this.options.suppressReservedKeysWarning) {
1306
+ const firstPieceOfPath = subpaths.length === 0 ? last : subpaths[0];
1307
+ if (Object.hasOwn(reserved, firstPieceOfPath) && !this.options.suppressReservedKeysWarning) {
1302
1308
  const errorMessage = `\`${firstPieceOfPath}\` is a reserved schema pathname and may break some functionality. ` +
1303
1309
  'You are allowed to use it, but use at your own risk. ' +
1304
1310
  'To disable this warning pass `suppressReservedKeysWarning` as a schema option.';
@@ -1311,8 +1317,6 @@ Schema.prototype.path = function(path, obj) {
1311
1317
  }
1312
1318
 
1313
1319
  // update the tree
1314
- const subpaths = path.split(/\./);
1315
- const last = subpaths.pop();
1316
1320
  let branch = this.tree;
1317
1321
  let fullPath = '';
1318
1322
 
@@ -1536,7 +1540,7 @@ function getMapPath(schema, path) {
1536
1540
  } else if (val.schema && path.startsWith(cleanPath + '.')) {
1537
1541
  let remnant = path.slice(cleanPath.length + 1);
1538
1542
  remnant = remnant.slice(remnant.indexOf('.') + 1);
1539
- return val.schema.paths[remnant];
1543
+ return val.schema.path(remnant);
1540
1544
  } else if (val.$isSchemaMap && path.startsWith(cleanPath + '.')) {
1541
1545
  let remnant = path.slice(cleanPath.length + 1);
1542
1546
  remnant = remnant.slice(remnant.indexOf('.') + 1);
@@ -3026,7 +3030,7 @@ Schema.prototype._getPathType = function(path) {
3026
3030
  };
3027
3031
  }
3028
3032
  return { schema: foundschema, pathType: 'real' };
3029
- } else if (p === parts.length && schema.nested[trypath]) {
3033
+ } else if (p === parts.length && Object.hasOwn(schema.nested, trypath)) {
3030
3034
  return { schema: schema, pathType: 'nested' };
3031
3035
  }
3032
3036
  }
@@ -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
 
@@ -12,6 +12,7 @@ const arrayPathSymbol = require('../../../helpers/symbols').arrayPathSymbol;
12
12
  const arraySchemaSymbol = require('../../../helpers/symbols').arraySchemaSymbol;
13
13
  const documentArrayParent = require('../../../helpers/symbols').documentArrayParent;
14
14
 
15
+ const _baseReverse = Array.prototype.reverse;
15
16
  const _baseToString = Array.prototype.toString;
16
17
 
17
18
  const methods = {
@@ -211,7 +212,7 @@ const methods = {
211
212
  },
212
213
 
213
214
  /**
214
- * Wraps [`Array#push`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/push) with proper change tracking.
215
+ * Wraps [`Array#push`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push) with proper change tracking.
215
216
  *
216
217
  * @param {...object} [args]
217
218
  * @api public
@@ -220,8 +221,37 @@ const methods = {
220
221
  */
221
222
 
222
223
  push() {
224
+ const shouldReindex = _shouldReindexAfterPush(arguments);
223
225
  const ret = ArrayMethods.push.apply(this, arguments);
224
226
 
227
+ if (shouldReindex) {
228
+ _updateSubdocIndexes(this);
229
+ }
230
+ _updateParentPopulated(this);
231
+
232
+ return ret;
233
+ },
234
+
235
+ /**
236
+ * Wraps [`Array#pop`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/pop) with proper change tracking.
237
+ * @api private
238
+ */
239
+
240
+ pop() {
241
+ const ret = ArrayMethods.pop.apply(this, arguments);
242
+
243
+ _updateParentPopulated(this);
244
+
245
+ return ret;
246
+ },
247
+
248
+ /*!
249
+ * ignore
250
+ */
251
+
252
+ $pop() {
253
+ const ret = ArrayMethods.$pop.apply(this, arguments);
254
+
225
255
  _updateParentPopulated(this);
226
256
 
227
257
  return ret;
@@ -239,32 +269,96 @@ const methods = {
239
269
  pull() {
240
270
  const ret = ArrayMethods.pull.apply(this, arguments);
241
271
 
272
+ _updateSubdocIndexes(this);
242
273
  _updateParentPopulated(this);
243
274
 
244
275
  return ret;
245
276
  },
246
277
 
247
278
  /**
248
- * Wraps [`Array#shift`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/unshift) with proper change tracking.
279
+ * Wraps [`Array#shift`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift) with proper change tracking.
249
280
  * @api private
250
281
  */
251
282
 
252
283
  shift() {
253
284
  const ret = ArrayMethods.shift.apply(this, arguments);
254
285
 
286
+ _updateSubdocIndexes(this);
287
+ _updateParentPopulated(this);
288
+
289
+ return ret;
290
+ },
291
+
292
+ /*!
293
+ * ignore
294
+ */
295
+
296
+ $shift() {
297
+ const ret = ArrayMethods.$shift.apply(this, arguments);
298
+
299
+ _updateSubdocIndexes(this);
255
300
  _updateParentPopulated(this);
256
301
 
257
302
  return ret;
258
303
  },
259
304
 
260
305
  /**
261
- * Wraps [`Array#splice`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/splice) with proper change tracking and casting.
306
+ * Wraps [`Array#splice`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) with proper change tracking and casting.
262
307
  * @api private
263
308
  */
264
309
 
265
310
  splice() {
266
311
  const ret = ArrayMethods.splice.apply(this, arguments);
267
312
 
313
+ _updateSubdocIndexes(this);
314
+ _updateParentPopulated(this);
315
+
316
+ return ret;
317
+ },
318
+
319
+ /**
320
+ * Wraps [`Array#reverse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse)
321
+ * with proper change tracking.
322
+ * @api private
323
+ */
324
+
325
+ reverse() {
326
+ _baseReverse.call(this.__array);
327
+ this._registerAtomic('$set', this);
328
+
329
+ _updateSubdocIndexes(this);
330
+ _updateParentPopulated(this);
331
+
332
+ return this;
333
+ },
334
+
335
+ /**
336
+ * Wraps [`Array#sort`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)
337
+ * with proper change tracking.
338
+ *
339
+ * @param {Function} [compareFunction]
340
+ * @api private
341
+ */
342
+
343
+ sort() {
344
+ const ret = ArrayMethods.sort.apply(this, arguments);
345
+
346
+ _updateSubdocIndexes(this);
347
+ _updateParentPopulated(this);
348
+
349
+ return ret;
350
+ },
351
+
352
+ /**
353
+ * Wraps [`Array#unshift`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift)
354
+ * with proper change tracking.
355
+ * @api private
356
+ */
357
+
358
+ unshift() {
359
+ const ret = ArrayMethods.unshift.apply(this, arguments);
360
+
361
+ _updateSubdocIndexes(this);
268
362
  _updateParentPopulated(this);
269
363
 
270
364
  return ret;
@@ -389,6 +483,26 @@ const methods = {
389
483
 
390
484
  module.exports = methods;
391
485
 
486
+ /*!
487
+ * ignore
488
+ */
489
+
490
+ function _updateSubdocIndexes(arr) {
491
+ const rawArray = utils.isMongooseArray(arr) ? arr.__array : arr;
492
+
493
+ for (let i = 0; i < rawArray.length; ++i) {
494
+ if (typeof rawArray[i]?.$setIndex === 'function') {
495
+ rawArray[i].$setIndex(i);
496
+ }
497
+ }
498
+ }
499
+
500
+ function _shouldReindexAfterPush(args) {
501
+ return args[0] != null &&
502
+ utils.hasUserDefinedProperty(args[0], '$each') &&
503
+ args[0].$position != null;
504
+ }
505
+
392
506
  /**
393
507
  * If this is a document array, each element may contain single
394
508
  * populated paths, so we need to modify the top-level document's
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.2",
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"