@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.
Files changed (73) hide show
  1. package/README.md +3 -9
  2. package/changes.json +3 -8
  3. package/eslint.config.mjs +4 -1
  4. package/lib/aggregate.js +54 -24
  5. package/lib/cast.js +1 -1
  6. package/lib/connection.js +20 -8
  7. package/lib/cursor/aggregationCursor.js +47 -16
  8. package/lib/cursor/queryCursor.js +23 -7
  9. package/lib/document.js +240 -105
  10. package/lib/drivers/node-mongodb-native/collection.js +47 -14
  11. package/lib/drivers/node-mongodb-native/connection.js +13 -0
  12. package/lib/error/cast.js +18 -9
  13. package/lib/error/divergentArray.js +1 -1
  14. package/lib/error/index.js +1 -1
  15. package/lib/error/messages.js +1 -0
  16. package/lib/helpers/clone.js +22 -5
  17. package/lib/helpers/document/applyDefaults.js +5 -0
  18. package/lib/helpers/populate/createPopulateQueryFilter.js +9 -1
  19. package/lib/helpers/populate/getModelsMapForPopulate.js +8 -6
  20. package/lib/helpers/populate/splitPopulateQuery.js +81 -0
  21. package/lib/helpers/query/castUpdate.js +4 -0
  22. package/lib/helpers/schema/getKeysInSchemaOrder.js +13 -16
  23. package/lib/helpers/update/applyTimestampsToUpdate.js +4 -2
  24. package/lib/model.js +159 -41
  25. package/lib/mongoose.js +3 -3
  26. package/lib/options/schemaTypeOptions.js +14 -0
  27. package/lib/plugins/saveSubdocs.js +16 -13
  28. package/lib/query.js +208 -102
  29. package/lib/queryHelpers.js +13 -12
  30. package/lib/schema/array.js +4 -6
  31. package/lib/schema/bigint.js +1 -3
  32. package/lib/schema/boolean.js +1 -3
  33. package/lib/schema/buffer.js +1 -3
  34. package/lib/schema/date.js +8 -8
  35. package/lib/schema/decimal128.js +1 -3
  36. package/lib/schema/documentArray.js +3 -5
  37. package/lib/schema/double.js +1 -3
  38. package/lib/schema/int32.js +1 -3
  39. package/lib/schema/map.js +1 -4
  40. package/lib/schema/number.js +2 -4
  41. package/lib/schema/objectId.js +7 -3
  42. package/lib/schema/string.js +20 -5
  43. package/lib/schema/subdocument.js +2 -4
  44. package/lib/schema/union.js +50 -0
  45. package/lib/schema/uuid.js +1 -3
  46. package/lib/schema.js +40 -14
  47. package/lib/schemaType.js +93 -12
  48. package/lib/standardSchema/convertErrorToIssues.js +20 -0
  49. package/lib/stateMachine.js +11 -6
  50. package/lib/tracing.js +38 -0
  51. package/lib/types/array/methods/index.js +4 -4
  52. package/lib/types/documentArray/methods/index.js +117 -3
  53. package/lib/types/subdocument.js +4 -2
  54. package/lib/utils.js +12 -2
  55. package/lib/validOptions.js +1 -0
  56. package/package.json +30 -33
  57. package/{tstyche.config.json → tstyche.json} +2 -1
  58. package/types/augmentations.d.ts +2 -2
  59. package/types/document.d.ts +61 -7
  60. package/types/expressions.d.ts +16 -0
  61. package/types/index.d.ts +25 -7
  62. package/types/inferhydrateddoctype.d.ts +1 -1
  63. package/types/inferrawdoctype.d.ts +6 -3
  64. package/types/inferschematype.d.ts +10 -2
  65. package/types/models.d.ts +30 -13
  66. package/types/mongooseoptions.d.ts +9 -1
  67. package/types/populate.d.ts +52 -0
  68. package/types/query.d.ts +39 -12
  69. package/types/schemaoptions.d.ts +5 -0
  70. package/types/schematypes.d.ts +10 -0
  71. package/types/tracing.d.ts +10 -0
  72. package/types/utility.d.ts +11 -2
  73. 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 = Array.from(arguments);
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 args[args.length - 1] == 'function') {
150
- argsToAdd = args.slice(0, args.length - 1);
163
+ if (typeof argsArray[argsArray.length - 1] == 'function') {
164
+ argsToAdd = argsArray.slice(0, argsArray.length - 1);
151
165
  } else {
152
- argsToAdd = args;
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
- this.$print(_this.name, i, args, color, shell);
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
- this.conn.emit('operation-start', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, params: args });
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
- this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, result });
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
- this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, error });
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
- this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, result: ret });
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
- this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, error: error });
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 $print
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
- if (reason != null &&
145
- typeof reason.constructor === 'function' &&
146
- reason.constructor.name !== 'AssertionError' &&
147
- reason.constructor.name !== 'Error') {
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 '
@@ -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 paramter
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`.
@@ -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}).';
@@ -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(), arr.$parent(), arr.$schemaType());
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(), arr.$parent(), arr.$schemaType());
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
- for (i = 0; i < len; ++i) {
223
- ret[i] = clone(arr[i], options, true);
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
- match[cur].$elemMatch[foreignField.slice(cur.length + 1)] = trusted({ $in: ids });
28
+ // Copy rather than mutate so the user's `match` stays unchanged and split populate
29
+ // queries (gh-5890) each get their own `$in` rather than sharing one object
30
+ match[cur] = {
31
+ ...match[cur],
32
+ $elemMatch: {
33
+ ...match[cur].$elemMatch,
34
+ [foreignField.slice(cur.length + 1)]: trusted({ $in: ids })
35
+ }
36
+ };
29
37
  delete match[foreignField];
30
38
  break;
31
39
  }
@@ -649,11 +649,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
649
649
  ids = matchIdsToRefPaths(ret, modelNamesForRefPath, modelName);
650
650
  }
651
651
 
652
- const perDocumentLimit = options.perDocumentLimit == null ?
653
- get(options, 'options.perDocumentLimit', null) :
654
- options.perDocumentLimit;
655
-
656
- if (!available[modelName] || perDocumentLimit != null) {
652
+ if (!available[modelName]) {
657
653
  const currentOptions = {
658
654
  model: Model
659
655
  };
@@ -682,6 +678,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
682
678
  isVirtual: data.isVirtual,
683
679
  virtual: data.virtual,
684
680
  count: data.count,
681
+ isRefPath: !!data.isRefPath,
685
682
  [populateModelSymbol]: Model
686
683
  };
687
684
  map.push(available[modelName]);
@@ -692,6 +689,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
692
689
  available[modelName].ids.push(ids);
693
690
  available[modelName].allIds.push(ret);
694
691
  available[modelName].unpopulatedValues.push(unpopulatedValue);
692
+ available[modelName].isRefPath = available[modelName].isRefPath || !!data.isRefPath;
695
693
  if (data.hasMatchFunction) {
696
694
  available[modelName].match.push(data.match);
697
695
  }
@@ -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 valKeys = new Set(Object.keys(val));
12
+ const remaining = new Set(valKeys);
8
13
 
9
- let keys;
10
- if (valKeys.size > 1) {
11
- keys = new Set();
12
- for (const key of schemaKeys) {
13
- if (valKeys.has(key)) {
14
- keys.add(key);
15
- }
14
+ const keys = [];
15
+ for (const key of schemaKeys) {
16
+ if (remaining.delete(key)) {
17
+ keys.push(key);
16
18
  }
17
- for (const key of valKeys) {
18
- if (!keys.has(key)) {
19
- keys.add(key);
20
- }
21
- }
22
- keys = Array.from(keys);
23
- } else {
24
- keys = Array.from(valKeys);
19
+ }
20
+ for (const key of remaining) {
21
+ keys.push(key);
25
22
  }
26
23
 
27
24
  return keys;
@@ -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 || currentUpdate.$set?.[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];