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 +4 -1
- package/lib/document.js +38 -33
- package/lib/helpers/document/applyDefaults.js +5 -0
- package/lib/helpers/populate/createPopulateQueryFilter.js +9 -1
- package/lib/helpers/populate/getModelsMapForPopulate.js +3 -5
- package/lib/helpers/populate/splitPopulateQuery.js +81 -0
- package/lib/helpers/schema/getKeysInSchemaOrder.js +13 -16
- package/lib/model.js +41 -11
- package/lib/plugins/saveSubdocs.js +16 -13
- package/lib/schema/objectId.js +7 -1
- package/lib/schema.js +11 -7
- package/lib/stateMachine.js +11 -6
- package/lib/types/documentArray/methods/index.js +117 -3
- package/package.json +3 -2
package/eslint.config.mjs
CHANGED
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
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 ?
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
12
|
+
const remaining = new Set(valKeys);
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
4664
|
-
|
|
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 (
|
|
4667
|
-
const mod =
|
|
4689
|
+
for (let i = 0; i < params.length; i++) {
|
|
4690
|
+
const mod = params[i][0];
|
|
4668
4691
|
if (mod.options?.options?._leanTransform) {
|
|
4669
|
-
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
return Promise.all(promises);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
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
|
-
|
|
74
|
+
return Promise.all(promises);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
|
|
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
|
-
|
|
90
|
+
return Promise.all(promises);
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
|
package/lib/schema/objectId.js
CHANGED
|
@@ -317,7 +317,13 @@ function resetId(v) {
|
|
|
317
317
|
*/
|
|
318
318
|
|
|
319
319
|
SchemaObjectId.prototype.toJSONSchema = function toJSONSchema(options) {
|
|
320
|
-
|
|
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
|
|
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 =
|
|
1301
|
-
if (reserved
|
|
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.
|
|
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
|
|
3033
|
+
} else if (p === parts.length && Object.hasOwn(schema.nested, trypath)) {
|
|
3030
3034
|
return { schema: schema, pathType: 'nested' };
|
|
3031
3035
|
}
|
|
3032
3036
|
}
|
package/lib/stateMachine.js
CHANGED
|
@@ -69,8 +69,10 @@ StateMachine.prototype._changeState = function _changeState(path, nextState) {
|
|
|
69
69
|
if (prevState === nextState) {
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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/
|
|
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.
|
|
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"
|