From 01dd4719d6bc4ab301bd21a2fe3e896b521f4a5e Mon Sep 17 00:00:00 2001 From: 0x0a0d Date: Tue, 16 Jul 2024 06:27:39 +0700 Subject: [PATCH 01/37] Reapply "fix(cast): remove empty conditions after strict applied" This reverts commit 1ca84b334f2ac8e75641eed108f5f17ad1c82f43. --- lib/cast.js | 9 ++++- test/docs/cast.test.js | 74 ++++++++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/lib/cast.js b/lib/cast.js index e7bf5b45a05..1bb16e87c81 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -65,11 +65,18 @@ module.exports = function cast(schema, obj, options, context) { if (!Array.isArray(val)) { throw new CastError('Array', val, path); } - for (let k = 0; k < val.length; ++k) { + for (let k = val.length - 1; k >= 0; k--) { if (val[k] == null || typeof val[k] !== 'object') { throw new CastError('Object', val[k], path + '.' + k); } val[k] = cast(schema, val[k], options, context); + if (Object.keys(val[k]).length === 0) { + val.splice(k, 1); + } + } + + if (val.length === 0) { + delete obj[path]; } } else if (path === '$where') { type = typeof val; diff --git a/test/docs/cast.test.js b/test/docs/cast.test.js index b2b9b4aef78..6184ac05cf1 100644 --- a/test/docs/cast.test.js +++ b/test/docs/cast.test.js @@ -101,40 +101,58 @@ describe('Cast Tutorial', function() { await query.exec(); }); - it('strictQuery true', async function() { - mongoose.deleteModel('Character'); - const schema = new mongoose.Schema({ name: String, age: Number }, { - strictQuery: true + describe('strictQuery', function() { + it('strictQuery true - simple object', async function() { + mongoose.deleteModel('Character'); + const schema = new mongoose.Schema({ name: String, age: Number }, { + strictQuery: true + }); + Character = mongoose.model('Character', schema); + + const query = Character.findOne({ notInSchema: { $lt: 'not a number' } }); + + await query.exec(); + query.getFilter(); // Empty object `{}`, Mongoose removes `notInSchema` + // acquit:ignore:start + assert.deepEqual(query.getFilter(), {}); + // acquit:ignore:end }); - Character = mongoose.model('Character', schema); - const query = Character.findOne({ notInSchema: { $lt: 'not a number' } }); + it('strictQuery true - conditions', async function() { + mongoose.deleteModel('Character'); + const schema = new mongoose.Schema({ name: String, age: Number }, { + strictQuery: true + }); + Character = mongoose.model('Character', schema); - await query.exec(); - query.getFilter(); // Empty object `{}`, Mongoose removes `notInSchema` - // acquit:ignore:start - assert.deepEqual(query.getFilter(), {}); - // acquit:ignore:end - }); + const query = Character.findOne({ $or: [{ notInSchema: { $lt: 'not a number' } }], $and: [{ name: 'abc' }, { age: { $gt: 18 } }, { notInSchema: { $lt: 'not a number' } }] }); - it('strictQuery throw', async function() { - mongoose.deleteModel('Character'); - const schema = new mongoose.Schema({ name: String, age: Number }, { - strictQuery: 'throw' + await query.exec(); + query.getFilter(); // Empty object `{}`, Mongoose removes `notInSchema` + // acquit:ignore:start + assert.deepEqual(query.getFilter(), { $and: [{ name: 'abc' }, { age: { $gt: 18 } }] }); + // acquit:ignore:end }); - Character = mongoose.model('Character', schema); - const query = Character.findOne({ notInSchema: { $lt: 'not a number' } }); - - const err = await query.exec().then(() => null, err => err); - err.name; // 'StrictModeError' - // Path "notInSchema" is not in schema and strictQuery is 'throw'. - err.message; - // acquit:ignore:start - assert.equal(err.name, 'StrictModeError'); - assert.equal(err.message, 'Path "notInSchema" is not in schema and ' + - 'strictQuery is \'throw\'.'); - // acquit:ignore:end + it('strictQuery throw', async function() { + mongoose.deleteModel('Character'); + const schema = new mongoose.Schema({ name: String, age: Number }, { + strictQuery: 'throw' + }); + Character = mongoose.model('Character', schema); + + const query = Character.findOne({ notInSchema: { $lt: 'not a number' } }); + + const err = await query.exec().then(() => null, err => err); + err.name; // 'StrictModeError' + // Path "notInSchema" is not in schema and strictQuery is 'throw'. + err.message; + // acquit:ignore:start + assert.equal(err.name, 'StrictModeError'); + assert.equal(err.message, 'Path "notInSchema" is not in schema and ' + + 'strictQuery is \'throw\'.'); + // acquit:ignore:end + }); }); it('implicit in', async function() { From e5eb2346126097c546d45fb1363ccd52c83f636b Mon Sep 17 00:00:00 2001 From: 0x0a0d Date: Tue, 16 Jul 2024 06:41:24 +0700 Subject: [PATCH 02/37] only remove object if it becomes empty because of casting --- lib/cast.js | 5 ++++- test/docs/cast.test.js | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/cast.js b/lib/cast.js index 1bb16e87c81..690c412a7a7 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -69,12 +69,15 @@ module.exports = function cast(schema, obj, options, context) { if (val[k] == null || typeof val[k] !== 'object') { throw new CastError('Object', val[k], path + '.' + k); } + + const beforeCastKeysLength = Object.keys(val[k]).length; val[k] = cast(schema, val[k], options, context); - if (Object.keys(val[k]).length === 0) { + if (Object.keys(val[k]).length === 0 && beforeCastKeysLength !== 0) { val.splice(k, 1); } } + // delete empty: {$or: []} -> {} if (val.length === 0) { delete obj[path]; } diff --git a/test/docs/cast.test.js b/test/docs/cast.test.js index 6184ac05cf1..21775ad0cb8 100644 --- a/test/docs/cast.test.js +++ b/test/docs/cast.test.js @@ -125,12 +125,16 @@ describe('Cast Tutorial', function() { }); Character = mongoose.model('Character', schema); - const query = Character.findOne({ $or: [{ notInSchema: { $lt: 'not a number' } }], $and: [{ name: 'abc' }, { age: { $gt: 18 } }, { notInSchema: { $lt: 'not a number' } }] }); + const query = Character.findOne({ + $or: [{ notInSchema: { $lt: 'not a number' } }], + $and: [{ name: 'abc' }, { age: { $gt: 18 } }, { notInSchema: { $lt: 'not a number' } }], + $nor: [{}] // should be kept + }); await query.exec(); query.getFilter(); // Empty object `{}`, Mongoose removes `notInSchema` // acquit:ignore:start - assert.deepEqual(query.getFilter(), { $and: [{ name: 'abc' }, { age: { $gt: 18 } }] }); + assert.deepEqual(query.getFilter(), { $and: [{ name: 'abc' }, { age: { $gt: 18 } }], $nor: [{}] }); // acquit:ignore:end }); From 19d694a5dd43a6d7979b60c5f9670e14117c404d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 19 Jul 2024 16:51:57 -0400 Subject: [PATCH 03/37] Update cast.test.js --- test/docs/cast.test.js | 100 ++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 51 deletions(-) diff --git a/test/docs/cast.test.js b/test/docs/cast.test.js index 21775ad0cb8..332c7b9d88d 100644 --- a/test/docs/cast.test.js +++ b/test/docs/cast.test.js @@ -101,62 +101,60 @@ describe('Cast Tutorial', function() { await query.exec(); }); - describe('strictQuery', function() { - it('strictQuery true - simple object', async function() { - mongoose.deleteModel('Character'); - const schema = new mongoose.Schema({ name: String, age: Number }, { - strictQuery: true - }); - Character = mongoose.model('Character', schema); - - const query = Character.findOne({ notInSchema: { $lt: 'not a number' } }); - - await query.exec(); - query.getFilter(); // Empty object `{}`, Mongoose removes `notInSchema` - // acquit:ignore:start - assert.deepEqual(query.getFilter(), {}); - // acquit:ignore:end + it('strictQuery true', async function() { + mongoose.deleteModel('Character'); + const schema = new mongoose.Schema({ name: String, age: Number }, { + strictQuery: true }); + Character = mongoose.model('Character', schema); + + const query = Character.findOne({ notInSchema: { $lt: 'not a number' } }); + + await query.exec(); + query.getFilter(); // Empty object `{}`, Mongoose removes `notInSchema` + // acquit:ignore:start + assert.deepEqual(query.getFilter(), {}); + // acquit:ignore:end + }); + + it('strictQuery throw', async function() { + mongoose.deleteModel('Character'); + const schema = new mongoose.Schema({ name: String, age: Number }, { + strictQuery: 'throw' + }); + Character = mongoose.model('Character', schema); + + const query = Character.findOne({ notInSchema: { $lt: 'not a number' } }); - it('strictQuery true - conditions', async function() { - mongoose.deleteModel('Character'); - const schema = new mongoose.Schema({ name: String, age: Number }, { - strictQuery: true - }); - Character = mongoose.model('Character', schema); - - const query = Character.findOne({ - $or: [{ notInSchema: { $lt: 'not a number' } }], - $and: [{ name: 'abc' }, { age: { $gt: 18 } }, { notInSchema: { $lt: 'not a number' } }], - $nor: [{}] // should be kept - }); - - await query.exec(); - query.getFilter(); // Empty object `{}`, Mongoose removes `notInSchema` - // acquit:ignore:start - assert.deepEqual(query.getFilter(), { $and: [{ name: 'abc' }, { age: { $gt: 18 } }], $nor: [{}] }); - // acquit:ignore:end + const err = await query.exec().then(() => null, err => err); + err.name; // 'StrictModeError' + // Path "notInSchema" is not in schema and strictQuery is 'throw'. + err.message; + // acquit:ignore:start + assert.equal(err.name, 'StrictModeError'); + assert.equal(err.message, 'Path "notInSchema" is not in schema and ' + + 'strictQuery is \'throw\'.'); + // acquit:ignore:end + }); + + it('strictQuery removes casted empty objects', async function() { + mongoose.deleteModel('Character'); + const schema = new mongoose.Schema({ name: String, age: Number }, { + strictQuery: true }); + Character = mongoose.model('Character', schema); - it('strictQuery throw', async function() { - mongoose.deleteModel('Character'); - const schema = new mongoose.Schema({ name: String, age: Number }, { - strictQuery: 'throw' - }); - Character = mongoose.model('Character', schema); - - const query = Character.findOne({ notInSchema: { $lt: 'not a number' } }); - - const err = await query.exec().then(() => null, err => err); - err.name; // 'StrictModeError' - // Path "notInSchema" is not in schema and strictQuery is 'throw'. - err.message; - // acquit:ignore:start - assert.equal(err.name, 'StrictModeError'); - assert.equal(err.message, 'Path "notInSchema" is not in schema and ' + - 'strictQuery is \'throw\'.'); - // acquit:ignore:end + const query = Character.findOne({ + $or: [{ notInSchema: { $lt: 'not a number' } }], + $and: [{ name: 'abc' }, { age: { $gt: 18 } }, { notInSchema: { $lt: 'not a number' } }], + $nor: [{}] // should be kept }); + + await query.exec(); + query.getFilter(); // Empty object `{}`, Mongoose removes `notInSchema` + // acquit:ignore:start + assert.deepEqual(query.getFilter(), { $and: [{ name: 'abc' }, { age: { $gt: 18 } }], $nor: [{}] }); + // acquit:ignore:end }); it('implicit in', async function() { From e3a9e65236b3212128fdafaa9adc6d8b22d81ecc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 19 Jul 2024 16:58:37 -0400 Subject: [PATCH 04/37] Update cast.js --- lib/cast.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cast.js b/lib/cast.js index 690c412a7a7..8eea0e0303e 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -65,7 +65,7 @@ module.exports = function cast(schema, obj, options, context) { if (!Array.isArray(val)) { throw new CastError('Array', val, path); } - for (let k = val.length - 1; k >= 0; k--) { + for (let k = 0; k < val.length; ++k) { if (val[k] == null || typeof val[k] !== 'object') { throw new CastError('Object', val[k], path + '.' + k); } From 630868671d3a2ecc71c96084afd7ebfca583f01c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 19 Jul 2024 17:04:42 -0400 Subject: [PATCH 05/37] style: fix lint --- test/docs/cast.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/docs/cast.test.js b/test/docs/cast.test.js index 332c7b9d88d..b24a9db657d 100644 --- a/test/docs/cast.test.js +++ b/test/docs/cast.test.js @@ -136,7 +136,7 @@ describe('Cast Tutorial', function() { 'strictQuery is \'throw\'.'); // acquit:ignore:end }); - + it('strictQuery removes casted empty objects', async function() { mongoose.deleteModel('Character'); const schema = new mongoose.Schema({ name: String, age: Number }, { From 91612bb9657f97e1c981545c2266ebdca57cc751 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 12 Aug 2024 13:26:38 -0400 Subject: [PATCH 06/37] Revert "Update cast.js" This reverts commit e3a9e65236b3212128fdafaa9adc6d8b22d81ecc. --- lib/cast.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cast.js b/lib/cast.js index 8eea0e0303e..690c412a7a7 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -65,7 +65,7 @@ module.exports = function cast(schema, obj, options, context) { if (!Array.isArray(val)) { throw new CastError('Array', val, path); } - for (let k = 0; k < val.length; ++k) { + for (let k = val.length - 1; k >= 0; k--) { if (val[k] == null || typeof val[k] !== 'object') { throw new CastError('Object', val[k], path + '.' + k); } From f4ee4ae7e68a450aea08bec2ee51957ceaa3ca62 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Wed, 21 Aug 2024 13:34:00 +0200 Subject: [PATCH 07/37] docs(middleware): update some more "remove" related notes 22f0722a217b69ed37caa905400f15a2a1cae889 was only partially updating everything related to "remove" --- docs/middleware.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/middleware.md b/docs/middleware.md index 433e1fb149d..0bd7e83cdc4 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -31,7 +31,6 @@ In document middleware functions, `this` refers to the document. To access the m * [validate](api/document.html#document_Document-validate) * [save](api/model.html#model_Model-save) -* [remove](api/model.html#model_Model-remove) * [updateOne](api/document.html#document_Document-updateOne) * [deleteOne](api/model.html#model_Model-deleteOne) * [init](api/document.html#document_Document-init) (note: init hooks are [synchronous](#synchronous)) @@ -51,7 +50,6 @@ In query middleware functions, `this` refers to the query. * [findOneAndRemove](api/query.html#query_Query-findOneAndRemove) * [findOneAndReplace](api/query.html#query_Query-findOneAndReplace) * [findOneAndUpdate](api/query.html#query_Query-findOneAndUpdate) -* [remove](api/model.html#model_Model-remove) * [replaceOne](api/query.html#query_Query-replaceOne) * [update](api/query.html#query_Query-update) * [updateOne](api/query.html#query_Query-updateOne) @@ -86,7 +84,6 @@ Here are the possible strings that can be passed to `pre()` * findOneAndUpdate * init * insertMany -* remove * replaceOne * save * update @@ -382,11 +379,11 @@ Mongoose has both query and document hooks for `deleteOne()`. ```javascript schema.pre('deleteOne', function() { console.log('Removing!'); }); -// Does **not** print "Removing!". Document middleware for `remove` is not executed by default +// Does **not** print "Removing!". Document middleware for `deleteOne` is not executed by default await doc.deleteOne(); // Prints "Removing!" -Model.remove(); +await Model.deleteOne(); ``` You can pass options to [`Schema.pre()`](api.html#schema_Schema-pre) @@ -400,8 +397,8 @@ schema.pre('deleteOne', { document: true, query: false }, function() { console.log('Deleting doc!'); }); -// Only query middleware. This will get called when you do `Model.remove()` -// but not `doc.remove()`. +// Only query middleware. This will get called when you do `Model.deleteOne()` +// but not `doc.deleteOne()`. schema.pre('deleteOne', { query: true, document: false }, function() { console.log('Deleting!'); }); From 98e4ae968a31be14691b47ffa3edc0326c586862 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Wed, 21 Aug 2024 13:44:23 +0200 Subject: [PATCH 08/37] docs(subdocs): fix invalid header id link --- docs/subdocs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/subdocs.md b/docs/subdocs.md index 2c64ba96d98..0e80970a42d 100644 --- a/docs/subdocs.md +++ b/docs/subdocs.md @@ -38,7 +38,7 @@ doc.child; ```
    -
  • What is a Subdocument?
  • +
  • What is a Subdocument?
  • Subdocuments versus Nested Paths
  • Subdocument Defaults
  • Finding a Subdocument
  • From 0deb2347e685f6a728ba5b0d891d809ec947f257 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 28 Aug 2024 13:36:36 -0400 Subject: [PATCH 09/37] docs: highlight idSetter as a breaking change in changelog re: https://github.com/Automattic/mongoose/commit/32a84b7a65cca90c1355ce6cd218f89a92ce1e4d#commitcomment-145631235 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 978abc41b4f..467527ad57f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -297,7 +297,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 + * BREAKING CHANGE: add `id` setter which allows modifying `_id` by setting `id` (Note this change was originally shipped as a `feat`, but later reverted in Mongoose 8 due to compatibility issues) #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 b6ab66f6bb5cf7864832d2a84832a518e3812304 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 6 Sep 2024 14:34:30 -0400 Subject: [PATCH 10/37] chore: release 6.13.1 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36d481adf2e..dcf3ad66899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +6.13.1 / 2024-09-06 +=================== + * fix: remove empty $and, $or, $not that were made empty by scrict mode #14749 #13086 [0x0a0d](https://github.com/0x0a0d) + 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 bd1ab99dc29..cc8f15d7b29 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.13.0", + "version": "6.13.1", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 2c4b0d5bbb4c82c9f11322262802b4fced7cb220 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 6 Sep 2024 14:42:42 -0400 Subject: [PATCH 11/37] pin @sinonjs/fake-timers version for node 12 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index cc8f15d7b29..3d384ff5e11 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "devDependencies": { "@babel/core": "7.20.12", "@babel/preset-env": "7.20.2", + "@sinonjs/fake-timers": "11.2.2", "@typescript-eslint/eslint-plugin": "5.50.0", "@typescript-eslint/parser": "5.50.0", "acquit": "1.3.0", From fe1705628189f3f805ba6ba875a07fd6613bf0d0 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 6 Sep 2024 15:03:55 -0400 Subject: [PATCH 12/37] try fixing deno test --- test/query.toconstructor.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/query.toconstructor.test.js b/test/query.toconstructor.test.js index b93c1afdecf..be31bf09d38 100644 --- a/test/query.toconstructor.test.js +++ b/test/query.toconstructor.test.js @@ -184,6 +184,7 @@ describe('Query:', function() { }); const Test = db.model('Test', schema); + await Test.deleteMany({}); const test = new Test({ name: 'Romero' }); const Q = Test.findOne({}).toConstructor(); From 91788059603cd6da5ad31d388fc241f233df8c86 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 10 Sep 2024 10:32:25 -0400 Subject: [PATCH 13/37] fix: backport #14870 to 6.x --- lib/document.js | 2 +- test/document.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index e82d2e73b71..1e9cec2e87b 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1224,7 +1224,7 @@ Document.prototype.$set = function $set(path, val, type, options) { this.$__setValue(path, null); cleanModifiedSubpaths(this, path); } else { - return this.$set(val, path, constructing); + return this.$set(val, path, constructing, options); } const keys = getKeysInSchemaOrder(this.$__schema, val, path); diff --git a/test/document.test.js b/test/document.test.js index 2aa0b332e2f..cd3f67ff5df 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -8078,6 +8078,38 @@ describe('document', function() { await person.save(); }); + it('set() merge option with double nested', async function () { + const PersonSchema = new Schema({ + info: { + address: { + city: String, + country: { type: String, default: "UK" }, + postcode: String + }, + } + }); + + const Person = db.model('Person', PersonSchema); + + + const person = new Person({ + info: { + address: { + country: "United States", + city: "New York" + }, + } + }); + + const update = { info: { address: { postcode: "12H" } } }; + + person.set(update, undefined, { merge: true }); + + assert.equal(person.info.address.city, "New York"); + assert.equal(person.info.address.postcode, "12H"); + assert.equal(person.info.address.country, "United States"); + }); + it('setting single nested subdoc with timestamps (gh-8251)', async function() { const ActivitySchema = Schema({ description: String }, { timestamps: true }); const RequestSchema = Schema({ activity: ActivitySchema }); From 39105274123a5827169f6c8e91042db687c902e3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 10 Sep 2024 10:34:02 -0400 Subject: [PATCH 14/37] fix lint --- test/document.test.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/document.test.js b/test/document.test.js index cd3f67ff5df..cf9d19d316c 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -8078,14 +8078,14 @@ describe('document', function() { await person.save(); }); - it('set() merge option with double nested', async function () { + it('set() merge option with double nested', async function() { const PersonSchema = new Schema({ info: { address: { city: String, - country: { type: String, default: "UK" }, + country: { type: String, default: 'UK' }, postcode: String - }, + } } }); @@ -8095,19 +8095,19 @@ describe('document', function() { const person = new Person({ info: { address: { - country: "United States", - city: "New York" - }, + country: 'United States', + city: 'New York' + } } }); - const update = { info: { address: { postcode: "12H" } } }; + const update = { info: { address: { postcode: '12H' } } }; person.set(update, undefined, { merge: true }); - - assert.equal(person.info.address.city, "New York"); - assert.equal(person.info.address.postcode, "12H"); - assert.equal(person.info.address.country, "United States"); + + assert.equal(person.info.address.city, 'New York'); + assert.equal(person.info.address.postcode, '12H'); + assert.equal(person.info.address.country, 'United States'); }); it('setting single nested subdoc with timestamps (gh-8251)', async function() { From becd799e7430ba3ff7be76c3dfdd9330bc1acfa6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 12 Sep 2024 19:25:07 -0400 Subject: [PATCH 15/37] chore: release 6.13.2 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcf3ad66899..4f6cc66bffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +6.13.2 / 2024-09-12 +=================== + * fix(document): make set() respect merge option on deeply nested objects #14870 #14878 + 6.13.1 / 2024-09-06 =================== * fix: remove empty $and, $or, $not that were made empty by scrict mode #14749 #13086 [0x0a0d](https://github.com/0x0a0d) diff --git a/package.json b/package.json index 3d384ff5e11..476d7371620 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.13.1", + "version": "6.13.2", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 762d06362baa729b73966c8866cdc1dc40b7a058 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 16 Sep 2024 14:45:34 -0400 Subject: [PATCH 16/37] fix: make getters convert uuid to string when calling toObject() and toJSON() Fix #14869 --- lib/schema/uuid.js | 27 ++++++++++++--------------- test/model.populate.test.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/lib/schema/uuid.js b/lib/schema/uuid.js index aa72c42107f..0c64ac07eeb 100644 --- a/lib/schema/uuid.js +++ b/lib/schema/uuid.js @@ -26,19 +26,6 @@ function hex2buffer(hex) { return buff; } -/** - * Helper function to convert the buffer input to a string - * @param {Buffer} buf The buffer to convert to a hex-string - * @returns {String} The buffer as a hex-string - * @api private - */ - -function binary2hex(buf) { - // use buffer built-in function to convert from buffer to hex-string - const hex = buf != null && buf.toString('hex'); - return hex; -} - /** * Convert a String to Binary * @param {String} uuidStr The value to process @@ -67,7 +54,7 @@ function binaryToString(uuidBin) { // i(hasezoey) dont quite know why, but "uuidBin" may sometimes also be the already processed string let hex; if (typeof uuidBin !== 'string' && uuidBin != null) { - hex = binary2hex(uuidBin); + hex = uuidBin != null && uuidBin.toString('hex'); const uuidStr = hex.substring(0, 8) + '-' + hex.substring(8, 8 + 4) + '-' + hex.substring(12, 12 + 4) + '-' + hex.substring(16, 16 + 4) + '-' + hex.substring(20, 20 + 12); return uuidStr; } @@ -90,7 +77,17 @@ function SchemaUUID(key, options) { if (value != null && value.$__ != null) { return value; } - return binaryToString(value); + if (Buffer.isBuffer(value)) { + return binaryToString(value); + } else if (value instanceof Binary) { + if (value instanceof Binary) { + return binaryToString(value.buffer); + } + } else if (utils.isPOJO(value) && value.type === 'Buffer' && Array.isArray(value.data)) { + // Cloned buffers look like `{ type: 'Buffer', data: [5, 224, ...] }` + return binaryToString(Buffer.from(value.data)); + } + return value; }); } diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 7f0fe844eb8..be7933882f1 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -11131,4 +11131,41 @@ describe('model: populate:', function() { } assert.equal(posts.length, 2); }); + + it('handles converting uuid documents to strings when calling toObject() (gh-14869)', async function() { + const nodeSchema = new Schema({ _id: { type: 'UUID' }, name: 'String' }); + const rootSchema = new Schema({ + _id: { type: 'UUID' }, + status: 'String', + node: [{ type: 'UUID', ref: 'Child' }] + }); + + const Node = db.model('Child', nodeSchema); + const Root = db.model('Parent', rootSchema); + + const node = new Node({ + _id: '65c7953e-c6e9-4c2f-8328-fe2de7df560d', + name: 'test' + }); + await node.save(); + + const root = new Root({ + _id: '05c7953e-c6e9-4c2f-8328-fe2de7df560d', + status: 'ok', + node: [node._id] + }); + await root.save(); + + const foundRoot = await Root.findById(root._id).populate('node'); + + let doc = foundRoot.toJSON({ getters: true }); + assert.strictEqual(doc._id, '05c7953e-c6e9-4c2f-8328-fe2de7df560d'); + assert.strictEqual(doc.node.length, 1); + assert.strictEqual(doc.node[0]._id, '65c7953e-c6e9-4c2f-8328-fe2de7df560d'); + + doc = foundRoot.toObject({ getters: true }); + assert.strictEqual(doc._id, '05c7953e-c6e9-4c2f-8328-fe2de7df560d'); + assert.strictEqual(doc.node.length, 1); + assert.strictEqual(doc.node[0]._id, '65c7953e-c6e9-4c2f-8328-fe2de7df560d'); + }); }); From 5b5f3d822f49908c4e257ee7350880eba9bf0bb2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 16 Sep 2024 14:48:14 -0400 Subject: [PATCH 17/37] style: fix lint --- lib/schema/uuid.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/schema/uuid.js b/lib/schema/uuid.js index 0c64ac07eeb..cbee764e4ab 100644 --- a/lib/schema/uuid.js +++ b/lib/schema/uuid.js @@ -80,9 +80,9 @@ function SchemaUUID(key, options) { if (Buffer.isBuffer(value)) { return binaryToString(value); } else if (value instanceof Binary) { - if (value instanceof Binary) { - return binaryToString(value.buffer); - } + if (value instanceof Binary) { + return binaryToString(value.buffer); + } } else if (utils.isPOJO(value) && value.type === 'Buffer' && Array.isArray(value.data)) { // Cloned buffers look like `{ type: 'Buffer', data: [5, 224, ...] }` return binaryToString(Buffer.from(value.data)); From 78623bafa5420805f8b8ec2e38e26392193d3110 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 16 Sep 2024 15:04:38 -0400 Subject: [PATCH 18/37] types(document): add generic param to depopulate() to allow updating properties Fix #14876 --- test/types/document.test.ts | 48 +++++++++++++++++++++++++++++++++++++ types/document.d.ts | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/test/types/document.test.ts b/test/types/document.test.ts index 84451edf0f2..cf45b9ce857 100644 --- a/test/types/document.test.ts +++ b/test/types/document.test.ts @@ -359,3 +359,51 @@ function gh13738() { expectType(person.get('dob')); expectType<{ theme: string; alerts: { sms: boolean } }>(person.get('settings')); } + +async function gh14876() { + type CarObjectInterface = { + make: string; + model: string; + year: number; + owner: Types.ObjectId; + }; + const carSchema = new Schema({ + make: { type: String, required: true }, + model: { type: String, required: true }, + year: { type: Number, required: true }, + owner: { type: Schema.Types.ObjectId, ref: 'User' } + }); + + type UserObjectInterface = { + name: string; + age: number; + }; + const userSchema = new Schema({ + name: String, + age: Number + }); + + const Car = model('Car', carSchema); + const User = model('User', userSchema); + + const user = await User.create({ name: 'John', age: 25 }); + const car = await Car.create({ + make: 'Toyota', + model: 'Camry', + year: 2020, + owner: user._id + }); + + const populatedCar = await Car.findById(car._id) + .populate<{ owner: UserObjectInterface }>('owner') + .exec(); + + if (!populatedCar) return; + + console.log(populatedCar.owner.name); // outputs John + + const depopulatedCar = populatedCar.depopulate<{ owner: Types.ObjectId }>('owner'); + + expectType(populatedCar.owner); + expectType(depopulatedCar.owner); +} diff --git a/types/document.d.ts b/types/document.d.ts index 5557269783f..20f5de4c429 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -138,7 +138,7 @@ declare module 'mongoose' { * Takes a populated field and returns it to its unpopulated state. If called with * no arguments, then all populated fields are returned to their unpopulated state. */ - depopulate(path?: string | string[]): this; + depopulate(path?: string | string[]): MergeType; /** * Returns the list of paths that have been directly modified. A direct From 499582a46e8680a9af084e16c5062e786282d6f9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 17 Sep 2024 11:18:46 -0400 Subject: [PATCH 19/37] Update lib/schema/uuid.js Co-authored-by: hasezoey --- lib/schema/uuid.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/schema/uuid.js b/lib/schema/uuid.js index cbee764e4ab..37906e3c4f0 100644 --- a/lib/schema/uuid.js +++ b/lib/schema/uuid.js @@ -80,9 +80,7 @@ function SchemaUUID(key, options) { if (Buffer.isBuffer(value)) { return binaryToString(value); } else if (value instanceof Binary) { - if (value instanceof Binary) { - return binaryToString(value.buffer); - } + return binaryToString(value.buffer); } else if (utils.isPOJO(value) && value.type === 'Buffer' && Array.isArray(value.data)) { // Cloned buffers look like `{ type: 'Buffer', data: [5, 224, ...] }` return binaryToString(Buffer.from(value.data)); From 02162ff62d13a23a2785532cabb5c3fbac2abb26 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 17 Sep 2024 11:18:58 -0400 Subject: [PATCH 20/37] Update lib/schema/uuid.js Co-authored-by: hasezoey --- lib/schema/uuid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/schema/uuid.js b/lib/schema/uuid.js index 37906e3c4f0..1fbfc38654d 100644 --- a/lib/schema/uuid.js +++ b/lib/schema/uuid.js @@ -54,7 +54,7 @@ function binaryToString(uuidBin) { // i(hasezoey) dont quite know why, but "uuidBin" may sometimes also be the already processed string let hex; if (typeof uuidBin !== 'string' && uuidBin != null) { - hex = uuidBin != null && uuidBin.toString('hex'); + hex = uuidBin.toString('hex'); const uuidStr = hex.substring(0, 8) + '-' + hex.substring(8, 8 + 4) + '-' + hex.substring(12, 12 + 4) + '-' + hex.substring(16, 16 + 4) + '-' + hex.substring(20, 20 + 12); return uuidStr; } From 8330f1ee0e0686ef0d3ec9328df1b0dd333b706f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 17 Sep 2024 16:28:22 -0400 Subject: [PATCH 21/37] chore: release 8.6.3 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8082d463a2..fccc71ad231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +8.6.3 / 2024-09-17 +================== + * fix: make getters convert uuid to string when calling toObject() and toJSON() #14890 #14869 + * fix: fix missing Aggregate re-exports for ESM #14886 [wongsean](https://github.com/wongsean) + * types(document): add generic param to depopulate() to allow updating properties #14891 #14876 + 8.6.2 / 2024-09-11 ================== * fix: make set merge deeply nested objects #14870 #14861 [ianHeydoc](https://github.com/ianHeydoc) diff --git a/package.json b/package.json index 48eb3eab36b..5b5fa843259 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "8.6.2", + "version": "8.6.3", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 062016e542bbbf94f5fdaa6bd5d75e309ba83587 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 18 Sep 2024 16:17:48 -0400 Subject: [PATCH 22/37] fix(projection): avoid setting projection to unknown exclusive/inclusive if elemMatch on a Date, ObjectId, etc. Fix #14893 --- lib/helpers/projection/isExclusive.js | 9 ++++++--- test/helpers/projection.isExclusive.test.js | 12 ++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 test/helpers/projection.isExclusive.test.js diff --git a/lib/helpers/projection/isExclusive.js b/lib/helpers/projection/isExclusive.js index b55cf468458..e6ca3cad5ec 100644 --- a/lib/helpers/projection/isExclusive.js +++ b/lib/helpers/projection/isExclusive.js @@ -1,6 +1,7 @@ 'use strict'; const isDefiningProjection = require('./isDefiningProjection'); +const isPOJO = require('../isPOJO'); /*! * ignore @@ -22,10 +23,12 @@ module.exports = function isExclusive(projection) { // Explicitly avoid `$meta` and `$slice` const key = keys[ki]; if (key !== '_id' && isDefiningProjection(projection[key])) { - exclude = (projection[key] != null && typeof projection[key] === 'object') ? - isExclusive(projection[key]) : + exclude = isPOJO(projection[key]) ? + (isExclusive(projection[key]) ?? exclude) : !projection[key]; - break; + if (exclude != null) { + break; + } } } } diff --git a/test/helpers/projection.isExclusive.test.js b/test/helpers/projection.isExclusive.test.js new file mode 100644 index 00000000000..2fc4a16b990 --- /dev/null +++ b/test/helpers/projection.isExclusive.test.js @@ -0,0 +1,12 @@ +'use strict'; + +const assert = require('assert'); + +require('../common'); // required for side-effect setup (so that the default driver is set-up) +const isExclusive = require('../../lib/helpers/projection/isExclusive'); + +describe('isExclusive', function() { + it('handles $elemMatch (gh-14893)', function() { + assert.strictEqual(isExclusive({ field: { $elemMatch: { test: new Date('2024-06-01') } }, otherProp: 1 }), false); + }); +}); From bd4440ae55e0148b4c5274200c76f7f4546cf651 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 18 Sep 2024 16:27:04 -0400 Subject: [PATCH 23/37] fix(projection): also handle value objects in isInclusive --- lib/helpers/projection/isInclusive.js | 3 ++- test/helpers/projection.isInclusive.test.js | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 test/helpers/projection.isInclusive.test.js diff --git a/lib/helpers/projection/isInclusive.js b/lib/helpers/projection/isInclusive.js index eebb412c4a3..c53bac02873 100644 --- a/lib/helpers/projection/isInclusive.js +++ b/lib/helpers/projection/isInclusive.js @@ -1,6 +1,7 @@ 'use strict'; const isDefiningProjection = require('./isDefiningProjection'); +const isPOJO = require('../isPOJO'); /*! * ignore @@ -26,7 +27,7 @@ module.exports = function isInclusive(projection) { // If field is truthy (1, true, etc.) and not an object, then this // projection must be inclusive. If object, assume its $meta, $slice, etc. if (isDefiningProjection(projection[prop]) && !!projection[prop]) { - if (projection[prop] != null && typeof projection[prop] === 'object') { + if (isPOJO(projection[prop])) { return isInclusive(projection[prop]); } else { return !!projection[prop]; diff --git a/test/helpers/projection.isInclusive.test.js b/test/helpers/projection.isInclusive.test.js new file mode 100644 index 00000000000..3bb93635a50 --- /dev/null +++ b/test/helpers/projection.isInclusive.test.js @@ -0,0 +1,12 @@ +'use strict'; + +const assert = require('assert'); + +require('../common'); // required for side-effect setup (so that the default driver is set-up) +const isInclusive = require('../../lib/helpers/projection/isInclusive'); + +describe('isInclusive', function() { + it('handles $elemMatch (gh-14893)', function() { + assert.strictEqual(isInclusive({ field: { $elemMatch: { test: new Date('2024-06-01') } }, otherProp: 1 }), true); + }); +}); From 42ffada151e9aa9e9179f82753e779a77ecc49b0 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:46:57 -0400 Subject: [PATCH 24/37] standardize comment placement --- lib/error/browserMissingSchema.js | 7 ++++--- lib/error/divergentArray.js | 12 +++++++----- lib/error/invalidSchemaOption.js | 12 +++++++----- lib/error/missingSchema.js | 12 +++++++----- lib/error/notFound.js | 10 ++++++---- lib/error/objectExpected.js | 15 ++++++++------- lib/error/objectParameter.js | 20 +++++++++++--------- lib/error/overwriteModel.js | 11 ++++++----- lib/error/parallelSave.js | 15 +++++++++------ lib/error/parallelValidate.js | 14 ++++++++------ lib/error/setOptionError.js | 14 ++++++++------ lib/error/strict.js | 20 +++++++++++--------- lib/error/strictPopulate.js | 18 ++++++++++-------- lib/error/validation.js | 16 +++++++++------- lib/error/validator.js | 15 ++++++++------- lib/error/version.js | 18 ++++++++++-------- 16 files changed, 129 insertions(+), 100 deletions(-) diff --git a/lib/error/browserMissingSchema.js b/lib/error/browserMissingSchema.js index 608cfd983e4..ffeffc77257 100644 --- a/lib/error/browserMissingSchema.js +++ b/lib/error/browserMissingSchema.js @@ -6,11 +6,12 @@ const MongooseError = require('./mongooseError'); +/** + * MissingSchema Error constructor. + */ class MissingSchemaError extends MongooseError { - /** - * MissingSchema Error constructor. - */ + constructor() { super('Schema hasn\'t been registered for document.\n' + 'Use mongoose.Document(name, schema)'); diff --git a/lib/error/divergentArray.js b/lib/error/divergentArray.js index f266dbde449..bc3f1816264 100644 --- a/lib/error/divergentArray.js +++ b/lib/error/divergentArray.js @@ -7,12 +7,14 @@ const MongooseError = require('./mongooseError'); +/** + * DivergentArrayError constructor. + * @param {Array} paths + * @api private + */ + class DivergentArrayError extends MongooseError { - /** - * DivergentArrayError constructor. - * @param {Array} paths - * @api private - */ + constructor(paths) { const msg = 'For your own good, using `document.save()` to update an array ' + 'which was selected using an $elemMatch projection OR ' diff --git a/lib/error/invalidSchemaOption.js b/lib/error/invalidSchemaOption.js index 089dc6a03ef..9e7e4ff4f17 100644 --- a/lib/error/invalidSchemaOption.js +++ b/lib/error/invalidSchemaOption.js @@ -7,12 +7,14 @@ const MongooseError = require('./mongooseError'); +/** + * InvalidSchemaOption Error constructor. + * @param {String} name + * @api private + */ + class InvalidSchemaOptionError extends MongooseError { - /** - * InvalidSchemaOption Error constructor. - * @param {String} name - * @api private - */ + constructor(name, option) { const msg = `Cannot create use schema for property "${name}" because the schema has the ${option} option enabled.`; super(msg); diff --git a/lib/error/missingSchema.js b/lib/error/missingSchema.js index 2b3bf242526..790f7853848 100644 --- a/lib/error/missingSchema.js +++ b/lib/error/missingSchema.js @@ -7,12 +7,14 @@ const MongooseError = require('./mongooseError'); +/** + * MissingSchema Error constructor. + * @param {String} name + * @api private + */ + class MissingSchemaError extends MongooseError { - /** - * MissingSchema Error constructor. - * @param {String} name - * @api private - */ + constructor(name) { const msg = 'Schema hasn\'t been registered for model "' + name + '".\n' + 'Use mongoose.model(name, schema)'; diff --git a/lib/error/notFound.js b/lib/error/notFound.js index 19a22f3a101..87fdd8bc649 100644 --- a/lib/error/notFound.js +++ b/lib/error/notFound.js @@ -7,11 +7,13 @@ const MongooseError = require('./mongooseError'); const util = require('util'); +/** + * OverwriteModel Error constructor. + * @api private + */ + class DocumentNotFoundError extends MongooseError { - /** - * OverwriteModel Error constructor. - * @api private - */ + constructor(filter, model, numAffected, result) { let msg; const messages = MongooseError.messages; diff --git a/lib/error/objectExpected.js b/lib/error/objectExpected.js index 9f7a8116618..bd89ffc77e1 100644 --- a/lib/error/objectExpected.js +++ b/lib/error/objectExpected.js @@ -6,15 +6,16 @@ const MongooseError = require('./mongooseError'); +/** + * Strict mode error constructor + * + * @param {string} type + * @param {string} value + * @api private + */ class ObjectExpectedError extends MongooseError { - /** - * Strict mode error constructor - * - * @param {string} type - * @param {string} value - * @api private - */ + constructor(path, val) { const typeDescription = Array.isArray(val) ? 'array' : 'primitive value'; super('Tried to set nested object field `' + path + diff --git a/lib/error/objectParameter.js b/lib/error/objectParameter.js index b3f5b80849d..0a2108e5c9b 100644 --- a/lib/error/objectParameter.js +++ b/lib/error/objectParameter.js @@ -6,16 +6,18 @@ const MongooseError = require('./mongooseError'); +/** + * Constructor for errors that happen when a parameter that's expected to be + * an object isn't an object + * + * @param {Any} value + * @param {String} paramName + * @param {String} fnName + * @api private + */ + class ObjectParameterError extends MongooseError { - /** - * Constructor for errors that happen when a parameter that's expected to be - * an object isn't an object - * - * @param {Any} value - * @param {String} paramName - * @param {String} fnName - * @api private - */ + constructor(value, paramName, fnName) { super('Parameter "' + paramName + '" to ' + fnName + '() must be an object, got "' + value.toString() + '" (type ' + typeof value + ')'); diff --git a/lib/error/overwriteModel.js b/lib/error/overwriteModel.js index 8904e4e74b3..ef828f91731 100644 --- a/lib/error/overwriteModel.js +++ b/lib/error/overwriteModel.js @@ -7,13 +7,14 @@ const MongooseError = require('./mongooseError'); +/** + * OverwriteModel Error constructor. + * @param {String} name + * @api private + */ class OverwriteModelError extends MongooseError { - /** - * OverwriteModel Error constructor. - * @param {String} name - * @api private - */ + constructor(name) { super('Cannot overwrite `' + name + '` model once compiled.'); } diff --git a/lib/error/parallelSave.js b/lib/error/parallelSave.js index 25e12481d49..fd554fa3bbc 100644 --- a/lib/error/parallelSave.js +++ b/lib/error/parallelSave.js @@ -6,13 +6,16 @@ const MongooseError = require('./mongooseError'); + +/** + * ParallelSave Error constructor. + * + * @param {Document} doc + * @api private + */ + class ParallelSaveError extends MongooseError { - /** - * ParallelSave Error constructor. - * - * @param {Document} doc - * @api private - */ + constructor(doc) { const msg = 'Can\'t save() the same doc multiple times in parallel. Document: '; super(msg + doc._doc._id); diff --git a/lib/error/parallelValidate.js b/lib/error/parallelValidate.js index 84b7940d6df..d70e296e869 100644 --- a/lib/error/parallelValidate.js +++ b/lib/error/parallelValidate.js @@ -7,13 +7,15 @@ const MongooseError = require('./mongooseError'); +/** + * ParallelValidate Error constructor. + * + * @param {Document} doc + * @api private + */ + class ParallelValidateError extends MongooseError { - /** - * ParallelValidate Error constructor. - * - * @param {Document} doc - * @api private - */ + constructor(doc) { const msg = 'Can\'t validate() the same doc multiple times in parallel. Document: '; super(msg + doc._doc._id); diff --git a/lib/error/setOptionError.js b/lib/error/setOptionError.js index b38a0d30244..369096fd306 100644 --- a/lib/error/setOptionError.js +++ b/lib/error/setOptionError.js @@ -8,13 +8,15 @@ const MongooseError = require('./mongooseError'); const util = require('util'); const combinePathErrors = require('../helpers/error/combinePathErrors'); +/** + * Mongoose.set Error + * + * @api private + * @inherits MongooseError + */ + class SetOptionError extends MongooseError { - /** - * Mongoose.set Error - * - * @api private - * @inherits MongooseError - */ + constructor() { super(''); diff --git a/lib/error/strict.js b/lib/error/strict.js index 6cf4cf91141..eda7d9ae6f5 100644 --- a/lib/error/strict.js +++ b/lib/error/strict.js @@ -6,17 +6,19 @@ const MongooseError = require('./mongooseError'); +/** + * Strict mode error constructor + * + * @param {String} path + * @param {String} [msg] + * @param {Boolean} [immutable] + * @inherits MongooseError + * @api private + */ + class StrictModeError extends MongooseError { - /** - * Strict mode error constructor - * - * @param {String} path - * @param {String} [msg] - * @param {Boolean} [immutable] - * @inherits MongooseError - * @api private - */ + constructor(path, msg, immutable) { msg = msg || 'Field `' + path + '` is not in schema and strict ' + 'mode is set to throw.'; diff --git a/lib/error/strictPopulate.js b/lib/error/strictPopulate.js index 288799897bc..d554d71271d 100644 --- a/lib/error/strictPopulate.js +++ b/lib/error/strictPopulate.js @@ -6,15 +6,17 @@ const MongooseError = require('./mongooseError'); +/** + * Strict mode error constructor + * + * @param {String} path + * @param {String} [msg] + * @inherits MongooseError + * @api private + */ + class StrictPopulateError extends MongooseError { - /** - * Strict mode error constructor - * - * @param {String} path - * @param {String} [msg] - * @inherits MongooseError - * @api private - */ + constructor(path, msg) { msg = msg || 'Cannot populate path `' + path + '` because it is not in your schema. ' + 'Set the `strictPopulate` option to false to override.'; super(msg); diff --git a/lib/error/validation.js b/lib/error/validation.js index 5e222e980f9..faa4ea799aa 100644 --- a/lib/error/validation.js +++ b/lib/error/validation.js @@ -9,14 +9,16 @@ const getConstructorName = require('../helpers/getConstructorName'); const util = require('util'); const combinePathErrors = require('../helpers/error/combinePathErrors'); +/** + * Document Validation Error + * + * @api private + * @param {Document} [instance] + * @inherits MongooseError + */ + class ValidationError extends MongooseError { - /** - * Document Validation Error - * - * @api private - * @param {Document} [instance] - * @inherits MongooseError - */ + constructor(instance) { let _message; if (getConstructorName(instance) === 'model') { diff --git a/lib/error/validator.js b/lib/error/validator.js index f7ee2ef4761..38f98f0087d 100644 --- a/lib/error/validator.js +++ b/lib/error/validator.js @@ -6,15 +6,16 @@ const MongooseError = require('./mongooseError'); +/** + * Schema validator error + * + * @param {Object} properties + * @param {Document} doc + * @api private + */ class ValidatorError extends MongooseError { - /** - * Schema validator error - * - * @param {Object} properties - * @param {Document} doc - * @api private - */ + constructor(properties, doc) { let msg = properties.message; if (!msg) { diff --git a/lib/error/version.js b/lib/error/version.js index 6bc2b5d3af5..4eb8054cdfb 100644 --- a/lib/error/version.js +++ b/lib/error/version.js @@ -6,15 +6,17 @@ const MongooseError = require('./mongooseError'); +/** + * Version Error constructor. + * + * @param {Document} doc + * @param {Number} currentVersion + * @param {Array} modifiedPaths + * @api private + */ + class VersionError extends MongooseError { - /** - * Version Error constructor. - * - * @param {Document} doc - * @param {Number} currentVersion - * @param {Array} modifiedPaths - * @api private - */ + constructor(doc, currentVersion, modifiedPaths) { const modifiedPathsStr = modifiedPaths.join(', '); super('No matching document found for id "' + doc._doc._id + From cd930c74d5cc40e7fd6caf990857e2f1071b8a93 Mon Sep 17 00:00:00 2001 From: dragontaek-lee Date: Sun, 22 Sep 2024 15:34:26 +0900 Subject: [PATCH 25/37] fix(model): skip applying static hooks by default if static name conflicts with aggregate middleware --- lib/constants.js | 10 ++++++++++ lib/helpers/model/applyStaticHooks.js | 9 +++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/constants.js b/lib/constants.js index 83a66832b55..f5f5c5a19b3 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -34,3 +34,13 @@ const queryMiddlewareFunctions = queryOperations.concat([ ]); exports.queryMiddlewareFunctions = queryMiddlewareFunctions; + +/*! + * ignore + */ + +const aggregateMiddlewareFunctions = [ + 'aggregate' +]; + +exports.aggregateMiddlewareFunctions = aggregateMiddlewareFunctions; diff --git a/lib/helpers/model/applyStaticHooks.js b/lib/helpers/model/applyStaticHooks.js index 957e94f2288..dc88bff1747 100644 --- a/lib/helpers/model/applyStaticHooks.js +++ b/lib/helpers/model/applyStaticHooks.js @@ -1,7 +1,12 @@ 'use strict'; -const middlewareFunctions = require('../../constants').queryMiddlewareFunctions; const promiseOrCallback = require('../promiseOrCallback'); +const { queryMiddlewareFunctions, aggregateMiddlewareFunctions } = require('../../constants'); + +const middlewareFunctions = [ + ...queryMiddlewareFunctions, + ...aggregateMiddlewareFunctions +]; module.exports = function applyStaticHooks(model, hooks, statics) { const kareemOptions = { @@ -10,7 +15,7 @@ module.exports = function applyStaticHooks(model, hooks, statics) { }; hooks = hooks.filter(hook => { - // If the custom static overwrites an existing query middleware, don't apply + // If the custom static overwrites an existing middleware, don't apply // middleware to it by default. This avoids a potential backwards breaking // change with plugins like `mongoose-delete` that use statics to overwrite // built-in Mongoose functions. From c2caa0f464b97a4354e9820227e9ceffafcb5b74 Mon Sep 17 00:00:00 2001 From: dragontaek-lee Date: Sun, 22 Sep 2024 15:37:48 +0900 Subject: [PATCH 26/37] fix(model): add test to skip applying static hooks by default when static name conflicts with aggregate middleware --- test/model.test.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/model.test.js b/test/model.test.js index 8e94a876694..c2e3f68c1e8 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -10,6 +10,7 @@ const assert = require('assert'); const { once } = require('events'); const random = require('./util').random; const util = require('./util'); +const model = require('../lib/model'); const mongoose = start.mongoose; const Schema = mongoose.Schema; @@ -5861,6 +5862,35 @@ describe('Model', function() { }); + it('custom statics that overwrite aggregate functions dont get hooks by default (gh-14903)', async function() { + + const schema = new Schema({ name: String }); + + schema.statics.aggregate = function(pipeline) { + return model.aggregate.apply(this, [pipeline]); + }; + + let called = 0; + schema.pre('aggregate', function(next) { + ++called; + next(); + }); + const Model = db.model('Test', schema); + + await Model.create({ name: 'foo' }); + + const res = await Model.aggregate([ + { + $match: { + name: 'foo' + } + } + ]); + + assert.ok(res[0].name); + assert.equal(called, 1); + }); + it('error handling middleware passes saved doc (gh-7832)', async function() { const schema = new Schema({ _id: Number }); From cf768dc73ba01485bd154bbbdc8fd54c370726cf Mon Sep 17 00:00:00 2001 From: dragontaek-lee Date: Sun, 22 Sep 2024 16:12:42 +0900 Subject: [PATCH 27/37] fix: add clarifying comment --- lib/helpers/model/applyStaticHooks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/model/applyStaticHooks.js b/lib/helpers/model/applyStaticHooks.js index dc88bff1747..34611c3b39c 100644 --- a/lib/helpers/model/applyStaticHooks.js +++ b/lib/helpers/model/applyStaticHooks.js @@ -15,7 +15,7 @@ module.exports = function applyStaticHooks(model, hooks, statics) { }; hooks = hooks.filter(hook => { - // If the custom static overwrites an existing middleware, don't apply + // If the custom static overwrites an existing query/aggregate middleware, don't apply // middleware to it by default. This avoids a potential backwards breaking // change with plugins like `mongoose-delete` that use statics to overwrite // built-in Mongoose functions. From 8655c6c8ac72e3acb083a6539f50f6c538b22fe6 Mon Sep 17 00:00:00 2001 From: dragontaek-lee Date: Tue, 24 Sep 2024 22:28:35 +0900 Subject: [PATCH 28/37] fix(model): skip applying static hooks by default if static name conflicts with model,document middleware --- lib/constants.js | 27 +++++++++++++++++++++++++++ lib/helpers/model/applyStaticHooks.js | 18 +++++++++++------- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/lib/constants.js b/lib/constants.js index f5f5c5a19b3..3a03bd502fc 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -44,3 +44,30 @@ const aggregateMiddlewareFunctions = [ ]; exports.aggregateMiddlewareFunctions = aggregateMiddlewareFunctions; + +/*! + * ignore + */ + +const modelMiddlewareFunctions = [ + 'bulkWrite', + 'createCollection', + 'insertMany' +]; + +exports.modelMiddlewareFunctions = modelMiddlewareFunctions; + +/*! + * ignore + */ + +const documentMiddlewareFunctions = [ + 'validate', + 'save', + 'remove', + 'updateOne', + 'deleteOne', + 'init' +]; + +exports.documentMiddlewareFunctions = documentMiddlewareFunctions; diff --git a/lib/helpers/model/applyStaticHooks.js b/lib/helpers/model/applyStaticHooks.js index 34611c3b39c..8e2c33a3bf9 100644 --- a/lib/helpers/model/applyStaticHooks.js +++ b/lib/helpers/model/applyStaticHooks.js @@ -1,11 +1,15 @@ 'use strict'; const promiseOrCallback = require('../promiseOrCallback'); -const { queryMiddlewareFunctions, aggregateMiddlewareFunctions } = require('../../constants'); +const { queryMiddlewareFunctions, aggregateMiddlewareFunctions, modelMiddlewareFunctions, documentMiddlewareFunctions } = require('../../constants'); const middlewareFunctions = [ - ...queryMiddlewareFunctions, - ...aggregateMiddlewareFunctions + ...[ + ...queryMiddlewareFunctions, + ...aggregateMiddlewareFunctions, + ...modelMiddlewareFunctions, + ...documentMiddlewareFunctions + ].reduce((s, hook) => s.add(hook), new Set()) ]; module.exports = function applyStaticHooks(model, hooks, statics) { @@ -14,8 +18,11 @@ module.exports = function applyStaticHooks(model, hooks, statics) { numCallbackParams: 1 }; + model.$__insertMany = hooks.createWrapper('insertMany', + model.$__insertMany, model, kareemOptions); + hooks = hooks.filter(hook => { - // If the custom static overwrites an existing query/aggregate middleware, don't apply + // If the custom static overwrites an existing middleware, don't apply // middleware to it by default. This avoids a potential backwards breaking // change with plugins like `mongoose-delete` that use statics to overwrite // built-in Mongoose functions. @@ -25,9 +32,6 @@ module.exports = function applyStaticHooks(model, hooks, statics) { return hook.model !== false; }); - model.$__insertMany = hooks.createWrapper('insertMany', - model.$__insertMany, model, kareemOptions); - for (const key of Object.keys(statics)) { if (hooks.hasHooks(key)) { const original = model[key]; From c647a051c5dcf61d0a3123dc28502afd39331fd1 Mon Sep 17 00:00:00 2001 From: dragontaek-lee Date: Tue, 24 Sep 2024 22:29:46 +0900 Subject: [PATCH 29/37] fix(model): skip applying static hooks by default if static name conflicts with model,document middleware --- test/model.test.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/test/model.test.js b/test/model.test.js index c2e3f68c1e8..f162ea8f005 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -5891,6 +5891,54 @@ describe('Model', function() { assert.equal(called, 1); }); + it('custom statics that overwrite model functions dont get hooks by default', async function() { + + const schema = new Schema({ name: String }); + + schema.statics.insertMany = function(docs) { + return model.insertMany.apply(this, [docs]); + }; + + let called = 0; + schema.pre('insertMany', function(next) { + ++called; + next(); + }); + const Model = db.model('Test', schema); + + const res = await Model.insertMany([ + { name: 'foo' }, + { name: 'boo' } + ]); + + assert.ok(res[0].name); + assert.ok(res[1].name); + assert.equal(called, 1); + }); + + it('custom statics that overwrite document functions dont get hooks by default', async function() { + + const schema = new Schema({ name: String }); + + schema.statics.save = async function() { + return 'foo'; + }; + + let called = 0; + schema.pre('save', function(next) { + ++called; + next(); + }); + + const Model = db.model('Test', schema); + + const doc = await Model.save(); + + assert.ok(doc); + assert.equal(doc, 'foo'); + assert.equal(called, 0); + }); + it('error handling middleware passes saved doc (gh-7832)', async function() { const schema = new Schema({ _id: Number }); From a06debe2363490bb140001a570ced20a4a0227e8 Mon Sep 17 00:00:00 2001 From: dragontaek-lee Date: Tue, 24 Sep 2024 23:46:34 +0900 Subject: [PATCH 30/37] fix: remove redundant async --- test/model.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/model.test.js b/test/model.test.js index f162ea8f005..cca70e32fd8 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -5920,7 +5920,7 @@ describe('Model', function() { const schema = new Schema({ name: String }); - schema.statics.save = async function() { + schema.statics.save = function() { return 'foo'; }; From 328ddaacced5ac0ecf4fb10e054a8609d9d6c4e9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 24 Sep 2024 15:49:14 -0400 Subject: [PATCH 31/37] fix(document): avoid massive perf degradation when saving new doc with 10 level deep subdocs Fix #14897 --- lib/document.js | 4 +++- test/document.test.js | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index 64e65df8494..c12e0f03fb6 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2724,7 +2724,9 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) { } if (doc.$isModified(fullPathToSubdoc, null, modifiedPaths) && - !doc.isDirectModified(fullPathToSubdoc) && + // Avoid using isDirectModified() here because that does additional checks on whether the parent path + // is direct modified, which can cause performance issues re: gh-14897 + !doc.$__.activePaths.getStatePaths('modify').hasOwnProperty(fullPathToSubdoc) && !doc.$isDefault(fullPathToSubdoc)) { paths.add(fullPathToSubdoc); diff --git a/test/document.test.js b/test/document.test.js index 6a5765fe116..7150ffe64b4 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -13905,6 +13905,27 @@ describe('document', function() { const objectWithGetters = result.toObject({ getters: true, virtuals: false }); assert.strictEqual(objectWithGetters.level1.level2.level3.property, 'TESTVALUE'); }); + + it('handles inserting and saving large document with 10-level deep subdocs (gh-14897)', async function() { + const levels = 10; + + let schema = new Schema({ test: { type: String, required: true } }); + let doc = { test: 'gh-14897' }; + for (let i = 0; i < levels; ++i) { + schema = new Schema({ level: Number, subdocs: [schema] }); + doc = { level: (levels - i), subdocs: [{ ...doc }, { ...doc }] }; + } + + const Test = db.model('Test', schema); + const savedDoc = await Test.create(doc); + + let cur = savedDoc; + for (let i = 0; i < levels - 1; ++i) { + cur = cur.subdocs[0]; + } + cur.subdocs[0] = { test: 'updated' }; + await savedDoc.save(); + }); }); describe('Check if instance function that is supplied in schema option is available', function() { From 04263295351f32e2b0c08d3ce79c9cd36fdf98ae Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 24 Sep 2024 16:00:48 -0400 Subject: [PATCH 32/37] perf: add createDeepNestedDocArray benchmark re: #14897 --- benchmarks/createDeepNestedDocArray.js | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 benchmarks/createDeepNestedDocArray.js diff --git a/benchmarks/createDeepNestedDocArray.js b/benchmarks/createDeepNestedDocArray.js new file mode 100644 index 00000000000..0f3ac6d4a7b --- /dev/null +++ b/benchmarks/createDeepNestedDocArray.js @@ -0,0 +1,37 @@ +'use strict'; + +const mongoose = require('../'); + +run().catch(err => { + console.error(err); + process.exit(-1); +}); + +async function run() { + await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_benchmark'); + + const levels = 12; + + let schema = new mongoose.Schema({ test: { type: String, required: true } }); + let doc = { test: 'gh-14897' }; + for (let i = 0; i < levels; ++i) { + schema = new mongoose.Schema({ level: Number, subdocs: [schema] }); + doc = { level: (levels - i), subdocs: [{ ...doc }, { ...doc }] }; + } + const Test = mongoose.model('Test', schema); + + if (!process.env.MONGOOSE_BENCHMARK_SKIP_SETUP) { + await Test.deleteMany({}); + } + + const insertStart = Date.now(); + await Test.create(doc); + const insertEnd = Date.now(); + + const results = { + 'create() time ms': +(insertEnd - insertStart).toFixed(2) + }; + + console.log(JSON.stringify(results, null, ' ')); + process.exit(0); +} \ No newline at end of file From 5851261c1501fc58d3722513f708a1e87da63f4e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 24 Sep 2024 17:30:33 -0400 Subject: [PATCH 33/37] perf(document): avoid unnecessarily pulling all subdocs when validating a subdoc --- lib/document.js | 66 ++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/lib/document.js b/lib/document.js index c12e0f03fb6..f17e0800ff7 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2689,7 +2689,7 @@ function _evaluateRequiredFunctions(doc) { * ignore */ -function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) { +function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate) { const doValidateOptions = {}; _evaluateRequiredFunctions(doc); @@ -2709,37 +2709,40 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) { Object.keys(doc.$__.activePaths.getStatePaths('default')).forEach(addToPaths); function addToPaths(p) { paths.add(p); } - const subdocs = doc.$getAllSubdocs(); - const modifiedPaths = doc.modifiedPaths(); - for (const subdoc of subdocs) { - if (subdoc.$basePath) { - const fullPathToSubdoc = subdoc.$isSingleNested ? subdoc.$__pathRelativeToParent() : subdoc.$__fullPathWithIndexes(); - - // Remove child paths for now, because we'll be validating the whole - // subdoc. - // The following is a faster take on looping through every path in `paths` - // and checking if the path starts with `fullPathToSubdoc` re: gh-13191 - for (const modifiedPath of subdoc.modifiedPaths()) { - paths.delete(fullPathToSubdoc + '.' + modifiedPath); - } + if (!isNestedValidate) { + // If we're validating a subdocument, all this logic will run anyway on the top-level document, so skip for subdocuments + const subdocs = doc.$getAllSubdocs(); + const modifiedPaths = doc.modifiedPaths(); + for (const subdoc of subdocs) { + if (subdoc.$basePath) { + const fullPathToSubdoc = subdoc.$isSingleNested ? subdoc.$__pathRelativeToParent() : subdoc.$__fullPathWithIndexes(); + + // Remove child paths for now, because we'll be validating the whole + // subdoc. + // The following is a faster take on looping through every path in `paths` + // and checking if the path starts with `fullPathToSubdoc` re: gh-13191 + for (const modifiedPath of subdoc.modifiedPaths()) { + paths.delete(fullPathToSubdoc + '.' + modifiedPath); + } - if (doc.$isModified(fullPathToSubdoc, null, modifiedPaths) && - // Avoid using isDirectModified() here because that does additional checks on whether the parent path - // is direct modified, which can cause performance issues re: gh-14897 - !doc.$__.activePaths.getStatePaths('modify').hasOwnProperty(fullPathToSubdoc) && - !doc.$isDefault(fullPathToSubdoc)) { - paths.add(fullPathToSubdoc); + if (doc.$isModified(fullPathToSubdoc, null, modifiedPaths) && + // Avoid using isDirectModified() here because that does additional checks on whether the parent path + // is direct modified, which can cause performance issues re: gh-14897 + !doc.$__.activePaths.getStatePaths('modify').hasOwnProperty(fullPathToSubdoc) && + !doc.$isDefault(fullPathToSubdoc)) { + paths.add(fullPathToSubdoc); - if (doc.$__.pathsToScopes == null) { - doc.$__.pathsToScopes = {}; - } - doc.$__.pathsToScopes[fullPathToSubdoc] = subdoc.$isDocumentArrayElement ? - subdoc.__parentArray : - subdoc.$parent(); + if (doc.$__.pathsToScopes == null) { + doc.$__.pathsToScopes = {}; + } + doc.$__.pathsToScopes[fullPathToSubdoc] = subdoc.$isDocumentArrayElement ? + subdoc.__parentArray : + subdoc.$parent(); - doValidateOptions[fullPathToSubdoc] = { skipSchemaValidators: true }; - if (subdoc.$isDocumentArrayElement && subdoc.__index != null) { - doValidateOptions[fullPathToSubdoc].index = subdoc.__index; + doValidateOptions[fullPathToSubdoc] = { skipSchemaValidators: true }; + if (subdoc.$isDocumentArrayElement && subdoc.__index != null) { + doValidateOptions[fullPathToSubdoc].index = subdoc.__index; + } } } } @@ -2974,7 +2977,7 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { paths = [...paths]; doValidateOptionsByPath = {}; } else { - const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip); + const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip, options && options._nestedValidate); paths = shouldValidateModifiedOnly ? pathDetails[0].filter((path) => this.$isModified(path)) : pathDetails[0]; @@ -3061,7 +3064,8 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { const doValidateOptions = { ...doValidateOptionsByPath[path], path: path, - validateAllPaths + validateAllPaths, + _nestedValidate: true }; schemaType.doValidate(val, function(err) { From 1f7c742c31e9763ca4ffa65bbd799d46aba81d57 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 25 Sep 2024 10:36:33 -0400 Subject: [PATCH 34/37] Update applyStaticHooks.js --- lib/helpers/model/applyStaticHooks.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/helpers/model/applyStaticHooks.js b/lib/helpers/model/applyStaticHooks.js index 8e2c33a3bf9..3d0e1297160 100644 --- a/lib/helpers/model/applyStaticHooks.js +++ b/lib/helpers/model/applyStaticHooks.js @@ -3,14 +3,14 @@ const promiseOrCallback = require('../promiseOrCallback'); const { queryMiddlewareFunctions, aggregateMiddlewareFunctions, modelMiddlewareFunctions, documentMiddlewareFunctions } = require('../../constants'); -const middlewareFunctions = [ - ...[ +const middlewareFunctions = Array.from( + new Set([ ...queryMiddlewareFunctions, ...aggregateMiddlewareFunctions, ...modelMiddlewareFunctions, ...documentMiddlewareFunctions - ].reduce((s, hook) => s.add(hook), new Set()) -]; + ]) +); module.exports = function applyStaticHooks(model, hooks, statics) { const kareemOptions = { From 95500b606765147cb92751b092d69463df55d40a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 25 Sep 2024 12:23:59 -0400 Subject: [PATCH 35/37] perf(document): remove unnecessary reset logic Re: #14897 Re: #10295 --- lib/document.js | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/lib/document.js b/lib/document.js index f17e0800ff7..8fe85a5a143 100644 --- a/lib/document.js +++ b/lib/document.js @@ -3484,44 +3484,9 @@ Document.prototype.$__reset = function reset() { // Skip for subdocuments const subdocs = !this.$isSubdocument ? this.$getAllSubdocs() : null; if (subdocs && subdocs.length > 0) { - const resetArrays = new Set(); for (const subdoc of subdocs) { - const fullPathWithIndexes = subdoc.$__fullPathWithIndexes(); subdoc.$__reset(); - if (this.isModified(fullPathWithIndexes) || isParentInit(fullPathWithIndexes)) { - if (subdoc.$isDocumentArrayElement) { - resetArrays.add(subdoc.parentArray()); - } else { - const parent = subdoc.$parent(); - if (parent === this) { - this.$__.activePaths.clearPath(subdoc.$basePath); - } else if (parent != null && parent.$isSubdocument) { - // If map path underneath subdocument, may end up with a case where - // map path is modified but parent still needs to be reset. See gh-10295 - parent.$__reset(); - } - } - } } - - for (const array of resetArrays) { - this.$__.activePaths.clearPath(array.$path()); - array[arrayAtomicsBackupSymbol] = array[arrayAtomicsSymbol]; - array[arrayAtomicsSymbol] = {}; - } - } - - function isParentInit(path) { - path = path.indexOf('.') === -1 ? [path] : path.split('.'); - let cur = ''; - for (let i = 0; i < path.length; ++i) { - cur += (cur.length ? '.' : '') + path[i]; - if (_this.$__.activePaths[cur] === 'init') { - return true; - } - } - - return false; } // clear atomics From 8522f39f1cac3cb3f14aa82023e93f710966a0f1 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 25 Sep 2024 12:26:49 -0400 Subject: [PATCH 36/37] style: fix lint --- lib/helpers/model/applyStaticHooks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/model/applyStaticHooks.js b/lib/helpers/model/applyStaticHooks.js index 3d0e1297160..40116462f26 100644 --- a/lib/helpers/model/applyStaticHooks.js +++ b/lib/helpers/model/applyStaticHooks.js @@ -9,7 +9,7 @@ const middlewareFunctions = Array.from( ...aggregateMiddlewareFunctions, ...modelMiddlewareFunctions, ...documentMiddlewareFunctions - ]) + ]) ); module.exports = function applyStaticHooks(model, hooks, statics) { From 3ede837b6c2446d9be89ec83f54ce269c5b813cb Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 25 Sep 2024 14:01:42 -0400 Subject: [PATCH 37/37] chore: release 7.8.2 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c03b7a48d8..7dba26f3222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +7.8.2 / 2024-09-25 +================== + * fix(projection): avoid setting projection to unknown exclusive/inclusive if elemMatch on a Date, ObjectId, etc. #14894 #14893 + 6.13.2 / 2024-09-12 =================== * fix(document): make set() respect merge option on deeply nested objects #14870 #14878 diff --git a/package.json b/package.json index 92169d360ca..8535736c6b1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.8.1", + "version": "7.8.2", "author": "Guillermo Rauch ", "keywords": [ "mongodb",