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/model.js CHANGED
@@ -7,6 +7,10 @@
7
7
  const Aggregate = require('./aggregate');
8
8
  const ChangeStream = require('./cursor/changeStream');
9
9
  const Document = require('./document');
10
+ const { createTracedChannel } = require('./tracing');
11
+ const { trace: traceSave } = createTracedChannel('mongoose:model:save');
12
+ const { trace: traceInsertMany } = createTracedChannel('mongoose:model:insertMany');
13
+ const { trace: traceBulkWrite } = createTracedChannel('mongoose:model:bulkWrite');
10
14
  const DocumentNotFoundError = require('./error/notFound');
11
15
  const EventEmitter = require('events').EventEmitter;
12
16
  const Kareem = require('kareem');
@@ -37,6 +41,7 @@ const applyVirtualsHelper = require('./helpers/document/applyVirtuals');
37
41
  const assignVals = require('./helpers/populate/assignVals');
38
42
  const castBulkWrite = require('./helpers/model/castBulkWrite');
39
43
  const clone = require('./helpers/clone');
44
+ const convertErrorToStandardSchemaIssues = require('./standardSchema/convertErrorToIssues');
40
45
  const createPopulateQueryFilter = require('./helpers/populate/createPopulateQueryFilter');
41
46
  const decorateUpdateWithVersionKey = require('./helpers/update/decorateUpdateWithVersionKey');
42
47
  const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult');
@@ -66,6 +71,7 @@ const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscrim
66
71
  const pushNestedArrayPaths = require('./helpers/model/pushNestedArrayPaths');
67
72
  const removeDeselectedForeignField = require('./helpers/populate/removeDeselectedForeignField');
68
73
  const setDottedPath = require('./helpers/path/setDottedPath');
74
+ const splitPopulateQuery = require('./helpers/populate/splitPopulateQuery');
69
75
  const { buildMiddlewareFilter } = require('./helpers/buildMiddlewareFilter');
70
76
  const util = require('util');
71
77
  const utils = require('./utils');
@@ -663,19 +669,29 @@ Model.prototype.save = async function save(options) {
663
669
 
664
670
  this.$__.saveOptions = options;
665
671
 
666
- try {
667
- await this.$__save(options);
668
- } catch (error) {
669
- this.$__handleReject(error);
670
- throw error;
671
- } finally {
672
- this.$__.saving = null;
673
- this.$__.saveOptions = null;
674
- this.$__.$versionError = null;
675
- this.$op = null;
676
- }
677
-
678
- return this;
672
+ const _this = this;
673
+ return traceSave(async function maybeTracedSave() {
674
+ try {
675
+ await _this.$__save(options);
676
+ } catch (error) {
677
+ _this.$__handleReject(error);
678
+ throw error;
679
+ } finally {
680
+ _this.$__.saving = null;
681
+ _this.$__.saveOptions = null;
682
+ _this.$__.$versionError = null;
683
+ _this.$op = null;
684
+ }
685
+
686
+ return _this;
687
+ }, () => ({
688
+ operation: 'save',
689
+ collection: _this.constructor.collection.name,
690
+ database: _this.constructor.db?.name,
691
+ serverAddress: _this.constructor.db?.host,
692
+ serverPort: _this.constructor.db?.port,
693
+ args: { options }
694
+ }));
679
695
  };
680
696
 
681
697
  Model.prototype.$save = Model.prototype.save;
@@ -3015,6 +3031,18 @@ Model.insertMany = async function insertMany(arr, options) {
3015
3031
  throw new MongooseError('Model.insertMany() no longer accepts a callback');
3016
3032
  }
3017
3033
 
