From 92fa6ebc448a226b16c264f5e58e06eefb5936ab Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 6 May 2024 07:02:24 -0400 Subject: [PATCH 01/25] types(model): allow passing strict type checking override to create() Fix #14548 --- test/types/create.test.ts | 9 +++++++++ types/models.d.ts | 12 ++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/test/types/create.test.ts b/test/types/create.test.ts index 033010ae9bb..f2de193182b 100644 --- a/test/types/create.test.ts +++ b/test/types/create.test.ts @@ -40,6 +40,15 @@ Test.create([{ name: 'test' }], { validateBeforeSave: true }).then(docs => { expectType(docs[0].name); }); +Test.create({}).then(doc => { + expectType(doc.name); +}); + +Test.create([{}]).then(docs => { + expectType(docs[0].name); +}); + +expectError(Test.create({})); Test.insertMany({ name: 'test' }, {}, (err, docs) => { expectType(err); diff --git a/types/models.d.ts b/types/models.d.ts index 1a02fd8a202..5504ec91a58 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -192,12 +192,12 @@ declare module 'mongoose' { countDocuments(callback?: Callback): QueryWithHelpers, TQueryHelpers, T>; /** Creates a new document or documents */ - create>(docs: Array, options?: SaveOptions): Promise[]>; - create>(docs: Array, options?: SaveOptions, callback?: Callback>>): Promise[]>; - create>(docs: Array, callback: Callback>>): void; - create>(doc: DocContents | T): Promise>; - create>(...docs: Array): Promise[]>; - create>(doc: T | DocContents, callback: Callback>): void; + create>(docs: Array, options?: SaveOptions): Promise[]>; + create>(docs: Array, options?: SaveOptions, callback?: Callback>>): Promise[]>; + create>(docs: Array, callback: Callback>>): void; + create>(doc: DocContents): Promise>; + create>(...docs: Array): Promise[]>; + create>(doc: DocContents, callback: Callback>): void; /** * Create the collection for this model. By default, if no indexes are specified, From ed7934840571db5b27704d5bb0ebf36131743d5f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 8 May 2024 19:37:07 -0400 Subject: [PATCH 02/25] test: address code review comments --- test/types/create.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/types/create.test.ts b/test/types/create.test.ts index f2de193182b..983aafa0bdc 100644 --- a/test/types/create.test.ts +++ b/test/types/create.test.ts @@ -50,6 +50,9 @@ Test.create([{}]).then(docs => { expectError(Test.create({})); +Test.create({ name: 'test' }); +Test.create({ _id: new Types.ObjectId('0'.repeat(24)), name: 'test' }); + Test.insertMany({ name: 'test' }, {}, (err, docs) => { expectType(err); expectType(docs[0]._id); From a70ecc2df638208586364ce6c42440f6986905e4 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 May 2024 11:57:24 -0400 Subject: [PATCH 03/25] fix(cast): cast $comment to string in query filters Fix #14576 --- lib/cast.js | 4 ++++ lib/schema/operators/text.js | 2 +- test/cast.test.js | 27 +++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/cast.js b/lib/cast.js index 8a5bb696999..e7bf5b45a05 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -8,6 +8,7 @@ const CastError = require('./error/cast'); const StrictModeError = require('./error/strict'); const Types = require('./schema/index'); const cast$expr = require('./helpers/query/cast$expr'); +const castString = require('./cast/string'); const castTextSearch = require('./schema/operators/text'); const get = require('./helpers/get'); const getConstructorName = require('./helpers/getConstructorName'); @@ -89,6 +90,9 @@ module.exports = function cast(schema, obj, options, context) { val = cast(schema, val, options, context); } else if (path === '$text') { val = castTextSearch(val, path); + } else if (path === '$comment' && !schema.paths.hasOwnProperty('$comment')) { + val = castString(val, path); + obj[path] = val; } else { if (!schema) { // no casting for Mixed types diff --git a/lib/schema/operators/text.js b/lib/schema/operators/text.js index 81e8fa21bff..79be4ff7cb6 100644 --- a/lib/schema/operators/text.js +++ b/lib/schema/operators/text.js @@ -15,7 +15,7 @@ const castString = require('../../cast/string'); * @api private */ -module.exports = function(val, path) { +module.exports = function castTextSearch(val, path) { if (val == null || typeof val !== 'object') { throw new CastError('$text', val, path); } diff --git a/test/cast.test.js b/test/cast.test.js index 51b97518ff3..c21861b130d 100644 --- a/test/cast.test.js +++ b/test/cast.test.js @@ -160,6 +160,33 @@ describe('cast: ', function() { }); }); + it('casts $comment (gh-14576)', function() { + const schema = new Schema({ name: String }); + + let res = cast(schema, { + $comment: 'test' + }); + assert.deepStrictEqual(res, { $comment: 'test' }); + + res = cast(schema, { + $comment: 42 + }); + assert.deepStrictEqual(res, { $comment: '42' }); + + assert.throws( + () => cast(schema, { + $comment: { name: 'taco' } + }), + /\$comment/ + ); + + const schema2 = new Schema({ $comment: Number }); + res = cast(schema2, { + $comment: 42 + }); + assert.deepStrictEqual(res, { $comment: 42 }); + }); + it('avoids setting stripped out nested schema values to undefined (gh-11291)', function() { const nested = new Schema({}, { id: false, From f4cfe1eb8a33486301b86c4469b7f2b0e30b177b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 16 May 2024 17:28:51 -0400 Subject: [PATCH 04/25] feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() Backport #13410 Backport #14587 Fix #14572 --- lib/error/bulkWriteError.js | 41 ++++++++++++++ lib/model.js | 48 +++++++++++++++++ test/model.test.js | 104 ++++++++++++++++++++++++++++++++++++ types/models.d.ts | 2 + 4 files changed, 195 insertions(+) create mode 100644 lib/error/bulkWriteError.js diff --git a/lib/error/bulkWriteError.js b/lib/error/bulkWriteError.js new file mode 100644 index 00000000000..1711b03b586 --- /dev/null +++ b/lib/error/bulkWriteError.js @@ -0,0 +1,41 @@ +/*! + * Module dependencies. + */ + +'use strict'; + +const MongooseError = require('./'); + + +/** + * If `bulkWrite()` or `insertMany()` has validation errors, but + * all valid operations succeed, and 'throwOnValidationError' is true, + * Mongoose will throw this error. + * + * @api private + */ + +class MongooseBulkWriteError extends MongooseError { + constructor(validationErrors, results, rawResult, operation) { + let preview = validationErrors.map(e => e.message).join(', '); + if (preview.length > 200) { + preview = preview.slice(0, 200) + '...'; + } + super(`${operation} failed with ${validationErrors.length} Mongoose validation errors: ${preview}`); + + this.validationErrors = validationErrors; + this.results = results; + this.rawResult = rawResult; + this.operation = operation; + } +} + +Object.defineProperty(MongooseBulkWriteError.prototype, 'name', { + value: 'MongooseBulkWriteError' +}); + +/*! + * exports + */ + +module.exports = MongooseBulkWriteError; diff --git a/lib/model.js b/lib/model.js index 313d1f4747f..b758e27c545 100644 --- a/lib/model.js +++ b/lib/model.js @@ -10,6 +10,7 @@ const Document = require('./document'); const DocumentNotFoundError = require('./error/notFound'); const DivergentArrayError = require('./error/divergentArray'); const EventEmitter = require('events').EventEmitter; +const MongooseBulkWriteError = require('./error/bulkWriteError'); const MongooseBuffer = require('./types/buffer'); const MongooseError = require('./error/index'); const OverwriteModelError = require('./error/overwriteModel'); @@ -3375,6 +3376,7 @@ Model.startSession = function() { * @param {Boolean} [options.lean=false] if `true`, skips hydrating the documents. This means Mongoose will **not** cast or validate any of the documents passed to `insertMany()`. This option is useful if you need the extra performance, but comes with data integrity risk. Consider using with [`castObject()`](#model_Model-castObject). * @param {Number} [options.limit=null] this limits the number of documents being processed (validation/casting) by mongoose in parallel, this does **NOT** send the documents in batches to MongoDB. Use this option if you're processing a large number of documents and your app is running out of memory. * @param {String|Object|Array} [options.populate=null] populates the result documents. This option is a no-op if `rawResult` is set. + * @param {Boolean} [options.throwOnValidationError=false] If true and `ordered: false`, throw an error if one of the operations failed validation, but all valid operations completed successfully. * @param {Function} [callback] callback * @return {Promise} resolving to the raw result from the MongoDB driver if `options.rawResult` was `true`, or the documents that passed validation, otherwise * @api public @@ -3419,6 +3421,7 @@ Model.$__insertMany = function(arr, options, callback) { const limit = options.limit || 1000; const rawResult = !!options.rawResult; const ordered = typeof options.ordered === 'boolean' ? options.ordered : true; + const throwOnValidationError = typeof options.throwOnValidationError === 'boolean' ? options.throwOnValidationError : false; const lean = !!options.lean; if (!Array.isArray(arr)) { @@ -3496,6 +3499,14 @@ Model.$__insertMany = function(arr, options, callback) { // Quickly escape while there aren't any valid docAttributes if (docAttributes.length === 0) { + if (throwOnValidationError) { + return callback(new MongooseBulkWriteError( + validationErrors, + results, + null, + 'insertMany' + )); + } if (rawResult) { const res = { acknowledged: true, @@ -3598,6 +3609,20 @@ Model.$__insertMany = function(arr, options, callback) { } } + if (ordered === false && throwOnValidationError && validationErrors.length > 0) { + for (let i = 0; i < results.length; ++i) { + if (results[i] === void 0) { + results[i] = docs[i]; + } + } + return callback(new MongooseBulkWriteError( + validationErrors, + results, + res, + 'insertMany' + )); + } + if (rawResult) { if (ordered === false) { for (let i = 0; i < results.length; ++i) { @@ -3728,6 +3753,7 @@ function _setIsNew(doc, val) { * @param {Boolean} [options.skipValidation=false] Set to true to skip Mongoose schema validation on bulk write operations. Mongoose currently runs validation on `insertOne` and `replaceOne` operations by default. * @param {Boolean} [options.bypassDocumentValidation=false] If true, disable [MongoDB server-side schema validation](https://www.mongodb.com/docs/manual/core/schema-validation/) for all writes in this bulk. * @param {Boolean} [options.strict=null] Overwrites the [`strict` option](/docs/guide.html#strict) on schema. If false, allows filtering and writing fields not defined in the schema for all writes in this bulk. + * @param {Boolean} [options.throwOnValidationError=false] If true and `ordered: false`, throw an error if one of the operations failed validation, but all valid operations completed successfully. * @param {Function} [callback] callback `function(error, bulkWriteOpResult) {}` * @return {Promise} resolves to a [`BulkWriteOpResult`](https://mongodb.github.io/node-mongodb-native/4.9/classes/BulkWriteResult.html) if the operation succeeds * @api public @@ -3777,6 +3803,7 @@ Model.bulkWrite = function(ops, options, callback) { let remaining = validations.length; let validOps = []; let validationErrors = []; + const results = []; if (remaining === 0) { completeUnorderedValidation.call(this); } else { @@ -3786,6 +3813,7 @@ Model.bulkWrite = function(ops, options, callback) { validOps.push(i); } else { validationErrors.push({ index: i, error: err }); + results[i] = err; } if (--remaining <= 0) { completeUnorderedValidation.call(this); @@ -3799,13 +3827,25 @@ Model.bulkWrite = function(ops, options, callback) { map(v => v.error); function completeUnorderedValidation() { + const validOpIndexes = validOps; validOps = validOps.sort().map(index => ops[index]); if (validOps.length === 0) { + if ('throwOnValidationError' in options && options.throwOnValidationError && validationErrors.length > 0) { + return cb(new MongooseBulkWriteError( + validationErrors.map(err => err.error), + results, + getDefaultBulkwriteResult(), + 'bulkWrite' + )); + } return cb(null, getDefaultBulkwriteResult()); } this.$__collection.bulkWrite(validOps, options, (error, res) => { + for (let i = 0; i < validOpIndexes.length; ++i) { + results[validOpIndexes[i]] = null; + } if (error) { if (validationErrors.length > 0) { error.mongoose = error.mongoose || {}; @@ -3816,6 +3856,14 @@ Model.bulkWrite = function(ops, options, callback) { } if (validationErrors.length > 0) { + if ('throwOnValidationError' in options && options.throwOnValidationError) { + return cb(new MongooseBulkWriteError( + validationErrors, + results, + res, + 'bulkWrite' + )); + } res.mongoose = res.mongoose || {}; res.mongoose.validationErrors = validationErrors; } diff --git a/test/model.test.js b/test/model.test.js index 00573da4440..c07debc6ff1 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -6111,6 +6111,71 @@ describe('Model', function() { const { num } = await Test.findById(_id); assert.equal(num, 99); }); + + it('bulkWrite should throw an error if there were operations that failed validation, ' + + 'but all operations that passed validation succeeded (gh-13256)', async function() { + const userSchema = new Schema({ age: { type: Number } }); + const User = db.model('User', userSchema); + + const createdUser = await User.create({ name: 'Test' }); + + const err = await User.bulkWrite([ + { + updateOne: { + filter: { _id: createdUser._id }, + update: { $set: { age: 'NaN' } }, + upsert: true + } + }, + { + updateOne: { + filter: { _id: createdUser._id }, + update: { $set: { age: 13 } }, + upsert: true + } + }, + { + updateOne: { + filter: { _id: createdUser._id }, + update: { $set: { age: 12 } }, + upsert: true + } + } + ], { ordered: false, throwOnValidationError: true }) + .then(() => null) + .catch(err => err); + + assert.ok(err); + assert.equal(err.name, 'MongooseBulkWriteError'); + assert.equal(err.validationErrors[0].path, 'age'); + assert.equal(err.results[0].path, 'age'); + }); + + it('throwOnValidationError (gh-14572) (gh-13256)', async function() { + const schema = new Schema({ + num: Number + }); + + const M = db.model('Test', schema); + + const ops = [ + { + insertOne: { + document: { + num: 'not a number' + } + } + } + ]; + + const err = await M.bulkWrite( + ops, + { ordered: false, throwOnValidationError: true } + ).then(() => null, err => err); + assert.ok(err); + assert.equal(err.name, 'MongooseBulkWriteError'); + assert.equal(err.validationErrors[0].errors['num'].name, 'CastError'); + }); }); it('insertMany with Decimal (gh-5190)', async function() { @@ -9028,6 +9093,45 @@ describe('Model', function() { assert.equal(TestModel.staticFn(), 'Returned from staticFn'); }); }); + + it('insertMany should throw an error if there were operations that failed validation, ' + + 'but all operations that passed validation succeeded (gh-13256)', async function() { + const userSchema = new Schema({ + age: { type: Number } + }); + + const User = db.model('User', userSchema); + + let err = await User.insertMany([ + new User({ age: 12 }), + new User({ age: 12 }), + new User({ age: 'NaN' }) + ], { ordered: false, throwOnValidationError: true }) + .then(() => null) + .catch(err => err); + + assert.ok(err); + assert.equal(err.name, 'MongooseBulkWriteError'); + assert.equal(err.validationErrors[0].errors['age'].name, 'CastError'); + assert.ok(err.results[2] instanceof Error); + assert.equal(err.results[2].errors['age'].name, 'CastError'); + + let docs = await User.find(); + assert.deepStrictEqual(docs.map(doc => doc.age), [12, 12]); + + err = await User.insertMany([ + new User({ age: 'NaN' }) + ], { ordered: false, throwOnValidationError: true }) + .then(() => null) + .catch(err => err); + + assert.ok(err); + assert.equal(err.name, 'MongooseBulkWriteError'); + assert.equal(err.validationErrors[0].errors['age'].name, 'CastError'); + + docs = await User.find(); + assert.deepStrictEqual(docs.map(doc => doc.age), [12, 12]); + }); }); diff --git a/types/models.d.ts b/types/models.d.ts index 5504ec91a58..f1cf0a57bc1 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -19,6 +19,7 @@ declare module 'mongoose' { skipValidation?: boolean; strict?: boolean; timestamps?: boolean | 'throw'; + throwOnValidationError?: boolean; } interface InsertManyOptions extends @@ -28,6 +29,7 @@ declare module 'mongoose' { rawResult?: boolean; ordered?: boolean; lean?: boolean; + throwOnValidationError?: boolean; } type InsertManyResult = mongodb.InsertManyResult & { From c3b4bdbb86d21c837a958a054951418d476f6d52 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 May 2024 10:22:52 -0400 Subject: [PATCH 05/25] chore: release 6.12.9 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bdcdeec521..9057fe4668d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +6.12.9 / 2024-05-24 +=================== + * fix(cast): cast $comment to string in query filters #14590 #14576 + * types(model): allow passing strict type checking override to create() #14571 #14548 + 6.12.8 / 2024-04-10 =================== * fix(document): handle virtuals that are stored as objects but getter returns string with toJSON #14468 #14446 diff --git a/package.json b/package.json index 6d887a254e2..ef60676d102 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.12.8", + "version": "6.12.9", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 218d50a868577bb744ada87cf9ba7336402b2a2e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 May 2024 12:20:10 -0400 Subject: [PATCH 06/25] fix(query): shallow clone $or and $and array elements to avoid mutating query filter arguments Fix #14610 --- lib/query.js | 12 ++++++++---- lib/utils.js | 2 ++ test/query.test.js | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/query.js b/lib/query.js index 5db5c0e7ef7..eb6f581f920 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2416,13 +2416,17 @@ Query.prototype.merge = function(source) { } opts.omit = {}; - if (this._conditions && this._conditions.$and && source.$and) { + if (this._conditions && Array.isArray(source.$and)) { opts.omit['$and'] = true; - this._conditions.$and = this._conditions.$and.concat(source.$and); + this._conditions.$and = (this._conditions.$and || []).concat( + source.$and.map(el => utils.isPOJO(el) ? utils.merge({}, el) : el) + ); } - if (this._conditions && this._conditions.$or && source.$or) { + if (this._conditions && Array.isArray(source.$or)) { opts.omit['$or'] = true; - this._conditions.$or = this._conditions.$or.concat(source.$or); + this._conditions.$or = (this._conditions.$or || []).concat( + source.$or.map(el => utils.isPOJO(el) ? utils.merge({}, el) : el) + ); } // plain object diff --git a/lib/utils.js b/lib/utils.js index 512b7728a3e..0172ac3e433 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -294,6 +294,8 @@ exports.merge = function merge(to, from, options, path) { to[key] = from[key]; } } + + return to; }; /** diff --git a/test/query.test.js b/test/query.test.js index 42354434d90..61e70674f44 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -4214,4 +4214,19 @@ describe('Query', function() { assert.strictEqual(doc.account.owner, undefined); assert.strictEqual(doc.account.taxIds, undefined); }); + + it('avoids mutating $or, $and elements when casting (gh-14610)', async function() { + const personSchema = new mongoose.Schema({ + name: String, + age: Number + }); + const Person = db.model('Person', personSchema); + + const filter = [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }]; + await Person.find({ $or: filter }); + assert.deepStrictEqual(filter, [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }]); + + await Person.find({ $and: filter }); + assert.deepStrictEqual(filter, [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }]); + }); }); From d4ad58efc0eecbf3b08e99acb9781d4eb708a633 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 May 2024 12:22:24 -0400 Subject: [PATCH 07/25] bring in some changes from #14580 --- lib/query.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/query.js b/lib/query.js index eb6f581f920..c4a413fad1f 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2416,14 +2416,20 @@ Query.prototype.merge = function(source) { } opts.omit = {}; - if (this._conditions && Array.isArray(source.$and)) { + if (Array.isArray(source.$and)) { opts.omit['$and'] = true; + if (!this._conditions) { + this._conditions = {}; + } this._conditions.$and = (this._conditions.$and || []).concat( source.$and.map(el => utils.isPOJO(el) ? utils.merge({}, el) : el) ); } - if (this._conditions && Array.isArray(source.$or)) { + if (Array.isArray(source.$or)) { opts.omit['$or'] = true; + if (!this._conditions) { + this._conditions = {}; + } this._conditions.$or = (this._conditions.$or || []).concat( source.$or.map(el => utils.isPOJO(el) ? utils.merge({}, el) : el) ); From da126f4d6c5f3fb5000c000efeb3d4e30b4ba5a6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 2 Jun 2024 11:40:58 -0400 Subject: [PATCH 08/25] docs(migrating_to_7): add id setter to Mongoose 7 migration guide --- docs/migrating_to_7.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/migrating_to_7.md b/docs/migrating_to_7.md index d9526322f4b..c4df3209089 100644 --- a/docs/migrating_to_7.md +++ b/docs/migrating_to_7.md @@ -16,6 +16,7 @@ If you're still on Mongoose 5.x, please read the [Mongoose 5.x to 6.x migration * [Dropped callback support](#dropped-callback-support) * [Removed `update()`](#removed-update) * [ObjectId requires `new`](#objectid-requires-new) +* [`id` setter](#id-setter) * [Discriminator schemas use base schema options by default](#discriminator-schemas-use-base-schema-options-by-default) * [Removed `castForQueryWrapper()`, updated `castForQuery()` signature](#removed-castforquerywrapper) * [Copy schema options in `Schema.prototype.add()`](#copy-schema-options-in-schema-prototype-add) @@ -196,6 +197,31 @@ In Mongoose 7, `ObjectId` is now a [JavaScript class](https://masteringjs.io/tut const oid = new mongoose.Types.ObjectId('0'.repeat(24)); ``` +

