mongoose 9.6.3 → 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/aggregate.js +50 -23
- package/lib/connection.js +1 -1
- package/lib/cursor/aggregationCursor.js +47 -16
- package/lib/cursor/queryCursor.js +23 -7
- package/lib/document.js +49 -34
- package/lib/error/divergentArray.js +1 -1
- package/lib/helpers/document/applyDefaults.js +5 -0
- package/lib/helpers/populate/createPopulateQueryFilter.js +9 -1
- package/lib/helpers/populate/getModelsMapForPopulate.js +8 -6
- package/lib/helpers/populate/splitPopulateQuery.js +81 -0
- package/lib/helpers/schema/getKeysInSchemaOrder.js +13 -16
- package/lib/model.js +136 -28
- package/lib/plugins/saveSubdocs.js +16 -13
- package/lib/query.js +44 -28
- package/lib/schema/date.js +7 -5
- package/lib/schema/documentArray.js +1 -1
- package/lib/schema/objectId.js +7 -1
- package/lib/schema/subdocument.js +1 -1
- package/lib/schema/union.js +2 -2
- package/lib/schemaType.js +2 -1
- package/lib/standardSchema/convertErrorToIssues.js +20 -0
- package/lib/stateMachine.js +11 -6
- package/lib/tracing.js +38 -0
- package/package.json +8 -7
- package/types/document.d.ts +21 -4
- package/types/index.d.ts +1 -0
- package/types/models.d.ts +42 -4
- package/types/tracing.d.ts +10 -0
package/lib/aggregate.js
CHANGED
|
@@ -15,6 +15,8 @@ const { buildMiddlewareFilter } = require('./helpers/buildMiddlewareFilter');
|
|
|
15
15
|
const stringifyFunctionOperators = require('./helpers/aggregate/stringifyFunctionOperators');
|
|
16
16
|
const utils = require('./utils');
|
|
17
17
|
const { modelSymbol } = require('./helpers/symbols');
|
|
18
|
+
const { createTracedChannel } = require('./tracing');
|
|
19
|
+
const { trace: traceAggregate } = createTracedChannel('mongoose:aggregate');
|
|
18
20
|
const read = Query.prototype.read;
|
|
19
21
|
const readConcern = Query.prototype.readConcern;
|
|
20
22
|
|
|
@@ -1071,8 +1073,20 @@ Aggregate.prototype.exec = async function exec() {
|
|
|
1071
1073
|
|
|
1072
1074
|
this._optionsForExec();
|
|
1073
1075
|
|
|
1074
|
-
const
|
|
1075
|
-
return
|
|
1076
|
+
const _this = this;
|
|
1077
|
+
return traceAggregate(async function maybeTracedConnectionAggregate() {
|
|
1078
|
+
const cursor = await _this._connection.client.db().aggregate(_this._pipeline, _this.options);
|
|
1079
|
+
return await cursor.toArray();
|
|
1080
|
+
}, () => ({
|
|
1081
|
+
operation: 'aggregate',
|
|
1082
|
+
database: _this._connection.name,
|
|
1083
|
+
serverAddress: _this._connection.host,
|
|
1084
|
+
serverPort: _this._connection.port,
|
|
1085
|
+
args: {
|
|
1086
|
+
pipeline: _this._pipeline,
|
|
1087
|
+
options: _this.options
|
|
1088
|
+
}
|
|
1089
|
+
}));
|
|
1076
1090
|
}
|
|
1077
1091
|
|
|
1078
1092
|
const model = this._model;
|
|
@@ -1090,32 +1104,45 @@ Aggregate.prototype.exec = async function exec() {
|
|
|
1090
1104
|
prepareDiscriminatorPipeline(this._pipeline, this._model.schema);
|
|
1091
1105
|
stringifyFunctionOperators(this._pipeline);
|
|
1092
1106
|
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1107
|
+
const _this = this;
|
|
1108
|
+
return traceAggregate(async function maybeTracedAggregateExec() {
|
|
1109
|
+
const preFilter = buildMiddlewareFilter(_this.options, 'pre');
|
|
1110
|
+
const postFilter = buildMiddlewareFilter(_this.options, 'post');
|
|
1095
1111
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1112
|
+
try {
|
|
1113
|
+
await model.hooks.execPre('aggregate', _this, [], { filter: preFilter });
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
return await model.hooks.execPost('aggregate', _this, [null], { error, filter: postFilter });
|
|
1116
|
+
}
|
|
1101
1117
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1118
|
+
if (!_this._pipeline.length) {
|
|
1119
|
+
throw new MongooseError('Aggregate has empty pipeline');
|
|
1120
|
+
}
|
|
1105
1121
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1122
|
+
const options = clone(_this.options || {});
|
|
1123
|
+
delete options.middleware;
|
|
1108
1124
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1125
|
+
let result;
|
|
1126
|
+
try {
|
|
1127
|
+
const cursor = await collection.aggregate(_this._pipeline, options);
|
|
1128
|
+
result = await cursor.toArray();
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
return await model.hooks.execPost('aggregate', _this, [null], { error, filter: postFilter });
|
|
1131
|
+
}
|
|
1116
1132
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1133
|
+
await model.hooks.execPost('aggregate', _this, [result], { error: null, filter: postFilter });
|
|
1134
|
+
return result;
|
|
1135
|
+
}, () => ({
|
|
1136
|
+
operation: 'aggregate',
|
|
1137
|
+
collection: collection.name,
|
|
1138
|
+
database: model.db?.name,
|
|
1139
|
+
serverAddress: model.db?.host,
|
|
1140
|
+
serverPort: model.db?.port,
|
|
1141
|
+
args: {
|
|
1142
|
+
pipeline: _this._pipeline,
|
|
1143
|
+
options: _this.options
|
|
1144
|
+
}
|
|
1145
|
+
}));
|
|
1119
1146
|
};
|
|
1120
1147
|
|
|
1121
1148
|
/**
|
package/lib/connection.js
CHANGED
|
@@ -1354,7 +1354,7 @@ Connection.prototype.onClose = function onClose(force) {
|
|
|
1354
1354
|
|
|
1355
1355
|
/**
|
|
1356
1356
|
* Retrieves a raw collection instance, creating it if not cached.
|
|
1357
|
-
* This method returns a thin wrapper around a [MongoDB Node.js driver collection](
|
|
1357
|
+
* This method returns a thin wrapper around a [MongoDB Node.js driver collection](https://mongodb.github.io/node-mongodb-native/Next/classes/Collection.html).
|
|
1358
1358
|
* Using a Collection bypasses Mongoose middleware, validation, and casting,
|
|
1359
1359
|
* letting you use [MongoDB Node.js driver](https://mongodb.github.io/node-mongodb-native/) functionality directly.
|
|
1360
1360
|
*
|
|
@@ -10,6 +10,7 @@ const eachAsync = require('../helpers/cursor/eachAsync');
|
|
|
10
10
|
const immediate = require('../helpers/immediate');
|
|
11
11
|
const kareem = require('kareem');
|
|
12
12
|
const util = require('util');
|
|
13
|
+
const { cursorNextChannel } = require('../tracing');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* An AggregationCursor is a concurrency primitive for processing aggregation
|
|
@@ -62,13 +63,7 @@ util.inherits(AggregationCursor, Readable);
|
|
|
62
63
|
*/
|
|
63
64
|
|
|
64
65
|
function _init(model, c, agg) {
|
|
65
|
-
|
|
66
|
-
model.hooks.execPre('aggregate', agg).then(() => onPreComplete(null), err => onPreComplete(err));
|
|
67
|
-
} else {
|
|
68
|
-
model.collection.emitter.once('queue', function() {
|
|
69
|
-
model.hooks.execPre('aggregate', agg).then(() => onPreComplete(null), err => onPreComplete(err));
|
|
70
|
-
});
|
|
71
|
-
}
|
|
66
|
+
model.hooks.execPre('aggregate', agg).then(() => onPreComplete(null), err => onPreComplete(err));
|
|
72
67
|
|
|
73
68
|
function onPreComplete(err) {
|
|
74
69
|
if (err != null) {
|
|
@@ -79,8 +74,28 @@ function _init(model, c, agg) {
|
|
|
79
74
|
c._transforms.push(agg.options.cursor.transform);
|
|
80
75
|
}
|
|
81
76
|
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
if (model.collection._shouldBufferCommands() && model.collection.buffer) {
|
|
78
|
+
model.collection.queue.push([
|
|
79
|
+
() => _getRawCursor(model, c, agg)
|
|
80
|
+
]);
|
|
81
|
+
} else {
|
|
82
|
+
_getRawCursor(model, c, agg);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/*!
|
|
88
|
+
* ignore
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
function _getRawCursor(model, aggregationCursor, agg) {
|
|
92
|
+
try {
|
|
93
|
+
const cursor = model.collection.aggregate(agg._pipeline, agg.options || {});
|
|
94
|
+
aggregationCursor.cursor = cursor;
|
|
95
|
+
aggregationCursor.emit('cursor', cursor);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
aggregationCursor._markError(err);
|
|
98
|
+
aggregationCursor.listeners('error').length > 0 && aggregationCursor.emit('error', aggregationCursor._error);
|
|
84
99
|
}
|
|
85
100
|
}
|
|
86
101
|
|
|
@@ -277,14 +292,30 @@ AggregationCursor.prototype.next = async function next() {
|
|
|
277
292
|
if (typeof arguments[0] === 'function') {
|
|
278
293
|
throw new MongooseError('AggregationCursor.prototype.next() no longer accepts a callback');
|
|
279
294
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
295
|
+
const _this = this;
|
|
296
|
+
const model = this.agg._model;
|
|
297
|
+
return cursorNextChannel.trace(function maybeTracedAggCursorNext() {
|
|
298
|
+
return new Promise((resolve, reject) => {
|
|
299
|
+
_next(_this, (err, res) => {
|
|
300
|
+
if (err != null) {
|
|
301
|
+
return reject(err);
|
|
302
|
+
}
|
|
303
|
+
resolve(res);
|
|
304
|
+
});
|
|
286
305
|
});
|
|
287
|
-
})
|
|
306
|
+
}, () => ({
|
|
307
|
+
operation: 'aggregate',
|
|
308
|
+
collection: model?.collection?.name,
|
|
309
|
+
database: model?.db?.name,
|
|
310
|
+
serverAddress: model?.db?.host,
|
|
311
|
+
serverPort: model?.db?.port,
|
|
312
|
+
batchSize: _this.agg.options?.cursor?.batchSize,
|
|
313
|
+
tailable: false,
|
|
314
|
+
args: {
|
|
315
|
+
pipeline: _this.agg._pipeline,
|
|
316
|
+
options: _this.agg.options
|
|
317
|
+
}
|
|
318
|
+
}));
|
|
288
319
|
};
|
|
289
320
|
|
|
290
321
|
/**
|
|
@@ -12,6 +12,7 @@ const kareem = require('kareem');
|
|
|
12
12
|
const immediate = require('../helpers/immediate');
|
|
13
13
|
const { once } = require('events');
|
|
14
14
|
const util = require('util');
|
|
15
|
+
const { cursorNextChannel } = require('../tracing');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* A QueryCursor is a concurrency primitive for processing query results
|
|
@@ -310,14 +311,29 @@ QueryCursor.prototype.next = async function next() {
|
|
|
310
311
|
if (this._closed) {
|
|
311
312
|
throw new MongooseError('Cannot call `next()` on a closed cursor');
|
|
312
313
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
314
|
+
const _this = this;
|
|
315
|
+
return cursorNextChannel.trace(function maybeTracedQueryCursorNext() {
|
|
316
|
+
return new Promise((resolve, reject) => {
|
|
317
|
+
_next(_this, function(error, doc) {
|
|
318
|
+
if (error) {
|
|
319
|
+
return reject(error);
|
|
320
|
+
}
|
|
321
|
+
resolve(doc);
|
|
322
|
+
});
|
|
319
323
|
});
|
|
320
|
-
})
|
|
324
|
+
}, () => ({
|
|
325
|
+
operation: _this.query.op || 'find',
|
|
326
|
+
collection: _this.query.mongooseCollection.name,
|
|
327
|
+
database: _this.model.db?.name,
|
|
328
|
+
serverAddress: _this.model.db?.host,
|
|
329
|
+
serverPort: _this.model.db?.port,
|
|
330
|
+
batchSize: _this.options.batchSize || _this.query.options?.batchSize,
|
|
331
|
+
tailable: _this.options.tailable || _this.query.options?.tailable || false,
|
|
332
|
+
args: {
|
|
333
|
+
filter: _this.query.getFilter(),
|
|
334
|
+
options: _this.query._mongooseOptions
|
|
335
|
+
}
|
|
336
|
+
}));
|
|
321
337
|
};
|
|
322
338
|
|
|
323
339
|
/**
|
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
|
};
|
|
@@ -3264,6 +3259,9 @@ function _handlePathsToSkip(paths, pathsToSkip) {
|
|
|
3264
3259
|
/**
|
|
3265
3260
|
* Executes registered validation rules (skipping asynchronous validators) for this document.
|
|
3266
3261
|
*
|
|
3262
|
+
* **Deprecated.** Use [`validate()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.validate()) instead.
|
|
3263
|
+
* `validateSync()` does not run validation middleware and will be removed in Mongoose 10.
|
|
3264
|
+
*
|
|
3267
3265
|
* #### Note:
|
|
3268
3266
|
*
|
|
3269
3267
|
* This method is useful if you need synchronous validation.
|
|
@@ -3281,14 +3279,21 @@ function _handlePathsToSkip(paths, pathsToSkip) {
|
|
|
3281
3279
|
* @param {object} [options] options for validation
|
|
3282
3280
|
* @param {boolean} [options.validateModifiedOnly=false] If `true`, Mongoose will only validate modified paths, as opposed to modified paths and `required` paths.
|
|
3283
3281
|
* @param {Array|string} [options.pathsToSkip] list of paths to skip. If set, Mongoose will validate every modified path that is not in this list.
|
|
3284
|
-
* @param {boolean|object} [options.middleware=true] set to `false` to skip all user-defined middleware
|
|
3285
|
-
* @param {boolean} [options.middleware.pre=true] set to `false` to skip only pre hooks
|
|
3286
|
-
* @param {boolean} [options.middleware.post=true] set to `false` to skip only post hooks
|
|
3287
3282
|
* @return {ValidationError|undefined} ValidationError if there are errors during validation, or undefined if there is no error.
|
|
3283
|
+
* @deprecated Use `validate()` instead. `validateSync()` does not run middleware and will be removed in Mongoose 10.
|
|
3288
3284
|
* @api public
|
|
3289
3285
|
*/
|
|
3290
3286
|
|
|
3291
|
-
Document.prototype.validateSync = function(
|
|
3287
|
+
Document.prototype.validateSync = function() {
|
|
3288
|
+
utils.warn('Mongoose: `Document.prototype.validateSync()` is deprecated and will be removed in Mongoose 10. Use `Document.prototype.validate()` instead.');
|
|
3289
|
+
return this.$__validateSync.apply(this, arguments);
|
|
3290
|
+
};
|
|
3291
|
+
|
|
3292
|
+
/*!
|
|
3293
|
+
* ignore
|
|
3294
|
+
*/
|
|
3295
|
+
|
|
3296
|
+
Document.prototype.$__validateSync = function(pathsToValidate, options) {
|
|
3292
3297
|
const _this = this;
|
|
3293
3298
|
|
|
3294
3299
|
if (arguments.length === 1 && typeof arguments[0] === 'object' && !Array.isArray(arguments[0])) {
|
|
@@ -3620,7 +3625,11 @@ Document.prototype.$__reset = function reset() {
|
|
|
3620
3625
|
// Clear atomics on dirty paths. Walk the modified and default paths
|
|
3621
3626
|
// directly instead of calling $__dirty(), which builds intermediate
|
|
3622
3627
|
// arrays, a Map, and does parent-path deduplication we don't need here.
|
|
3623
|
-
|
|
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
|
+
}
|
|
3624
3633
|
|
|
3625
3634
|
this.$__.backup = {};
|
|
3626
3635
|
this.$__.backup.activePaths = {
|
|
@@ -5266,6 +5275,12 @@ function checkDivergentArray(doc, path, array) {
|
|
|
5266
5275
|
// If any array was selected using an $elemMatch projection, we deny the update.
|
|
5267
5276
|
// NOTE: MongoDB only supports projected $elemMatch on top level array.
|
|
5268
5277
|
const top = path.split('.')[0];
|
|
5278
|
+
if (doc.$__.selected[top] && doc.$__.selected[top].$slice != null && utils.isMongooseArray(array)) {
|
|
5279
|
+
const atomics = array[arrayAtomicsSymbol];
|
|
5280
|
+
if (atomics.$set) {
|
|
5281
|
+
return top;
|
|
5282
|
+
}
|
|
5283
|
+
}
|
|
5269
5284
|
if (doc.$__.selected[top + '.$']) {
|
|
5270
5285
|
return top;
|
|
5271
5286
|
}
|
|
@@ -17,7 +17,7 @@ class DivergentArrayError extends MongooseError {
|
|
|
17
17
|
|
|
18
18
|
constructor(paths) {
|
|
19
19
|
const msg = 'For your own good, using `document.save()` to update an array '
|
|
20
|
-
+ 'which was selected using an $elemMatch projection OR '
|
|
20
|
+
+ 'which was selected using an $elemMatch or $slice projection OR '
|
|
21
21
|
+ 'populated using skip, limit, query conditions, or exclusion of '
|
|
22
22
|
+ 'the _id field when the operation results in a $pop or $set of '
|
|
23
23
|
+ 'the entire array is not supported. The following '
|
|
@@ -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
|
}
|
|
@@ -873,7 +871,11 @@ function _findRefPathForDiscriminators(doc, modelSchema, data, options, normaliz
|
|
|
873
871
|
}
|
|
874
872
|
|
|
875
873
|
/**
|
|
876
|
-
* Throw an error if there are any $where keys
|
|
874
|
+
* Throw an error if there are any $where keys to defend against [CVE-2024-53900](https://nvd.nist.gov/vuln/detail/CVE-2024-53900)
|
|
875
|
+
*
|
|
876
|
+
* Note that this is ONLY for $where because sift executes $where in Node.js memory.
|
|
877
|
+
* Other forms of MongoDB server-side execution, like $expr, are NOT filtered out.
|
|
878
|
+
* This function is not meant to protect against server-side execution in MongoDB.
|
|
877
879
|
*/
|
|
878
880
|
|
|
879
881
|
function throwOn$where(match) {
|
|
@@ -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;
|