3034
+ const ThisModel = this;
3035
+ return traceInsertMany(function maybeTracedInsertMany() { return _insertMany.call(ThisModel, arr, options); }, () => ({
3036
+ operation: 'insertMany',
3037
+ collection: ThisModel.collection.name,
3038
+ database: ThisModel.db?.name,
3039
+ serverAddress: ThisModel.db?.host,
3040
+ serverPort: ThisModel.db?.port,
3041
+ args: { docs: arr, options }
3042
+ }));
3043
+ };
3044
+
3045
+ async function _insertMany(arr, options) {
3018
3046
  options = options || {};
3019
3047
  const preFilter = buildMiddlewareFilter(options, 'pre');
3020
3048
  const postFilter = buildMiddlewareFilter(options, 'post');
@@ -3250,7 +3278,7 @@ Model.insertMany = async function insertMany(arr, options) {
3250
3278
 
3251
3279
  const [result] = await this._middleware.execPost('insertMany', this, [docAttributes], { filter: postFilter });
3252
3280
  return result;
3253
- };
3281
+ }
3254
3282
 
3255
3283
  /*!
3256
3284
  * ignore
@@ -3378,6 +3406,19 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
3378
3406
  typeof arguments[2] === 'function') {
3379
3407
  throw new MongooseError('Model.bulkWrite() no longer accepts a callback');
3380
3408
  }
3409
+
3410
+ const ThisModel = this;
3411
+ return traceBulkWrite(function maybeTracedBulkWrite() { return _bulkWrite.call(ThisModel, ops, options); }, () => ({
3412
+ operation: 'bulkWrite',
3413
+ collection: ThisModel.collection.name,
3414
+ database: ThisModel.db?.name,
3415
+ serverAddress: ThisModel.db?.host,
3416
+ serverPort: ThisModel.db?.port,
3417
+ args: { ops, options }
3418
+ }));
3419
+ };
3420
+
3421
+ async function _bulkWrite(ops, options) {
3381
3422
  options = options || {};
3382
3423
  const preFilter = buildMiddlewareFilter(options, 'pre');
3383
3424
  const postFilter = buildMiddlewareFilter(options, 'post');
@@ -3516,7 +3557,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
3516
3557
  await this.hooks.execPost('bulkWrite', this, [res], { filter: postFilter });
3517
3558
 
3518
3559
  return res;
3519
- };
3560
+ }
3520
3561
 
3521
3562
  /**
3522
3563
  * Takes an array of documents, gets the changes and inserts/updates documents in the database
@@ -3861,7 +3902,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
3861
3902
  throw new MongooseError(`documents.${i} was not a mongoose document, documents must be an array of mongoose documents (instanceof mongoose.Document).`);
3862
3903
  }
3863
3904
  if (options.validateBeforeSave == null || options.validateBeforeSave) {
3864
- const err = document.validateSync();
3905
+ const err = document.$__validateSync();
3865
3906
  if (err != null) {
3866
3907
  throw err;
3867
3908
  }
@@ -4188,13 +4229,24 @@ Model.aggregate = function aggregate(pipeline, options) {
4188
4229
  * age: { type: Number, required: true }
4189
4230
  * });
4190
4231
  *
4232
+ * // Succeeds
4233
+ * await Model.validate({ name: 'John Smith', age: 31 });
4234
+ *
4191
4235
  * try {
4192
- * await Model.validate({ name: null }, ['name'])
4236
+ * await Model.validate({ name: null });
4193
4237
  * } catch (err) {
4194
4238
  * err instanceof mongoose.Error.ValidationError; // true
4195
4239
  * Object.keys(err.errors); // ['name']
4196
4240
  * }
4197
4241
  *
4242
+ * Note: the `pathsToSkip` and `pathsToValidate` options **only** apply to validation, not
4243
+ * casting. This function will still throw an error for values that cannot be casted to the
4244
+ * schema-specified type. Remove any paths you do not want to cast.
4245
+ *
4246
+ * // The following will still throw an error because the value of `age` cannot be
4247
+ * // casted to a number. Remove the `age` property before calling `validate()`.
4248
+ * await Model.validate({ name: 'Test', age: 'not a number' }, ['name']);
4249
+ *
4198
4250
  * @param {object} obj
4199
4251
  * @param {object|Array|string} pathsOrOptions
4200
4252
  * @param {object} [context]
@@ -4299,6 +4351,33 @@ Model.validate = async function validate(obj, pathsOrOptions, context) {
4299
4351
  return obj;
4300
4352
  };
4301
4353
 
4354
+ /**
4355
+ * Standard Schema adapter for this model.
4356
+ * Calls [`Model.validate()`](https://mongoosejs.com/docs/api/model.html#Model.validate()) internally with the provided `libraryOptions`
4357
+ * to cast and validate the given value.
4358
+ *
4359
+ * @api public
4360
+ * @property ~standard
4361
+ * @memberOf Model
4362
+ * @static
4363
+ */
4364
+
4365
+ Object.defineProperty(Model, '~standard', {
4366
+ configurable: true,
4367
+ get() {
4368
+ return {
4369
+ version: 1,
4370
+ vendor: 'mongoose',
4371
+ validate: (value, options) => {
4372
+ return this.validate(value, options?.libraryOptions).then(
4373
+ value => ({ value }),
4374
+ error => ({ issues: convertErrorToStandardSchemaIssues(error) })
4375
+ );
4376
+ }
4377
+ };
4378
+ }
4379
+ });
4380
+
4302
4381
  /**
4303
4382
  * Populates document references.
4304
4383
  *
@@ -4465,7 +4544,6 @@ async function _populatePath(model, docs, populateOptions) {
4465
4544
  mod.foreignField.clear();
4466
4545
  mod.foreignField.add(populateOptions.foreignField);
4467
4546
  }
4468
- const match = createPopulateQueryFilter(ids, mod.match, mod.foreignField, mod.model, mod.options.skipInvalidIds);
4469
4547
  if (assignmentOpts.excludeId) {
4470
4548
  // override the exclusion from the query so we can use the _id
4471
4549
  // for document matching during assignment. we'll delete the
@@ -4486,6 +4564,17 @@ async function _populatePath(model, docs, populateOptions) {
4486
4564
  } else if (mod.options.limit != null) {
4487
4565
  assignmentOpts.originalLimit = mod.options.limit;
4488
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);
4489
4578
  params.push([mod, match, select, assignmentOpts]);
4490
4579
  }
4491
4580
  if (!hasOne) {
@@ -4506,6 +4595,9 @@ async function _populatePath(model, docs, populateOptions) {
4506
4595
 
4507
4596
  // Track deferred populates per-param (per model) to avoid mixing them
4508
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 = [];
4509
4601
 
4510
4602
  if (populateOptions.ordered) {
4511
4603
  // Populate in series, primarily for transactions because MongoDB doesn't support multiple operations on
@@ -4513,7 +4605,10 @@ async function _populatePath(model, docs, populateOptions) {
4513
4605
  for (let i = 0; i < params.length; i++) {
4514
4606
  const arr = params[i];
4515
4607
  const { docs, deferredPopulates } = await _execPopulateQuery.apply(null, arr);
4516
- vals = vals.concat(docs);
4608
+ valsByParam.push(docs);
4609
+ if (!arr[0]._assignFromOwnResults) {
4610
+ vals = vals.concat(docs);
4611
+ }
4517
4612
  if (deferredPopulates.length > 0) {
4518
4613
  deferredPopulatesPerParam.set(i, deferredPopulates);
4519
4614
  }
@@ -4528,7 +4623,10 @@ async function _populatePath(model, docs, populateOptions) {
4528
4623
  const results = await Promise.all(promises);
4529
4624
  for (let i = 0; i < results.length; i++) {
4530
4625
  const { docs, deferredPopulates } = results[i];
4531
- vals = vals.concat(docs);
4626
+ valsByParam.push(docs);
4627
+ if (!params[i][0]._assignFromOwnResults) {
4628
+ vals = vals.concat(docs);
4629
+ }
4532
4630
  if (deferredPopulates.length > 0) {
4533
4631
  deferredPopulatesPerParam.set(i, deferredPopulates);
4534
4632
  }
@@ -4536,13 +4634,15 @@ async function _populatePath(model, docs, populateOptions) {
4536
4634
  }
4537
4635
 
4538
4636
 
4539
- for (const arr of params) {
4637
+ for (let i = 0; i < params.length; i++) {
4638
+ const arr = params[i];
4540
4639
  const mod = arr[0];
4541
4640
  const assignmentOpts = arr[3];
4542
- for (const val of vals) {
4641
+ const valsForMod = mod._assignFromOwnResults ? valsByParam[i] : vals;
4642
+ for (const val of valsForMod) {
4543
4643
  mod.options._childDocs.push(val);
4544
4644
  }
4545
- _assign(model, vals, mod, assignmentOpts);
4645
+ _assign(model, valsForMod, mod, assignmentOpts);
4546
4646
  }
4547
4647
 
4548
4648
  // Handle deferred populate for cases with per-document match functions.
@@ -4582,13 +4682,15 @@ async function _populatePath(model, docs, populateOptions) {
4582
4682
  }
4583
4683
  }
4584
4684
 
4585
- for (const arr of params) {
4586
- removeDeselectedForeignField(arr[0].foreignField, arr[0].options, vals);
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);
4587
4688
  }
4588
- for (const arr of params) {
4589
- const mod = arr[0];
4689
+ for (let i = 0; i < params.length; i++) {
4690
+ const mod = params[i][0];
4590
4691
  if (mod.options?.options?._leanTransform) {
4591
- for (const doc of vals) {
4692
+ const valsForMod = mod._assignFromOwnResults ? valsByParam[i] : vals;
4693
+ for (const doc of valsForMod) {
4592
4694
  mod.options.options._leanTransform(doc);
4593
4695
  }
4594
4696
  }
@@ -4600,6 +4702,12 @@ async function _populatePath(model, docs, populateOptions) {
4600
4702
  */