id Setter

+ +Starting in Mongoose 7.4, Mongoose's built-in `id` virtual (which stores the document's `_id` as a string) has a setter which allows modifying the document's `_id` property via `id`. + +```javascript +const doc = await TestModel.findOne(); + +doc.id = '000000000000000000000000'; +doc._id; // ObjectId('000000000000000000000000') +``` + +This can cause surprising behavior if you create a `new TestModel(obj)` where `obj` contains both an `id` and an `_id`, or if you use `doc.set()` + +```javascript +// Because `id` is after `_id`, the `id` will overwrite the `_id` +const doc = new TestModel({ + _id: '000000000000000000000000', + id: '111111111111111111111111' +}); + +doc._id; // ObjectId('111111111111111111111111') +``` + +[The `id` setter was later removed in Mongoose 8](/docs/migrating_to_8.html#removed-id-setter) due to compatibility issues. +

Discriminator schemas use base schema options by default

When you use `Model.discriminator()`, Mongoose will now use the discriminator base schema's options by default. From 40ec8139fc0c3ee06e742b95c2ecc802e9bdc4ac Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 May 2024 10:03:43 -0400 Subject: [PATCH 09/25] types: pass DocType down to subdocuments so `HydratedSingleSubdocument` and `HydratedArraySubdocument` `toObject()` returns correct type Fix #14601 --- test/types/subdocuments.test.ts | 43 ++++++++++++++++++++++++++++++++- types/index.d.ts | 4 +-- types/types.d.ts | 2 +- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/test/types/subdocuments.test.ts b/test/types/subdocuments.test.ts index 59b58716f5f..14386816fe5 100644 --- a/test/types/subdocuments.test.ts +++ b/test/types/subdocuments.test.ts @@ -1,4 +1,13 @@ -import { Schema, model, Model, Document, Types } from 'mongoose'; +import { + Schema, + model, + Model, + Document, + Types, + HydratedArraySubdocument, + HydratedSingleSubdocument +} from 'mongoose'; +import { expectAssignable } from 'tsd'; const childSchema: Schema = new Schema({ name: String }); @@ -108,3 +117,35 @@ function gh13040(): void { product.ownerDocument(); }); } + +function gh14601() { + interface ISub { + field1: string; + } + interface IMain { + f1: string; + f2: HydratedSingleSubdocument; + f3: HydratedArraySubdocument[]; + } + + const subSchema = new Schema({ field1: String }, { _id: false }); + + const mainSchema = new Schema({ + f1: String, + f2: { type: subSchema }, + f3: { type: [subSchema] } + }); + const MainModel = model('Main', mainSchema); + + const item = new MainModel({ + f1: 'test', + f2: { field1: 'test' }, + f3: [{ field1: 'test' }] + }); + + const obj = item.toObject(); + + const obj2 = item.f2.toObject(); + + expectAssignable<{ _id: Types.ObjectId, field1: string }>(obj2); +} diff --git a/types/index.d.ts b/types/index.d.ts index 05578ccc3ca..0422b776090 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -153,8 +153,8 @@ declare module 'mongoose' { > > >; - export type HydratedSingleSubdocument = Types.Subdocument & Require_id & TOverrides; - export type HydratedArraySubdocument = Types.ArraySubdocument & Require_id & TOverrides; + export type HydratedSingleSubdocument = Types.Subdocument, DocType> & Require_id & TOverrides; + export type HydratedArraySubdocument = Types.ArraySubdocument, DocType> & Require_id & TOverrides; export type HydratedDocumentFromSchema = HydratedDocument< InferSchemaType, diff --git a/types/types.d.ts b/types/types.d.ts index 55b48138ad3..b1707c245f7 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -95,7 +95,7 @@ declare module 'mongoose' { $parent(): Document; } - class ArraySubdocument extends Subdocument { + class ArraySubdocument extends Subdocument { /** Returns this sub-documents parent array. */ parentArray(): Types.DocumentArray; } From 7e6db5f24ead97aebb72060efc55f44de7b452c8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 5 Jun 2024 16:08:12 -0400 Subject: [PATCH 10/25] types: cherry-pick #14612 back to 7.x and fix conflicts --- types/types.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/types.d.ts b/types/types.d.ts index b1707c245f7..5d99ab38ee5 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -82,7 +82,7 @@ declare module 'mongoose' { class ObjectId extends mongodb.ObjectId { } - class Subdocument extends Document { + class Subdocument extends Document { $isSingleNested: true; /** Returns the top level document of this sub-document. */ From 0f99e994d4c2306a6a33d7c8ddabecc12e3c48dc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 5 Jun 2024 16:14:04 -0400 Subject: [PATCH 11/25] chore: release 7.6.13 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76916f787df..3f2fe8526f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +7.6.13 / 2024-06-05 +=================== + * fix(query): shallow clone $or and $and array elements to avoid mutating query filter arguments #14614 #14610 + * types: pass DocType down to subdocuments so HydratedSingleSubdocument and HydratedArraySubdocument toObject() returns correct type #14612 #14601 + * docs(migrating_to_7): add id setter to Mongoose 7 migration guide #14645 #13672 + 7.6.12 / 2024-05-21 =================== * fix(array): avoid converting to $set when calling pull() on an element in the middle of the array #14531 #14502 diff --git a/package.json b/package.json index 14eac926273..d4a78b6b0da 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.12", + "version": "7.6.13", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 53d382b0f9df9590fcb1c1468de38dbeb56b7f03 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 6 Jun 2024 15:31:00 -0400 Subject: [PATCH 12/25] chore: release 6.13.0 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9057fe4668d..36d481adf2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +6.13.0 / 2024-06-06 +=================== + * feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() #14599 #14587 #14572 #13410 + 6.12.9 / 2024-05-24 =================== * fix(cast): cast $comment to string in query filters #14590 #14576 diff --git a/package.json b/package.json index ef60676d102..bd1ab99dc29 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.12.9", + "version": "6.13.0", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 0198236ec2a4eb62e181cd20dec152ecfe829de2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 6 Jun 2024 16:23:13 -0400 Subject: [PATCH 13/25] types: remove duplicate key --- types/models.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/types/models.d.ts b/types/models.d.ts index aabc64bdd2e..7cb082bc372 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -28,7 +28,6 @@ declare module 'mongoose' { throwOnValidationError?: boolean; strict?: boolean; timestamps?: boolean | 'throw'; - throwOnValidationError?: boolean; } interface InsertManyOptions extends From 061bb828c706512d79c79f5d42774316bf167357 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 18 Jun 2024 16:09:58 -0400 Subject: [PATCH 14/25] chore: release 7.7.0 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e426df3b9c..be735e4aad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +7.7.0 / 2024-06-18 +================== + * feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() #14599 #14587 #14572 #13410 + 6.13.0 / 2024-06-06 =================== * feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() #14599 #14587 #14572 #13410 diff --git a/package.json b/package.json index d4a78b6b0da..d2877b4c2b6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.13", + "version": "7.7.0", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 1ced0150feca358586e16ee1265c089ba54f28e8 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Wed, 10 Jul 2024 13:14:03 +0200 Subject: [PATCH 15/25] types(query): fix usage of "RawDocType" where "DocType" should be passed --- types/query.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/query.d.ts b/types/query.d.ts index 39ff69de9df..87fabb10a7c 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -374,7 +374,7 @@ declare module 'mongoose' { ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find'>; find( filter: FilterQuery - ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find'>; + ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find'>; find(): QueryWithHelpers, DocType, THelpers, RawDocType, 'find'>; /** Declares the query a findOne operation. When executed, returns the first found document. */ @@ -389,7 +389,7 @@ declare module 'mongoose' { ): QueryWithHelpers; findOne( filter?: FilterQuery - ): QueryWithHelpers; + ): QueryWithHelpers; /** Creates a `findOneAndDelete` query: atomically finds the given document, deletes it, and returns the document as it was before deletion. */ findOneAndDelete( From ec619000a0db61a8d8ea638938176e0b704e4acb Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 11 Jul 2024 12:15:58 -0400 Subject: [PATCH 16/25] feat: add transactionAsyncLocalStorage option to opt in to automatically setting session on all transactions Backport #14583 to 7.x Re: #13889 --- docs/transactions.md | 31 ++++++++++++++++++--- lib/aggregate.js | 5 ++++ lib/connection.js | 13 ++++++--- lib/index.js | 16 +++++++++-- lib/model.js | 7 +++++ lib/query.js | 5 ++++ lib/validoptions.js | 1 + test/docs/transactions.test.js | 49 ++++++++++++++++++++++++++++++++++ types/mongooseoptions.d.ts | 7 +++++ 9 files changed, 126 insertions(+), 8 deletions(-) diff --git a/docs/transactions.md b/docs/transactions.md index 901282dac44..4251cd5d017 100644 --- a/docs/transactions.md +++ b/docs/transactions.md @@ -1,8 +1,6 @@ # Transactions in Mongoose -[Transactions](https://www.mongodb.com/transactions) are new in MongoDB -4.0 and Mongoose 5.2.0. Transactions let you execute multiple operations -in isolation and potentially undo all the operations if one of them fails. +[Transactions](https://www.mongodb.com/transactions) let you execute multiple operations in isolation and potentially undo all the operations if one of them fails. This guide will get you started using transactions with Mongoose.

Getting Started with Transactions

@@ -86,6 +84,33 @@ Below is an example of executing an aggregation within a transaction. [require:transactions.*aggregate] ``` +

Using AsyncLocalStorage

+ +One major pain point with transactions in Mongoose is that you need to remember to set the `session` option on every operation. +If you don't, your operation will execute outside of the transaction. +Mongoose 8.4 is able to set the `session` operation on all operations within a `Connection.prototype.transaction()` executor function using Node's [AsyncLocalStorage API](https://nodejs.org/api/async_context.html#class-asynclocalstorage). +Set the `transactionAsyncLocalStorage` option using `mongoose.set('transactionAsyncLocalStorage', true)` to enable this feature. + +```javascript +mongoose.set('transactionAsyncLocalStorage', true); + +const Test = mongoose.model('Test', mongoose.Schema({ name: String })); + +const doc = new Test({ name: 'test' }); + +// Save a new doc in a transaction that aborts +await connection.transaction(async() => { + await doc.save(); // Notice no session here + throw new Error('Oops'); +}).catch(() => {}); + +// false, `save()` was rolled back +await Test.exists({ _id: doc._id }); +``` + +With `transactionAsyncLocalStorage`, you no longer need to pass sessions to every operation. +Mongoose will add the session by default under the hood. +

Advanced Usage

Advanced users who want more fine-grained control over when they commit or abort transactions diff --git a/lib/aggregate.js b/lib/aggregate.js index 450f9e34d12..0d3ec1bf927 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -1022,6 +1022,11 @@ Aggregate.prototype.exec = async function exec() { applyGlobalMaxTimeMS(this.options, model); applyGlobalDiskUse(this.options, model); + const asyncLocalStorage = this.model()?.db?.base.transactionAsyncLocalStorage?.getStore(); + if (!this.options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { + this.options.session = asyncLocalStorage.session; + } + if (this.options && this.options.cursor) { return new AggregationCursor(this); } diff --git a/lib/connection.js b/lib/connection.js index c116a3cde32..bec66623d0b 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -517,7 +517,7 @@ Connection.prototype.startSession = async function startSession(options) { Connection.prototype.transaction = function transaction(fn, options) { return this.startSession().then(session => { session[sessionNewDocuments] = new Map(); - return session.withTransaction(() => _wrapUserTransaction(fn, session), options). + return session.withTransaction(() => _wrapUserTransaction(fn, session, this.base), options). then(res => { delete session[sessionNewDocuments]; return res; @@ -536,9 +536,16 @@ Connection.prototype.transaction = function transaction(fn, options) { * Reset document state in between transaction retries re: gh-13698 */ -async function _wrapUserTransaction(fn, session) { +async function _wrapUserTransaction(fn, session, mongoose) { try { - const res = await fn(session); + const res = mongoose.transactionAsyncLocalStorage == null + ? await fn(session) + : await new Promise(resolve => { + mongoose.transactionAsyncLocalStorage.run( + { session }, + () => resolve(fn(session)) + ); + }); return res; } catch (err) { _resetSessionDocuments(session); diff --git a/lib/index.js b/lib/index.js index 38197ec50ae..5b716293a4c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -40,6 +40,8 @@ require('./helpers/printJestWarning'); const objectIdHexRegexp = /^[0-9A-Fa-f]{24}$/; +const { AsyncLocalStorage } = require('node:async_hooks'); + /** * Mongoose constructor. * @@ -102,6 +104,10 @@ function Mongoose(options) { } this.Schema.prototype.base = this; + if (options?.transactionAsyncLocalStorage) { + this.transactionAsyncLocalStorage = new AsyncLocalStorage(); + } + Object.defineProperty(this, 'plugins', { configurable: false, enumerable: true, @@ -258,7 +264,7 @@ Mongoose.prototype.set = function(key, value) { if (optionKey === 'objectIdGetter') { if (optionValue) { - Object.defineProperty(mongoose.Types.ObjectId.prototype, '_id', { + Object.defineProperty(_mongoose.Types.ObjectId.prototype, '_id', { enumerable: false, configurable: true, get: function() { @@ -266,7 +272,13 @@ Mongoose.prototype.set = function(key, value) { } }); } else { - delete mongoose.Types.ObjectId.prototype._id; + delete _mongoose.Types.ObjectId.prototype._id; + } + } else if (optionKey === 'transactionAsyncLocalStorage') { + if (optionValue && !_mongoose.transactionAsyncLocalStorage) { + _mongoose.transactionAsyncLocalStorage = new AsyncLocalStorage(); + } else if (!optionValue && _mongoose.transactionAsyncLocalStorage) { + delete _mongoose.transactionAsyncLocalStorage; } } } diff --git a/lib/model.js b/lib/model.js index 3aba07e3563..96b0c78398a 100644 --- a/lib/model.js +++ b/lib/model.js @@ -288,8 +288,11 @@ Model.prototype.$__handleSave = function(options, callback) { } const session = this.$session(); + const asyncLocalStorage = this[modelDbSymbol].base.transactionAsyncLocalStorage?.getStore(); if (!saveOptions.hasOwnProperty('session') && session != null) { saveOptions.session = session; + } else if (asyncLocalStorage?.session != null) { + saveOptions.session = asyncLocalStorage.session; } if (this.$isNew) { @@ -3463,6 +3466,10 @@ Model.bulkWrite = async function bulkWrite(ops, options) { const ordered = options.ordered == null ? true : options.ordered; const validations = ops.map(op => castBulkWrite(this, op, options)); + const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore(); + if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) { + options = { ...options, session: asyncLocalStorage.session }; + } return new Promise((resolve, reject) => { if (ordered) { diff --git a/lib/query.js b/lib/query.js index c4a413fad1f..3f6fc76fb89 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1980,6 +1980,11 @@ Query.prototype._optionsForExec = function(model) { // Apply schema-level `writeConcern` option applyWriteConcern(model.schema, options); + const asyncLocalStorage = this.model?.db?.base.transactionAsyncLocalStorage?.getStore(); + if (!this.options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { + options.session = asyncLocalStorage.session; + } + const readPreference = model && model.schema && model.schema.options && diff --git a/lib/validoptions.js b/lib/validoptions.js index af4e116deec..ee087337bbf 100644 --- a/lib/validoptions.js +++ b/lib/validoptions.js @@ -31,6 +31,7 @@ const VALID_OPTIONS = Object.freeze([ 'strictQuery', 'toJSON', 'toObject', + 'transactionAsyncLocalStorage', 'translateAliases' ]); diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index d196fab7180..bde95f4b176 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -421,4 +421,53 @@ describe('transactions', function() { assert.equal(i, 3); }); + + describe('transactionAsyncLocalStorage option', function() { + let m; + before(async function() { + m = new mongoose.Mongoose(); + m.set('transactionAsyncLocalStorage', true); + + await m.connect(start.uri); + }); + + after(async function() { + await m.disconnect(); + }); + + it('transaction() sets `session` by default if transactionAsyncLocalStorage option is set', async function() { + const Test = m.model('Test', m.Schema({ name: String })); + + await Test.createCollection(); + await Test.deleteMany({}); + + const doc = new Test({ name: 'test_transactionAsyncLocalStorage' }); + await assert.rejects( + () => m.connection.transaction(async() => { + await doc.save(); + + await Test.updateOne({ name: 'foo' }, { name: 'foo' }, { upsert: true }); + + let docs = await Test.aggregate([{ $match: { _id: doc._id } }]); + assert.equal(docs.length, 1); + + docs = await Test.find({ _id: doc._id }); + assert.equal(docs.length, 1); + + docs = await async function test() { + return await Test.findOne({ _id: doc._id }); + }(); + assert.equal(doc.name, 'test_transactionAsyncLocalStorage'); + + throw new Error('Oops!'); + }), + /Oops!/ + ); + let exists = await Test.exists({ _id: doc._id }); + assert.ok(!exists); + + exists = await Test.exists({ name: 'foo' }); + assert.ok(!exists); + }); + }); }); diff --git a/types/mongooseoptions.d.ts b/types/mongooseoptions.d.ts index 7fec10b208f..9c35ab8222b 100644 --- a/types/mongooseoptions.d.ts +++ b/types/mongooseoptions.d.ts @@ -203,6 +203,13 @@ declare module 'mongoose' { */ toObject?: ToObjectOptions; + /** + * Set to true to make Mongoose use Node.js' built-in AsyncLocalStorage (Node >= 16.0.0) + * to set `session` option on all operations within a `connection.transaction(fn)` call + * by default. Defaults to false. + */ + transactionAsyncLocalStorage?: boolean; + /** * If `true`, convert any aliases in filter, projection, update, and distinct * to their database property names. Defaults to false. From b9deadb9ce76cecd43db325cc6b064104915f8e6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 12 Jul 2024 10:39:29 -0400 Subject: [PATCH 17/25] Update docs/transactions.md Co-authored-by: hasezoey --- docs/transactions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/transactions.md b/docs/transactions.md index 4251cd5d017..2fb0a6f395d 100644 --- a/docs/transactions.md +++ b/docs/transactions.md @@ -88,7 +88,7 @@ Below is an example of executing an aggregation within a transaction. One major pain point with transactions in Mongoose is that you need to remember to set the `session` option on every operation. If you don't, your operation will execute outside of the transaction. -Mongoose 8.4 is able to set the `session` operation on all operations within a `Connection.prototype.transaction()` executor function using Node's [AsyncLocalStorage API](https://nodejs.org/api/async_context.html#class-asynclocalstorage). +Mongoose 7.8 is able to set the `session` operation on all operations within a `Connection.prototype.transaction()` executor function using Node's [AsyncLocalStorage API](https://nodejs.org/api/async_context.html#class-asynclocalstorage). Set the `transactionAsyncLocalStorage` option using `mongoose.set('transactionAsyncLocalStorage', true)` to enable this feature. ```javascript From 378d1155487d80d33f2a30816fc0db396fb0185b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 12 Jul 2024 10:39:46 -0400 Subject: [PATCH 18/25] Update types/mongooseoptions.d.ts Co-authored-by: hasezoey --- types/mongooseoptions.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/mongooseoptions.d.ts b/types/mongooseoptions.d.ts index 9c35ab8222b..9aee6c9e206 100644 --- a/types/mongooseoptions.d.ts +++ b/types/mongooseoptions.d.ts @@ -204,7 +204,7 @@ declare module 'mongoose' { toObject?: ToObjectOptions; /** - * Set to true to make Mongoose use Node.js' built-in AsyncLocalStorage (Node >= 16.0.0) + * Set to true to make Mongoose use Node.js' built-in AsyncLocalStorage (Added in: v13.10.0, v12.17.0; Stable since 16.4.0) * to set `session` option on all operations within a `connection.transaction(fn)` call * by default. Defaults to false. */ From 3f21bfaedf235440c8040a3d58cdf2a9faf9a8a8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 12 Jul 2024 11:08:14 -0400 Subject: [PATCH 19/25] fix: backport #14743 --- lib/model.js | 5 +++++ test/docs/transactions.test.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/lib/model.js b/lib/model.js index 96b0c78398a..0e38daa8942 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3113,6 +3113,11 @@ Model.$__insertMany = function(arr, options, callback) { const throwOnValidationError = typeof options.throwOnValidationError === 'boolean' ? options.throwOnValidationError : false; const lean = !!options.lean; + const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore(); + if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) { + options = { ...options, session: asyncLocalStorage.session }; + } + if (!Array.isArray(arr)) { arr = [arr]; } diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index bde95f4b176..d93ea32e60d 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -459,6 +459,8 @@ describe('transactions', function() { }(); assert.equal(doc.name, 'test_transactionAsyncLocalStorage'); + await Test.insertMany([{ name: 'bar' }]); + throw new Error('Oops!'); }), /Oops!/ @@ -468,6 +470,9 @@ describe('transactions', function() { exists = await Test.exists({ name: 'foo' }); assert.ok(!exists); + + exists = await Test.exists({ name: 'bar' }); + assert.ok(!exists); }); }); }); From e909e0b940f946dcbf8b4436de99e015c99061a7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 15 Jul 2024 09:35:47 -0400 Subject: [PATCH 20/25] fix: support session: null option for save() to opt out of automatic session option with transactionAsyncLocalStorage; backport #14744 --- lib/model.js | 5 +++-- test/docs/transactions.test.js | 13 ++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/model.js b/lib/model.js index 0e38daa8942..f649ce33651 100644 --- a/lib/model.js +++ b/lib/model.js @@ -289,9 +289,10 @@ Model.prototype.$__handleSave = function(options, callback) { const session = this.$session(); const asyncLocalStorage = this[modelDbSymbol].base.transactionAsyncLocalStorage?.getStore(); - if (!saveOptions.hasOwnProperty('session') && session != null) { + if (session != null) { saveOptions.session = session; - } else if (asyncLocalStorage?.session != null) { + } else if (!options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { + // Only set session from asyncLocalStorage if `session` option wasn't originally passed in options saveOptions.session = asyncLocalStorage.session; } diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index d93ea32e60d..a6f2a501250 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -441,7 +441,7 @@ describe('transactions', function() { await Test.createCollection(); await Test.deleteMany({}); - const doc = new Test({ name: 'test_transactionAsyncLocalStorage' }); + let doc = new Test({ name: 'test_transactionAsyncLocalStorage' }); await assert.rejects( () => m.connection.transaction(async() => { await doc.save(); @@ -473,6 +473,17 @@ describe('transactions', function() { exists = await Test.exists({ name: 'bar' }); assert.ok(!exists); + + doc = new Test({ name: 'test_transactionAsyncLocalStorage' }); + await assert.rejects( + () => m.connection.transaction(async() => { + await doc.save({ session: null }); + throw new Error('Oops!'); + }), + /Oops!/ + ); + exists = await Test.exists({ _id: doc._id }); + assert.ok(exists); }); }); }); From 0c11d129cefb2bc966d5597a23dd8aa27253c082 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 23 Jul 2024 12:50:54 -0400 Subject: [PATCH 21/25] fix(query): handle casting $switch in $expr Fix #14751 Backport #14755 to 7.x --- lib/helpers/query/cast$expr.js | 8 ++++++-- test/helpers/query.cast$expr.test.js | 29 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/helpers/query/cast$expr.js b/lib/helpers/query/cast$expr.js index a13190b1c41..efa2cace344 100644 --- a/lib/helpers/query/cast$expr.js +++ b/lib/helpers/query/cast$expr.js @@ -92,8 +92,12 @@ function _castExpression(val, schema, strictQuery) { } else if (val.$ifNull != null) { val.$ifNull.map(v => _castExpression(v, schema, strictQuery)); } else if (val.$switch != null) { - val.branches.map(v => _castExpression(v, schema, strictQuery)); - val.default = _castExpression(val.default, schema, strictQuery); + if (Array.isArray(val.$switch.branches)) { + val.$switch.branches = val.$switch.branches.map(v => _castExpression(v, schema, strictQuery)); + } + if ('default' in val.$switch) { + val.$switch.default = _castExpression(val.$switch.default, schema, strictQuery); + } } const keys = Object.keys(val); diff --git a/test/helpers/query.cast$expr.test.js b/test/helpers/query.cast$expr.test.js index 416db9af5c6..fd3f7cd5bfe 100644 --- a/test/helpers/query.cast$expr.test.js +++ b/test/helpers/query.cast$expr.test.js @@ -118,4 +118,33 @@ describe('castexpr', function() { res = cast$expr({ $eq: [{ $round: ['$value'] }, 2] }, testSchema); assert.deepStrictEqual(res, { $eq: [{ $round: ['$value'] }, 2] }); }); + + it('casts $switch (gh-14751)', function() { + const testSchema = new Schema({ + name: String, + scores: [Number] + }); + const res = cast$expr({ + $eq: [ + { + $switch: { + branches: [{ case: { $eq: ['$$NOW', '$$NOW'] }, then: true }], + default: false + } + }, + true + ] + }, testSchema); + assert.deepStrictEqual(res, { + $eq: [ + { + $switch: { + branches: [{ case: { $eq: ['$$NOW', '$$NOW'] }, then: true }], + default: false + } + }, + true + ] + }); + }); }); From e8b0933538655bd8e624e2e8860a39c75eef8f4e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 23 Jul 2024 17:04:40 -0400 Subject: [PATCH 22/25] chore: release 7.8.0 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be735e4aad7..03043834ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +7.8.0 / 2024-07-23 +================== + * feat: add transactionAsyncLocalStorage option to opt in to automatically setting session on all transactions #14744 #14742 #14583 #13889 + * types(query): fix usage of "RawDocType" where "DocType" should be passed #14737 [hasezoey](https://github.com/hasezoey) + 7.7.0 / 2024-06-18 ================== * feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() #14599 #14587 #14572 #13410 diff --git a/package.json b/package.json index d2877b4c2b6..cbd4adc15af 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.7.0", + "version": "7.8.0", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 87fb3824eeaa612af3cb904fb5d80e53f69a6853 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 12 Aug 2024 14:02:01 -0400 Subject: [PATCH 23/25] add id setter to changelog re: #13517 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03043834ea9..3b00baf559e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -292,6 +292,7 @@ ================== * perf: speed up mapOfSubdocs benchmark by 4x by avoiding unnecessary O(n^2) loop in getPathsToValidate() #13614 * feat: upgrade to MongoDB Node.js driver 5.7.0 #13591 + * feat: add `id` setter which allows modifying `_id` by setting `id` (Note this change was reverted in Mongoose 8) #13517 * feat: support generating custom cast error message with a function #13608 #3162 * feat(query): support MongoDB driver's includeResultMetadata option for findOneAndUpdate #13584 #13539 * feat(connection): add Connection.prototype.removeDb() for removing a related connection #13580 #11821 From b446a2e5f7c0c3f9fd877b9630b583146218f2fe Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 16 Aug 2024 17:17:46 -0400 Subject: [PATCH 24/25] docs(mongoose): remove out-of-date callback-based example for `mongoose.connect()` Fix #14810 --- lib/index.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/index.js b/lib/index.js index 5b716293a4c..367006ff282 100644 --- a/lib/index.js +++ b/lib/index.js @@ -330,7 +330,7 @@ Mongoose.prototype.get = Mongoose.prototype.set; * * // initialize now, connect later * db = mongoose.createConnection(); - * db.openUri('127.0.0.1', 'database', port, [opts]); + * await db.openUri('mongodb://127.0.0.1:27017/database'); * * @param {String} uri mongodb URI to connect to * @param {Object} [options] passed down to the [MongoDB driver's `connect()` function](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html), except for 4 mongoose-specific options explained below. @@ -378,11 +378,10 @@ Mongoose.prototype.createConnection = function(uri, options) { * // with options * mongoose.connect(uri, options); * - * // optional callback that gets fired when initial connection completed + * // Using `await` throws "MongooseServerSelectionError: Server selection timed out after 30000 ms" + * // if Mongoose can't connect. * const uri = 'mongodb://nonexistent.domain:27000'; - * mongoose.connect(uri, function(error) { - * // if error is truthy, the initial connection failed. - * }) + * await mongoose.connect(uri); * * @param {String} uri mongodb URI to connect to * @param {Object} [options] passed down to the [MongoDB driver's `connect()` function](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html), except for 4 mongoose-specific options explained below. From 9dcd8aa32ab328ec5445de9f79db634ffa644434 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 13:11:29 -0400 Subject: [PATCH 25/25] chore: release 7.8.1 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b00baf559e..978abc41b4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +7.8.1 / 2024-08-19 +================== + * fix(query): handle casting $switch in $expr #14761 + * docs(mongoose): remove out-of-date callback-based example for mongoose.connect() #14811 #14810 + 7.8.0 / 2024-07-23 ================== * feat: add transactionAsyncLocalStorage option to opt in to automatically setting session on all transactions #14744 #14742 #14583 #13889 diff --git a/package.json b/package.json index cbd4adc15af..92169d360ca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.8.0", + "version": "7.8.1", "author": "Guillermo Rauch ", "keywords": [ "mongodb",