mongoose 8.6.0 → 8.6.2

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/connection.js CHANGED
@@ -71,6 +71,9 @@ function Connection(base) {
71
71
  } else {
72
72
  this.id = base.nextConnectionId;
73
73
  }
74
+
75
+ // Internal queue of objects `{ fn, ctx, args }` that Mongoose calls when this connection is successfully
76
+ // opened. In `onOpen()`, Mongoose calls every entry in `_queue` and empties the queue.
74
77
  this._queue = [];
75
78
  }
76
79
 
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  const EventEmitter = require('events').EventEmitter;
8
+ const MongooseError = require('../error/mongooseError');
8
9
 
9
10
  /*!
10
11
  * ignore
@@ -25,6 +26,7 @@ class ChangeStream extends EventEmitter {
25
26
  this.bindedEvents = false;
26
27
  this.pipeline = pipeline;
27
28
  this.options = options;
29
+ this.errored = false;
28
30
 
29
31
  if (options && options.hydrate && !options.model) {
30
32
  throw new Error(
@@ -33,19 +35,36 @@ class ChangeStream extends EventEmitter {
33
35
  );
34
36
  }
35
37
 
38
+ let syncError = null;
36
39
  this.$driverChangeStreamPromise = new Promise((resolve, reject) => {
37
40
  // This wrapper is necessary because of buffering.
38
- changeStreamThunk((err, driverChangeStream) => {
39
- if (err != null) {
40
- this.emit('error', err);
41
- return reject(err);
42
- }
41
+ try {
42
+ changeStreamThunk((err, driverChangeStream) => {
43
+ if (err != null) {
44
+ this.errored = true;
45
+ this.emit('error', err);
46
+ return reject(err);
47
+ }
43
48
 
44
- this.driverChangeStream = driverChangeStream;
45
- this.emit('ready');
46
- resolve();
47
- });
49
+ this.driverChangeStream = driverChangeStream;
50
+ this.emit('ready');
51
+ resolve();
52
+ });
53
+ } catch (err) {
54
+ syncError = err;
55
+ this.errored = true;
56
+ this.emit('error', err);
57
+ reject(err);
58
+ }
48
59
  });
60
+
61
+ // Because a ChangeStream is an event emitter, there's no way to register an 'error' handler
62
+ // that catches errors which occur in the constructor, unless we force sync errors into async
63
+ // errors with setImmediate(). For cleaner stack trace, we just immediately throw any synchronous
64
+ // errors that occurred with changeStreamThunk().
65
+ if (syncError != null) {
66
+ throw syncError;
67
+ }
49
68
  }
50
69
 
51
70
  _bindEvents() {
@@ -92,10 +111,16 @@ class ChangeStream extends EventEmitter {
92
111
  }
93
112
 
94
113
  hasNext(cb) {
114
+ if (this.errored) {
115
+ throw new MongooseError('Cannot call hasNext() on errored ChangeStream');
116
+ }
95
117
  return this.driverChangeStream.hasNext(cb);
96
118
  }
97
119
 
98
120
  next(cb) {
121
+ if (this.errored) {
122
+ throw new MongooseError('Cannot call next() on errored ChangeStream');
123
+ }
99
124
  if (this.options && this.options.hydrate) {
100
125
  if (cb != null) {
101
126
  const originalCb = cb;
@@ -126,16 +151,25 @@ class ChangeStream extends EventEmitter {
126
151
  }
127
152
 
128
153
  addListener(event, handler) {
154
+ if (this.errored) {
155
+ throw new MongooseError('Cannot call addListener() on errored ChangeStream');
156
+ }
129
157
  this._bindEvents();
130
158
  return super.addListener(event, handler);
131
159
  }
132
160
 
133
161
  on(event, handler) {
162
+ if (this.errored) {
163
+ throw new MongooseError('Cannot call on() on errored ChangeStream');
164
+ }
134
165
  this._bindEvents();
135
166
  return super.on(event, handler);
136
167
  }
137
168
 
138
169
  once(event, handler) {
170
+ if (this.errored) {
171
+ throw new MongooseError('Cannot call once() on errored ChangeStream');
172
+ }
139
173
  this._bindEvents();
140
174
  return super.once(event, handler);
141
175
  }
@@ -10,7 +10,7 @@ const eachAsync = require('../helpers/cursor/eachAsync');
10
10
  const helpers = require('../queryHelpers');
11
11
  const kareem = require('kareem');
12
12
  const immediate = require('../helpers/immediate');
13
- const { once } = require('node:events');
13
+ const { once } = require('events');
14
14
  const util = require('util');
15
15
 
16
16
  /**
package/lib/document.js CHANGED
@@ -1213,7 +1213,7 @@ Document.prototype.$set = function $set(path, val, type, options) {
1213
1213
  this.$__setValue(path, null);
1214
1214
  cleanModifiedSubpaths(this, path);
1215
1215
  } else {
1216
- return this.$set(val, path, constructing);
1216
+ return this.$set(val, path, constructing, options);
1217
1217
  }
1218
1218
 
1219
1219
  const keys = getKeysInSchemaOrder(this.$__schema, val, path);
@@ -3856,7 +3856,6 @@ Document.prototype.$toObject = function(options, json) {
3856
3856
  // Parent options should only bubble down for subdocuments, not populated docs
3857
3857
  options._parentOptions = this.$isSubdocument ? options : null;
3858
3858
 
3859
- options._skipSingleNestedGetters = false;
3860
3859
  // remember the root transform function
3861
3860
  // to save it from being overwritten by sub-transform functions
3862
3861
  // const originalTransform = options.transform;
@@ -3870,13 +3869,13 @@ Document.prototype.$toObject = function(options, json) {
3870
3869
  ret = clone(this._doc, options) || {};
3871
3870
  }
3872
3871
 
3873
- options._skipSingleNestedGetters = true;
3874
3872
  const getters = options._calledWithOptions.getters
3875
3873
  ?? options.getters
3876
3874
  ?? defaultOptions.getters
3877
3875
  ?? false;
3876
+
3878
3877
  if (getters) {
3879
- applyGetters(this, ret, options);
3878
+ applyGetters(this, ret);
3880
3879
 
3881
3880
  if (options.minimize) {
3882
3881
  ret = minimize(ret) || {};
@@ -4187,12 +4186,11 @@ function applyVirtuals(self, json, options, toObjectOptions) {
4187
4186
  *
4188
4187
  * @param {Document} self
4189
4188
  * @param {Object} json
4190
- * @param {Object} [options]
4191
4189
  * @return {Object} `json`
4192
4190
  * @api private
4193
4191
  */