4601
4703
 
4602
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
+ }
4603
4711
  let subPopulate = clone(mod.options.populate);
4604
4712
  const queryOptions = {};
4605
4713
  if (mod.options.skip !== undefined) {
@@ -16,7 +16,10 @@ module.exports = function saveSubdocs(schema) {
16
16
  };
17
17
 
18
18
 
19
- async function saveSubdocsPreSave() {
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
- await Promise.all(subdocs.map(subdoc => subdoc._execDocumentPreHooks('save', options, [options])));
32
-
33
- // Invalidate subdocs cache because subdoc pre hooks can add new subdocuments
34
- if (this.$__.saveOptions) {
35
- this.$__.saveOptions.__subdocs = null;
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
- async function saveSubdocsPostSave() {
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
- await Promise.all(promises);
59
+ return Promise.all(promises);
57
60
  }
58
61
 
59
- async function saveSubdocsPreDeleteOne() {
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
- await Promise.all(promises);
74
+ return Promise.all(promises);
72
75
  }
73
76
 
74
- async function saveSubdocsPostDeleteOne() {
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
- await Promise.all(promises);
90
+ return Promise.all(promises);
88
91
  }
89
92
 
90
93
 
package/lib/query.js CHANGED
@@ -42,6 +42,8 @@ const updateValidators = require('./helpers/updateValidators');
42
42
  const util = require('util');
43
43
  const utils = require('./utils');
44
44
  const queryMiddlewareFunctions = require('./constants').queryMiddlewareFunctions;
45
+ const { createTracedChannel } = require('./tracing');
46
+ const { trace: traceQuery } = createTracedChannel('mongoose:query');
45
47
 
46
48
  const queryOptionMethods = new Set([
47
49
  'allowDiskUse',
@@ -4778,42 +4780,56 @@ Query.prototype.exec = async function exec(op) {
4778
4780
  }
4779
4781
  this._execCount++;
4780
4782
 
4781
- let skipWrappedFunction = null;
4782
- try {
4783
- await this._hooks.execPre('exec', this, []);
4784
- } catch (err) {
4785
- if (err instanceof Kareem.skipWrappedFunction) {
4786
- skipWrappedFunction = err;
4787
- } else {
4788
- throw err;
4783
+ const _this = this;
4784
+ return traceQuery(async function maybeTracedQueryExec() {
4785
+ let skipWrappedFunction = null;
4786
+ try {
4787
+ await _this._hooks.execPre('exec', _this, []);
4788
+ } catch (err) {
4789
+ if (err instanceof Kareem.skipWrappedFunction) {
4790
+ skipWrappedFunction = err;
4791
+ } else {
4792
+ throw err;
4793
+ }
4789
4794
  }
4790
- }
4791
4795
 
4792
- let res;
4796
+ let res;
4793
4797
 
4794
- let error = null;
4795
- try {
4796
- await _executePreHooks(this);
4797
- res = skipWrappedFunction ? skipWrappedFunction.args[0] : await this[thunk]();
4798
+ let error = null;
4799
+ try {
4800
+ await _executePreHooks(_this);
4801
+ res = skipWrappedFunction ? skipWrappedFunction.args[0] : await _this[thunk]();
4798
4802
 
4799
- for (const fn of this._transforms) {
4800
- res = fn(res);
4801
- }
4802
- } catch (err) {
4803
- if (err instanceof Kareem.skipWrappedFunction) {
4804
- res = err.args[0];
4805
- } else {
4806
- error = err;
4807
- }
4803
+ for (const fn of _this._transforms) {
4804
+ res = fn(res);
4805
+ }
4806
+ } catch (err) {
4807
+ if (err instanceof Kareem.skipWrappedFunction) {
4808
+ res = err.args[0];
4809
+ } else {
4810
+ error = err;
4811
+ }
4808
4812
 
4809
- error = this.model.schema._transformDuplicateKeyError(error);
4810
- }
4813
+ error = _this.model.schema._transformDuplicateKeyError(error);
4814
+ }
4811
4815
 
4812
- res = await _executePostHooks(this, res, error);
4816
+ res = await _executePostHooks(_this, res, error);
4813
4817
 
4814
- await this._hooks.execPost('exec', this, []);
4818
+ await _this._hooks.execPost('exec', _this, []);
4815
4819
 
4816
- return res;
4820
+ return res;
4821
+ }, () => ({
4822
+ operation: _this.op,
4823
+ collection: _this.mongooseCollection.name,
4824
+ database: _this.model.db?.name,
4825
+ serverAddress: _this.model.db?.host,
4826
+ serverPort: _this.model.db?.port,
4827
+ args: {
4828
+ filter: _this.getFilter(),
4829
+ fields: _this._fields,
4830
+ options: _this._mongooseOptions
4831
+ }
4832
+ }));
4817
4833
  };
4818
4834
 
4819
4835
  /*!
@@ -188,16 +188,18 @@ SchemaDate.prototype.expires = function(when) {
188
188
  SchemaDate._checkRequired = v => v instanceof Date;
189
189
 
190
190
  /**
191
- * Override the function the required validator uses to check whether a string
191
+ * Override the function the required validator uses to check whether a date
192
192
  * passes the `required` check.
193
193
  *
194
194
  * #### Example:
195
195
  *
196
- * // Allow empty strings to pass `required` check
197
- * mongoose.Schema.Types.String.checkRequired(v => v != null);
196
+ * // Disallow "invalid date"
197
+ * mongoose.Schema.Types.Date.checkRequired(v => v instanceof Date && !isNaN(v.valueOf()));
198
198
  *
199
- * const M = mongoose.model({ str: { type: String, required: true } });
200
- * new M({ str: '' }).validateSync(); // `null`, validation passes!
199
+ * const schema = new mongoose.Schema({ myDate: { type: Date, required: true } });
200
+ * const M = mongoose.model('Test', schema);
201
+ * // returns a ValidationError due to date being invalid
202
+ * new M({ myDate: new Date('invalid') }).validateSync();
201
203
  *
202
204
  * @param {Function} fn
203
205
  * @return {Function}
@@ -314,7 +314,7 @@ SchemaDocumentArray.prototype.doValidateSync = function(array, scope, options) {
314
314
  continue;
315
315
  }
316
316
 
317
- const subdocValidateError = doc.validateSync(options);
317
+ const subdocValidateError = doc.$__validateSync(options);
318
318
 
319
319
  if (subdocValidateError && resultError == null) {
320
320
  resultError = subdocValidateError;
@@ -317,7 +317,13 @@ function resetId(v) {
317
317
  */
318
318
 
319
319
  SchemaObjectId.prototype.toJSONSchema = function toJSONSchema(options) {
320
- return this._createJSONSchemaTypeDefinition('string', 'objectId', options);
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() {
@@ -310,7 +310,7 @@ SchemaSubdocument.prototype.doValidateSync = function(value, scope, options) {
310
310
  if (!value) {
311
311
  return;
312
312
  }
313
- return value.validateSync();
313
+ return value.$__validateSync();
314
314
  };
315
315
 
316
316
  /**
@@ -112,8 +112,8 @@ class Union extends SchemaType {
112
112
  return schemaTypeError;
113
113
  }
114
114
  }
115
- if (value != null && typeof value.validateSync === 'function') {
116
- return value.validateSync();
115
+ if (value != null && typeof value.$__validateSync === 'function') {
116
+ return value.$__validateSync();
117
117
  }
118
118
  }
119
119
 
package/lib/schemaType.js CHANGED
@@ -1198,7 +1198,8 @@ SchemaType.prototype.required = function(required, message) {
1198
1198
  * name: { type: String, allowNull: false }
1199
1199
  * });
1200
1200
  *
1201
- * new Model({ name: undefined }).validateSync(); // OK
1201
+ * new Model({}).validateSync(); // OK
1202
+ * new Model({ name: undefined }).validateSync(); // OK, Mongoose strips out `name` when its value is `undefined`
1202
1203
  * new Model({ name: null }).validateSync(); // ValidationError
1203
1204
  *
1204
1205
  * @param {boolean} allowNull
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const ValidationError = require('../error/validation');
4
+
5
+ module.exports = function convertErrorToIssues(error) {
6
+ if (error instanceof ValidationError) {
7
+ return Object.keys(error.errors).map(path => {
8
+ const err = error.errors[path];
9
+ return {
10
+ message: err.message,
11
+ path: path.split('.').map(part => {
12
+ const num = +part;
13
+ return Number.isInteger(num) && String(num) === part ? num : part;
14
+ })
15
+ };
16
+ });
17
+ }
18
+
19
+ return [{ message: error.message }];
20
+ };
@@ -69,8 +69,10 @@ StateMachine.prototype._changeState = function _changeState(path, nextState) {
69
69
  if (prevState === nextState) {
70
70
  return;
71
71
  }
72
- const prevBucket = this.states[prevState];
73
- if (prevBucket) delete prevBucket[path];
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
- path = keys[i];
94
- delete this.states[state][path];
95
- delete this.paths[path];
100
+ delete this.paths[keys[i]];
96
101
  }
97
102
  };
98
103
 
package/lib/tracing.js ADDED
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ let dc;
4
+ try {
5
+ dc = (typeof process !== 'undefined' && 'getBuiltinModule' in process)
6
+ ? process.getBuiltinModule('node:diagnostics_channel')
7
+ : require('node:diagnostics_channel');
8
+ } catch {
9
+ // diagnostics_channel not available
10
+ }
11
+
12
+ const hasTracingChannel = !!dc && typeof dc.tracingChannel === 'function';
13
+
14
+ function shouldTrace(channel) {
15
+ // Node 18 and Bun don't expose hasSubscribers on TracingChannel
16
+ // so undefined means it may or may not have subscribers, so we'll trace anyway
17
+ return !!channel && channel.hasSubscribers !== false;
18
+ }
19
+
20
+ function createTracedChannel(name) {
21
+ const ch = hasTracingChannel ? dc.tracingChannel(name) : undefined;
22
+
23
+ function trace(fn, contextFactory) {
24
+ if (!shouldTrace(ch)) {
25
+ return fn();
26
+ }
27
+
28
+ const traced = ch.tracePromise(fn, contextFactory());
29
+ return traced;
30
+ }
31
+
32
+ return { channel: ch, trace };
33
+ }
34
+
35
+ module.exports = {
36
+ createTracedChannel,
37
+ cursorNextChannel: createTracedChannel('mongoose:cursor:next')
38
+ };