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 +29 -30
- 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/stateMachine.js +11 -6
- package/package.json +3 -2
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
|
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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/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
|
|
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.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"
|