4194
4192
 
4195
- function applyGetters(self, json, options) {
4193
+ function applyGetters(self, json) {
4196
4194
  const schema = self.$__schema;
4197
4195
  const paths = Object.keys(schema.paths);
4198
4196
  let i = paths.length;
@@ -4228,8 +4226,10 @@ function applyGetters(self, json, options) {
4228
4226
  if (branch != null && typeof branch !== 'object') {
4229
4227
  break;
4230
4228
  } else if (ii === last) {
4231
- const val = self.$get(path);
4232
- branch[part] = clone(val, options);
4229
+ branch[part] = schema.paths[path].applyGetters(
4230
+ branch[part],
4231
+ self
4232
+ );
4233
4233
  if (Array.isArray(branch[part]) && schema.paths[path].$embeddedSchemaType) {
4234
4234
  for (let i = 0; i < branch[part].length; ++i) {
4235
4235
  branch[part][i] = schema.paths[path].$embeddedSchemaType.applyGetters(
@@ -96,7 +96,7 @@ NativeConnection.prototype.useDb = function(name, options) {
96
96
  if (this.db && this._readyState === STATES.connected) {
97
97
  wireup();
98
98
  } else {
99
- this.once('connected', wireup);
99
+ this._queue.push({ fn: wireup });
100
100
  }
101
101
 
102
102
  function wireup() {
@@ -40,11 +40,6 @@ function clone(obj, options, isArrayChild) {
40
40
 
41
41
  if (isMongooseObject(obj)) {
42
42
  if (options) {
43
- // Single nested subdocs should apply getters later in `applyGetters()`
44
- // when calling `toObject()`. See gh-7442, gh-8295
45
- if (options._skipSingleNestedGetters && obj.$isSingleNested) {
46
- options._calledWithOptions = Object.assign({}, options._calledWithOptions || {}, { getters: false });
47
- }
48
43
  if (options.retainDocuments && obj.$__ != null) {
49
44
  const clonedDoc = obj.$clone();
50
45
  if (obj.__index != null) {
package/lib/model.js CHANGED
@@ -3368,11 +3368,19 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
3368
3368
  };
3369
3369
 
3370
3370
  /**
3371
- * takes an array of documents, gets the changes and inserts/updates documents in the database
3372
- * according to whether or not the document is new, or whether it has changes or not.
3371
+ * Takes an array of documents, gets the changes and inserts/updates documents in the database
3372
+ * according to whether or not the document is new, or whether it has changes or not.
3373
3373
  *
3374
3374
  * `bulkSave` uses `bulkWrite` under the hood, so it's mostly useful when dealing with many documents (10K+)
3375
3375
  *
3376
+ * `bulkSave()` throws errors under the following conditions:
3377
+ *
3378
+ * - one of the provided documents fails validation. In this case, `bulkSave()` does not send a `bulkWrite()`, and throws the first validation error.
3379
+ * - `bulkWrite()` fails (for example, due to being unable to connect to MongoDB or due to duplicate key error)
3380
+ * - `bulkWrite()` did not insert or update **any** documents. In this case, `bulkSave()` will throw a DocumentNotFound error.
3381
+ *
3382
+ * Note that `bulkSave()` will **not** throw an error if only some of the `save()` calls succeeded.
3383
+ *
3376
3384
  * @param {Array<Document>} documents
3377
3385
  * @param {Object} [options] options passed to the underlying `bulkWrite()`
3378
3386
  * @param {Boolean} [options.timestamps] defaults to `null`, when set to false, mongoose will not add/update timestamps to the documents.
@@ -3380,7 +3388,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
3380
3388
  * @param {String|number} [options.w=1] The [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/). See [`Query#w()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.w()) for more information.
3381
3389
  * @param {number} [options.wtimeout=null] The [write concern timeout](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout).
3382
3390
  * @param {Boolean} [options.j=true] If false, disable [journal acknowledgement](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option)
3383
- *
3391
+ * @return {BulkWriteResult} the return value from `bulkWrite()`
3384
3392
  */
3385
3393
  Model.bulkSave = async function bulkSave(documents, options) {
3386
3394
  options = options || {};
@@ -3408,18 +3416,31 @@ Model.bulkSave = async function bulkSave(documents, options) {
3408
3416
  (err) => ({ bulkWriteResult: null, bulkWriteError: err })
3409
3417
  );
3410
3418
 
3411
- await Promise.all(
3412
- documents.map(async(document) => {
3413
- const documentError = bulkWriteError && bulkWriteError.writeErrors.find(writeError => {
3414
- const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;
3415
- return writeErrorDocumentId.toString() === document._doc._id.toString();
3416
- });
3419
+ const matchedCount = bulkWriteResult?.matchedCount ?? 0;
3420
+ const insertedCount = bulkWriteResult?.insertedCount ?? 0;
3421
+ if (writeOperations.length > 0 && matchedCount + insertedCount === 0 && !bulkWriteError) {
3422
+ throw new DocumentNotFoundError(
3423
+ writeOperations.filter(op => op.updateOne).map(op => op.updateOne.filter),
3424
+ this.modelName,
3425
+ writeOperations.length,
3426
+ bulkWriteResult
3427
+ );
3428
+ }
3417
3429
 
3418
- if (documentError == null) {
3419
- await handleSuccessfulWrite(document);
3420
- }
3421
- })
3422
- );
3430
+ const successfulDocuments = [];
3431
+ for (let i = 0; i < documents.length; i++) {
3432
+ const document = documents[i];
3433
+ const documentError = bulkWriteError && bulkWriteError.writeErrors.find(writeError => {
3434
+ const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;
3435
+ return writeErrorDocumentId.toString() === document._doc._id.toString();
3436
+ });
3437
+
3438
+ if (documentError == null) {
3439
+ successfulDocuments.push(document);
3440
+ }
3441
+ }
3442
+
3443
+ await Promise.all(successfulDocuments.map(document => handleSuccessfulWrite(document)));
3423
3444
 
3424
3445
  if (bulkWriteError && bulkWriteError.writeErrors && bulkWriteError.writeErrors.length) {
3425
3446
  throw bulkWriteError;
@@ -27,13 +27,6 @@ module.exports = function trackTransaction(schema) {
27
27
  initialState.atomics = _getAtomics(this);
28
28
 
29
29
  session[sessionNewDocuments].set(this, initialState);
30
- } else {
31
- const state = session[sessionNewDocuments].get(this);
32
-
33
- for (const path of Object.keys(this.$__.activePaths.getStatePaths('modify'))) {
34
- state.modifiedPaths.add(path);
35
- }
36
- state.atomics = _getAtomics(this, state.atomics);
37
30
  }
38
31
  });
39
32
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongoose",
3
3
  "description": "Mongoose MongoDB ODM",
4
- "version": "8.6.0",
4
+ "version": "8.6.2",
5
5
  "author": "Guillermo Rauch <guillermo@learnboost.com>",
6
6
  "keywords": [
7
7
  "mongodb",
@@ -29,9 +29,9 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@babel/core": "7.24.7",
32
- "@babel/preset-env": "7.25.3",
33
- "@typescript-eslint/eslint-plugin": "^6.21.0",
34
- "@typescript-eslint/parser": "^6.21.0",
32
+ "@babel/preset-env": "7.25.4",
33
+ "@typescript-eslint/eslint-plugin": "^8.4.0",
34
+ "@typescript-eslint/parser": "^8.4.0",
35
35
  "acquit": "1.3.0",
36
36
  "acquit-ignore": "0.2.1",
37
37
  "acquit-require": "0.1.1",
@@ -40,7 +40,7 @@
40
40
  "babel-loader": "8.2.5",
41
41
  "broken-link-checker": "^0.7.8",
42
42
  "buffer": "^5.6.0",
43
- "cheerio": "1.0.0-rc.12",
43
+ "cheerio": "1.0.0",
44
44
  "crypto-browserify": "3.12.0",
45
45
  "dotenv": "16.4.5",
46
46
  "dox": "1.0.0",
@@ -53,9 +53,9 @@
53
53
  "lodash.isequal": "4.5.0",
54
54
  "lodash.isequalwith": "4.4.0",
55
55
  "markdownlint-cli2": "^0.13.0",
56
- "marked": "14.0.0",
56
+ "marked": "14.1.0",
57
57
  "mkdirp": "^3.0.1",
58
- "mocha": "10.7.0",
58
+ "mocha": "10.7.3",
59
59
  "moment": "2.30.1",
60
60
  "mongodb-memory-server": "10.0.0",
61
61
  "ncp": "^2.0.0",
package/types/cursor.d.ts CHANGED
@@ -8,6 +8,7 @@ declare module 'mongoose' {
8
8
  parallel?: number;
9
9
  batchSize?: number;
10
10
  continueOnError?: boolean;
11
+ signal?: AbortSignal;
11
12
  }
12
13
 
13
14
  class Cursor<DocType = any, Options = never> extends stream.Readable {
@@ -91,8 +91,8 @@ declare module 'mongoose' {
91
91
  IfEquals<PathValueType, String> extends true ? PathEnumOrString<Options['enum']> :
92
92
  PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray<any> ? Options['enum'][number] : number :
93
93
  IfEquals<PathValueType, Schema.Types.Number> extends true ? number :
94
- PathValueType extends DateSchemaDefinition ? Date :
95
- IfEquals<PathValueType, Schema.Types.Date> extends true ? Date :
94
+ PathValueType extends DateSchemaDefinition ? NativeDate :
95
+ IfEquals<PathValueType, Schema.Types.Date> extends true ? NativeDate :
96
96
  PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer :
97
97
  PathValueType extends BooleanSchemaDefinition ? boolean :
98
98
  IfEquals<PathValueType, Schema.Types.Boolean> extends true ? boolean :
@@ -281,8 +281,8 @@ type ResolvePathType<PathValueType, Options extends SchemaTypeOptions<PathValueT
281
281
  IfEquals<PathValueType, String> extends true ? PathEnumOrString<Options['enum']> :
282
282
  PathValueType extends NumberSchemaDefinition ? Options['enum'] extends ReadonlyArray<any> ? Options['enum'][number] : number :
283
283
  IfEquals<PathValueType, Schema.Types.Number> extends true ? number :
284
- PathValueType extends DateSchemaDefinition ? Date :
285
- IfEquals<PathValueType, Schema.Types.Date> extends true ? Date :
284
+ PathValueType extends DateSchemaDefinition ? NativeDate :
285
+ IfEquals<PathValueType, Schema.Types.Date> extends true ? NativeDate :
286
286
  PathValueType extends typeof Buffer | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer :
287
287
  PathValueType extends BooleanSchemaDefinition ? boolean :
288
288
  IfEquals<PathValueType, Schema.Types.Boolean> extends true ? boolean :
package/types/query.d.ts CHANGED
@@ -12,7 +12,7 @@ declare module 'mongoose' {
12
12
  */
13
13
  type RootFilterQuery<T> = FilterQuery<T> | Query<any, any> | Types.ObjectId;
14
14
 
15
- type FilterQuery<T> ={
15
+ type FilterQuery<T> = {
16
16
  [P in keyof T]?: Condition<T[P]>;
17
17
  } & RootQuerySelector<T> & { _id?: Condition<string>; };
18
18
 
@@ -116,10 +116,9 @@ declare module 'mongoose' {
116
116
  $where?: string | Function;
117
117
  /** @see https://www.mongodb.com/docs/manual/reference/operator/query/comment/#op._S_comment */
118
118
  $comment?: string;
119
- // we could not find a proper TypeScript generic to support nested queries e.g. 'user.friends.name'
120
- // this will mark all unrecognized properties as any (including nested queries) only if
121
- // they include a "." (to avoid generically allowing any unexpected keys)
122
- [nestedSelector: `${string}.${string}`]: any;
119
+ $expr?: Record<string, any>;
120
+ // this will mark all unrecognized properties as any (including nested queries)
121
+ [key: string]: any;
123
122
  };
124
123
 
125
124
  interface QueryTimestampsConfig {
@@ -216,6 +216,9 @@ declare module 'mongoose' {
216
216
  /** Attaches a getter for all instances of this schema type. */
217
217
  static get(getter: (value: any) => any): void;
218
218
 
219
+ /** Array containing default setters for all instances of this SchemaType */
220
+ static setters: ((val?: unknown, priorVal?: unknown, doc?: Document<unknown>, options?: Record<string, any> | null) => unknown)[];
221
+
219
222
  /** The class that Mongoose uses internally to instantiate this SchemaType's `options` property. */
220
223
  OptionsConstructor: SchemaTypeOptions<T>;
221
224