@depup/mongoose 9.3.3-depup.0 → 9.8.0-depup.0
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/README.md +3 -9
- package/changes.json +3 -8
- package/eslint.config.mjs +4 -1
- package/lib/aggregate.js +54 -24
- package/lib/cast.js +1 -1
- package/lib/connection.js +20 -8
- package/lib/cursor/aggregationCursor.js +47 -16
- package/lib/cursor/queryCursor.js +23 -7
- package/lib/document.js +240 -105
- package/lib/drivers/node-mongodb-native/collection.js +47 -14
- package/lib/drivers/node-mongodb-native/connection.js +13 -0
- package/lib/error/cast.js +18 -9
- package/lib/error/divergentArray.js +1 -1
- package/lib/error/index.js +1 -1
- package/lib/error/messages.js +1 -0
- package/lib/helpers/clone.js +22 -5
- 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/query/castUpdate.js +4 -0
- package/lib/helpers/schema/getKeysInSchemaOrder.js +13 -16
- package/lib/helpers/update/applyTimestampsToUpdate.js +4 -2
- package/lib/model.js +159 -41
- package/lib/mongoose.js +3 -3
- package/lib/options/schemaTypeOptions.js +14 -0
- package/lib/plugins/saveSubdocs.js +16 -13
- package/lib/query.js +208 -102
- package/lib/queryHelpers.js +13 -12
- package/lib/schema/array.js +4 -6
- package/lib/schema/bigint.js +1 -3
- package/lib/schema/boolean.js +1 -3
- package/lib/schema/buffer.js +1 -3
- package/lib/schema/date.js +8 -8
- package/lib/schema/decimal128.js +1 -3
- package/lib/schema/documentArray.js +3 -5
- package/lib/schema/double.js +1 -3
- package/lib/schema/int32.js +1 -3
- package/lib/schema/map.js +1 -4
- package/lib/schema/number.js +2 -4
- package/lib/schema/objectId.js +7 -3
- package/lib/schema/string.js +20 -5
- package/lib/schema/subdocument.js +2 -4
- package/lib/schema/union.js +50 -0
- package/lib/schema/uuid.js +1 -3
- package/lib/schema.js +40 -14
- package/lib/schemaType.js +93 -12
- package/lib/standardSchema/convertErrorToIssues.js +20 -0
- package/lib/stateMachine.js +11 -6
- package/lib/tracing.js +38 -0
- package/lib/types/array/methods/index.js +4 -4
- package/lib/types/documentArray/methods/index.js +117 -3
- package/lib/types/subdocument.js +4 -2
- package/lib/utils.js +12 -2
- package/lib/validOptions.js +1 -0
- package/package.json +30 -33
- package/{tstyche.config.json → tstyche.json} +2 -1
- package/types/augmentations.d.ts +2 -2
- package/types/document.d.ts +61 -7
- package/types/expressions.d.ts +16 -0
- package/types/index.d.ts +25 -7
- package/types/inferhydrateddoctype.d.ts +1 -1
- package/types/inferrawdoctype.d.ts +6 -3
- package/types/inferschematype.d.ts +10 -2
- package/types/models.d.ts +30 -13
- package/types/mongooseoptions.d.ts +9 -1
- package/types/populate.d.ts +52 -0
- package/types/query.d.ts +39 -12
- package/types/schemaoptions.d.ts +5 -0
- package/types/schematypes.d.ts +10 -0
- package/types/tracing.d.ts +10 -0
- package/types/utility.d.ts +11 -2
- package/lib/helpers/createJSONSchemaTypeDefinition.js +0 -24
|
@@ -89,12 +89,11 @@ NativeCollection.prototype._getCollection = function _getCollection() {
|
|
|
89
89
|
function iter(i) {
|
|
90
90
|
NativeCollection.prototype[i] = function() {
|
|
91
91
|
const collection = this._getCollection();
|
|
92
|
-
const args =
|
|
92
|
+
const args = arguments;
|
|
93
93
|
const _this = this;
|
|
94
94
|
const globalDebug = _this?.conn?.base?.options?.debug;
|
|
95
95
|
const connectionDebug = _this?.conn?.options?.debug;
|
|
96
96
|
const debug = connectionDebug == null ? globalDebug : connectionDebug;
|
|
97
|
-
const opId = new ObjectId();
|
|
98
97
|
|
|
99
98
|
// If user force closed, queueing will hang forever. See #5664
|
|
100
99
|
if (this.conn.$wasForceClosed) {
|
|
@@ -108,9 +107,21 @@ function iter(i) {
|
|
|
108
107
|
}
|
|
109
108
|
}
|
|
110
109
|
|
|
110
|
+
// Lazily generate opId and argsArray only if necessary because they have
|
|
111
|
+
// a non-trivial performance cost.
|
|
112
|
+
let opId = null;
|
|
113
|
+
let argsArray = null;
|
|
114
|
+
const hasOperationListeners = this.conn.listenerCount('operation-start') > 0 || this.conn.listenerCount('operation-end') > 0;
|
|
115
|
+
if (hasOperationListeners || debug) {
|
|
116
|
+
opId = new ObjectId();
|
|
117
|
+
}
|
|
118
|
+
|
|
111
119
|
let timeout = null;
|
|
112
120
|
let waitForBufferPromise = null;
|
|
113
121
|
if (this._shouldBufferCommands() && this.buffer) {
|
|
122
|
+
if (opId == null) {
|
|
123
|
+
opId = new ObjectId();
|
|
124
|
+
}
|
|
114
125
|
this.conn.emit('buffer', {
|
|
115
126
|
_id: opId,
|
|
116
127
|
modelName: _this.modelName,
|
|
@@ -145,11 +156,14 @@ function iter(i) {
|
|
|
145
156
|
|
|
146
157
|
if (debug) {
|
|
147
158
|
if (typeof debug === 'function') {
|
|
159
|
+
if (argsArray == null) {
|
|
160
|
+
argsArray = Array.from(args);
|
|
161
|
+
}
|
|
148
162
|
let argsToAdd = null;
|
|
149
|
-
if (typeof
|
|
150
|
-
argsToAdd =
|
|
163
|
+
if (typeof argsArray[argsArray.length - 1] == 'function') {
|
|
164
|
+
argsToAdd = argsArray.slice(0, argsArray.length - 1);
|
|
151
165
|
} else {
|
|
152
|
-
argsToAdd =
|
|
166
|
+
argsToAdd = argsArray;
|
|
153
167
|
}
|
|
154
168
|
debug.apply(_this,
|
|
155
169
|
[_this.name, i].concat(argsToAdd));
|
|
@@ -158,11 +172,17 @@ function iter(i) {
|
|
|
158
172
|
} else {
|
|
159
173
|
const color = debug.color == null ? true : debug.color;
|
|
160
174
|
const shell = debug.shell == null ? false : debug.shell;
|
|
161
|
-
|
|
175
|
+
const timestamp = debug.timestamp == null ? false : debug.timestamp;
|
|
176
|
+
this.$print(_this.name, i, args, color, shell, timestamp);
|
|
162
177
|
}
|
|
163
178
|
}
|
|
164
179
|
|
|
165
|
-
|
|
180
|
+
if (hasOperationListeners) {
|
|
181
|
+
if (argsArray == null) {
|
|
182
|
+
argsArray = Array.from(args);
|
|
183
|
+
}
|
|
184
|
+
this.conn.emit('operation-start', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, params: argsArray });
|
|
185
|
+
}
|
|
166
186
|
|
|
167
187
|
try {
|
|
168
188
|
if (collection == null) {
|
|
@@ -179,20 +199,26 @@ function iter(i) {
|
|
|
179
199
|
if (timeout != null) {
|
|
180
200
|
clearTimeout(timeout);
|
|
181
201
|
}
|
|
182
|
-
|
|
202
|
+
if (hasOperationListeners) {
|
|
203
|
+
this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, result });
|
|
204
|
+
}
|
|
183
205
|
return result;
|
|
184
206
|
},
|
|
185
207
|
error => {
|
|
186
208
|
if (timeout != null) {
|
|
187
209
|
clearTimeout(timeout);
|
|
188
210
|
}
|
|
189
|
-
|
|
211
|
+
if (hasOperationListeners) {
|
|
212
|
+
this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, error });
|
|
213
|
+
}
|
|
190
214
|
throw error;
|
|
191
215
|
}
|
|
192
216
|
);
|
|
193
217
|
}
|
|
194
218
|
|
|
195
|
-
|
|
219
|
+
if (hasOperationListeners) {
|
|
220
|
+
this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, result: ret });
|
|
221
|
+
}
|
|
196
222
|
if (timeout != null) {
|
|
197
223
|
clearTimeout(timeout);
|
|
198
224
|
}
|
|
@@ -201,7 +227,9 @@ function iter(i) {
|
|
|
201
227
|
if (timeout != null) {
|
|
202
228
|
clearTimeout(timeout);
|
|
203
229
|
}
|
|
204
|
-
|
|
230
|
+
if (hasOperationListeners) {
|
|
231
|
+
this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, error: error });
|
|
232
|
+
}
|
|
205
233
|
throw error;
|
|
206
234
|
}
|
|
207
235
|
};
|
|
@@ -229,7 +257,12 @@ for (const key of Object.getOwnPropertyNames(Collection.prototype)) {
|
|
|
229
257
|
* @method $print
|
|
230
258
|
*/
|
|
231
259
|
|
|
232
|
-
NativeCollection.prototype.$print = function(name, i, args, color, shell) {
|
|
260
|
+
NativeCollection.prototype.$print = function(name, i, args, color, shell, timestamp) {
|
|
261
|
+
let prefix = '';
|
|
262
|
+
if (timestamp) {
|
|
263
|
+
const ts = new Date().toISOString();
|
|
264
|
+
prefix = color ? `\x1B[0;90m[${ts}]\x1B[0m ` : `[${ts}] `;
|
|
265
|
+
}
|
|
233
266
|
const moduleName = color ? '\x1B[0;36mMongoose:\x1B[0m ' : 'Mongoose: ';
|
|
234
267
|
const functionCall = [name, i].join('.');
|
|
235
268
|
const _args = [];
|
|
@@ -240,14 +273,14 @@ NativeCollection.prototype.$print = function(name, i, args, color, shell) {
|
|
|
240
273
|
}
|
|
241
274
|
const params = '(' + _args.join(', ') + ')';
|
|
242
275
|
|
|
243
|
-
console.info(moduleName + functionCall + params);
|
|
276
|
+
console.info(prefix + moduleName + functionCall + params);
|
|
244
277
|
};
|
|
245
278
|
|
|
246
279
|
/**
|
|
247
280
|
* Debug print helper
|
|
248
281
|
*
|
|
249
282
|
* @api public
|
|
250
|
-
* @method $
|
|
283
|
+
* @method $printToStream
|
|
251
284
|
*/
|
|
252
285
|
|
|
253
286
|
NativeCollection.prototype.$printToStream = function(name, i, args, stream) {
|
|
@@ -14,6 +14,10 @@ const setTimeout = require('../../helpers/timers').setTimeout;
|
|
|
14
14
|
const utils = require('../../utils');
|
|
15
15
|
const Schema = require('../../schema');
|
|
16
16
|
|
|
17
|
+
// Snapshot the native Date constructor to ensure Date.now()
|
|
18
|
+
// bypasses timer mocks such as those set up by useFakeTimers().
|
|
19
|
+
const Date = globalThis.Date;
|
|
20
|
+
|
|
17
21
|
/**
|
|
18
22
|
* A [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) connection implementation.
|
|
19
23
|
*
|
|
@@ -482,6 +486,15 @@ function _setClient(conn, client, options, dbName) {
|
|
|
482
486
|
for (const otherDb of conn.otherDbs) {
|
|
483
487
|
otherDb._lastHeartbeatAt = conn._lastHeartbeatAt;
|
|
484
488
|
}
|
|
489
|
+
// Flush buffered operations if the connection is no longer stale (gh-16183)
|
|
490
|
+
if (conn._queue.length > 0 && conn.readyState === STATES.connected) {
|
|
491
|
+
conn._flushQueue();
|
|
492
|
+
}
|
|
493
|
+
for (const otherDb of conn.otherDbs) {
|
|
494
|
+
if (otherDb._queue.length > 0 && otherDb.readyState === STATES.connected) {
|
|
495
|
+
otherDb._flushQueue();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
485
498
|
});
|
|
486
499
|
|
|
487
500
|
if (options.monitorCommands) {
|
package/lib/error/cast.js
CHANGED
|
@@ -42,6 +42,7 @@ class CastError extends MongooseError {
|
|
|
42
42
|
message: this.message
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
|
|
45
46
|
/*!
|
|
46
47
|
* ignore
|
|
47
48
|
*/
|
|
@@ -76,7 +77,7 @@ class CastError extends MongooseError {
|
|
|
76
77
|
*/
|
|
77
78
|
setModel(model) {
|
|
78
79
|
this.message = formatMessage(model, this.kind, this.value, this.path,
|
|
79
|
-
this.messageFormat, this.valueType);
|
|
80
|
+
this.messageFormat, this.valueType, this.reason);
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -122,10 +123,11 @@ function getMessageFormat(schemaType) {
|
|
|
122
123
|
function formatMessage(model, kind, value, path, messageFormat, valueType, reason) {
|
|
123
124
|
if (typeof messageFormat === 'string') {
|
|
124
125
|
const stringValue = getStringValue(value);
|
|
125
|
-
let ret = messageFormat
|
|
126
|
-
replace('{KIND}', kind)
|
|
127
|
-
replace('{VALUE}', stringValue)
|
|
128
|
-
replace('{PATH}', path);
|
|
126
|
+
let ret = messageFormat
|
|
127
|
+
.replace('{KIND}', kind)
|
|
128
|
+
.replace('{VALUE}', stringValue)
|
|
129
|
+
.replace('{PATH}', path);
|
|
130
|
+
|
|
129
131
|
if (model != null) {
|
|
130
132
|
ret = ret.replace('{MODEL}', model.modelName);
|
|
131
133
|
}
|
|
@@ -136,17 +138,24 @@ function formatMessage(model, kind, value, path, messageFormat, valueType, reaso
|
|
|
136
138
|
} else {
|
|
137
139
|
const stringValue = getStringValue(value);
|
|
138
140
|
const valueTypeMsg = valueType ? ' (type ' + valueType + ')' : '';
|
|
141
|
+
|
|
139
142
|
let ret = 'Cast to ' + kind + ' failed for value ' +
|
|
140
143
|
stringValue + valueTypeMsg + ' at path "' + path + '"';
|
|
144
|
+
|
|
141
145
|
if (model != null) {
|
|
142
146
|
ret += ' for model "' + model.modelName + '"';
|
|
143
147
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
reason != null &&
|
|
151
|
+
typeof reason.constructor === 'function' &&
|
|
152
|
+
reason.constructor.name !== 'AssertionError' &&
|
|
153
|
+
reason.constructor.name !== 'Error' &&
|
|
154
|
+
reason.constructor.name !== 'BSONError'
|
|
155
|
+
) {
|
|
148
156
|
ret += ' because of "' + reason.constructor.name + '"';
|
|
149
157
|
}
|
|
158
|
+
|
|
150
159
|
return ret;
|
|
151
160
|
}
|
|
152
161
|
}
|
|
@@ -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 '
|
package/lib/error/index.js
CHANGED
|
@@ -32,7 +32,7 @@ const MongooseError = require('./mongooseError');
|
|
|
32
32
|
* - `ValidationError`: error returned from [`validate()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.validate()) or [`validateSync()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.validateSync()). Contains zero or more `ValidatorError` instances in `.errors` property.
|
|
33
33
|
* - `MissingSchemaError`: You called `mongoose.Document()` without a schema
|
|
34
34
|
* - `ObjectExpectedError`: Thrown when you set a nested path to a non-object value with [strict mode set](https://mongoosejs.com/docs/guide.html#strict).
|
|
35
|
-
* - `ObjectParameterError`: Thrown when you pass a non-object value to a function which expects an object as a
|
|
35
|
+
* - `ObjectParameterError`: Thrown when you pass a non-object value to a function which expects an object as a parameter
|
|
36
36
|
* - `OverwriteModelError`: Thrown when you call [`mongoose.model()`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.model()) to re-define a model that was already defined.
|
|
37
37
|
* - `ParallelSaveError`: Thrown when you call [`save()`](https://mongoosejs.com/docs/api/model.html#Model.prototype.save()) on a document when the same document instance is already saving.
|
|
38
38
|
* - `StrictModeError`: Thrown when you set a path that isn't the schema and [strict mode](https://mongoosejs.com/docs/guide.html#strict) is set to `throw`.
|
package/lib/error/messages.js
CHANGED
|
@@ -30,6 +30,7 @@ msg.DocumentNotFoundError = null;
|
|
|
30
30
|
msg.general = {};
|
|
31
31
|
msg.general.default = 'Validator failed for path `{PATH}` with value `{VALUE}`';
|
|
32
32
|
msg.general.required = 'Path `{PATH}` is required.';
|
|
33
|
+
msg.general.allowNull = 'Path `{PATH}` does not allow null values.';
|
|
33
34
|
|
|
34
35
|
msg.Number = {};
|
|
35
36
|
msg.Number.min = 'Path `{PATH}` ({VALUE}) is less than minimum allowed value ({MIN}).';
|
package/lib/helpers/clone.js
CHANGED
|
@@ -202,10 +202,15 @@ function cloneArray(arr, options) {
|
|
|
202
202
|
|
|
203
203
|
let ret = null;
|
|
204
204
|
if (options?.retainDocuments) {
|
|
205
|
+
// Use the cloned parent doc (options.parentDoc) when available so the new
|
|
206
|
+
// array is wired up to the clone, not the source. Falling back to the
|
|
207
|
+
// source's parent preserves the previous behavior for callers that don't
|
|
208
|
+
// pass parentDoc.
|
|
209
|
+
const newParent = options.parentDoc ?? (arr.isMongooseArray ? arr.$parent() : undefined);
|
|
205
210
|
if (arr.isMongooseDocumentArray) {
|
|
206
|
-
ret = new (arr.$schemaType().schema.base.Types.DocumentArray)([], arr.$path(),
|
|
211
|
+
ret = new (arr.$schemaType().schema.base.Types.DocumentArray)([], arr.$path(), newParent, arr.$schemaType());
|
|
207
212
|
} else if (arr.isMongooseArray) {
|
|
208
|
-
ret = new (arr.$parent().schema.base.Types.Array)([], arr.$path(),
|
|
213
|
+
ret = new (arr.$parent().schema.base.Types.Array)([], arr.$path(), newParent, arr.$schemaType());
|
|
209
214
|
} else {
|
|
210
215
|
ret = new Array(len);
|
|
211
216
|
}
|
|
@@ -218,9 +223,21 @@ function cloneArray(arr, options) {
|
|
|
218
223
|
// Create new options object to avoid mutating the shared options.
|
|
219
224
|
// Subdocs need parentArray to point to their own cloned array.
|
|
220
225
|
options = { ...options, parentArray: ret };
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
// Skip change-tracking while populating the freshly cloned array: every
|
|
227
|
+
// index assignment would otherwise call markModified on the cloned parent
|
|
228
|
+
// doc and (before this guard) on the source parent doc.
|
|
229
|
+
for (i = 0; i < len; ++i) {
|
|
230
|
+
const doc = clone(arr[i], options, true);
|
|
231
|
+
// Document arrays can contain null entries, so only restamp actual subdocs.
|
|
232
|
+
if (typeof doc?.$setIndex === 'function') {
|
|
233
|
+
doc.$setIndex(i);
|
|
234
|
+
}
|
|
235
|
+
ret.set(i, doc, true);
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
for (i = 0; i < len; ++i) {
|
|
239
|
+
ret[i] = clone(arr[i], options, true);
|
|
240
|
+
}
|
|
224
241
|
}
|
|
225
242
|
|
|
226
243
|
return ret;
|
|
@@ -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
|
+
}
|
|
@@ -98,6 +98,10 @@ module.exports = function castUpdate(schema, obj, options, context, filter) {
|
|
|
98
98
|
moveImmutableProperties(schema, obj, context);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
if (obj?.$__ && typeof obj.toObject === 'function') {
|
|
102
|
+
obj = obj.toObject(internalToObjectOptions);
|
|
103
|
+
}
|
|
104
|
+
|
|
101
105
|
const ops = Object.keys(obj);
|
|
102
106
|
let i = ops.length;
|
|
103
107
|
const ret = {};
|
|
@@ -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;
|
|
@@ -82,7 +82,9 @@ function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, optio
|
|
|
82
82
|
|
|
83
83
|
if (!skipCreatedAt && createdAt) {
|
|
84
84
|
const overwriteImmutable = get(options, 'overwriteImmutable', false);
|
|
85
|
-
const hasUserCreatedAt = currentUpdate[createdAt] != null
|
|
85
|
+
const hasUserCreatedAt = currentUpdate[createdAt] != null
|
|
86
|
+
|| currentUpdate.$set?.[createdAt] != null
|
|
87
|
+
|| currentUpdate.$setOnInsert?.[createdAt] != null;
|
|
86
88
|
|
|
87
89
|
// If overwriteImmutable is true and user provided createdAt, keep their value
|
|
88
90
|
if (overwriteImmutable && hasUserCreatedAt) {
|
|
@@ -91,7 +93,7 @@ function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, optio
|
|
|
91
93
|
updates.$set[createdAt] = currentUpdate[createdAt];
|
|
92
94
|
delete currentUpdate[createdAt];
|
|
93
95
|
}
|
|
94
|
-
// User's value is already in $set, nothing more to do
|
|
96
|
+
// User's value is already in $set or $setOnInsert, nothing more to do
|
|
95
97
|
} else {
|
|
96
98
|
if (currentUpdate[createdAt]) {
|
|
97
99
|
delete currentUpdate[createdAt];
|