From fc65900d5ebcbf20b884d216b49e685e9f9e2b99 Mon Sep 17 00:00:00 2001 From: Franco Sanguineti <44387191+sanguineti@users.noreply.github.com> Date: Mon, 2 Oct 2023 12:38:15 -0300 Subject: [PATCH 001/191] chore(deps): upgrade mongodb driver to v5.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 42979b01dd6..22412addc3f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "bson": "^5.4.0", "kareem": "2.5.1", - "mongodb": "5.8.1", + "mongodb": "5.9.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", From 582273235562c9465357b848213a1d6a7c2b5364 Mon Sep 17 00:00:00 2001 From: Ronan Jouchet Date: Thu, 5 Oct 2023 14:18:59 -0400 Subject: [PATCH 002/191] 6.x populate.md: fix edit whoopsie scrapping a line a setting half of the document in an unclosed code tag See https://mongoosejs.com/docs/6.x/docs/populate.html#dynamic-ref --- docs/populate.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/populate.md b/docs/populate.md index a850383688a..ddd9f4f0367 100644 --- a/docs/populate.md +++ b/docs/populate.md @@ -487,7 +487,8 @@ const events = await Event. ```

Dynamic References via refPath

- + +Mongoose can also populate from multiple collections based on the value of a property in the document. Let's say you're building a schema for storing comments. A user may comment on either a blog post or a product. From cb668b148f2b5d76a88104c4ca22d8353c6c3fbc Mon Sep 17 00:00:00 2001 From: Gaston Casini Date: Sun, 30 Jul 2023 19:18:58 -0300 Subject: [PATCH 003/191] fix: document.isModified support for list of keys as a string --- lib/document.js | 2 +- test/document.modified.test.js | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index 07f0f962bc2..cee384fcf7c 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2227,7 +2227,7 @@ Document.prototype.isModified = function(paths, modifiedPaths) { } if (typeof paths === 'string') { - paths = [paths]; + paths = paths.indexOf(' ') === -1 ? [paths] : paths.split(' '); } for (const path of paths) { diff --git a/test/document.modified.test.js b/test/document.modified.test.js index 3a0ce0bfb35..72d9f172e5a 100644 --- a/test/document.modified.test.js +++ b/test/document.modified.test.js @@ -176,6 +176,23 @@ describe('document modified', function() { assert.equal(post.isModified('title'), false); }); + it('should support passing a string of keys separated by a blank space as the first argument', function() { + const post = new BlogPost(); + post.init({ + title: 'Test', + slug: 'test', + date: new Date() + }); + + assert.equal(post.isModified('title'), false); + post.set('title', 'modified title'); + assert.equal(post.isModified('title'), true); + assert.equal(post.isModified('slug'), false); + assert.equal(post.isModified('title slug'), true); + }); + + + describe('on DocumentArray', function() { it('work', function() { const post = new BlogPost(); From 0ae97d17a525b3cc43cbbd1c1364bd0e56bc0d1f Mon Sep 17 00:00:00 2001 From: k-chop Date: Fri, 6 Oct 2023 15:00:51 +0900 Subject: [PATCH 004/191] format --- test/document.modified.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/document.modified.test.js b/test/document.modified.test.js index 72d9f172e5a..4518b2746cc 100644 --- a/test/document.modified.test.js +++ b/test/document.modified.test.js @@ -192,7 +192,6 @@ describe('document modified', function() { }); - describe('on DocumentArray', function() { it('work', function() { const post = new BlogPost(); From ea85361818a326ed39ddaac90a44853b57f56f2b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 1 Oct 2023 11:45:52 -0400 Subject: [PATCH 005/191] fix(mongoose): correctly handle global applyPluginsToChildSchemas option Fix #13887 --- lib/index.js | 4 +++- test/index.test.js | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index d071cd9e4bb..94b8082ff38 100644 --- a/lib/index.js +++ b/lib/index.js @@ -718,7 +718,9 @@ Mongoose.prototype._applyPlugins = function(schema, options) { options = options || {}; options.applyPluginsToDiscriminators = _mongoose.options && _mongoose.options.applyPluginsToDiscriminators || false; - options.applyPluginsToChildSchemas = typeof (_mongoose.options && _mongoose.options.applyPluginsToDiscriminators) === 'boolean' ? _mongoose.options.applyPluginsToDiscriminators : true; + options.applyPluginsToChildSchemas = typeof (_mongoose.options && _mongoose.options.applyPluginsToChildSchemas) === 'boolean' ? + _mongoose.options.applyPluginsToChildSchemas : + true; applyPlugins(schema, _mongoose.plugins, options, '$globalPluginsApplied'); }; diff --git a/test/index.test.js b/test/index.test.js index 1d4583a2531..585142d196c 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -430,6 +430,25 @@ describe('mongoose module:', function() { return Promise.resolve(); }); + it('global plugins with applyPluginsToChildSchemas (gh-13887)', function() { + const m = new Mongoose(); + m.set('applyPluginsToChildSchemas', false); + + const called = []; + m.plugin(function(s) { + called.push(s); + }); + + const schema = new m.Schema({ + subdoc: new m.Schema({ name: String }), + arr: [new m.Schema({ name: String })] + }); + + m.model('Test', schema); + assert.equal(called.length, 1); + assert.ok(called.indexOf(schema) !== -1); + }); + it('global plugins recompile schemas (gh-7572)', function() { function helloPlugin(schema) { schema.virtual('greeting').get(() => 'hello'); From b721f54d694bb9d6af06cda25f0dce34a05aa1a2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 6 Oct 2023 16:21:23 -0400 Subject: [PATCH 006/191] chore: release 7.6.0 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 894dfb515e7..26ed929c06e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +7.6.0 / 2023-10-06 +================== + * feat: upgrade mongodb node driver -> 5.9.0 #13927 #13926 [sanguineti](https://github.com/sanguineti) + * fix: avoid CastError when passing different value of discriminator key in `$or` #13938 #13906 + 7.5.4 / 2023-10-04 ================== * fix: avoid stripping out `id` property when `_id` is set #13933 #13892 #13867 diff --git a/package.json b/package.json index da284e1ef51..210ed93912a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.5.4", + "version": "7.6.0", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 872c4be3ed3ee55eb1679b2497af8147ed22763f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 6 Oct 2023 17:51:09 -0400 Subject: [PATCH 007/191] fix(schema): avoid creating unnecessary subpaths for array elements to avoid `subpaths` growing without bound Re: #13874 --- lib/schema.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index b608961b753..821f5b7ca6d 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1017,7 +1017,7 @@ Schema.prototype.path = function(path, obj) { // subpaths? return /\.\d+\.?.*$/.test(path) - ? getPositionalPath(this, path) + ? getPositionalPath(this, path, cleanPath) : undefined; } @@ -1634,7 +1634,7 @@ Schema.prototype.pathType = function(path) { } if (/\.\d+\.|\.\d+$/.test(path)) { - return getPositionalPathType(this, path); + return getPositionalPathType(this, path, cleanPath); } return 'adhocOrUndefined'; }; @@ -1678,7 +1678,7 @@ Schema.prototype.setupTimestamp = function(timestamps) { * @api private */ -function getPositionalPathType(self, path) { +function getPositionalPathType(self, path, cleanPath) { const subpaths = path.split(/\.(\d+)\.|\.(\d+)$/).filter(Boolean); if (subpaths.length < 2) { return self.paths.hasOwnProperty(subpaths[0]) ? @@ -1729,7 +1729,7 @@ function getPositionalPathType(self, path) { val = val.schema.path(subpath); } - self.subpaths[path] = val; + self.subpaths[cleanPath] = val; if (val) { return 'real'; } @@ -1744,9 +1744,9 @@ function getPositionalPathType(self, path) { * ignore */ -function getPositionalPath(self, path) { - getPositionalPathType(self, path); - return self.subpaths[path]; +function getPositionalPath(self, path, cleanPath) { + getPositionalPathType(self, path, cleanPath); + return self.subpaths[cleanPath]; } /** @@ -2638,6 +2638,9 @@ Schema.prototype._getSchema = function(path) { // Re: gh-5628, because `schema.path()` doesn't take $ into account. parts[i] = '0'; } + if (/^\d+$/.test(parts[i])) { + parts[i] = '$'; + } } return search(parts, _this); }; From 12b82cd1788d3fcba4bba6a0a22f59a3dbd6a4c5 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sat, 7 Oct 2023 15:21:41 +0200 Subject: [PATCH 008/191] chore(npmignore): ignore newer files npmignore has not been updated for some files --- .npmignore | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.npmignore b/.npmignore index 5dc3e0cdabf..0196665f03d 100644 --- a/.npmignore +++ b/.npmignore @@ -47,4 +47,12 @@ webpack.base.config.js notes.md list.out -eslintrc.json \ No newline at end of file +# config files +lgtm.yml +.mocharc.yml +.eslintrc.js +.markdownlint-cli2.cjs + +# scripts +scripts/ +tools/ From 11821f7d3f97e4ecd819e37a7c375c586f1a8eba Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sat, 7 Oct 2023 15:27:18 +0200 Subject: [PATCH 009/191] chore: bump bson to match mongodb@5.9.0 exactly --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 210ed93912a..ab7c12064ad 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ ], "license": "MIT", "dependencies": { - "bson": "^5.4.0", + "bson": "^5.5.0", "kareem": "2.5.1", "mongodb": "5.9.0", "mpath": "0.9.0", From 6401884bfc862fcf5ea1b58c36d2adde03aa1e67 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sat, 7 Oct 2023 15:37:39 +0200 Subject: [PATCH 010/191] chore: move mocha config from package.json to mocharc --- .mocharc.yml | 4 ++++ package.json | 8 -------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.mocharc.yml b/.mocharc.yml index efa9bf8a87b..4ba8d185851 100644 --- a/.mocharc.yml +++ b/.mocharc.yml @@ -2,3 +2,7 @@ reporter: spec # better to identify failing / slow tests than "dot" ui: bdd # explicitly setting, even though it is mocha default require: - test/mocha-fixtures.js +extension: + - test.js +watch-files: + - test/**/*.js diff --git a/package.json b/package.json index 210ed93912a..b9620622d6e 100644 --- a/package.json +++ b/package.json @@ -130,14 +130,6 @@ }, "homepage": "https://mongoosejs.com", "browser": "./dist/browser.umd.js", - "mocha": { - "extension": [ - "test.js" - ], - "watch-files": [ - "test/**/*.js" - ] - }, "config": { "mongodbMemoryServer": { "disablePostinstall": true From b87eaf0e853ea53f47ee1d593fccc92f0106f395 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sat, 7 Oct 2023 15:46:06 +0200 Subject: [PATCH 011/191] chore(dev-deps): remove bluebird --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 210ed93912a..8b728f08759 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "axios": "1.1.3", "babel-loader": "8.2.5", "benchmark": "2.1.4", - "bluebird": "3.7.2", "broken-link-checker": "^0.7.8", "buffer": "^5.6.0", "cheerio": "1.0.0-rc.12", From 4a2e3f5c4d9682eac9fbe57de660343bfa7727b0 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sat, 7 Oct 2023 15:50:16 +0200 Subject: [PATCH 012/191] chore(npmignore): ignore "tsconfig.json" --- .npmignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.npmignore b/.npmignore index 0196665f03d..7ad7dde6864 100644 --- a/.npmignore +++ b/.npmignore @@ -52,6 +52,7 @@ lgtm.yml .mocharc.yml .eslintrc.js .markdownlint-cli2.cjs +tsconfig.json # scripts scripts/ From 75dc4a154533d25b56a0262d0482db344fa1e118 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 8 Oct 2023 16:06:53 -0400 Subject: [PATCH 013/191] test: add test case for #13874 --- test/document.test.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/document.test.js b/test/document.test.js index f3f557350dd..2e298708d50 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12520,6 +12520,36 @@ describe('document', function() { await doc.save(); assert.strictEqual(attachmentSchemaPreValidateCalls, 1); }); + + it('avoids creating separate subpaths entry for every element in array (gh-13874)', async function() { + const tradeSchema = new mongoose.Schema({ tradeId: Number, content: String }); + + const testSchema = new mongoose.Schema( + { + userId: Number, + tradeMap: { + type: Map, + of: tradeSchema + } + } + ); + + const TestModel = db.model('Test', testSchema); + + + const userId = 100; + const user = await TestModel.create({ userId, tradeMap: new Map() }); + + // add subDoc + for (let id = 1; id <= 10; id++) { + const trade = { tradeId: id, content: 'test' }; + user.tradeMap.set(trade.tradeId.toString(), trade); + } + await user.save(); + await TestModel.deleteOne({ userId }); + + assert.equal(Object.keys(TestModel.schema.subpaths).length, 3); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() { From 73950d0e438a931ea5d1a86c76e1e1933259e0ef Mon Sep 17 00:00:00 2001 From: Simon Tretter Date: Mon, 9 Oct 2023 09:59:22 +0200 Subject: [PATCH 014/191] fix: raw result deprecation message according to https://mongoosejs.com/docs/deprecations.html#rawresult it mus tbe true, not false. it would also make more sense tbh --- lib/query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/query.js b/lib/query.js index 7a3d00fe699..30f22255d95 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1680,7 +1680,7 @@ Query.prototype.setOptions = function(options, overwrite) { const printRawResultDeprecationWarning = util.deprecate( function printRawResultDeprecationWarning() {}, - 'The `rawResult` option for Mongoose queries is deprecated. Use `includeResultMetadata: false` as a replacement for `rawResult: true`.' + 'The `rawResult` option for Mongoose queries is deprecated. Use `includeResultMetadata: true` as a replacement for `rawResult: true`.' ); /*! From b8aa2092c9552c02fe5f5d3efb085c0853343f1c Mon Sep 17 00:00:00 2001 From: Simon Tretter Date: Mon, 9 Oct 2023 10:46:31 +0200 Subject: [PATCH 015/191] Update models.d.ts --- types/models.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/types/models.d.ts b/types/models.d.ts index a3e315ad05b..e74754f0710 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -32,7 +32,9 @@ declare module 'mongoose' { PopulateOption, SessionOption { limit?: number; + // @deprecated, use includeResultMetadata instead rawResult?: boolean; + includeResultMetadata?: boolean; ordered?: boolean; lean?: boolean; throwOnValidationError?: boolean; From d2fec37c122eeb5ef43f86801560875790f7fc5a Mon Sep 17 00:00:00 2001 From: Simon Tretter Date: Mon, 9 Oct 2023 10:48:15 +0200 Subject: [PATCH 016/191] Update query.d.ts --- types/query.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/types/query.d.ts b/types/query.d.ts index 3963481a20b..1d66e75221c 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -124,9 +124,14 @@ declare module 'mongoose' { overwriteDiscriminatorKey?: boolean; projection?: ProjectionType; /** + * @deprecated use includeResultMetadata instead. * if true, returns the raw result from the MongoDB driver */ rawResult?: boolean; + /** + * if ture, includes meta data for the result from the MongoDB driver + */ + includeResultMetadata?: boolean; readPreference?: string | mongodb.ReadPreferenceMode; /** * An alias for the `new` option. `returnOriginal: false` is equivalent to `new: true`. From 914b4b41996a3e5adc501761c838ca8b1dc08230 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 9 Oct 2023 13:54:18 -0400 Subject: [PATCH 017/191] fix(schema): handle embedded discriminators defined using `Schema.prototype.discriminator()` Fix #13898 --- lib/schema.js | 13 --------- lib/schema/SubdocumentPath.js | 6 +++++ lib/schema/documentarray.js | 6 +++++ test/document.test.js | 50 +++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 13 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index b608961b753..e8fc8f6828f 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -723,19 +723,6 @@ Schema.prototype.add = function add(obj, prefix) { for (const key in val[0].discriminators) { schemaType.discriminator(key, val[0].discriminators[key]); } - } else if (val[0] != null && val[0].instanceOfSchema && val[0]._applyDiscriminators instanceof Map) { - const applyDiscriminators = val[0]._applyDiscriminators; - const schemaType = this.path(prefix + key); - for (const disc of applyDiscriminators.keys()) { - schemaType.discriminator(disc, applyDiscriminators.get(disc)); - } - } - else if (val != null && val.instanceOfSchema && val._applyDiscriminators instanceof Map) { - const applyDiscriminators = val._applyDiscriminators; - const schemaType = this.path(prefix + key); - for (const disc of applyDiscriminators.keys()) { - schemaType.discriminator(disc, applyDiscriminators.get(disc)); - } } } else if (Object.keys(val).length < 1) { // Special-case: {} always interpreted as Mixed path so leaf at this node diff --git a/lib/schema/SubdocumentPath.js b/lib/schema/SubdocumentPath.js index 2f8e32d5ace..17f8a5aa79b 100644 --- a/lib/schema/SubdocumentPath.js +++ b/lib/schema/SubdocumentPath.js @@ -55,6 +55,12 @@ function SubdocumentPath(schema, path, options) { this.$isSingleNested = true; this.base = schema.base; SchemaType.call(this, path, options, 'Embedded'); + + if (schema._applyDiscriminators != null) { + for (const disc of schema._applyDiscriminators.keys()) { + this.discriminator(disc, schema._applyDiscriminators.get(disc)); + } + } } /*! diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js index 7f8a39a04d6..a15a1ca2e79 100644 --- a/lib/schema/documentarray.js +++ b/lib/schema/documentarray.js @@ -88,6 +88,12 @@ function DocumentArrayPath(key, schema, options, schemaOptions) { this.$embeddedSchemaType.caster = this.Constructor; this.$embeddedSchemaType.schema = this.schema; + + if (schema._applyDiscriminators != null) { + for (const disc of schema._applyDiscriminators.keys()) { + this.discriminator(disc, schema._applyDiscriminators.get(disc)); + } + } } /** diff --git a/test/document.test.js b/test/document.test.js index f3f557350dd..a074a9d9ee2 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12520,6 +12520,56 @@ describe('document', function() { await doc.save(); assert.strictEqual(attachmentSchemaPreValidateCalls, 1); }); + + it('handles embedded discriminators defined using Schema.prototype.discriminator (gh-13898)', async function() { + const baseNestedDiscriminated = new Schema({ + type: { type: Number, required: true } + }, { discriminatorKey: 'type' }); + + class BaseClass { + whoAmI() { + return 'I am baseNestedDiscriminated'; + } + } + BaseClass.type = 1; + + baseNestedDiscriminated.loadClass(BaseClass); + + class NumberTyped extends BaseClass { + whoAmI() { + return 'I am NumberTyped'; + } + } + NumberTyped.type = 3; + + class StringTyped extends BaseClass { + whoAmI() { + return 'I am StringTyped'; + } + } + StringTyped.type = 4; + + baseNestedDiscriminated.discriminator(1, new Schema({}).loadClass(NumberTyped)); + baseNestedDiscriminated.discriminator('3', new Schema({}).loadClass(StringTyped)); + + const containsNestedSchema = new Schema({ + nestedDiscriminatedTypes: { type: [baseNestedDiscriminated], required: true } + }); + + class ContainsNested { + whoAmI() { + return 'I am ContainsNested'; + } + } + containsNestedSchema.loadClass(ContainsNested); + + const Test = db.model('Test', containsNestedSchema); + const instance = await Test.create({ type: 1, nestedDiscriminatedTypes: [{ type: 1 }, { type: '3' }] }); + assert.deepStrictEqual( + instance.nestedDiscriminatedTypes.map(i => i.whoAmI()), + ['I am NumberTyped', 'I am StringTyped'] + ); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() { From 71e6ad468946ffecf333201a12d974820ca954d2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 9 Oct 2023 14:29:38 -0400 Subject: [PATCH 018/191] fix(model): make bulkSave() save changes in discriminator paths if calling bulkSave() on base model Fix #13907 --- lib/model.js | 7 +++++++ test/model.test.js | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/model.js b/lib/model.js index 436f9710831..bbd5f8a770d 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3780,6 +3780,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op } setDefaultOptions(); + const discriminatorKey = this.schema.options.discriminatorKey; const writeOperations = documents.reduce((accumulator, document, i) => { if (!options.skipValidation) { @@ -3810,6 +3811,12 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op _applyCustomWhere(document, where); + // Set the discriminator key, so bulk write casting knows which + // schema to use re: gh-13907 + if (document[discriminatorKey] != null && !(discriminatorKey in where)) { + where[discriminatorKey] = document[discriminatorKey]; + } + document.$__version(where, delta); const writeOperation = { updateOne: { filter: where, update: changes } }; utils.injectTimestampsOption(writeOperation.updateOne, options.timestamps); diff --git a/test/model.test.js b/test/model.test.js index 9e08437fd77..d7e6bc0aa01 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -6239,6 +6239,27 @@ describe('Model', function() { assert.equal(writeOperations.length, 3); }); + it('saves changes in discriminators if calling `bulkSave()` on base model (gh-13907)', async() => { + const schema = new mongoose.Schema( + { value: String }, + { discriminatorKey: 'type' } + ); + const typeASchema = new mongoose.Schema({ aValue: String }); + schema.discriminator('A', typeASchema); + + const TestModel = db.model('Test', schema); + const testData = { value: 'initValue', type: 'A', aValue: 'initAValue' }; + const doc = await TestModel.create(testData); + + doc.value = 'updatedValue1'; + doc.aValue = 'updatedValue2'; + await TestModel.bulkSave([doc]); + + const findDoc = await TestModel.findById(doc._id); + assert.strictEqual(findDoc.value, 'updatedValue1'); + assert.strictEqual(findDoc.aValue, 'updatedValue2'); + }); + it('accepts `timestamps: false` (gh-12059)', async() => { // Arrange const userSchema = new Schema({ From 098c64405ca48d95e9b4cc2fc0cf6180accde6ff Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 9 Oct 2023 15:23:47 -0400 Subject: [PATCH 019/191] types(schematypes): allow defining map path using `type: 'Map'` in addition to `type: Map` Fix #13755 --- test/types/maps.test.ts | 16 ++++++++++++++++ test/types/schemaTypeOptions.test.ts | 2 +- types/schematypes.d.ts | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/test/types/maps.test.ts b/test/types/maps.test.ts index 36020a33bd2..b0a51e15219 100644 --- a/test/types/maps.test.ts +++ b/test/types/maps.test.ts @@ -71,3 +71,19 @@ function gh10872(): void { doc.toJSON().map1.foo; } + +function gh13755() { + class Test { + instance: Map; + constructor() { + this.instance = new Map(); + } + } + + const testSchema = new Schema({ + instance: { + type: 'Map', + of: 'Mixed' + } + }); +} diff --git a/test/types/schemaTypeOptions.test.ts b/test/types/schemaTypeOptions.test.ts index 5fc0e23a21d..6a73d38ea30 100644 --- a/test/types/schemaTypeOptions.test.ts +++ b/test/types/schemaTypeOptions.test.ts @@ -22,7 +22,7 @@ expectType(new SchemaTypeOptions() expectType(new SchemaTypeOptions().type); expectType(new SchemaTypeOptions().type); expectType(new SchemaTypeOptions().type); -expectType | undefined>(new SchemaTypeOptions>().type); +expectType | SchemaDefinition<'Map'> | undefined>(new SchemaTypeOptions>().type); expectType | undefined>(new SchemaTypeOptions().type); expectType(new SchemaTypeOptions().type); expectType | AnyArray> | undefined>(new SchemaTypeOptions().type); diff --git a/types/schematypes.d.ts b/types/schematypes.d.ts index 088bc27c598..64fa5e4bd12 100644 --- a/types/schematypes.d.ts +++ b/types/schematypes.d.ts @@ -45,7 +45,7 @@ declare module 'mongoose' { T extends number ? NumberSchemaDefinition : T extends boolean ? BooleanSchemaDefinition : T extends NativeDate ? DateSchemaDefinition : - T extends Map ? SchemaDefinition : + T extends Map ? SchemaDefinition | SchemaDefinition<'Map'> : T extends Buffer ? SchemaDefinition : T extends Types.ObjectId ? ObjectIdSchemaDefinition : T extends Types.ObjectId[] ? AnyArray | AnyArray> : From 5d7ec67b755eefb3a54539959c46f8a6c065bf98 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 9 Oct 2023 16:07:36 -0400 Subject: [PATCH 020/191] refactor: address code review comments by moving regexp to constant --- lib/schema.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/schema.js b/lib/schema.js index 821f5b7ca6d..48aa0d8b3be 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -37,6 +37,8 @@ const isPOJO = utils.isPOJO; let id = 0; +const numberRE = /^\d+$/; + /** * Schema constructor. * @@ -2638,7 +2640,7 @@ Schema.prototype._getSchema = function(path) { // Re: gh-5628, because `schema.path()` doesn't take $ into account. parts[i] = '0'; } - if (/^\d+$/.test(parts[i])) { + if (numberRE.test(parts[i])) { parts[i] = '$'; } } From 1f6449576aac47dcb43f9974bb187a4f13d413d3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 9 Oct 2023 16:31:38 -0400 Subject: [PATCH 021/191] chore: release 7.6.1 --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ed929c06e..576efcbe660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +7.6.1 / 2023-10-09 +================== + * fix: bump bson to match mongodb@5.9.0 exactly #13947 [hasezoey](https://github.com/hasezoey) + * fix: raw result deprecation message #13954 [simllll](https://github.com/simllll) + * type: add types for includeResultMetadata #13955 [simllll](https://github.com/simllll) + * perf(npmignore): ignore newer files #13946 [hasezoey](https://github.com/hasezoey) + * perf: move mocha config from package.json to mocharc #13948 [hasezoey](https://github.com/hasezoey) + 7.6.0 / 2023-10-06 ================== * feat: upgrade mongodb node driver -> 5.9.0 #13927 #13926 [sanguineti](https://github.com/sanguineti) diff --git a/package.json b/package.json index bb1342f1121..d4ecbbcece4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.0", + "version": "7.6.1", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From c4416a60ce596be64829619e48f8fabadfdab0f6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 10 Oct 2023 14:53:36 -0400 Subject: [PATCH 022/191] types(schematypes): allow defining map path using `type: 'Map'` Fix #13755 --- test/types/maps.test.ts | 18 ++++++++---------- test/types/schemaTypeOptions.test.ts | 2 +- types/inferschematype.d.ts | 2 +- types/schematypes.d.ts | 2 +- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/test/types/maps.test.ts b/test/types/maps.test.ts index b0a51e15219..496a7072830 100644 --- a/test/types/maps.test.ts +++ b/test/types/maps.test.ts @@ -1,4 +1,5 @@ import { Schema, model, Document, Model, Types } from 'mongoose'; +import { expectType } from 'tsd'; interface ITest { map1: Map, @@ -73,17 +74,14 @@ function gh10872(): void { } function gh13755() { - class Test { - instance: Map; - constructor() { - this.instance = new Map(); - } - } - - const testSchema = new Schema({ + const testSchema = new Schema({ instance: { type: 'Map', - of: 'Mixed' + of: String } - }); + } as const); + + const TestModel = model('Test', testSchema); + const doc = new TestModel(); + expectType | undefined>(doc.instance); } diff --git a/test/types/schemaTypeOptions.test.ts b/test/types/schemaTypeOptions.test.ts index 6a73d38ea30..5fc0e23a21d 100644 --- a/test/types/schemaTypeOptions.test.ts +++ b/test/types/schemaTypeOptions.test.ts @@ -22,7 +22,7 @@ expectType(new SchemaTypeOptions() expectType(new SchemaTypeOptions().type); expectType(new SchemaTypeOptions().type); expectType(new SchemaTypeOptions().type); -expectType | SchemaDefinition<'Map'> | undefined>(new SchemaTypeOptions>().type); +expectType | undefined>(new SchemaTypeOptions>().type); expectType | undefined>(new SchemaTypeOptions().type); expectType(new SchemaTypeOptions().type); expectType | AnyArray> | undefined>(new SchemaTypeOptions().type); diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 75240ed1675..77ec76ca4c6 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -219,7 +219,7 @@ type ResolvePathType extends true ? Buffer : - PathValueType extends MapConstructor ? Map> : + PathValueType extends MapConstructor | 'Map' ? Map> : IfEquals extends true ? Map> : PathValueType extends ArrayConstructor ? any[] : PathValueType extends typeof Schema.Types.Mixed ? any: diff --git a/types/schematypes.d.ts b/types/schematypes.d.ts index 64fa5e4bd12..088bc27c598 100644 --- a/types/schematypes.d.ts +++ b/types/schematypes.d.ts @@ -45,7 +45,7 @@ declare module 'mongoose' { T extends number ? NumberSchemaDefinition : T extends boolean ? BooleanSchemaDefinition : T extends NativeDate ? DateSchemaDefinition : - T extends Map ? SchemaDefinition | SchemaDefinition<'Map'> : + T extends Map ? SchemaDefinition : T extends Buffer ? SchemaDefinition : T extends Types.ObjectId ? ObjectIdSchemaDefinition : T extends Types.ObjectId[] ? AnyArray | AnyArray> : From 82f2ca8b39d18cb4d48c6a924ac6f8803ca1b7da Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 10 Oct 2023 14:57:09 -0400 Subject: [PATCH 023/191] style: fix lint --- test/document.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/document.test.js b/test/document.test.js index a03b0cda96d..3ff896a9f79 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12550,7 +12550,7 @@ describe('document', function() { assert.equal(Object.keys(TestModel.schema.subpaths).length, 3); }); - + it('handles embedded discriminators defined using Schema.prototype.discriminator (gh-13898)', async function() { const baseNestedDiscriminated = new Schema({ type: { type: Number, required: true } From 430f7ad82224236f0cfc97e329fee5722863f70c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 10 Oct 2023 15:25:01 -0400 Subject: [PATCH 024/191] fix(document): allow calling `$model()` with no args for TypeScript Fix #13878 --- lib/model.js | 33 +++++++++++++++++++-------------- test/document.test.js | 8 ++++++++ test/types/document.test.ts | 10 ++++++++++ types/document.d.ts | 1 + 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/lib/model.js b/lib/model.js index bbd5f8a770d..f53b0ae6229 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1060,40 +1060,45 @@ Model.prototype.$__deleteOne = function $__deleteOne(options, cb) { }; /** - * Returns another Model instance. + * Returns the model instance used to create this document if no `name` specified. + * If `name` specified, returns the model with the given `name`. * * #### Example: * - * const doc = new Tank; - * await doc.model('User').findById(id); + * const doc = new Tank({}); + * doc.$model() === Tank; // true + * await doc.$model('User').findById(id); * - * @param {String} name model name - * @method model + * @param {String} [name] model name + * @method $model * @api public * @return {Model} */ -Model.prototype.model = function model(name) { +Model.prototype.$model = function $model(name) { + if (arguments.length === 0) { + return this.constructor; + } return this[modelDbSymbol].model(name); }; /** - * Returns another Model instance. + * Returns the model instance used to create this document if no `name` specified. + * If `name` specified, returns the model with the given `name`. * * #### Example: * - * const doc = new Tank; - * await doc.model('User').findById(id); + * const doc = new Tank({}); + * doc.$model() === Tank; // true + * await doc.$model('User').findById(id); * - * @param {String} name model name - * @method $model + * @param {String} [name] model name + * @method model * @api public * @return {Model} */ -Model.prototype.$model = function $model(name) { - return this[modelDbSymbol].model(name); -}; +Model.prototype.model = Model.prototype.$model; /** * Returns a document with `_id` only if at least one document exists in the database that matches diff --git a/test/document.test.js b/test/document.test.js index 3ff896a9f79..ae5994b8c3d 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12521,6 +12521,14 @@ describe('document', function() { assert.strictEqual(attachmentSchemaPreValidateCalls, 1); }); + it('returns constructor if using $model() with no args (gh-13878)', async function() { + const testSchema = new Schema({ name: String }); + const Test = db.model('Test', testSchema); + + const doc = new Test(); + assert.strictEqual(doc.$model(), Test); + }); + it('avoids creating separate subpaths entry for every element in array (gh-13874)', async function() { const tradeSchema = new mongoose.Schema({ tradeId: Number, content: String }); diff --git a/test/types/document.test.ts b/test/types/document.test.ts index f4fb56fc359..df66030a5cd 100644 --- a/test/types/document.test.ts +++ b/test/types/document.test.ts @@ -294,6 +294,16 @@ function gh12290() { user.isDirectModified('name'); } +function gh13878() { + const schema = new Schema({ + name: String, + age: Number + }); + const User = model('User', schema); + const user = new User({ name: 'John', age: 30 }); + expectType(user.$model()); +} + function gh13094() { type UserDocumentNever = HydratedDocument<{ name: string }, Record>; diff --git a/types/document.d.ts b/types/document.d.ts index 6a3a857db2d..c646b9e8aaf 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -75,6 +75,7 @@ declare module 'mongoose' { /** Returns the model with the given name on this document's associated connection. */ $model>(name: string): ModelType; + $model>(): ModelType; /** * A string containing the current operation that Mongoose is executing From 7a88a6ac219e698debcb2ee77252d6aff5876cea Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 10 Oct 2023 16:50:47 -0400 Subject: [PATCH 025/191] types(models): add cleaner type definitions for `insertMany()` with no generics to prevent errors when using `insertMany()` in generic classes Fix #13957 --- test/types/models.test.ts | 24 ++++++++++++++++++++++++ types/models.d.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index fdeef76a164..347d08d5321 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -709,3 +709,27 @@ async function gh13746() { expectType(findOneAndUpdateRes.lastErrorObject?.upserted); expectType(findOneAndUpdateRes.ok); } + +function gh13957() { + class RepositoryBase { + protected model: mongoose.Model; + + constructor(schemaModel: mongoose.Model) { + this.model = schemaModel; + } + + // Testing that the following compiles successfully + async insertMany(elems: T[]): Promise { + elems = await this.model.insertMany(elems); + return elems; + } + } + + interface ITest { + name?: string + } + const schema = new Schema({ name: String }); + const TestModel = model('Test', schema); + const repository = new RepositoryBase(TestModel); + expectType>(repository.insertMany([{ name: 'test' }])); +} diff --git a/types/models.d.ts b/types/models.d.ts index e74754f0710..3dd591994ab 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -362,6 +362,34 @@ declare module 'mongoose' { init(): Promise; /** Inserts one or more new documents as a single `insertMany` call to the MongoDB server. */ + insertMany( + docs: Array + ): Promise>; + insertMany( + docs: Array, + options: InsertManyOptions & { lean: true; } + ): Promise>>; + insertMany( + doc: Array, + options: InsertManyOptions & { ordered: false; rawResult: true; } + ): Promise> & { + mongoose: { + validationErrors: Error[]; + results: Array< + Error | + Object | + THydratedDocumentType + > + } + }>; + insertMany( + docs: Array, + options: InsertManyOptions & { lean: true, rawResult: true; } + ): Promise>>; + insertMany( + docs: Array, + options: InsertManyOptions & { rawResult: true; } + ): Promise>>; insertMany( docs: Array, options: InsertManyOptions & { lean: true; } From d2a4f730bb497054f85e9ee811584130b73bd907 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 10 Oct 2023 17:20:42 -0400 Subject: [PATCH 026/191] types(model): make InsertManyResult consistent with return type of insertMany Fix #13904 --- test/types/models.test.ts | 18 ++++++++++++++++++ types/models.d.ts | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index fdeef76a164..5385b92ffc0 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -9,6 +9,7 @@ import mongoose, { CallbackError, HydratedDocument, HydratedDocumentFromSchema, + InsertManyResult, Query, UpdateWriteOpResult, AggregateOptions, @@ -709,3 +710,20 @@ async function gh13746() { expectType(findOneAndUpdateRes.lastErrorObject?.upserted); expectType(findOneAndUpdateRes.ok); } + +function gh13904() { + const schema = new Schema({ name: String }); + + interface ITest { + name?: string; + } + const Test = model("Test", schema); + + expectAssignable>>(Test.insertMany( + [{ name: "test" }], + { + ordered: false, + rawResult: true + } + )); +} \ No newline at end of file diff --git a/types/models.d.ts b/types/models.d.ts index e74754f0710..dbc543c1070 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -375,7 +375,7 @@ declare module 'mongoose' { options: InsertManyOptions & { ordered: false; rawResult: true; } ): Promise> & { mongoose: { - validationErrors: Error[]; + validationErrors: (CastError | Error.ValidatorError)[]; results: Array< Error | Object | From 867221ac6e953717bca408b312716259f014822c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 10 Oct 2023 17:22:01 -0400 Subject: [PATCH 027/191] style: fix lint --- test/types/models.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 5385b92ffc0..d1ba9f8b6fe 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -717,13 +717,13 @@ function gh13904() { interface ITest { name?: string; } - const Test = model("Test", schema); + const Test = model('Test', schema); expectAssignable>>(Test.insertMany( - [{ name: "test" }], + [{ name: 'test' }], { ordered: false, rawResult: true } )); -} \ No newline at end of file +} From 83d1d75658eb15838470bbff4a006d8db7273ea3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 11 Oct 2023 16:04:59 -0400 Subject: [PATCH 028/191] fix: fix merge conflict issue for #13904 with #13964 --- types/models.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/models.d.ts b/types/models.d.ts index b4775413388..cfa23bc40da 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -374,7 +374,7 @@ declare module 'mongoose' { options: InsertManyOptions & { ordered: false; rawResult: true; } ): Promise> & { mongoose: { - validationErrors: Error[]; + validationErrors: (CastError | Error.ValidatorError)[]; results: Array< Error | Object | From 46a6ecc8910f40439c92b1f775b0222fc2b04efd Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 11 Oct 2023 16:13:06 -0400 Subject: [PATCH 029/191] types(model): add missing function signature for `model()` to match `$model()` re: #13963 --- test/types/document.test.ts | 1 + types/document.d.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/test/types/document.test.ts b/test/types/document.test.ts index df66030a5cd..58959d5bcfc 100644 --- a/test/types/document.test.ts +++ b/test/types/document.test.ts @@ -302,6 +302,7 @@ function gh13878() { const User = model('User', schema); const user = new User({ name: 'John', age: 30 }); expectType(user.$model()); + expectType(user.model()); } function gh13094() { diff --git a/types/document.d.ts b/types/document.d.ts index c646b9e8aaf..1c35e48c89c 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -192,6 +192,10 @@ declare module 'mongoose' { markModified(path: T, scope?: any): void; markModified(path: string, scope?: any): void; + /** Returns the model with the given name on this document's associated connection. */ + model>(name: string): ModelType; + model>(): ModelType; + /** Returns the list of paths that have been modified. */ modifiedPaths(options?: { includeChildren?: boolean }): Array; From 157823cc2c3b2550731ab7e2668756947978a054 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 11 Oct 2023 16:45:10 -0400 Subject: [PATCH 030/191] fix(document): avoid triggering setter when initializing `Model.prototype.collection` to allow defining `collection` as a schema path name Fix #13956 --- lib/model.js | 15 ++++++++------- test/document.test.js | 12 ++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/model.js b/lib/model.js index bbd5f8a770d..5978f2ed35b 100644 --- a/lib/model.js +++ b/lib/model.js @@ -4759,8 +4759,6 @@ Model.compile = function compile(name, schema, collectionName, connection, base) schema._preCompile(); - model.prototype.$__setSchema(schema); - const _userProvidedOptions = schema._userProvidedOptions || {}; const collectionOptions = { @@ -4773,13 +4771,16 @@ Model.compile = function compile(name, schema, collectionName, connection, base) collectionOptions.autoCreate = schema.options.autoCreate; } - model.prototype.collection = connection.collection( + const collection = connection.collection( collectionName, collectionOptions ); - model.prototype.$collection = model.prototype.collection; - model.prototype[modelCollectionSymbol] = model.prototype.collection; + model.prototype.collection = collection; + model.prototype.$collection = collection; + model.prototype[modelCollectionSymbol] = collection; + + model.prototype.$__setSchema(schema); // apply methods and statics applyMethods(model, schema); @@ -4788,8 +4789,8 @@ Model.compile = function compile(name, schema, collectionName, connection, base) applyStaticHooks(model, schema.s.hooks, schema.statics); model.schema = model.prototype.$__schema; - model.collection = model.prototype.collection; - model.$__collection = model.collection; + model.collection = collection; + model.$__collection = collection; // Create custom query constructor model.Query = function() { diff --git a/test/document.test.js b/test/document.test.js index 3ff896a9f79..f5ce29b0b8f 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12600,6 +12600,18 @@ describe('document', function() { ['I am NumberTyped', 'I am StringTyped'] ); }); + + it('can use `collection` as schema name (gh-13956)', async function() { + const schema = new mongoose.Schema({ name: String, collection: String }); + const Test = db.model('Test', schema); + + const doc = await Test.create({ name: 'foo', collection: 'bar' }); + assert.strictEqual(doc.collection, 'bar'); + doc.collection = 'baz'; + await doc.save(); + const { collection } = await Test.findById(doc); + assert.strictEqual(collection, 'baz'); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() { From 6586bf2faa747bf25ed5d1c69014d05218fcd795 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 12 Oct 2023 13:36:29 -0400 Subject: [PATCH 031/191] chore: release 6.12.1 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f10b20fa139..757a298a207 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +6.12.1 / 2023-10-12 +=================== + * fix(mongoose): correctly handle global applyPluginsToChildSchemas option #13945 #13887 [hasezoey](https://github.com/hasezoey) + * fix: Document.prototype.isModified support for a string of keys as first parameter #13940 #13674 [k-chop](https://github.com/k-chop) + 6.12.0 / 2023-08-24 =================== * feat: use mongodb driver v4.17.1 diff --git a/package.json b/package.json index 86e16d60367..b0bc46ef6c9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.12.0", + "version": "6.12.1", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 35fd92060752d6cbf45110f0b7fec6ff1237a07d Mon Sep 17 00:00:00 2001 From: Daniel Coker Date: Fri, 13 Oct 2023 13:31:20 +0100 Subject: [PATCH 032/191] Fix typo in timestamps docs --- docs/timestamps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/timestamps.md b/docs/timestamps.md index 8dee886d397..0658df625e4 100644 --- a/docs/timestamps.md +++ b/docs/timestamps.md @@ -199,7 +199,7 @@ Mongoose: users.findOneAndUpdate({}, { '$setOnInsert': { createdAt: new Date("Su Notice the `$setOnInsert` for `createdAt` and `$set` for `updatedAt`. MongoDB's [`$setOnInsert` operator](https://www.mongodb.com/docs/manual/reference/operator/update/setOnInsert/) applies the update only if a new document is [upserted](https://masteringjs.io/tutorials/mongoose/upsert). -So, for example, if you want to *only* set `updatedAt` if the document if a new document is created, you can disable the `updatedAt` timestamp and set it yourself as shown below: +So, for example, if you want to *only* set `updatedAt` of the document if a new document is created, you can disable the `updatedAt` timestamp and set it yourself as shown below: ```javascript await User.findOneAndUpdate({}, { $setOnInsert: { updatedAt: new Date() } }, { From 8cbb224634e0a2d0981ceafce32dd34d4b73c242 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 13 Oct 2023 09:36:19 -0400 Subject: [PATCH 033/191] chore: release 7.6.2 --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 576efcbe660..d6100039dc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +7.6.2 / 2023-10-13 +================== + * perf: avoid storing a separate entry in schema subpaths for every element in an array #13953 #13874 + * fix(document): avoid triggering setter when initializing Model.prototype.collection to allow defining collection as a schema path name #13968 #13956 + * fix(model): make bulkSave() save changes in discriminator paths if calling bulkSave() on base model #13959 #13907 + * fix(document): allow calling $model() with no args for TypeScript #13963 #13878 + * fix(schema): handle embedded discriminators defined using Schema.prototype.discriminator() #13958 #13898 + * types(model): make InsertManyResult consistent with return type of insertMany #13965 #13904 + * types(models): add cleaner type definitions for insertMany() with no generics to prevent errors when using insertMany() in generic classes #13964 #13957 + * types(schematypes): allow defining map path using type: 'Map' in addition to type: Map #13960 #13755 + 7.6.1 / 2023-10-09 ================== * fix: bump bson to match mongodb@5.9.0 exactly #13947 [hasezoey](https://github.com/hasezoey) diff --git a/package.json b/package.json index d4ecbbcece4..9ac82103413 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.1", + "version": "7.6.2", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 77bd7817645a43cf006afb1aaead595099f5b43a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 13 Oct 2023 09:43:06 -0400 Subject: [PATCH 034/191] Update timestamps.md --- docs/timestamps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/timestamps.md b/docs/timestamps.md index 0658df625e4..d475fa70af3 100644 --- a/docs/timestamps.md +++ b/docs/timestamps.md @@ -199,7 +199,7 @@ Mongoose: users.findOneAndUpdate({}, { '$setOnInsert': { createdAt: new Date("Su Notice the `$setOnInsert` for `createdAt` and `$set` for `updatedAt`. MongoDB's [`$setOnInsert` operator](https://www.mongodb.com/docs/manual/reference/operator/update/setOnInsert/) applies the update only if a new document is [upserted](https://masteringjs.io/tutorials/mongoose/upsert). -So, for example, if you want to *only* set `updatedAt` of the document if a new document is created, you can disable the `updatedAt` timestamp and set it yourself as shown below: +So, for example, if you want to *only* set `updatedAt` if a new document is created, you can disable the `updatedAt` timestamp and set it yourself as shown below: ```javascript await User.findOneAndUpdate({}, { $setOnInsert: { updatedAt: new Date() } }, { From 49f5296eb22f10e56c88bce2f435ba75d4d9bb4f Mon Sep 17 00:00:00 2001 From: Zach Levi Date: Sun, 15 Oct 2023 18:08:50 +0300 Subject: [PATCH 035/191] Fix Typescript definition of schema.discriminator to allow strings. --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index f38324417a9..3f94524ba98 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -260,7 +260,7 @@ declare module 'mongoose' { /** Returns a copy of this schema */ clone(): T; - discriminator(name: string, schema: DisSchema): this; + discriminator(name: string | number, schema: DisSchema): this; /** Returns a new schema that has the picked `paths` from this schema. */ pick(paths: string[], options?: SchemaOptions): T; From fa059289e5bb14531a225b239fe6f9c29d0e4730 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 16 Oct 2023 16:16:16 -0400 Subject: [PATCH 036/191] fix(model): add versionKey to bulkWrite when inserting or upserting Fix #13944 --- lib/helpers/model/castBulkWrite.js | 21 +++++++++ .../update/decorateUpdateWithVersionKey.js | 26 +++++++++++ lib/model.js | 29 ++----------- test/model.test.js | 43 +++++++++++++++++++ 4 files changed, 93 insertions(+), 26 deletions(-) create mode 100644 lib/helpers/update/decorateUpdateWithVersionKey.js diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js index fb3dab06161..71d9150f848 100644 --- a/lib/helpers/model/castBulkWrite.js +++ b/lib/helpers/model/castBulkWrite.js @@ -6,6 +6,7 @@ const applyTimestampsToChildren = require('../update/applyTimestampsToChildren') const applyTimestampsToUpdate = require('../update/applyTimestampsToUpdate'); const cast = require('../../cast'); const castUpdate = require('../query/castUpdate'); +const decorateUpdateWithVersionKey = require('../update/decorateUpdateWithVersionKey'); const { inspect } = require('util'); const setDefaultsOnInsert = require('../setDefaultsOnInsert'); @@ -33,6 +34,10 @@ module.exports = function castBulkWrite(originalModel, op, options) { if (options.session != null) { doc.$session(options.session); } + const versionKey = model?.schema?.options?.versionKey; + if (versionKey && doc[versionKey] == null) { + doc[versionKey] = 0; + } op['insertOne']['document'] = doc; if (options.skipValidation || op['insertOne'].skipValidation) { @@ -81,6 +86,12 @@ module.exports = function castBulkWrite(originalModel, op, options) { }); } + decorateUpdateWithVersionKey( + op['updateOne']['update'], + op['updateOne'], + model.schema.options.versionKey + ); + op['updateOne']['filter'] = cast(model.schema, op['updateOne']['filter'], { strict: strict, upsert: op['updateOne'].upsert @@ -133,6 +144,12 @@ module.exports = function castBulkWrite(originalModel, op, options) { _addDiscriminatorToObject(schema, op['updateMany']['filter']); + decorateUpdateWithVersionKey( + op['updateMany']['update'], + op['updateMany'], + model.schema.options.versionKey + ); + op['updateMany']['filter'] = cast(model.schema, op['updateMany']['filter'], { strict: strict, upsert: op['updateMany'].upsert @@ -173,6 +190,10 @@ module.exports = function castBulkWrite(originalModel, op, options) { if (options.session != null) { doc.$session(options.session); } + const versionKey = model?.schema?.options?.versionKey; + if (versionKey && doc[versionKey] == null) { + doc[versionKey] = 0; + } op['replaceOne']['replacement'] = doc; if (options.skipValidation || op['replaceOne'].skipValidation) { diff --git a/lib/helpers/update/decorateUpdateWithVersionKey.js b/lib/helpers/update/decorateUpdateWithVersionKey.js new file mode 100644 index 00000000000..161729844cc --- /dev/null +++ b/lib/helpers/update/decorateUpdateWithVersionKey.js @@ -0,0 +1,26 @@ +'use strict'; + +const modifiedPaths = require('./modifiedPaths'); + +/** + * Decorate the update with a version key, if necessary + * @api private + */ + +module.exports = function decorateUpdateWithVersionKey(update, options, versionKey) { + if (!versionKey || !(options && options.upsert || false)) { + return; + } + + const updatedPaths = modifiedPaths(update); + if (!updatedPaths[versionKey]) { + if (options.overwrite) { + update[versionKey] = 0; + } else { + if (!update.$setOnInsert) { + update.$setOnInsert = {}; + } + update.$setOnInsert[versionKey] = 0; + } + } +}; diff --git a/lib/model.js b/lib/model.js index b0b48c7e33f..5d9990d88e5 100644 --- a/lib/model.js +++ b/lib/model.js @@ -33,6 +33,7 @@ const assignVals = require('./helpers/populate/assignVals'); const castBulkWrite = require('./helpers/model/castBulkWrite'); const clone = require('./helpers/clone'); const createPopulateQueryFilter = require('./helpers/populate/createPopulateQueryFilter'); +const decorateUpdateWithVersionKey = require('./helpers/update/decorateUpdateWithVersionKey'); const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult'); const getSchemaDiscriminatorByValue = require('./helpers/discriminator/getSchemaDiscriminatorByValue'); const discriminator = require('./helpers/model/discriminator'); @@ -54,7 +55,6 @@ const isPathExcluded = require('./helpers/projection/isPathExcluded'); const decorateDiscriminatorIndexOptions = require('./helpers/indexes/decorateDiscriminatorIndexOptions'); const isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive'); const leanPopulateMap = require('./helpers/populate/leanPopulateMap'); -const modifiedPaths = require('./helpers/update/modifiedPaths'); const parallelLimit = require('./helpers/parallelLimit'); const parentPaths = require('./helpers/path/parentPaths'); const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscriminatorPipeline'); @@ -2451,7 +2451,7 @@ Model.findOneAndUpdate = function(conditions, update, options) { _isNested: true }); - _decorateUpdateWithVersionKey(update, options, this.schema.options.versionKey); + decorateUpdateWithVersionKey(update, options, this.schema.options.versionKey); const mq = new this.Query({}, {}, this, this.$__collection); mq.select(fields); @@ -2459,29 +2459,6 @@ Model.findOneAndUpdate = function(conditions, update, options) { return mq.findOneAndUpdate(conditions, update, options); }; -/** - * Decorate the update with a version key, if necessary - * @api private - */ - -function _decorateUpdateWithVersionKey(update, options, versionKey) { - if (!versionKey || !(options && options.upsert || false)) { - return; - } - - const updatedPaths = modifiedPaths(update); - if (!updatedPaths[versionKey]) { - if (options.overwrite) { - update[versionKey] = 0; - } else { - if (!update.$setOnInsert) { - update.$setOnInsert = {}; - } - update.$setOnInsert[versionKey] = 0; - } - } -} - /** * Issues a mongodb findOneAndUpdate command by a document's _id field. * `findByIdAndUpdate(id, ...)` is equivalent to `findOneAndUpdate({ _id: id }, ...)`. @@ -4022,7 +3999,7 @@ function _update(model, op, conditions, doc, options) { model.schema && model.schema.options && model.schema.options.versionKey || null; - _decorateUpdateWithVersionKey(doc, options, versionKey); + decorateUpdateWithVersionKey(doc, options, versionKey); return mq[op](conditions, doc, options); } diff --git a/test/model.test.js b/test/model.test.js index d7e6bc0aa01..e86ea268dfd 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -4043,6 +4043,49 @@ describe('Model', function() { }); + it('sets version key (gh-13944)', async function() { + const userSchema = new Schema({ + firstName: { type: String, required: true }, + lastName: { type: String } + }); + const User = db.model('User', userSchema); + + await User.bulkWrite([ + { + updateOne: { + filter: { lastName: 'Gibbons' }, + update: { firstName: 'Peter' }, + upsert: true + } + }, + { + insertOne: { + document: { + firstName: 'Michael', + lastName: 'Bolton' + } + } + }, + { + replaceOne: { + filter: { lastName: 'Lumbergh' }, + replacement: { firstName: 'Bill', lastName: 'Lumbergh' }, + upsert: true + } + } + ], { ordered: false }); + + const users = await User.find(); + assert.deepStrictEqual( + users.map(user => user.firstName).sort(), + ['Bill', 'Michael', 'Peter'] + ); + assert.deepStrictEqual( + users.map(user => user.__v), + [0, 0, 0] + ); + }); + it('with single nested and setOnInsert (gh-7534)', function() { const nested = new Schema({ name: String }); const schema = new Schema({ nested: nested }); From e44e75b07ae5a4974249905761249d73d28d5951 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 16 Oct 2023 16:35:04 -0400 Subject: [PATCH 037/191] fix(update): avoid applying defaults on query filter when upserting with empty update Fix #13962 --- lib/helpers/query/castUpdate.js | 3 ++- test/model.findOneAndUpdate.test.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/helpers/query/castUpdate.js b/lib/helpers/query/castUpdate.js index 78422033079..25fbb456ea1 100644 --- a/lib/helpers/query/castUpdate.js +++ b/lib/helpers/query/castUpdate.js @@ -126,7 +126,8 @@ module.exports = function castUpdate(schema, obj, options, context, filter) { Object.keys(filter).length > 0) { // Trick the driver into allowing empty upserts to work around // https://github.com/mongodb/node-mongodb-native/pull/2490 - return { $setOnInsert: filter }; + // Shallow clone to avoid passing defaults in re: gh-13962 + return { $setOnInsert: { ...filter } }; } return ret; }; diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index eee320aa968..ed0afa96408 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -2200,4 +2200,21 @@ describe('model: findOneAndUpdate:', function() { assert.ok(document); assert.equal(document.name, 'test'); }); + + it('skips adding defaults to filter when passing empty update (gh-13962)', async function() { + const schema = new Schema({ + myField: Number, + defaultField: { type: String, default: 'default' } + }, { versionKey: false }); + const Test = db.model('Test', schema); + + await Test.create({ myField: 1, defaultField: 'some non-default value' }); + + const updated = await Test.findOneAndUpdate( + { myField: 1 }, + {}, + { upsert: true, returnDocument: 'after' } + ); + assert.equal(updated.defaultField, 'some non-default value'); + }); }); From 6419a8787f2b4ce2dbaaf143dcfa0cd1feb733e9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 16 Oct 2023 17:15:01 -0400 Subject: [PATCH 038/191] fix(populate): handle multiple spaces when specifying paths to populate using space-delimited paths Fix #13951 --- lib/utils.js | 13 ++++++++----- test/model.populate.test.js | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index b5f3dd4acfc..4fca9502d56 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -31,6 +31,9 @@ exports.isMongooseDocumentArray = isMongooseDocumentArray.isMongooseDocumentArra exports.registerMongooseArray = isMongooseArray.registerMongooseArray; exports.registerMongooseDocumentArray = isMongooseDocumentArray.registerMongooseDocumentArray; +const oneSpaceRE = /\s/; +const manySpaceRE = /\s+/; + /** * Produces a collection name from model `name`. By default, just returns * the model name @@ -572,8 +575,8 @@ exports.populate = function populate(path, select, model, match, options, subPop function makeSingles(arr) { const ret = []; arr.forEach(function(obj) { - if (/[\s]/.test(obj.path)) { - const paths = obj.path.split(' '); + if (oneSpaceRE.test(obj.path)) { + const paths = obj.path.split(manySpaceRE); paths.forEach(function(p) { const copy = Object.assign({}, obj); copy.path = p; @@ -592,9 +595,9 @@ function _populateObj(obj) { if (Array.isArray(obj.populate)) { const ret = []; obj.populate.forEach(function(obj) { - if (/[\s]/.test(obj.path)) { + if (oneSpaceRE.test(obj.path)) { const copy = Object.assign({}, obj); - const paths = copy.path.split(' '); + const paths = copy.path.split(manySpaceRE); paths.forEach(function(p) { copy.path = p; ret.push(exports.populate(copy)[0]); @@ -609,7 +612,7 @@ function _populateObj(obj) { } const ret = []; - const paths = obj.path.split(' '); + const paths = oneSpaceRE.test(obj.path) ? obj.path.split(manySpaceRE) : [obj.path]; if (obj.options != null) { obj.options = clone(obj.options); } diff --git a/test/model.populate.test.js b/test/model.populate.test.js index c1007f69315..00d16d01b6c 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -2819,7 +2819,28 @@ describe('model: populate:', function() { assert.equal(blogposts[0].user.name, 'Fan 1'); assert.equal(blogposts[0].title, 'Test 1'); + }); + + it('handles multiple spaces in between paths to populate (gh-13951)', async function() { + const BlogPost = db.model('BlogPost', new Schema({ + title: String, + user: { type: ObjectId, ref: 'User' }, + fans: [{ type: ObjectId, ref: 'User' }] + })); + const User = db.model('User', new Schema({ name: String })); + + const fans = await User.create([{ name: 'Fan 1' }]); + const posts = [ + { title: 'Test 1', user: fans[0]._id, fans: [fans[0]._id] } + ]; + await BlogPost.create(posts); + const blogPost = await BlogPost. + findOne({ title: 'Test 1' }). + populate('user \t fans'); + assert.equal(blogPost.user.name, 'Fan 1'); + assert.equal(blogPost.fans[0].name, 'Fan 1'); + assert.equal(blogPost.title, 'Test 1'); }); it('maps results back to correct document (gh-1444)', async function() { From 8831f031f3d759802422f15d8602922f4def31d7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 17 Oct 2023 09:42:52 -0400 Subject: [PATCH 039/191] chore: release 7.6.3 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6100039dc7..07cffa69145 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +7.6.3 / 2023-10-17 +================== + * fix(populate): handle multiple spaces when specifying paths to populate using space-delimited paths #13984 #13951 + * fix(update): avoid applying defaults on query filter when upserting with empty update #13983 #13962 + * fix(model): add versionKey to bulkWrite when inserting or upserting #13981 #13944 + * docs: fix typo in timestamps docs #13976 [danielcoker](https://github.com/danielcoker) + 7.6.2 / 2023-10-13 ================== * perf: avoid storing a separate entry in schema subpaths for every element in an array #13953 #13874 diff --git a/package.json b/package.json index 9ac82103413..0d04d2975e4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.2", + "version": "7.6.3", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From eacb5ab893d0218eddd64f7515431d07a903d7f5 Mon Sep 17 00:00:00 2001 From: Norio Suzuki Date: Wed, 18 Oct 2023 07:32:58 +0900 Subject: [PATCH 040/191] fix(document): fix missing import and change wrong variable name --- docs/typescript/query-helpers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/typescript/query-helpers.md b/docs/typescript/query-helpers.md index 163fe90d36c..0f1c67a5483 100644 --- a/docs/typescript/query-helpers.md +++ b/docs/typescript/query-helpers.md @@ -29,7 +29,7 @@ The 2nd generic parameter, `TQueryHelpers`, should be an interface that contains Below is an example of creating a `ProjectModel` with a `byName` query helper. ```typescript -import { HydratedDocument, Model, Query, Schema, model } from 'mongoose'; +import { HydratedDocument, Model, QueryWithHelpers, Schema, model, connect } from 'mongoose'; interface Project { name?: string; @@ -64,7 +64,7 @@ ProjectSchema.query.byName = function byName( }; // 2nd param to `model()` is the Model class to return. -const ProjectModel = model('Project', schema); +const ProjectModel = model('Project', ProjectSchema); run().catch(err => console.log(err)); From e0b03ed5eec25f238036dd42d418084cf91e3395 Mon Sep 17 00:00:00 2001 From: Nicolas Polizzo Date: Fri, 20 Oct 2023 13:16:48 +0200 Subject: [PATCH 041/191] Add fullPath to ValidatorProps --- lib/schematype.js | 2 ++ types/validation.d.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/lib/schematype.js b/lib/schematype.js index c7d27c6a467..5003a8948f6 100644 --- a/lib/schematype.js +++ b/lib/schematype.js @@ -1305,6 +1305,7 @@ SchemaType.prototype.doValidate = function(value, fn, scope, options) { const validatorProperties = isSimpleValidator(v) ? Object.assign({}, v) : clone(v); validatorProperties.path = options && options.path ? options.path : path; + validatorProperties.fullPath = this.$fullPath; validatorProperties.value = value; if (validator instanceof RegExp) { @@ -1426,6 +1427,7 @@ SchemaType.prototype.doValidateSync = function(value, scope, options) { const validator = v.validator; const validatorProperties = isSimpleValidator(v) ? Object.assign({}, v) : clone(v); validatorProperties.path = options && options.path ? options.path : path; + validatorProperties.fullPath = this.$fullPath; validatorProperties.value = value; let ok = false; diff --git a/types/validation.d.ts b/types/validation.d.ts index 7d8924c5880..693261250e7 100644 --- a/types/validation.d.ts +++ b/types/validation.d.ts @@ -4,6 +4,7 @@ declare module 'mongoose' { interface ValidatorProps { path: string; + fullPath: string; value: any; } From 627a53e229c0461ec24bf37edbf414cdf59a0d72 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:40:23 -0400 Subject: [PATCH 042/191] write test --- lib/model.js | 3 +- lib/query.js | 1 - test/model.populate.test.js | 113 ++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 3 deletions(-) diff --git a/lib/model.js b/lib/model.js index 5d9990d88e5..cacb1c97358 100644 --- a/lib/model.js +++ b/lib/model.js @@ -4263,9 +4263,7 @@ Model.populate = async function populate(docs, paths) { if (typeof paths === 'function' || typeof arguments[2] === 'function') { throw new MongooseError('Model.populate() no longer accepts a callback'); } - const _this = this; - // normalized paths paths = utils.populate(paths); // data that should persist across subPopulate calls @@ -4322,6 +4320,7 @@ const excludeIdRegGlobal = /\s?-_id\s?/g; function populate(model, docs, options, callback) { const populateOptions = options; + // playing hide and seek with strictPopulate if (options.strictPopulate == null) { if (options._localModel != null && options._localModel.schema._userProvidedOptions.strictPopulate != null) { populateOptions.strictPopulate = options._localModel.schema._userProvidedOptions.strictPopulate; diff --git a/lib/query.js b/lib/query.js index 30f22255d95..98112653776 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2305,7 +2305,6 @@ Query.prototype._find = async function _find() { _completeManyLean(_this.model.schema, docs, null, completeManyOptions) : completeMany(_this.model, docs, fields, userProvidedFields, completeManyOptions); } - const pop = helpers.preparePopulationOptionsMQ(_this, mongooseOptions); if (mongooseOptions.lean) { diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 00d16d01b6c..2a9f039d9db 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -10330,6 +10330,119 @@ describe('model: populate:', function() { }); describe('strictPopulate', function() { + it('does not throw an error when using strictPopulate on a nested path (gh-13863)', async function() { + const testSchema = new mongoose.Schema({ + name: String, + location: String, + occupation: String, + }); + + const ASchema = new mongoose.Schema({ + name: String, + age: Number, + weight: Number, + dModel: { + type: 'ObjectId', + ref: 'gh13863DModel' + }, + fModel: { + type: 'ObjectId', + ref: 'gh13863FModel' + } + }); + + const XSchema = new mongoose.Schema({ + name: String, + car: String, + make: String, + model: String, + aModel: { + type: 'ObjectId', + ref: 'gh13863AModel' + }, + testModel: { + type: 'ObjectId', + ref: 'gh13863Test' + }, + cModel: { + type: 'ObjectId', + ref: 'gh13863CModel' + } + }); + + const CSchema = new Schema({ + name: String, + hobbies: String + }) + + const DSchema = new Schema({ + name: String, + }); + + const FSchema = new Schema({ + name: String, + }); + + const GSchema = new Schema({ + name: String + }); + + const Test = db.model('gh13863Test', testSchema); + const AModel = db.model('gh13863AModel', ASchema); + const XModel = db.model('gh13863XModel', XSchema); + const CModel = db.model('gh13863CModel', CSchema); + const DModel = db.model('gh13863DModel', DSchema); + const FModel = db.model('gh13863FModel', FSchema); + const GModel = db.model('gh13863GModel', GSchema); + + const gDoc = await GModel.create({ + name: 'G-Man' + }); + const dDoc = await DModel.create({ + name: 'Dirty Dan', + }); + + const fDoc = await FModel.create({ + name: 'Filthy Frank', + }); + + const testDoc = await Test.create({ + name: 'Test Testserson', + location: 'Florida', + occupation: 'Tester' + }); + + const aDoc = await AModel.create({ + name: 'A-men', + age: 4, + weight: 404, + dModel: dDoc._id, + fModel: fDoc._id + }); + + const cDoc = await CModel.create({ + name: 'C-ya', + hobbies: 'Leaving' + }); + + await XModel.create({ + name: 'XCOM', + aModel: aDoc._id, + cModel: cDoc._id, + testModel: testDoc._id + }); + + const res = await XModel.find().populate({ + path: 'aModel testModel cModel', + populate: { + path: 'dModel fModel', + populate: 'gModel', + strictPopulate: false + } + }); + + assert.ok(res); + }); it('reports full path when throwing `strictPopulate` error with deep populate (gh-10923)', async function() { const L2 = db.model('Test', new Schema({ name: String })); From 1eee4f8baf93932ec0f90f4c8e040263aa97fcc1 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 20 Oct 2023 15:06:50 -0400 Subject: [PATCH 043/191] fix: lint --- test/model.populate.test.js | 38 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 2a9f039d9db..52a5ab197af 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -10334,9 +10334,9 @@ describe('model: populate:', function() { const testSchema = new mongoose.Schema({ name: String, location: String, - occupation: String, + occupation: String }); - + const ASchema = new mongoose.Schema({ name: String, age: Number, @@ -10350,7 +10350,7 @@ describe('model: populate:', function() { ref: 'gh13863FModel' } }); - + const XSchema = new mongoose.Schema({ name: String, car: String, @@ -10369,20 +10369,20 @@ describe('model: populate:', function() { ref: 'gh13863CModel' } }); - + const CSchema = new Schema({ name: String, hobbies: String - }) - + }); + const DSchema = new Schema({ - name: String, + name: String }); - + const FSchema = new Schema({ - name: String, + name: String }); - + const GSchema = new Schema({ name: String }); @@ -10395,23 +10395,23 @@ describe('model: populate:', function() { const FModel = db.model('gh13863FModel', FSchema); const GModel = db.model('gh13863GModel', GSchema); - const gDoc = await GModel.create({ + await GModel.create({ name: 'G-Man' }); const dDoc = await DModel.create({ - name: 'Dirty Dan', + name: 'Dirty Dan' }); - + const fDoc = await FModel.create({ - name: 'Filthy Frank', + name: 'Filthy Frank' }); - + const testDoc = await Test.create({ name: 'Test Testserson', location: 'Florida', occupation: 'Tester' }); - + const aDoc = await AModel.create({ name: 'A-men', age: 4, @@ -10419,19 +10419,19 @@ describe('model: populate:', function() { dModel: dDoc._id, fModel: fDoc._id }); - + const cDoc = await CModel.create({ name: 'C-ya', hobbies: 'Leaving' }); - + await XModel.create({ name: 'XCOM', aModel: aDoc._id, cModel: cDoc._id, testModel: testDoc._id }); - + const res = await XModel.find().populate({ path: 'aModel testModel cModel', populate: { From 967912f833e39b76c5a4513998101d1782baf7bf Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 20 Oct 2023 17:48:59 -0400 Subject: [PATCH 044/191] fix(populate): allow using `options: { strictPopulate: false }` to disable strict populate Re: #13863 --- .../populate/getModelsMapForPopulate.js | 3 +- test/model.populate.test.js | 131 ++++++------------ 2 files changed, 41 insertions(+), 93 deletions(-) diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js index dd70d436203..c0c8ad4a3e3 100644 --- a/lib/helpers/populate/getModelsMapForPopulate.js +++ b/lib/helpers/populate/getModelsMapForPopulate.js @@ -45,7 +45,8 @@ module.exports = function getModelsMapForPopulate(model, docs, options) { let allSchemaTypes = getSchemaTypes(model, modelSchema, null, options.path); allSchemaTypes = Array.isArray(allSchemaTypes) ? allSchemaTypes : [allSchemaTypes].filter(v => v != null); - if (allSchemaTypes.length === 0 && options.strictPopulate !== false && options._localModel != null) { + const isStrictPopulateDisabled = options.strictPopulate === false || options.options?.strictPopulate === false; + if (!isStrictPopulateDisabled && allSchemaTypes.length === 0 && options._localModel != null) { return new StrictPopulate(options._fullPath || options.path); } diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 52a5ab197af..e53c2f7de47 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -10331,117 +10331,64 @@ describe('model: populate:', function() { describe('strictPopulate', function() { it('does not throw an error when using strictPopulate on a nested path (gh-13863)', async function() { - const testSchema = new mongoose.Schema({ - name: String, - location: String, - occupation: String + const l4Schema = new mongoose.Schema({ + name: String }); - const ASchema = new mongoose.Schema({ - name: String, - age: Number, - weight: Number, - dModel: { - type: 'ObjectId', - ref: 'gh13863DModel' - }, - fModel: { + const l3aSchema = new mongoose.Schema({ + l4: { type: 'ObjectId', - ref: 'gh13863FModel' + ref: 'L4' } }); + const l3bSchema = new mongoose.Schema({ + otherProp: String + }); - const XSchema = new mongoose.Schema({ - name: String, - car: String, - make: String, - model: String, - aModel: { - type: 'ObjectId', - ref: 'gh13863AModel' - }, - testModel: { + const l2Schema = new mongoose.Schema({ + l3a: { type: 'ObjectId', - ref: 'gh13863Test' + ref: 'L3A' }, - cModel: { + l3b: { type: 'ObjectId', - ref: 'gh13863CModel' + ref: 'L3B' } }); - const CSchema = new Schema({ - name: String, - hobbies: String - }); - - const DSchema = new Schema({ - name: String - }); - - const FSchema = new Schema({ - name: String - }); - - const GSchema = new Schema({ - name: String - }); - - const Test = db.model('gh13863Test', testSchema); - const AModel = db.model('gh13863AModel', ASchema); - const XModel = db.model('gh13863XModel', XSchema); - const CModel = db.model('gh13863CModel', CSchema); - const DModel = db.model('gh13863DModel', DSchema); - const FModel = db.model('gh13863FModel', FSchema); - const GModel = db.model('gh13863GModel', GSchema); - - await GModel.create({ - name: 'G-Man' - }); - const dDoc = await DModel.create({ - name: 'Dirty Dan' - }); - - const fDoc = await FModel.create({ - name: 'Filthy Frank' - }); - - const testDoc = await Test.create({ - name: 'Test Testserson', - location: 'Florida', - occupation: 'Tester' - }); - - const aDoc = await AModel.create({ - name: 'A-men', - age: 4, - weight: 404, - dModel: dDoc._id, - fModel: fDoc._id + const l1Schema = new mongoose.Schema({ + l2: { + type: 'ObjectId', + ref: 'L2' + } }); - const cDoc = await CModel.create({ - name: 'C-ya', - hobbies: 'Leaving' - }); + const L1 = db.model('L1', l1Schema); + const L2 = db.model('L2', l2Schema); + const L3A = db.model('L3A', l3aSchema); + const L3B = db.model('L3B', l3bSchema); + const L4 = db.model('L4', l4Schema); - await XModel.create({ - name: 'XCOM', - aModel: aDoc._id, - cModel: cDoc._id, - testModel: testDoc._id - }); + const { _id: l4 } = await L4.create({ name: 'test l4' }); + const { _id: l3a } = await L3A.create({ l4 }); + const { _id: l3b } = await L3B.create({ name: 'test l3' }); + const { _id: l2 } = await L2.create({ l3a, l3b }); + const { _id: l1 } = await L1.create({ l2 }); - const res = await XModel.find().populate({ - path: 'aModel testModel cModel', + const res = await L1.findById(l1).populate({ + path: 'l2', populate: { - path: 'dModel fModel', - populate: 'gModel', - strictPopulate: false + path: 'l3a l3b', + populate: { + path: 'l4', + options: { + strictPopulate: false + } + } } }); - - assert.ok(res); + assert.equal(res.l2.l3a.l4.name, 'test l4'); + assert.equal(res.l2.l3b.l4, undefined); }); it('reports full path when throwing `strictPopulate` error with deep populate (gh-10923)', async function() { const L2 = db.model('Test', new Schema({ name: String })); From c5f75bcdeaefbfe3390e5637dc4a3c825ad5be93 Mon Sep 17 00:00:00 2001 From: Norio Suzuki Date: Mon, 23 Oct 2023 08:11:03 +0900 Subject: [PATCH 045/191] fix(document): fix differences between sample codes and documentation --- docs/plugins.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins.md b/docs/plugins.md index 23819093645..978a11cfcad 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -44,7 +44,7 @@ const playerSchema = new Schema({ /* ... */ }); playerSchema.plugin(loadedAtPlugin); ``` -We just added last-modified behavior to both our `Game` and `Player` schemas and declared an index on the `lastMod` path of our Games to boot. Not bad for a few lines of code. +We just added loaded-time behavior to both our `Game` and `Player` schemas and declared an index on the `loadedAt` path of our Games to boot. Not bad for a few lines of code.

Global Plugins

From 9bc2cf25056c4825cbfb4b1070a1c866262ab650 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 25 Oct 2023 13:43:40 -0400 Subject: [PATCH 046/191] Update model.js --- lib/model.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index cacb1c97358..dd59c39d8dc 100644 --- a/lib/model.js +++ b/lib/model.js @@ -4320,7 +4320,6 @@ const excludeIdRegGlobal = /\s?-_id\s?/g; function populate(model, docs, options, callback) { const populateOptions = options; - // playing hide and seek with strictPopulate if (options.strictPopulate == null) { if (options._localModel != null && options._localModel.schema._userProvidedOptions.strictPopulate != null) { populateOptions.strictPopulate = options._localModel.schema._userProvidedOptions.strictPopulate; From 7c9eb3c8b37ac0dadee6c0eaabe1deaa627452f8 Mon Sep 17 00:00:00 2001 From: Nicolas Polizzo Date: Fri, 20 Oct 2023 13:16:48 +0200 Subject: [PATCH 047/191] Add fullPath to ValidatorProps --- lib/schematype.js | 2 ++ types/validation.d.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/lib/schematype.js b/lib/schematype.js index 2b62c2208f7..85e0241b5ea 100644 --- a/lib/schematype.js +++ b/lib/schematype.js @@ -1287,6 +1287,7 @@ SchemaType.prototype.doValidate = function(value, fn, scope, options) { const validatorProperties = isSimpleValidator(v) ? Object.assign({}, v) : utils.clone(v); validatorProperties.path = options && options.path ? options.path : path; + validatorProperties.fullPath = this.$fullPath; validatorProperties.value = value; if (validator instanceof RegExp) { @@ -1408,6 +1409,7 @@ SchemaType.prototype.doValidateSync = function(value, scope, options) { const validator = v.validator; const validatorProperties = isSimpleValidator(v) ? Object.assign({}, v) : utils.clone(v); validatorProperties.path = options && options.path ? options.path : path; + validatorProperties.fullPath = this.$fullPath; validatorProperties.value = value; let ok = false; diff --git a/types/validation.d.ts b/types/validation.d.ts index 7d8924c5880..693261250e7 100644 --- a/types/validation.d.ts +++ b/types/validation.d.ts @@ -4,6 +4,7 @@ declare module 'mongoose' { interface ValidatorProps { path: string; + fullPath: string; value: any; } From fbb1f5dee897a4079516a31483a014d9ad8d9cb7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 25 Oct 2023 14:15:10 -0400 Subject: [PATCH 048/191] chore: release 6.12.2 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 757a298a207..d711eb97a5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +6.12.2 / 2023-10-25 +=================== + * fix: add fullPath to ValidatorProps #13995 [Freezystem](https://github.com/Freezystem) + 6.12.1 / 2023-10-12 =================== * fix(mongoose): correctly handle global applyPluginsToChildSchemas option #13945 #13887 [hasezoey](https://github.com/hasezoey) diff --git a/package.json b/package.json index b0bc46ef6c9..82a5e705477 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.12.1", + "version": "6.12.2", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 266804b995ae715cd4746d0e5b687e7fb96441fc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 26 Oct 2023 17:12:17 -0400 Subject: [PATCH 049/191] fix: handle casting $or within $elemMatch Fix #13974 --- lib/schema/array.js | 4 ++-- test/model.query.casting.test.js | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/schema/array.js b/lib/schema/array.js index 6d1c6de9628..db775d4d09b 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -623,8 +623,8 @@ function cast$elemMatch(val, context) { discriminators[val[discriminatorKey]] != null) { return cast(discriminators[val[discriminatorKey]], val, null, this && this.$$context); } - - return cast(this.casterConstructor.schema, val, null, this && this.$$context); + const schema = this.casterConstructor.schema ?? context.schema; + return cast(schema, val, null, this && this.$$context); } const handle = SchemaArray.prototype.$conditionalHandlers = {}; diff --git a/test/model.query.casting.test.js b/test/model.query.casting.test.js index a88e74dde52..c287089461c 100644 --- a/test/model.query.casting.test.js +++ b/test/model.query.casting.test.js @@ -754,7 +754,7 @@ describe('model query casting', function() { assert.strictEqual(doc.outerArray[0].innerArray[0], 'onetwothree'); }); }); - it('should not throw a cast error when dealing with an array of an array of strings in combination with $elemMach and $not (gh-13880)', async function() { + it('should not throw a cast error when dealing with an array of an array of strings in combination with $elemMatch and $not (gh-13880)', async function() { const testSchema = new Schema({ arr: [[String]] }); @@ -766,6 +766,31 @@ describe('model query casting', function() { assert(res); assert(res[0].arr); }); + it('should not throw a cast error when dealing with an array of objects in combination with $elemMatch (gh-13974)', async function() { + const testSchema = new Schema({ + arr: [Object] + }); + + const Test = db.model('Test', testSchema); + const obj1 = new Test({ arr: [{ id: 'one' }, { id: 'two' }] }); + await obj1.save(); + + const obj2 = new Test({ arr: [{ id: 'two' }, { id: 'three' }] }); + await obj2.save(); + + const obj3 = new Test({ arr: [{ id: 'three' }, { id: 'four' }] }); + await obj3.save(); + + const res = await Test.find({ + arr: { + $elemMatch: { + $or: [{ id: 'one' }, { id: 'two' }] + } + } + }).sort({ _id: 1 }); + assert.ok(res); + assert.deepStrictEqual(res.map(doc => doc.arr[1].id), ['two', 'three']); + }); }); function _geojsonPoint(coordinates) { From 64f20987576e555064d986d262cf3243de9f9ca5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 27 Oct 2023 16:23:39 -0400 Subject: [PATCH 050/191] fix(schema): handle recursive schemas in discriminator definitions Fix #13978 --- lib/schema.js | 4 +++- lib/schema/SubdocumentPath.js | 9 ++++++--- test/schema.test.js | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index 75b2399be86..a4b6e6c2d54 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -412,7 +412,9 @@ Schema.prototype._clone = function _clone(Constructor) { s.s.hooks = this.s.hooks.clone(); s.tree = clone(this.tree); - s.paths = clone(this.paths); + s.paths = Object.fromEntries( + Object.entries(this.paths).map(([key, value]) => ([key, value.clone()])) + ); s.nested = clone(this.nested); s.subpaths = clone(this.subpaths); for (const schemaType of Object.values(s.paths)) { diff --git a/lib/schema/SubdocumentPath.js b/lib/schema/SubdocumentPath.js index 17f8a5aa79b..24f225e94b4 100644 --- a/lib/schema/SubdocumentPath.js +++ b/lib/schema/SubdocumentPath.js @@ -56,7 +56,7 @@ function SubdocumentPath(schema, path, options) { this.base = schema.base; SchemaType.call(this, path, options, 'Embedded'); - if (schema._applyDiscriminators != null) { + if (schema._applyDiscriminators != null && !options?._skipApplyDiscriminators) { for (const disc of schema._applyDiscriminators.keys()) { this.discriminator(disc, schema._applyDiscriminators.get(disc)); } @@ -388,8 +388,11 @@ SubdocumentPath.prototype.toJSON = function toJSON() { */ SubdocumentPath.prototype.clone = function() { - const options = Object.assign({}, this.options); - const schematype = new this.constructor(this.schema, this.path, options); + const schematype = new this.constructor( + this.schema, + this.path, + { ...this.options, _skipApplyDiscriminators: true } + ); schematype.validators = this.validators.slice(); if (this.requiredValidator !== undefined) { schematype.requiredValidator = this.requiredValidator; diff --git a/test/schema.test.js b/test/schema.test.js index f41620101dc..a0afbc90aef 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -3172,4 +3172,20 @@ describe('schema', function() { const res = await Test.findOne({ _id: { $eq: doc._id, $type: 'objectId' } }); assert.equal(res.name, 'Test Testerson'); }); + + it('handles recursive definitions in discriminators (gh-13978)', function() { + const base = new Schema({ + type: { type: Number, required: true } + }, { discriminatorKey: 'type' }); + + const recursive = new Schema({ + self: { type: base, required: true } + }); + + base.discriminator(1, recursive); + const TestModel = db.model('Test', base); + + const doc = new TestModel({ type: 1, self: { type: 1 } }); + assert.strictEqual(doc.self.type, 1); + }); }); From c795ce46ad1a744f2f6064c3aac56cfcc67dc588 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 29 Oct 2023 13:43:24 -0400 Subject: [PATCH 051/191] fix(connection): retain modified status for documents created outside a transaction during transaction retries Fix #13973 --- lib/connection.js | 4 ++++ lib/document.js | 10 +++++++++- test/docs/transactions.test.js | 25 +++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/connection.js b/lib/connection.js index 78b69c60c77..c116a3cde32 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -563,6 +563,10 @@ function _resetSessionDocuments(session) { doc.$__.activePaths.states.modify = {}; } for (const path of state.modifiedPaths) { + const currentState = doc.$__.activePaths.paths[path]; + if (currentState != null) { + delete doc.$__.activePaths[currentState][path]; + } doc.$__.activePaths.paths[path] = 'modify'; doc.$__.activePaths.states.modify[path] = true; } diff --git a/lib/document.js b/lib/document.js index e0d5674faf8..9b58a931e63 100644 --- a/lib/document.js +++ b/lib/document.js @@ -53,6 +53,7 @@ const scopeSymbol = require('./helpers/symbols').scopeSymbol; const schemaMixedSymbol = require('./schema/symbols').schemaMixedSymbol; const parentPaths = require('./helpers/path/parentPaths'); const getDeepestSubdocumentForPath = require('./helpers/document/getDeepestSubdocumentForPath'); +const sessionNewDocuments = require('./helpers/symbols').sessionNewDocuments; let DocumentArray; let MongooseArray; @@ -1474,7 +1475,14 @@ Document.prototype.$set = function $set(path, val, type, options) { this.$__set(pathToMark, path, options, constructing, parts, schema, val, priorVal); - if (savedState != null && savedState.hasOwnProperty(savedStatePath) && utils.deepEqual(val, savedState[savedStatePath])) { + if (savedState != null && + savedState.hasOwnProperty(savedStatePath) && + this.$__.session && + this.$__.session[sessionNewDocuments] && + this.$__.session[sessionNewDocuments].has(this) && + this.$__.session[sessionNewDocuments].get(this).modifiedPaths && + !this.$__.session[sessionNewDocuments].get(this).modifiedPaths.has(savedStatePath) && + utils.deepEqual(val, savedState[savedStatePath])) { this.unmarkModified(path); } } diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index 64323c9f2c4..9ddd4f28df9 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -396,4 +396,29 @@ describe('transactions', function() { assert.equal(docs.length, 1); assert.equal(docs[0].name, 'test'); }); + + it('transaction() retains modified status for documents created outside the transaction (gh-13973)', async function() { + db.deleteModel(/Test/); + const Test = db.model('Test', Schema({ status: String })); + + await Test.createCollection(); + await Test.deleteMany({}); + + const { _id } = await Test.create({ status: 'test' }); + const doc = await Test.findById(_id); + + let i = 0; + await db.transaction(async(session) => { + doc.status = 'test2'; + assert.ok(doc.$isModified('status')); + await doc.save({ session }); + if (++i < 3) { + throw new mongoose.mongo.MongoServerError({ + errorLabels: ['TransientTransactionError'] + }); + } + }); + + assert.equal(i, 3); + }); }); From 6c4d836461378d1737340f59f36e6665281c566b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 29 Oct 2023 13:54:47 -0400 Subject: [PATCH 052/191] fix(document): handle #9396 case by only applying #13973 logic if in transaction --- lib/document.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/document.js b/lib/document.js index 9b58a931e63..592268611c6 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1475,13 +1475,15 @@ Document.prototype.$set = function $set(path, val, type, options) { this.$__set(pathToMark, path, options, constructing, parts, schema, val, priorVal); + const isInTransaction = !!this.$__.session?.transaction; + const isModifiedWithinTransaction = this.$__.session && + this.$__.session[sessionNewDocuments] && + this.$__.session[sessionNewDocuments].has(this) && + this.$__.session[sessionNewDocuments].get(this).modifiedPaths && + !this.$__.session[sessionNewDocuments].get(this).modifiedPaths.has(savedStatePath); if (savedState != null && savedState.hasOwnProperty(savedStatePath) && - this.$__.session && - this.$__.session[sessionNewDocuments] && - this.$__.session[sessionNewDocuments].has(this) && - this.$__.session[sessionNewDocuments].get(this).modifiedPaths && - !this.$__.session[sessionNewDocuments].get(this).modifiedPaths.has(savedStatePath) && + (!isInTransaction || isModifiedWithinTransaction) && utils.deepEqual(val, savedState[savedStatePath])) { this.unmarkModified(path); } From a5e4ec26dc4cd2b19c54a4fc7317e72f64b21c46 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 30 Oct 2023 12:37:32 -0400 Subject: [PATCH 053/191] Update test/schema.test.js Co-authored-by: hasezoey --- test/schema.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/schema.test.js b/test/schema.test.js index a0afbc90aef..e4e2e1914e7 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -3183,7 +3183,7 @@ describe('schema', function() { }); base.discriminator(1, recursive); - const TestModel = db.model('Test', base); + const TestModel = db.model('gh13978', base); const doc = new TestModel({ type: 1, self: { type: 1 } }); assert.strictEqual(doc.self.type, 1); From a1dc45bdf923b32ff6f09abbdc7e24f2165d0a83 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 30 Oct 2023 12:42:12 -0400 Subject: [PATCH 054/191] test: modify test name re: code review comments --- test/docs/transactions.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index 9ddd4f28df9..d196fab7180 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -397,7 +397,7 @@ describe('transactions', function() { assert.equal(docs[0].name, 'test'); }); - it('transaction() retains modified status for documents created outside the transaction (gh-13973)', async function() { + it('transaction() retains modified status for documents created outside of the transaction then modified inside the transaction (gh-13973)', async function() { db.deleteModel(/Test/); const Test = db.model('Test', Schema({ status: String })); From 4ff1916ef0833bfd8a2d70e3ca71719e57869cf9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 30 Oct 2023 18:08:32 -0400 Subject: [PATCH 055/191] chore: release 7.6.4 --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07cffa69145..1749f7871e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +7.6.4 / 2023-10-30 +================== + * fix(connection): retain modified status for documents created outside a transaction during transaction retries #14017 #13973 + * fix(schema): handle recursive schemas in discriminator definitions #14011 #13978 + * fix: handle casting $or underneath $elemMatch #14007 #13974 + * fix(populate): allow using options: { strictPopulate: false } to disable strict populate #13863 + * docs: fix differences between sample codes and documentation #13998 [suzuki](https://github.com/suzuki) + * docs: fix missing import and change wrong variable name #13992 [suzuki](https://github.com/suzuki) + 7.6.3 / 2023-10-17 ================== * fix(populate): handle multiple spaces when specifying paths to populate using space-delimited paths #13984 #13951 diff --git a/package.json b/package.json index 0d04d2975e4..78854480792 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.3", + "version": "7.6.4", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From f73ee6681b11210e6586eadac44ebe2af16df936 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 30 Oct 2023 18:21:39 -0400 Subject: [PATCH 056/191] docs: add 7.x publish script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 78854480792..ebe75d0e6dc 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "docs:prepare:publish:stable": "npm run docs:checkout:gh-pages && npm run docs:merge:stable && npm run docs:clean:stable && npm run docs:generate && npm run docs:generate:search", "docs:prepare:publish:5x": "npm run docs:checkout:5x && npm run docs:merge:5x && npm run docs:clean:stable && npm run docs:generate && npm run docs:copy:tmp && npm run docs:checkout:gh-pages && npm run docs:copy:tmp:5x", "docs:prepare:publish:6x": "npm run docs:checkout:6x && npm run docs:merge:6x && npm run docs:clean:stable && env DOCS_DEPLOY=true npm run docs:generate && npm run docs:move:6x:tmp && npm run docs:checkout:gh-pages && npm run docs:copy:tmp:6x", + "docs:prepare:publish:7x": "npm run docs:checkout:gh-pages && git merge 7.x && npm run docs:clean:stable && npm run docs:generate && npm run docs:generate:search", "docs:check-links": "blc http://127.0.0.1:8089 -ro", "lint": "eslint .", "lint-js": "eslint . --ext .js --ext .cjs", From be441a0300c2b88737e8c374f53b511c05d94c98 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 6 Apr 2023 12:09:06 -0400 Subject: [PATCH 057/191] fix(schema): fix dangling reference to virtual in `tree` after `removeVirtual()` Fix #13085 --- lib/schema.js | 5 ++++ test/model.populate.test.js | 55 +++++++++++++++++++++++++++++++++++++ test/schema.test.js | 6 +++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/lib/schema.js b/lib/schema.js index fb5b0277d52..791a1b9253c 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -2334,6 +2334,11 @@ Schema.prototype.removeVirtual = function(path) { for (const virtual of path) { delete this.paths[virtual]; delete this.virtuals[virtual]; + if (virtual.indexOf('.') !== -1) { + mpath.unset(virtual, this.tree); + } else { + delete this.tree[virtual]; + } } } return this; diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 03aed108234..9826093facb 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -10966,6 +10966,61 @@ describe('model: populate:', function() { assert.equal(person.stories[0].title, 'Casino Royale'); }); + it('supports removing and then recreating populate virtual using schema clone (gh-13085)', async function() { + const personSch = new mongoose.Schema( + { + firstName: { type: mongoose.SchemaTypes.String, required: true }, + surname: { type: mongoose.SchemaTypes.String, trim: true }, + nat: { type: mongoose.SchemaTypes.String, required: true, uppercase: true, minLength: 2, maxLength: 2 } + }, + { strict: true, timestamps: true } + ); + personSch.virtual('nationality', { + localField: 'nat', + foreignField: 'key', + ref: 'Nat', + justOne: true + }); + let Person = db.model('Person', personSch.clone(), 'people'); + + const natSch = new mongoose.Schema( + { + key: { type: mongoose.SchemaTypes.String, uppercase: true, index: true, minLength: 2, maxLength: 2 }, + desc: { type: mongoose.SchemaTypes.String, trim: true } + }, + { strict: true } + ); + const Nat = db.model('Nat', natSch); + let n = new Nat({ key: 'ES', desc: 'Spain' }); + await n.save(); + n = new Nat({ key: 'IT', desc: 'Italy' }); + await n.save(); + n = new Nat({ key: 'FR', desc: 'French' }); + await n.save(); + + let p = new Person({ firstName: 'Pepe', surname: 'Pérez', nat: 'it' }); + await p.save(); + p = new Person({ firstName: 'Paco', surname: 'Matinez', nat: 'es' }); + await p.save(); + p = new Person({ firstName: 'John', surname: 'Doe', nat: 'us' }); + await p.save(); + + personSch.removeVirtual('nationality'); + personSch.virtual('nationality', { + localField: 'nat', + foreignField: 'key', + ref: 'Nat', + justOne: true + }); + Person = db.model('Person', personSch.clone(), 'people', { overwriteModels: true }); + + const peopleList = await Person.find(). + sort({ firstName: 1 }). + populate({ path: 'nationality', match: { desc: 'Spain' } }); + assert.deepStrictEqual(peopleList.map(p => p.nationality?.key), [undefined, 'ES', undefined]); + + }); + describe('strictPopulate', function() { it('reports full path when throwing `strictPopulate` error with deep populate (gh-10923)', async function() { diff --git a/test/schema.test.js b/test/schema.test.js index ae19dbe4564..eae0035cd32 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -3016,9 +3016,13 @@ describe('schema', function() { assert.ok(schema.virtuals.foo); schema.removeVirtual('foo'); assert.ok(!schema.virtuals.foo); + assert.ok(!schema.tree.foo); + + schema.virtual('foo').get(v => v || 99); + const Test = db.model('gh-8397', schema); const doc = new Test({ name: 'Test' }); - assert.equal(doc.foo, undefined); + assert.equal(doc.foo, 99); }); it('should allow deleting multiple virtuals gh-8397', async function() { From ad5b21e4124d57959c319603d26fe96bfcb175c1 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 31 Oct 2023 07:03:05 -0400 Subject: [PATCH 058/191] docs(migrating_to_7): add note about requiring `new` with `ObjectId` Fix #14020 --- docs/migrating_to_7.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/migrating_to_7.md b/docs/migrating_to_7.md index 0a294266c6e..d9526322f4b 100644 --- a/docs/migrating_to_7.md +++ b/docs/migrating_to_7.md @@ -15,6 +15,7 @@ If you're still on Mongoose 5.x, please read the [Mongoose 5.x to 6.x migration * [Removed `remove()`](#removed-remove) * [Dropped callback support](#dropped-callback-support) * [Removed `update()`](#removed-update) +* [ObjectId requires `new`](#objectid-requires-new) * [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) @@ -178,6 +179,23 @@ await Model.updateOne(filter, update); await doc.updateOne(update); ``` +

ObjectId requires new

+ +In Mongoose 6 and older, you could define a new ObjectId without using the `new` keyword: + +```javascript +// Works in Mongoose 6 +// Throws "Class constructor ObjectId cannot be invoked without 'new'" in Mongoose 7 +const oid = mongoose.Types.ObjectId('0'.repeat(24)); +``` + +In Mongoose 7, `ObjectId` is now a [JavaScript class](https://masteringjs.io/tutorials/fundamentals/class), so you need to use the `new` keyword. + +```javascript +// Works in Mongoose 6 and Mongoose 7 +const oid = new mongoose.Types.ObjectId('0'.repeat(24)); +``` +

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 c9842d3c5bc841261d104327a7735ebb7a24fabb Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Wed, 1 Nov 2023 17:45:45 -0400 Subject: [PATCH 059/191] fix: diffIndexes treats namespace error as empty --- lib/model.js | 7 ++++++- test/model.indexes.test.js | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index dd59c39d8dc..8b0a99e05b6 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1516,7 +1516,12 @@ Model.diffIndexes = async function diffIndexes() { const model = this; - let dbIndexes = await model.listIndexes(); + let dbIndexes = await model.listIndexes().catch(err => { + if (err.codeName == 'NamespaceNotFound') { + return undefined; + } + throw err; + }); if (dbIndexes === undefined) { dbIndexes = []; } diff --git a/test/model.indexes.test.js b/test/model.indexes.test.js index ed25a0818db..d94935ebff1 100644 --- a/test/model.indexes.test.js +++ b/test/model.indexes.test.js @@ -714,5 +714,15 @@ describe('model', function() { assert.deepStrictEqual(result.toDrop, ['age_1', 'weight_1']); assert.deepStrictEqual(result.toCreate, [{ password: 1 }, { email: 1 }]); }); + + it('running diffIndexes with a non-existent collection should not throw an error (gh-14010)', async function() { + const testSchema = new mongoose.Schema({ + name: String + }); + + const Test = db.model('gh14010', testSchema); + const res = await Test.diffIndexes(); + assert.ok(res); + }); }); }); From 70a1a6f6ff1f0d1f2e300f21e979154d6b2752c1 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 5 Nov 2023 12:38:02 -0500 Subject: [PATCH 060/191] fix(ChangeStream): correctly handle `hydrate` option when using change stream as stream instead of iterator Fix #14049 --- lib/cursor/ChangeStream.js | 3 +++ test/model.test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/cursor/ChangeStream.js b/lib/cursor/ChangeStream.js index 72b844ac67d..24c2f55665a 100644 --- a/lib/cursor/ChangeStream.js +++ b/lib/cursor/ChangeStream.js @@ -83,6 +83,9 @@ class ChangeStream extends EventEmitter { if (ev === 'error' && this.closed) { return; } + if (data != null && data.fullDocument != null && this.options && this.options.hydrate) { + data.fullDocument = this.options.model.hydrate(data.fullDocument); + } this.emit(ev, data); }); }); diff --git a/test/model.test.js b/test/model.test.js index 4cf5198eb83..2ac7e412090 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -5364,6 +5364,32 @@ describe('Model', function() { assert.equal(changeData.fullDocument.get('name'), 'Tony Stark'); }); + it('fullDocument with immediate watcher and hydrate (gh-14049)', async function() { + const MyModel = db.model('Test', new Schema({ name: String })); + + const doc = await MyModel.create({ name: 'Ned Stark' }); + + const changes = []; + const p = new Promise((resolve) => { + MyModel.watch([], { + fullDocument: 'updateLookup', + hydrate: true + }).on('change', change => { + changes.push(change); + resolve(change); + }); + }); + + await MyModel.updateOne({ _id: doc._id }, { name: 'Tony Stark' }); + + const changeData = await p; + assert.equal(changeData.operationType, 'update'); + assert.equal(changeData.fullDocument._id.toHexString(), + doc._id.toHexString()); + assert.ok(changeData.fullDocument.$__); + assert.equal(changeData.fullDocument.get('name'), 'Tony Stark'); + }); + it('respects discriminators (gh-11007)', async function() { const BaseModel = db.model('Test', new Schema({ name: String })); const ChildModel = BaseModel.discriminator('Test1', new Schema({ email: String })); From af49bbeb057f92bf23b091e7679a2c439a54908d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 5 Nov 2023 12:41:38 -0500 Subject: [PATCH 061/191] test: fix tests re: #13085 --- test/model.populate.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 9826093facb..b44ef265f2d 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -11017,7 +11017,7 @@ describe('model: populate:', function() { const peopleList = await Person.find(). sort({ firstName: 1 }). populate({ path: 'nationality', match: { desc: 'Spain' } }); - assert.deepStrictEqual(peopleList.map(p => p.nationality?.key), [undefined, 'ES', undefined]); + assert.deepStrictEqual(peopleList.map(p => p.nationality.key), [undefined, 'ES', undefined]); }); From 0077c5fc797d8eab3b4a3d4f9a91485eb8d3f058 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 5 Nov 2023 12:44:48 -0500 Subject: [PATCH 062/191] test: correctly work around lack of elvis operator in node v2 for #13085 test --- test/model.populate.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/model.populate.test.js b/test/model.populate.test.js index b44ef265f2d..2a9bd4b8b58 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -11017,8 +11017,10 @@ describe('model: populate:', function() { const peopleList = await Person.find(). sort({ firstName: 1 }). populate({ path: 'nationality', match: { desc: 'Spain' } }); - assert.deepStrictEqual(peopleList.map(p => p.nationality.key), [undefined, 'ES', undefined]); - + assert.deepStrictEqual( + peopleList.map(p => p.personality ? p.nationality.key : undefined), + [undefined, 'ES', undefined] + ); }); From c9136f50cd64a0315b99bded0e2e6d46229db776 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 5 Nov 2023 12:48:16 -0500 Subject: [PATCH 063/191] test: typo fix --- test/model.populate.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 2a9bd4b8b58..78a6c2108bb 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -11018,7 +11018,7 @@ describe('model: populate:', function() { sort({ firstName: 1 }). populate({ path: 'nationality', match: { desc: 'Spain' } }); assert.deepStrictEqual( - peopleList.map(p => p.personality ? p.nationality.key : undefined), + peopleList.map(p => p.nationality ? p.nationality.key : undefined), [undefined, 'ES', undefined] ); }); From c7a9eb61d14623e8fd55b9dda91b26999cda52c0 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 5 Nov 2023 14:12:22 -0500 Subject: [PATCH 064/191] fix(document): avoid unmarking modified on nested path if no initial value stored and already modified Fix #14022 --- lib/document.js | 5 ++++- test/document.test.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index cee384fcf7c..d7886165a72 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1204,6 +1204,7 @@ Document.prototype.$set = function $set(path, val, type, options) { this.invalidate(path, new MongooseError.CastError('Object', val, path)); return this; } + const wasModified = this.$isModified(path); const hasInitialVal = this.$__.savedState != null && this.$__.savedState.hasOwnProperty(path); if (this.$__.savedState != null && !this.$isNew && !this.$__.savedState.hasOwnProperty(path)) { const initialVal = this.$__getValue(path); @@ -1228,7 +1229,9 @@ Document.prototype.$set = function $set(path, val, type, options) { for (const key of keys) { this.$set(path + '.' + key, val[key], constructing, options); } - if (priorVal != null && utils.deepEqual(hasInitialVal ? this.$__.savedState[path] : priorVal, val)) { + if (priorVal != null && + (!wasModified || hasInitialVal) && + utils.deepEqual(hasInitialVal ? this.$__.savedState[path] : priorVal, val)) { this.unmarkModified(path); } else { this.markModified(path); diff --git a/test/document.test.js b/test/document.test.js index f81adace7dc..68faff11b9c 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -9239,6 +9239,49 @@ describe('document', function() { assert.ok(foo.isModified('subdoc.bar')); }); + it('does not unmark modified if there is no initial value (gh-9396)', async function() { + const IClientSchema = new Schema({ + jwt: { + token_crypt: { type: String, template: false, maxSize: 8 * 1024 }, + token_salt: { type: String, template: false } + } + }); + + const encrypt = function(doc, path, value) { + doc.set(path + '_crypt', value + '_crypt'); + doc.set(path + '_salt', value + '_salt'); + }; + + const decrypt = function(doc, path) { + return doc.get(path + '_crypt').replace('_crypt', ''); + }; + + IClientSchema.virtual('jwt.token') + .get(function() { + return decrypt(this, 'jwt.token'); + }) + .set(function(value) { + encrypt(this, 'jwt.token', value); + }); + + + const iclient = db.model('Test', IClientSchema); + const test = new iclient({ + jwt: { + token: 'firstToken' + } + }); + + await test.save(); + const entry = await iclient.findById(test._id).orFail(); + entry.set('jwt.token', 'secondToken'); + entry.set(entry.toJSON()); + await entry.save(); + + const { jwt } = await iclient.findById(test._id).orFail(); + assert.strictEqual(jwt.token, 'secondToken'); + }); + it('correctly tracks saved state for deeply nested objects (gh-10773) (gh-9396)', async function() { const PaymentSchema = Schema({ status: String }, { _id: false }); const OrderSchema = new Schema({ From cc75c7bd91dba3641d996bf4865a0d0ebc3ebb5b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 5 Nov 2023 14:19:51 -0500 Subject: [PATCH 065/191] test: fix tests --- test/model.populate.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 9826093facb..0007f797610 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -11017,8 +11017,10 @@ describe('model: populate:', function() { const peopleList = await Person.find(). sort({ firstName: 1 }). populate({ path: 'nationality', match: { desc: 'Spain' } }); - assert.deepStrictEqual(peopleList.map(p => p.nationality?.key), [undefined, 'ES', undefined]); - + assert.deepStrictEqual(peopleList.map( + p => p.nationality ? p.nationality.key : undefined), + [undefined, 'ES', undefined] + ); }); From 4bd592703f9f4930fff5f3270ea36a998cbc2690 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 5 Nov 2023 15:38:30 -0500 Subject: [PATCH 066/191] fix(document): consistently avoid marking subpaths of nested paths as modified Fix #14022 --- lib/document.js | 2 +- test/document.test.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index d7886165a72..1143a1d37cc 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1227,7 +1227,7 @@ Document.prototype.$set = function $set(path, val, type, options) { this.$__setValue(path, {}); for (const key of keys) { - this.$set(path + '.' + key, val[key], constructing, options); + this.$set(path + '.' + key, val[key], constructing, { ...options, _skipMarkModified: true }); } if (priorVal != null && (!wasModified || hasInitialVal) && diff --git a/test/document.test.js b/test/document.test.js index 68faff11b9c..e6b343dba2f 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -3731,6 +3731,11 @@ describe('document', function() { assert.deepEqual( kitty.modifiedPaths(), + ['surnames'] + ); + + assert.deepEqual( + kitty.modifiedPaths({ includeChildren: true }), ['surnames', 'surnames.docarray'] ); }); @@ -12355,6 +12360,29 @@ describe('document', function() { const nestedProjectionDoc = await User.findOne({}, { name: 1, 'sub.propertyA': 1, 'sub.propertyB': 1 }); assert.strictEqual(nestedProjectionDoc.sub.propertyA, 'A'); }); + + it('avoids adding nested paths to markModified() output if adding a new field (gh-14024)', async function() { + const eventSchema = new Schema({ + name: { type: String }, + __stateBeforeSuspension: { + field1: { type: String }, + field2: { type: String }, + jsonField: { + name: { type: String }, + name1: { type: String } + } + } + }); + const Event = db.model('Event', eventSchema); + const eventObj = new Event({ name: 'event object', __stateBeforeSuspension: { field1: 'test', jsonField: { name: 'test3' } } }); + await eventObj.save(); + const newObject = { field1: 'test', jsonField: { name: 'test3', name1: 'test4' } }; + eventObj.set('__stateBeforeSuspension', newObject); + assert.deepEqual( + eventObj.modifiedPaths(), + ['__stateBeforeSuspension', '__stateBeforeSuspension.jsonField'] + ); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() { From fd781c14da84efaa1adfa7222859297da84e2f74 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 6 Nov 2023 12:45:27 -0500 Subject: [PATCH 067/191] test: remove unused var re: code review comments --- test/model.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/model.test.js b/test/model.test.js index 2ac7e412090..700dfb87502 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -5369,13 +5369,11 @@ describe('Model', function() { const doc = await MyModel.create({ name: 'Ned Stark' }); - const changes = []; const p = new Promise((resolve) => { MyModel.watch([], { fullDocument: 'updateLookup', hydrate: true }).on('change', change => { - changes.push(change); resolve(change); }); }); From 3dce0341cafc5d854722a13e39dbc9e3585297fb Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 7 Nov 2023 12:50:30 -0500 Subject: [PATCH 068/191] chore: release 6.12.3 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d711eb97a5f..70d84ba4379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +6.12.3 / 2023-11-07 +=================== + * fix(ChangeStream): correctly handle hydrate option when using change stream as stream instead of iterator #14052 + * fix(schema): fix dangling reference to virtual in tree after `removeVirtual()` #14019 #13085 + * fix(document): avoid unmarking modified on nested path if no initial value stored and already modified #14053 #14024 + * fix(document): consistently avoid marking subpaths of nested paths as modified #14053 #14022 + 6.12.2 / 2023-10-25 =================== * fix: add fullPath to ValidatorProps #13995 [Freezystem](https://github.com/Freezystem) diff --git a/package.json b/package.json index 82a5e705477..9c5d4d94955 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.12.2", + "version": "6.12.3", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 6760c54601129dec14289936b44d3a84a44ea8a5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 7 Nov 2023 13:06:38 -0500 Subject: [PATCH 069/191] chore: publish 6.x under 6x tag --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9c5d4d94955..17d4fb86464 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "prepublishOnly": "npm run build-browser", "release": "git pull && git push origin master --tags && npm publish", "release-5x": "git pull origin 5.x && git push origin 5.x && git push origin 5.x --tags && npm publish --tag 5x", - "release-6x": "git pull origin 6.x && git push origin 6.x && git push origin 6.x --tags && npm publish --tag legacy", + "release-6x": "git pull origin 6.x && git push origin 6.x && git push origin 6.x --tags && npm publish --tag 6x", "mongo": "node ./tools/repl.js", "test": "mocha --exit ./test/*.test.js", "test-deno": "deno run --allow-env --allow-read --allow-net --allow-run --allow-sys --allow-write ./test/deno.js", From 6336ed6d2d7c5a14007a543e5db0e3b2e26d9adb Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:53:56 -0500 Subject: [PATCH 070/191] one failing test --- lib/schema.js | 3 ++- lib/schema/SubdocumentPath.js | 1 - test/schema.test.js | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index a4b6e6c2d54..544c2a72bca 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -412,8 +412,9 @@ Schema.prototype._clone = function _clone(Constructor) { s.s.hooks = this.s.hooks.clone(); s.tree = clone(this.tree); + // recursion is triggered here s.paths = Object.fromEntries( - Object.entries(this.paths).map(([key, value]) => ([key, value.clone()])) + Object.entries(this.paths).map(([key, value]) => ([key, value])) ); s.nested = clone(this.nested); s.subpaths = clone(this.subpaths); diff --git a/lib/schema/SubdocumentPath.js b/lib/schema/SubdocumentPath.js index 24f225e94b4..2edba587321 100644 --- a/lib/schema/SubdocumentPath.js +++ b/lib/schema/SubdocumentPath.js @@ -55,7 +55,6 @@ function SubdocumentPath(schema, path, options) { this.$isSingleNested = true; this.base = schema.base; SchemaType.call(this, path, options, 'Embedded'); - if (schema._applyDiscriminators != null && !options?._skipApplyDiscriminators) { for (const disc of schema._applyDiscriminators.keys()) { this.discriminator(disc, schema._applyDiscriminators.get(disc)); diff --git a/test/schema.test.js b/test/schema.test.js index e4e2e1914e7..0df19a65790 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -3188,4 +3188,18 @@ describe('schema', function() { const doc = new TestModel({ type: 1, self: { type: 1 } }); assert.strictEqual(doc.self.type, 1); }); + it('handles recursive definitions of arrays in discriminators (gh-14055)', function() { + const base = new Schema({ + type: { type: Number, required: true } + }, { discriminatorKey: 'type' }); + + const recursive = new Schema({ + self: { type: [base], required: true } + }); + + base.discriminator(1, recursive); + const baseModel = db.model('gh14055', base); + const doc = new baseModel({ type: 1, self: [{ type: 1 }] }); + assert.equal(doc.self[0].type, 1); + }); }); From 14c9c44bcf2490fab451b8d528099f7903781fd2 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Wed, 8 Nov 2023 18:04:25 -0500 Subject: [PATCH 071/191] Update schema.js --- lib/schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/schema.js b/lib/schema.js index 544c2a72bca..863cc7a2331 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -414,7 +414,7 @@ Schema.prototype._clone = function _clone(Constructor) { s.tree = clone(this.tree); // recursion is triggered here s.paths = Object.fromEntries( - Object.entries(this.paths).map(([key, value]) => ([key, value])) + Object.entries(this.paths).map(([key, value]) => ([key, value.$isSingleNested ? value.clone() : value])) ); s.nested = clone(this.nested); s.subpaths = clone(this.subpaths); From 855cee0c1ea371274db94a63468fd698636d184e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 9 Nov 2023 14:32:34 -0500 Subject: [PATCH 072/191] fix: handle update validators and single nested doc with numberic paths Fix #13977 --- lib/schema.js | 2 +- test/model.updateOne.test.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/schema.js b/lib/schema.js index a4b6e6c2d54..1e234064446 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -2577,7 +2577,7 @@ Schema.prototype._getSchema = function(path) { if (parts[p] === '$' || isArrayFilter(parts[p])) { if (p + 1 === parts.length) { // comments.$ - return foundschema; + return foundschema.$embeddedSchemaType; } // comments.$.comments.$.title ret = search(parts.slice(p + 1), foundschema.schema); diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index 138b2ce43cb..8eab40a6a6b 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -3069,6 +3069,22 @@ describe('model: updateOne: ', function() { const doc = await Test.findById(_id); assert.equal(doc.$myKey, 'gh13786'); }); + it('works with update validators and single nested doc with numberic paths (gh-13977)', async function() { + const subdoc = new mongoose.Schema({ + 1: { type: String, required: true, validate: () => true } + }); + const schema = new mongoose.Schema({ subdoc }); + const Test = db.model('Test', schema); + + const _id = new mongoose.Types.ObjectId(); + await Test.updateOne( + { _id }, + { subdoc: { 1: 'foobar' } }, + { upsert: true, runValidators: true } + ); + const doc = await Test.findById(_id); + assert.equal(doc.subdoc['1'], 'foobar'); + }); }); async function delay(ms) { From fd94cd1db3a4e7f88b0ddad3bb1d75c98975e43d Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Thu, 9 Nov 2023 15:36:52 -0500 Subject: [PATCH 073/191] Update schema.js --- lib/schema.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index 863cc7a2331..a21f845699d 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -412,9 +412,8 @@ Schema.prototype._clone = function _clone(Constructor) { s.s.hooks = this.s.hooks.clone(); s.tree = clone(this.tree); - // recursion is triggered here s.paths = Object.fromEntries( - Object.entries(this.paths).map(([key, value]) => ([key, value.$isSingleNested ? value.clone() : value])) + Object.entries(this.paths).map(([key, value]) => ([key, !value.$isMongooseDocumentArray ? value.clone() : value])) ); s.nested = clone(this.nested); s.subpaths = clone(this.subpaths); From ab360ff6be0ed41b764d4933c64746bf7c6540bc Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:11:39 -0500 Subject: [PATCH 074/191] true fix --- lib/schema.js | 2 +- lib/schema/documentarray.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index a21f845699d..a4b6e6c2d54 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -413,7 +413,7 @@ Schema.prototype._clone = function _clone(Constructor) { s.tree = clone(this.tree); s.paths = Object.fromEntries( - Object.entries(this.paths).map(([key, value]) => ([key, !value.$isMongooseDocumentArray ? value.clone() : value])) + Object.entries(this.paths).map(([key, value]) => ([key, value.clone()])) ); s.nested = clone(this.nested); s.subpaths = clone(this.subpaths); diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js index a15a1ca2e79..b0cef6401a2 100644 --- a/lib/schema/documentarray.js +++ b/lib/schema/documentarray.js @@ -89,7 +89,7 @@ function DocumentArrayPath(key, schema, options, schemaOptions) { this.$embeddedSchemaType.caster = this.Constructor; this.$embeddedSchemaType.schema = this.schema; - if (schema._applyDiscriminators != null) { + if (schema._applyDiscriminators != null && !options?._skipApplyDiscriminators) { for (const disc of schema._applyDiscriminators.keys()) { this.discriminator(disc, schema._applyDiscriminators.get(disc)); } @@ -528,7 +528,7 @@ DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) { DocumentArrayPath.prototype.clone = function() { const options = Object.assign({}, this.options); - const schematype = new this.constructor(this.path, this.schema, options, this.schemaOptions); + const schematype = new this.constructor(this.path, this.schema, { ...options, _skipApplyDiscriminators: true }, this.schemaOptions); schematype.validators = this.validators.slice(); if (this.requiredValidator !== undefined) { schematype.requiredValidator = this.requiredValidator; From 13150a3c8362f1f23038d25678b5a56900d6e2a5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 14 Nov 2023 16:13:17 -0500 Subject: [PATCH 075/191] chore: release 7.6.5 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24deaef35b8..282262f803b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +7.6.5 / 2023-11-14 +================== + * fix: handle update validators and single nested doc with numeric paths #14066 #13977 + * fix: handle recursive schema array in discriminator definition #14068 #14055 + * fix: diffIndexes treats namespace error as empty #14048 #14029 + * docs(migrating_to_7): add note about requiring new with ObjectId #14021 #14020 + 6.12.3 / 2023-11-07 =================== * fix(ChangeStream): correctly handle hydrate option when using change stream as stream instead of iterator #14052 diff --git a/package.json b/package.json index 980435bb8d1..27e3842393e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.4", + "version": "7.6.5", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From c6f11215a4ad9e5cdb995ebc86e2a109b134c684 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 14 Nov 2023 16:21:31 -0500 Subject: [PATCH 076/191] chore: add publish script for 7.x --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 27e3842393e..a7d13fbdada 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "release-5x": "git pull origin 5.x && git push origin 5.x && git push origin 5.x --tags && npm publish --tag 5x", "release-6x": "git pull origin 6.x && git push origin 6.x && git push origin 6.x --tags && npm publish --tag 6x", "mongo": "node ./tools/repl.js", + "publish-7x": "npm publish --tag 7x", "test": "mocha --exit ./test/*.test.js", "test-deno": "deno run --allow-env --allow-read --allow-net --allow-run --allow-sys --allow-write ./test/deno.js", "test-rs": "START_REPLICA_SET=1 mocha --timeout 30000 --exit ./test/*.test.js", From db7a9838d553823f40482fd078f2d95083a8bacf Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 14 Nov 2023 16:40:34 -0500 Subject: [PATCH 077/191] chore: improve 7.x docs deploy script --- package.json | 2 +- scripts/website.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index a7d13fbdada..7373405da55 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "docs:prepare:publish:stable": "npm run docs:checkout:gh-pages && npm run docs:merge:stable && npm run docs:clean:stable && npm run docs:generate && npm run docs:generate:search", "docs:prepare:publish:5x": "npm run docs:checkout:5x && npm run docs:merge:5x && npm run docs:clean:stable && npm run docs:generate && npm run docs:copy:tmp && npm run docs:checkout:gh-pages && npm run docs:copy:tmp:5x", "docs:prepare:publish:6x": "npm run docs:checkout:6x && npm run docs:merge:6x && npm run docs:clean:stable && env DOCS_DEPLOY=true npm run docs:generate && npm run docs:move:6x:tmp && npm run docs:checkout:gh-pages && npm run docs:copy:tmp:6x", - "docs:prepare:publish:7x": "npm run docs:checkout:gh-pages && git merge 7.x && npm run docs:clean:stable && npm run docs:generate && npm run docs:generate:search", + "docs:prepare:publish:7x": "git checkout 7.x && npm run docs:clean:stable && env DOCS_DEPLOY=true npm run docs:generate && mv ./docs/7.x ./tmp && npm run docs:checkout:gh-pages && rimraf ./docs/7.x && ncp ./tmp ./docs/7.x", "docs:check-links": "blc http://127.0.0.1:8089 -ro", "lint": "eslint .", "lint-js": "eslint . --ext .js --ext .cjs", diff --git a/scripts/website.js b/scripts/website.js index b1ea3eb0c56..7f6a8044788 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -210,6 +210,7 @@ const versionObj = (() => { currentVersion: getCurrentVersion(), latestVersion: getLatestVersion(), pastVersions: [ + getLatestVersionOf(7), getLatestVersionOf(6), getLatestVersionOf(5), ] From 7f935cf58ac2be0783606a121bce6744ab4b1efc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 14 Nov 2023 16:56:06 -0500 Subject: [PATCH 078/191] chore: use 8.x as default search version --- docs/js/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/js/search.js b/docs/js/search.js index f154dbbb514..2725d686144 100644 --- a/docs/js/search.js +++ b/docs/js/search.js @@ -2,7 +2,7 @@ const root = 'https://mongoosejs.azurewebsites.net/api'; -const defaultVersion = '7.x'; +const defaultVersion = '8.x'; const versionFromUrl = window.location.pathname.match(/^\/docs\/(\d+\.x)/); const version = versionFromUrl ? versionFromUrl[1] : defaultVersion; From fd407997e49ca2c9fb2663e1faab6d6ae96ca43c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 15 Nov 2023 11:26:30 -0500 Subject: [PATCH 079/191] chore: fix docs search generation for 8.x release --- scripts/generateSearch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generateSearch.js b/scripts/generateSearch.js index e98421bdad7..d2da68065d8 100644 --- a/scripts/generateSearch.js +++ b/scripts/generateSearch.js @@ -134,7 +134,7 @@ async function run() { await Content.deleteMany({ version }); let count = 0; for (const content of contents) { - if (version === '7.x') { + if (version === '8.x') { let url = content.url.startsWith('/') ? content.url : `/${content.url}`; if (!url.startsWith('/docs')) { url = '/docs' + url; From 8fb5ecec36d0091761cf61ca4af306f6060f20fa Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 15 Nov 2023 11:31:00 -0500 Subject: [PATCH 080/191] chore: quick fix for 7.x docs build --- scripts/generateSearch.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/generateSearch.js b/scripts/generateSearch.js index d2da68065d8..d8cb16f104b 100644 --- a/scripts/generateSearch.js +++ b/scripts/generateSearch.js @@ -141,8 +141,11 @@ async function run() { } content.url = url; } else { - const url = content.url.startsWith('/') ? content.url : `/${content.url}`; - content.url = `/docs/${version}/docs${url}`; + let url = content.url.startsWith('/') ? content.url : `/${content.url}`; + if (!url.startsWith('/docs')) { + url = '/docs' + url; + } + content.url = `/docs/${version}${url}`; } console.log(`${++count} / ${contents.length}`); await content.save(); From a62b0ec4bf4521f118d3abb6f94ce9b7b3294ef1 Mon Sep 17 00:00:00 2001 From: Lorand Horvath <72015221+lorand-horvath@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:18:55 +0200 Subject: [PATCH 081/191] Bump mongodb driver to 5.9.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7373405da55..cc6a75c1362 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "bson": "^5.5.0", "kareem": "2.5.1", - "mongodb": "5.9.0", + "mongodb": "5.9.1", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", From d8f94f9ffcf90702ea14c435285b4f52ab91f01f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 16 Nov 2023 18:00:48 -0500 Subject: [PATCH 082/191] types(model): support calling `Model.validate()` with `pathsToSkip` option Re: #14003 --- types/models.d.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/types/models.d.ts b/types/models.d.ts index cfa23bc40da..e4d62b3947d 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -461,8 +461,11 @@ declare module 'mongoose' { /** Casts and validates the given object against this model's schema, passing the given `context` to custom validators. */ validate(): Promise; - validate(optional: any): Promise; - validate(optional: any, pathsToValidate: PathsToValidate): Promise; + validate(obj: any): Promise; + validate( + obj: any, + pathsOrOptions: PathsToValidate | { pathsToSkip?: PathsToValidate } + ): Promise; /** Watches the underlying collection for changes using [MongoDB change streams](https://www.mongodb.com/docs/manual/changeStreams/). */ watch(pipeline?: Array>, options?: mongodb.ChangeStreamOptions & { hydrate?: boolean }): mongodb.ChangeStream; From 6ee82c15117f35349b11e5dad9b8b2088738f2e8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 16 Nov 2023 18:05:28 -0500 Subject: [PATCH 083/191] test: add test for #14003 --- test/types/models.test.ts | 8 ++++++++ types/models.d.ts | 6 ++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 7865a5e6855..8bc4033deda 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -751,3 +751,11 @@ function gh13957() { const repository = new RepositoryBase(TestModel); expectType>(repository.insertMany([{ name: 'test' }])); } + +async function gh14003() { + const schema = new Schema({ name: String }); + const TestModel = model('Test', schema); + + await TestModel.validate({ name: 'foo' }, ['name']); + await TestModel.validate({ name: 'foo' }, { pathsToSkip: ['name'] }); +} diff --git a/types/models.d.ts b/types/models.d.ts index e4d62b3947d..b8796641af4 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -462,10 +462,8 @@ declare module 'mongoose' { /** Casts and validates the given object against this model's schema, passing the given `context` to custom validators. */ validate(): Promise; validate(obj: any): Promise; - validate( - obj: any, - pathsOrOptions: PathsToValidate | { pathsToSkip?: PathsToValidate } - ): Promise; + validate(obj: any, pathsOrOptions: PathsToValidate): Promise; + validate(obj: any, pathsOrOptions: { pathsToSkip?: pathsToSkip }): Promise; /** Watches the underlying collection for changes using [MongoDB change streams](https://www.mongodb.com/docs/manual/changeStreams/). */ watch(pipeline?: Array>, options?: mongodb.ChangeStreamOptions & { hydrate?: boolean }): mongodb.ChangeStream; From 0e3b20570e39d60d7959474e74589c9d708c43d5 Mon Sep 17 00:00:00 2001 From: Pratham Vaidya Date: Fri, 17 Nov 2023 04:39:34 +0530 Subject: [PATCH 084/191] Fix: Mongoose types incorrect for when includeResultMetadata: true is set (#14078) * Added Seperate type for Middlewares that support includeResultMetadata * Added Return Type as Raw Result when includeResultMetadata is TRUE for all middlewares * Added Seperate post query type for middlewares that support includeResultMetadata * Added Seperate post query type for middlewares that support includeResultMetadata, Fixed Lint * Updated MongooseRawResultQueryMiddleware and removed duplicate and redundant middlewares * Removed 'findByIdAndRemove' type definations : DEPRECATED, Fixed Linting Errors * Removed test cases for 'findByIdAndRemove', Added test case for 'findOneAndDeleteRes', 'findByIdAndDeleteRes' * Updated findOneAndDelete, Added test case for findOneAndUpdate, findOneAndReplace middlewares --- test/types/middleware.test.ts | 14 ++++++++++++-- test/types/models.test.ts | 13 ++++++++++--- types/index.d.ts | 1 + types/middlewares.d.ts | 4 +++- types/models.d.ts | 32 +++++++------------------------- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/test/types/middleware.test.ts b/test/types/middleware.test.ts index a0f3e53caa1..47fa0ed03d6 100644 --- a/test/types/middleware.test.ts +++ b/test/types/middleware.test.ts @@ -1,4 +1,4 @@ -import { Schema, model, Model, Document, SaveOptions, Query, Aggregate, HydratedDocument, PreSaveMiddlewareFunction } from 'mongoose'; +import { Schema, model, Model, Document, SaveOptions, Query, Aggregate, HydratedDocument, PreSaveMiddlewareFunction, ModifyResult } from 'mongoose'; import { expectError, expectType, expectNotType, expectAssignable } from 'tsd'; const preMiddlewareFn: PreSaveMiddlewareFunction = function(next, opts) { @@ -109,7 +109,17 @@ schema.post>('countDocuments', function(count, next) { }); schema.post>('findOneAndDelete', function(res, next) { - expectType(res); + expectType | null>(res); + next(); +}); + +schema.post>('findOneAndUpdate', function(res, next) { + expectType | null>(res); + next(); +}); + +schema.post>('findOneAndReplace', function(res, next) { + expectType | null>(res); next(); }); diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 7865a5e6855..fe68d015f07 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -673,9 +673,6 @@ async function gh13705() { const findByIdAndDeleteRes = await TestModel.findByIdAndDelete('0'.repeat(24), { lean: true }); expectType(findByIdAndDeleteRes); - const findByIdAndRemoveRes = await TestModel.findByIdAndRemove('0'.repeat(24), { lean: true }); - expectType(findByIdAndRemoveRes); - const findByIdAndUpdateRes = await TestModel.findByIdAndUpdate('0'.repeat(24), {}, { lean: true }); expectType(findByIdAndUpdateRes); @@ -709,6 +706,16 @@ async function gh13746() { expectType(findOneAndUpdateRes.lastErrorObject?.updatedExisting); expectType(findOneAndUpdateRes.lastErrorObject?.upserted); expectType(findOneAndUpdateRes.ok); + + const findOneAndDeleteRes = await TestModel.findOneAndDelete({ _id: '0'.repeat(24) }, { includeResultMetadata: true }); + expectType(findOneAndDeleteRes.lastErrorObject?.updatedExisting); + expectType(findOneAndDeleteRes.lastErrorObject?.upserted); + expectType(findOneAndDeleteRes.ok); + + const findByIdAndDeleteRes = await TestModel.findByIdAndDelete('0'.repeat(24), { includeResultMetadata: true }); + expectType(findByIdAndDeleteRes.lastErrorObject?.updatedExisting); + expectType(findByIdAndDeleteRes.lastErrorObject?.upserted); + expectType(findByIdAndDeleteRes.ok); } function gh13904() { diff --git a/types/index.d.ts b/types/index.d.ts index 3f94524ba98..d8d9e0c629a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -336,6 +336,7 @@ declare module 'mongoose' { post(method: MongooseDistinctDocumentMiddleware|MongooseDistinctDocumentMiddleware[], options: SchemaPostOptions & SchemaPostOptions, fn: PostMiddlewareFunction): this; post(method: MongooseQueryOrDocumentMiddleware | MongooseQueryOrDocumentMiddleware[] | RegExp, options: SchemaPostOptions & { document: true, query: false }, fn: PostMiddlewareFunction): this; // this = Query + post>(method: MongooseRawResultQueryMiddleware|MongooseRawResultQueryMiddleware[], fn: PostMiddlewareFunction | ModifyResult>>): this; post>(method: MongooseDefaultQueryMiddleware|MongooseDefaultQueryMiddleware[], fn: PostMiddlewareFunction>): this; post>(method: MongooseDistinctQueryMiddleware|MongooseDistinctQueryMiddleware[], options: SchemaPostOptions, fn: PostMiddlewareFunction>): this; post>(method: MongooseQueryOrDocumentMiddleware | MongooseQueryOrDocumentMiddleware[] | RegExp, options: SchemaPostOptions & { document: false, query: true }, fn: PostMiddlewareFunction>): this; diff --git a/types/middlewares.d.ts b/types/middlewares.d.ts index 43ca1974b81..9302b9b7d48 100644 --- a/types/middlewares.d.ts +++ b/types/middlewares.d.ts @@ -5,7 +5,9 @@ declare module 'mongoose' { type MongooseDistinctDocumentMiddleware = 'save' | 'init' | 'validate'; type MongooseDocumentMiddleware = MongooseDistinctDocumentMiddleware | MongooseQueryAndDocumentMiddleware; - type MongooseDistinctQueryMiddleware = 'count' | 'estimatedDocumentCount' | 'countDocuments' | 'deleteMany' | 'distinct' | 'find' | 'findOne' | 'findOneAndDelete' | 'findOneAndRemove' | 'findOneAndReplace' | 'findOneAndUpdate' | 'replaceOne' | 'updateMany'; + type MongooseRawResultQueryMiddleware = 'findOneAndUpdate' | 'findOneAndReplace' | 'findOneAndDelete'; + type MongooseDistinctQueryMiddleware = 'estimatedDocumentCount' | 'countDocuments' | 'deleteMany' | 'distinct' | 'find' | 'findOne' | 'findOneAndDelete' | 'findOneAndReplace' | 'findOneAndUpdate' | 'replaceOne' | 'updateMany'; + type MongooseDefaultQueryMiddleware = MongooseDistinctQueryMiddleware | 'updateOne' | 'deleteOne'; type MongooseQueryMiddleware = MongooseDistinctQueryMiddleware | MongooseQueryAndDocumentMiddleware; diff --git a/types/models.d.ts b/types/models.d.ts index cfa23bc40da..ddb28542ed7 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -548,21 +548,9 @@ declare module 'mongoose' { >; findByIdAndDelete( id?: mongodb.ObjectId | any, - options?: QueryOptions | null - ): QueryWithHelpers; - - /** Creates a `findByIdAndRemove` query, filtering by the given `_id`. */ - findByIdAndRemove( - id: mongodb.ObjectId | any, - options: QueryOptions & { lean: true } - ): QueryWithHelpers< - GetLeanResultType | null, - ResultDoc, - TQueryHelpers, - TRawDocType, - 'findOneAndDelete' - >; - findByIdAndRemove( + options?: QueryOptions & { includeResultMetadata: true } + ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndDelete'>; + findByIdAndDelete( id?: mongodb.ObjectId | any, options?: QueryOptions | null ): QueryWithHelpers; @@ -584,11 +572,6 @@ declare module 'mongoose' { update: UpdateQuery, options: QueryOptions & { rawResult: true } ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndUpdate'>; - findByIdAndUpdate( - id: mongodb.ObjectId | any, - update: UpdateQuery, - options: QueryOptions & { includeResultMetadata: true } - ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndUpdate'>; findByIdAndUpdate( id: mongodb.ObjectId | any, update: UpdateQuery, @@ -615,6 +598,10 @@ declare module 'mongoose' { TRawDocType, 'findOneAndDelete' >; + findOneAndDelete( + filter?: FilterQuery, + options?: QueryOptions & { includeResultMetadata: true } + ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndDelete'>; findOneAndDelete( filter?: FilterQuery, options?: QueryOptions | null @@ -643,11 +630,6 @@ declare module 'mongoose' { replacement: TRawDocType | AnyObject, options: QueryOptions & { rawResult: true } ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndReplace'>; - findOneAndReplace( - filter: FilterQuery, - replacement: TRawDocType | AnyObject, - options: QueryOptions & { includeResultMetadata: true } - ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndReplace'>; findOneAndReplace( filter: FilterQuery, replacement: TRawDocType | AnyObject, From e6d2fbe5fa246d6b6e527b79809a2f9130887ed3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 16 Nov 2023 18:19:55 -0500 Subject: [PATCH 085/191] types: fix issues backporting #14078 to 7.x --- types/middlewares.d.ts | 2 +- types/models.d.ts | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/types/middlewares.d.ts b/types/middlewares.d.ts index 9302b9b7d48..5cf3af901c0 100644 --- a/types/middlewares.d.ts +++ b/types/middlewares.d.ts @@ -6,7 +6,7 @@ declare module 'mongoose' { type MongooseDocumentMiddleware = MongooseDistinctDocumentMiddleware | MongooseQueryAndDocumentMiddleware; type MongooseRawResultQueryMiddleware = 'findOneAndUpdate' | 'findOneAndReplace' | 'findOneAndDelete'; - type MongooseDistinctQueryMiddleware = 'estimatedDocumentCount' | 'countDocuments' | 'deleteMany' | 'distinct' | 'find' | 'findOne' | 'findOneAndDelete' | 'findOneAndReplace' | 'findOneAndUpdate' | 'replaceOne' | 'updateMany'; + type MongooseDistinctQueryMiddleware = 'count' | 'estimatedDocumentCount' | 'countDocuments' | 'deleteMany' | 'distinct' | 'find' | 'findOne' | 'findOneAndDelete' | 'findOneAndReplace' | 'findOneAndRemove' | 'findOneAndUpdate' | 'replaceOne' | 'updateMany'; type MongooseDefaultQueryMiddleware = MongooseDistinctQueryMiddleware | 'updateOne' | 'deleteOne'; type MongooseQueryMiddleware = MongooseDistinctQueryMiddleware | MongooseQueryAndDocumentMiddleware; diff --git a/types/models.d.ts b/types/models.d.ts index ddb28542ed7..24ceae501d2 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -555,6 +555,18 @@ declare module 'mongoose' { options?: QueryOptions | null ): QueryWithHelpers; + /** Creates a `findByIdAndRemove` query, filtering by the given `_id`. */ + findByIdAndRemove( + id: mongodb.ObjectId | any, + options: QueryOptions & { lean: true } + ): QueryWithHelpers< + GetLeanResultType | null, + ResultDoc, + TQueryHelpers, + TRawDocType, + 'findOneAndDelete' + >; + /** Creates a `findOneAndUpdate` query, filtering by the given `_id`. */ findByIdAndUpdate( id: mongodb.ObjectId | any, @@ -572,6 +584,11 @@ declare module 'mongoose' { update: UpdateQuery, options: QueryOptions & { rawResult: true } ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndUpdate'>; + findByIdAndUpdate( + id: mongodb.ObjectId | any, + update: UpdateQuery, + options: QueryOptions & { includeResultMetadata: true } + ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndUpdate'>; findByIdAndUpdate( id: mongodb.ObjectId | any, update: UpdateQuery, @@ -630,6 +647,11 @@ declare module 'mongoose' { replacement: TRawDocType | AnyObject, options: QueryOptions & { rawResult: true } ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndReplace'>; + findOneAndReplace( + filter: FilterQuery, + replacement: TRawDocType | AnyObject, + options: QueryOptions & { includeResultMetadata: true } + ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndReplace'>; findOneAndReplace( filter: FilterQuery, replacement: TRawDocType | AnyObject, From 64317eb3b8129c33a2ab4de362f7706f1d4e4356 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 17 Nov 2023 07:26:17 -0500 Subject: [PATCH 086/191] docs: remove "DEPRECATED" warning mistakenly added to `read()` tags param Fix #13980 --- lib/aggregate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aggregate.js b/lib/aggregate.js index a160f57420c..450f9e34d12 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -665,7 +665,7 @@ Aggregate.prototype.unionWith = function(options) { * await Model.aggregate(pipeline).read('primaryPreferred'); * * @param {String|ReadPreference} pref one of the listed preference options or their aliases - * @param {Array} [tags] optional tags for this query. DEPRECATED + * @param {Array} [tags] optional tags for this query. * @return {Aggregate} this * @api public * @see mongodb https://www.mongodb.com/docs/manual/applications/replication/#read-preference From 66f23ac43ecf3717c6f78e5c2cdafdb322abfe57 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 17 Nov 2023 17:59:03 -0500 Subject: [PATCH 087/191] initial draft --- scripts/website.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/scripts/website.js b/scripts/website.js index 7f6a8044788..4f5039355b2 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -76,6 +76,38 @@ const tests = [ ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/schemas.test.js')).toString()) ]; +function refreshDocs() { + deleteAllHtmlFiles(); + if (process.env.DOCS_DEPLOY) { + moveDocsToTemp(); + } +} + +function deleteAllHtmlFiles() { + fs.unlinkSync('../index.html'); + const locations = ['../docs','../docs/tutorials', '../docs/typescript'] + for (let i = 0; i < locations.length; i++) { + const files = fs.readdirSync(locations[i]); + for (let index = 0; index < files.length; index++) { + if (files[index].endsWith('.html')) { + fs.unlinkSync(files[index]); + } + } + } + const folders = ['../docs/api', '../docs/source/_docs', '../tmp']; + for (let i = 0; i < folders.length; i++) { + fs.rmdirSync(folders[i]) + } +} + +function moveDocsToTemp() { + const folder = '../docs/7.x'; + const directory = fs.readdirSync(folder); + for (let i = 0; i < directory.length; i++) { + fs.renameSync(`${folder}/${directory[i]}`, `./tmp/${directory[i]}`); + } +} + /** * Array of array of semver numbers, sorted with highest number first * @example From 782db453cfe9020cf02c32064b14088a22ed5f38 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 18 Nov 2023 16:12:34 -0500 Subject: [PATCH 088/191] test(query): remove unnecessary query test code Fix #13970 --- test/query.test.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/test/query.test.js b/test/query.test.js index b69f0ea374e..7840e9510b7 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -1373,7 +1373,7 @@ describe('Query', function() { }); describe('setOptions', function() { - it('works', async function() { + it('works', function() { const q = new Query(); q.setOptions({ thing: 'cat' }); q.setOptions({ populate: ['fans'] }); @@ -1397,16 +1397,6 @@ describe('Query', function() { assert.equal(q.options.hint.index2, -1); assert.equal(q.options.readPreference.mode, 'secondary'); assert.equal(q.options.readPreference.tags[0].dc, 'eu'); - - const Product = db.model('Product', productSchema); - const [, doc2] = await Product.create([ - { numbers: [3, 4, 5] }, - { strings: 'hi there'.split(' '), w: 'majority' } - ]); - - const docs = await Product.find().setOptions({ limit: 1, sort: { _id: -1 }, read: 'n' }); - assert.equal(docs.length, 1); - assert.equal(docs[0].id, doc2.id); }); it('populate as array in options (gh-4446)', function() { From 0cfad3080c468630ae48c6189528a931902e00e8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 18 Nov 2023 16:24:44 -0500 Subject: [PATCH 089/191] fix: upgrade mongodb -> 5.9.1 to fix #13829 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7373405da55..cc6a75c1362 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "bson": "^5.5.0", "kareem": "2.5.1", - "mongodb": "5.9.0", + "mongodb": "5.9.1", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", From 020a8f3a48990fd7d4588964755420b90844baef Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 20 Nov 2023 15:41:20 -0500 Subject: [PATCH 090/191] chore: move cleanup logic from npm scripts to website.js script --- package.json | 2 +- scripts/website.js | 65 ++++++++++++++++++++++++++++++---------------- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index cc6a75c1362..d3954404199 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "docs:prepare:publish:stable": "npm run docs:checkout:gh-pages && npm run docs:merge:stable && npm run docs:clean:stable && npm run docs:generate && npm run docs:generate:search", "docs:prepare:publish:5x": "npm run docs:checkout:5x && npm run docs:merge:5x && npm run docs:clean:stable && npm run docs:generate && npm run docs:copy:tmp && npm run docs:checkout:gh-pages && npm run docs:copy:tmp:5x", "docs:prepare:publish:6x": "npm run docs:checkout:6x && npm run docs:merge:6x && npm run docs:clean:stable && env DOCS_DEPLOY=true npm run docs:generate && npm run docs:move:6x:tmp && npm run docs:checkout:gh-pages && npm run docs:copy:tmp:6x", - "docs:prepare:publish:7x": "git checkout 7.x && npm run docs:clean:stable && env DOCS_DEPLOY=true npm run docs:generate && mv ./docs/7.x ./tmp && npm run docs:checkout:gh-pages && rimraf ./docs/7.x && ncp ./tmp ./docs/7.x", + "docs:prepare:publish:7x": "env DOCS_DEPLOY=true npm run docs:generate && npm run docs:checkout:gh-pages && rimraf ./docs/7.x && ncp ./tmp ./docs/7.x", "docs:check-links": "blc http://127.0.0.1:8089 -ro", "lint": "eslint .", "lint-js": "eslint . --ext .js --ext .cjs", diff --git a/scripts/website.js b/scripts/website.js index 4f5039355b2..e8e3d6ba99b 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -4,9 +4,11 @@ Error.stackTraceLimit = Infinity; const acquit = require('acquit'); const fs = require('fs'); +const fsextra = require('fs-extra'); const path = require('path'); const pug = require('pug'); const pkg = require('../package.json'); +const rimraf = require('rimraf'); const transform = require('acquit-require'); const childProcess = require("child_process"); @@ -76,35 +78,45 @@ const tests = [ ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/schemas.test.js')).toString()) ]; -function refreshDocs() { - deleteAllHtmlFiles(); - if (process.env.DOCS_DEPLOY) { - moveDocsToTemp(); - } -} - function deleteAllHtmlFiles() { - fs.unlinkSync('../index.html'); - const locations = ['../docs','../docs/tutorials', '../docs/typescript'] - for (let i = 0; i < locations.length; i++) { - const files = fs.readdirSync(locations[i]); + try { + fs.unlinkSync('./index.html'); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + const foldersToClean = [ + './docs', + './docs/tutorials', + './docs/typescript', + './docs/api', + './docs/source/_docs', + './tmp' + ]; + for (const folder of foldersToClean) { + let files = []; + try { + files = fs.readdirSync(folder); + } catch (err) { + if (err.code === 'ENOENT') { + continue; + } + } for (let index = 0; index < files.length; index++) { if (files[index].endsWith('.html')) { - fs.unlinkSync(files[index]); + fs.unlinkSync(`${folder}/${files[index]}`); } } } - const folders = ['../docs/api', '../docs/source/_docs', '../tmp']; - for (let i = 0; i < folders.length; i++) { - fs.rmdirSync(folders[i]) - } } function moveDocsToTemp() { - const folder = '../docs/7.x'; + rimraf.sync('./tmp'); + const folder = `./docs/7.x`; const directory = fs.readdirSync(folder); for (let i = 0; i < directory.length; i++) { - fs.renameSync(`${folder}/${directory[i]}`, `./tmp/${directory[i]}`); + fsextra.moveSync(`${folder}/${directory[i]}`, `./tmp/${directory[i]}`); } } @@ -520,7 +532,6 @@ async function copyAllRequiredFiles() { return; } - const fsextra = require('fs-extra'); await Promise.all(pathsToCopy.map(async v => { const resultPath = path.resolve(cwd, path.join('.', versionObj.versionedPath, v)); await fsextra.copy(v, resultPath); @@ -537,8 +548,16 @@ exports.cwd = cwd; // only run the following code if this file is the main module / entry file if (isMain) { - console.log(`Processing ~${files.length} files`); - Promise.all([pugifyAllFiles(), copyAllRequiredFiles()]).then(() => { - console.log("Done Processing"); - }) + (async function main() { + console.log(`Processing ~${files.length} files`); + + await deleteAllHtmlFiles(); + await pugifyAllFiles(); + await copyAllRequiredFiles(); + if (process.env.DOCS_DEPLOY) { + await moveDocsToTemp(); + } + + console.log('Done Processing'); + })(); } From a848de06bbb5ec9c822d0eba49e2399b66a2e2a9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 19 Nov 2023 14:44:05 -0500 Subject: [PATCH 091/191] Merge pull request #14099 from csy1204/fix/gh-14098 fix(populate): fix curPath to update appropriately --- lib/helpers/populate/assignVals.js | 3 +- test/model.populate.test.js | 52 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/helpers/populate/assignVals.js b/lib/helpers/populate/assignVals.js index 92f0ebecd05..9a30ce28299 100644 --- a/lib/helpers/populate/assignVals.js +++ b/lib/helpers/populate/assignVals.js @@ -144,7 +144,7 @@ module.exports = function assignVals(o) { const parts = _path.split('.'); let cur = docs[i]; - const curPath = parts[0]; + let curPath = parts[0]; for (let j = 0; j < parts.length - 1; ++j) { // If we get to an array with a dotted path, like `arr.foo`, don't set // `foo` on the array. @@ -167,6 +167,7 @@ module.exports = function assignVals(o) { cur[parts[j]] = {}; } cur = cur[parts[j]]; + curPath += parts[j + 1] ? `.${parts[j + 1]}` : ''; // If the property in MongoDB is a primitive, we won't be able to populate // the nested path, so skip it. See gh-7545 if (typeof cur !== 'object') { diff --git a/test/model.populate.test.js b/test/model.populate.test.js index e53c2f7de47..d8db2a67124 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -8259,6 +8259,58 @@ describe('model: populate:', function() { assert.deepEqual(populatedRides[1].files, []); }); + it('doesnt insert empty document when lean populating a path within an underneath non-existent document array (gh-14098)', async function() { + const userSchema = new mongoose.Schema({ + fullName: String, + company: String + }); + const User = db.model('User', userSchema); + + const fileSchema = new mongoose.Schema({ + _id: String, + uploaderId: { + type: mongoose.ObjectId, + ref: 'User' + } + }, { toObject: { virtuals: true }, toJSON: { virtuals: true } }); + fileSchema.virtual('uploadedBy', { + ref: 'User', + localField: 'uploaderId', + foreignField: '_id', + justOne: true + }); + + const contentSchema = new mongoose.Schema({ + memo: String, + files: { type: [fileSchema], default: [] } + }, { toObject: { virtuals: true }, toJSON: { virtuals: true }, _id: false }); + + const postSchema = new mongoose.Schema({ + title: String, + content: { type: contentSchema } + }, { toObject: { virtuals: true }, toJSON: { virtuals: true } }); + const Post = db.model('Test1', postSchema); + + const user = await User.create({ fullName: 'John Doe', company: 'GitHub' }); + await Post.create([ + { title: 'London-Paris' }, + { + title: 'Berlin-Moscow', + content: { + memo: 'Not Easy', + files: [{ _id: '123', uploaderId: user._id }] + } + } + ]); + await Post.updateMany({}, { $unset: { 'content.files': 1 } }); + const populatedRides = await Post.find({}).populate({ + path: 'content.files.uploadedBy', + justOne: true + }).lean(); + assert.equal(populatedRides[0].content.files, undefined); + assert.equal(populatedRides[1].content.files, undefined); + }); + it('sets empty array if populating undefined path (gh-8455)', async function() { const TestSchema = new Schema({ thingIds: [mongoose.ObjectId] From 4b065f2aed1391dc9b0b5f5f914d7f01847a3723 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 22 Nov 2023 21:24:48 -0500 Subject: [PATCH 092/191] refactor: address code review comments --- scripts/website.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/scripts/website.js b/scripts/website.js index e8e3d6ba99b..c3063df5807 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -8,7 +8,6 @@ const fsextra = require('fs-extra'); const path = require('path'); const pug = require('pug'); const pkg = require('../package.json'); -const rimraf = require('rimraf'); const transform = require('acquit-require'); const childProcess = require("child_process"); @@ -103,20 +102,20 @@ function deleteAllHtmlFiles() { continue; } } - for (let index = 0; index < files.length; index++) { - if (files[index].endsWith('.html')) { - fs.unlinkSync(`${folder}/${files[index]}`); + for (const file of files) { + if (file.endsWith('.html')) { + fs.unlinkSync(`${folder}/${file}`); } } } } function moveDocsToTemp() { - rimraf.sync('./tmp'); - const folder = `./docs/7.x`; + fs.rmSync('./tmp', { recursive: true }); + const folder = versionObj.versionedPath.replace(/^\//, ''); const directory = fs.readdirSync(folder); - for (let i = 0; i < directory.length; i++) { - fsextra.moveSync(`${folder}/${directory[i]}`, `./tmp/${directory[i]}`); + for (const file of directory) { + fsextra.moveSync(`${folder}/${file}`, `./tmp/${file}`); } } From b2b0acc638103ec9cfb324e713242d6103871b29 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Thu, 23 Nov 2023 18:02:29 +0100 Subject: [PATCH 093/191] style(scripts/website): directly call "markdown.use" --- scripts/website.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/website.js b/scripts/website.js index 7f6a8044788..e3c94eee7cd 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -31,7 +31,7 @@ require('acquit-ignore')(); const { marked: markdown } = require('marked'); const highlight = require('highlight.js'); const { promisify } = require("util"); -const renderer = { +markdown.use({ heading: function(text, level, raw, slugger) { const slug = slugger.slug(raw); return ` @@ -40,7 +40,7 @@ const renderer = { \n`; } -}; +}); markdown.setOptions({ highlight: function(code, language) { if (!language) { @@ -52,7 +52,6 @@ markdown.setOptions({ return highlight.highlight(code, { language }).value; } }); -markdown.use({ renderer }); const testPath = path.resolve(cwd, 'test') From 319979c588d045467c8a5ae5da2ecc60cf6ac7e4 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Thu, 23 Nov 2023 18:06:19 +0100 Subject: [PATCH 094/191] chore(scripts/website): refactor acquit file loading to make use of parallel promises also improve use-ability --- scripts/website.js | 63 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/scripts/website.js b/scripts/website.js index e3c94eee7cd..e6a0953ade4 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -55,25 +55,50 @@ markdown.setOptions({ const testPath = path.resolve(cwd, 'test') -const tests = [ - ...acquit.parse(fs.readFileSync(path.join(testPath, 'geojson.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/transactions.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'schema.alias.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'model.middleware.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/date.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/lean.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/cast.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/findoneandupdate.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/custom-casting.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/getters-setters.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/virtuals.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/defaults.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/discriminators.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/promises.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/schematypes.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/validation.test.js')).toString()), - ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/schemas.test.js')).toString()) +/** additional test files to scan, relative to `test/` */ +const additionalTestFiles = [ + 'geojson.test.js', + 'schema.alias.test.js' ]; +/** ignored files from `test/docs/` */ +const ignoredTestFiles = [ + // ignored because acquit does not like "for await" + 'asyncIterator.test.js' +]; + +/** + * Load all test file contents with acquit + * @returns {Object[]} acquit ast array + */ +async function getTests() { + const promiseArray = []; + + for (const file of additionalTestFiles) { + const filePath = path.join(testPath, file); + promiseArray.push(fs.promises.readFile(filePath).then(v => ({value: v.toString(), path: filePath}))); + } + + const testDocs = path.resolve(testPath, 'docs'); + + for (const file of await fs.promises.readdir(testDocs)) { + if (ignoredTestFiles.includes(file)) { + continue; + } + + const filePath = path.join(testDocs, file); + promiseArray.push(fs.promises.readFile(filePath).then(v => ({value: v.toString(), path: filePath}))); + } + + return (await Promise.all(promiseArray)).flatMap(v => { + try { + return acquit.parse(v.value); + } catch (err) { + // add a file path to a acquit error, for better debugging + err.filePath = v.path; + throw err; + } + }) +} /** * Array of array of semver numbers, sorted with highest number first @@ -351,7 +376,7 @@ async function pugify(filename, options, isReload = false) { let contents = fs.readFileSync(path.resolve(cwd, inputFile)).toString(); if (options.acquit) { - contents = transform(contents, tests); + contents = transform(contents, await getTests()); contents = contents.replaceAll(/^```acquit$/gmi, "```javascript"); } From c8a8da2a1d957b518c64506c34f396974cbae25a Mon Sep 17 00:00:00 2001 From: hasezoey Date: Thu, 23 Nov 2023 18:17:07 +0100 Subject: [PATCH 095/191] chore(scripts/website): convert a comment to JSDOC --- scripts/website.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/website.js b/scripts/website.js index e6a0953ade4..f99758280ff 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -435,7 +435,7 @@ async function pugify(filename, options, isReload = false) { }); } -// extra function to start watching for file-changes, without having to call this file directly with "watch" +/** extra function to start watching for file-changes, without having to call this file directly with "watch" */ function startWatch() { Object.entries(docsFilemap.fileMap).forEach(([file, fileValue]) => { let watchPath = path.resolve(cwd, file); From 93fb4e8563a6e48710a340e462ee669bac8a43da Mon Sep 17 00:00:00 2001 From: hasezoey Date: Thu, 23 Nov 2023 18:22:04 +0100 Subject: [PATCH 096/191] chore(scripts/website): add some missing ";" --- scripts/website.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/website.js b/scripts/website.js index f99758280ff..626a64d8407 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -53,7 +53,7 @@ markdown.setOptions({ } }); -const testPath = path.resolve(cwd, 'test') +const testPath = path.resolve(cwd, 'test'); /** additional test files to scan, relative to `test/` */ const additionalTestFiles = [ @@ -503,7 +503,7 @@ const pathsToCopy = [ 'docs/js', 'docs/css', 'docs/images' -] +]; /** Copy all static files when versionedDeploy is used */ async function copyAllRequiredFiles() { From 9e3185ad40d24836b75ff1ef113e9451b2d3fa44 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 Nov 2023 10:16:08 -0500 Subject: [PATCH 097/191] types(query): base filters and projections off of RawDocType instead of DocType so autocomplete doesn't show populate Fix #14077 --- types/query.d.ts | 92 ++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/types/query.d.ts b/types/query.d.ts index 1d66e75221c..2234e66f656 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -217,7 +217,7 @@ declare module 'mongoose' { allowDiskUse(value: boolean): this; /** Specifies arguments for an `$and` condition. */ - and(array: FilterQuery[]): this; + and(array: FilterQuery[]): this; /** Specifies the batchSize option. */ batchSize(val: number): this; @@ -265,7 +265,7 @@ declare module 'mongoose' { comment(val: string): this; /** Specifies this query as a `count` query. */ - count(criteria?: FilterQuery): QueryWithHelpers< + count(criteria?: FilterQuery): QueryWithHelpers< number, DocType, THelpers, @@ -275,7 +275,7 @@ declare module 'mongoose' { /** Specifies this query as a `countDocuments` query. */ countDocuments( - criteria?: FilterQuery, + criteria?: FilterQuery, options?: QueryOptions ): QueryWithHelpers; @@ -291,10 +291,10 @@ declare module 'mongoose' { * collection, regardless of the value of `single`. */ deleteMany( - filter?: FilterQuery, + filter?: FilterQuery, options?: QueryOptions ): QueryWithHelpers; - deleteMany(filter: FilterQuery): QueryWithHelpers< + deleteMany(filter: FilterQuery): QueryWithHelpers< any, DocType, THelpers, @@ -309,10 +309,10 @@ declare module 'mongoose' { * option. */ deleteOne( - filter?: FilterQuery, + filter?: FilterQuery, options?: QueryOptions ): QueryWithHelpers; - deleteOne(filter: FilterQuery): QueryWithHelpers< + deleteOne(filter: FilterQuery): QueryWithHelpers< any, DocType, THelpers, @@ -324,7 +324,7 @@ declare module 'mongoose' { /** Creates a `distinct` query: returns the distinct values of the given `field` that match `filter`. */ distinct( field: string, - filter?: FilterQuery + filter?: FilterQuery ): QueryWithHelpers, DocType, THelpers, RawDocType, 'distinct'>; /** Specifies a `$elemMatch` query condition. When called with one argument, the most recent path passed to `where()` is used. */ @@ -364,71 +364,71 @@ declare module 'mongoose' { /** Creates a `find` query: gets a list of documents that match `filter`. */ find( - filter: FilterQuery, - projection?: ProjectionType | null, + filter: FilterQuery, + projection?: ProjectionType | null, options?: QueryOptions | null ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find'>; find( - filter: FilterQuery, - projection?: ProjectionType | null + filter: FilterQuery, + projection?: ProjectionType | null ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find'>; find( - filter: FilterQuery - ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find'>; + filter: FilterQuery + ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find'>; find(): QueryWithHelpers, DocType, THelpers, RawDocType, 'find'>; /** Declares the query a findOne operation. When executed, returns the first found document. */ findOne( - filter?: FilterQuery, - projection?: ProjectionType | null, + filter?: FilterQuery, + projection?: ProjectionType | null, options?: QueryOptions | null ): QueryWithHelpers; findOne( - filter?: FilterQuery, - projection?: ProjectionType | null + filter?: FilterQuery, + projection?: ProjectionType | null ): QueryWithHelpers; findOne( - filter?: FilterQuery - ): QueryWithHelpers; + filter?: FilterQuery + ): QueryWithHelpers; /** Creates a `findOneAndDelete` query: atomically finds the given document, deletes it, and returns the document as it was before deletion. */ findOneAndDelete( - filter?: FilterQuery, + filter?: FilterQuery, options?: QueryOptions | null ): QueryWithHelpers; /** Creates a `findOneAndRemove` query: atomically finds the given document and deletes it. */ findOneAndRemove( - filter?: FilterQuery, + filter?: FilterQuery, options?: QueryOptions | null ): QueryWithHelpers; /** Creates a `findOneAndUpdate` query: atomically find the first document that matches `filter` and apply `update`. */ findOneAndUpdate( - filter: FilterQuery, - update: UpdateQuery, + filter: FilterQuery, + update: UpdateQuery, options: QueryOptions & { rawResult: true } ): QueryWithHelpers, DocType, THelpers, RawDocType, 'findOneAndUpdate'>; findOneAndUpdate( - filter: FilterQuery, - update: UpdateQuery, + filter: FilterQuery, + update: UpdateQuery, options: QueryOptions & { upsert: true } & ReturnsNewDoc ): QueryWithHelpers; findOneAndUpdate( - filter?: FilterQuery, - update?: UpdateQuery, + filter?: FilterQuery, + update?: UpdateQuery, options?: QueryOptions | null ): QueryWithHelpers; /** Declares the query a findById operation. When executed, returns the document with the given `_id`. */ findById( id: mongodb.ObjectId | any, - projection?: ProjectionType | null, + projection?: ProjectionType | null, options?: QueryOptions | null ): QueryWithHelpers; findById( id: mongodb.ObjectId | any, - projection?: ProjectionType | null + projection?: ProjectionType | null ): QueryWithHelpers; findById( id: mongodb.ObjectId | any @@ -443,22 +443,22 @@ declare module 'mongoose' { /** Creates a `findOneAndUpdate` query, filtering by the given `_id`. */ findByIdAndUpdate( id: mongodb.ObjectId | any, - update: UpdateQuery, + update: UpdateQuery, options: QueryOptions & { rawResult: true } ): QueryWithHelpers; findByIdAndUpdate( id: mongodb.ObjectId | any, - update: UpdateQuery, + update: UpdateQuery, options: QueryOptions & { upsert: true } & ReturnsNewDoc ): QueryWithHelpers; findByIdAndUpdate( id?: mongodb.ObjectId | any, - update?: UpdateQuery, + update?: UpdateQuery, options?: QueryOptions | null ): QueryWithHelpers; findByIdAndUpdate( id: mongodb.ObjectId | any, - update: UpdateQuery + update: UpdateQuery ): QueryWithHelpers; /** Specifies a `$geometry` condition */ @@ -472,7 +472,7 @@ declare module 'mongoose' { get(path: string): any; /** Returns the current query filter (also known as conditions) as a POJO. */ - getFilter(): FilterQuery; + getFilter(): FilterQuery; /** Gets query options. */ getOptions(): QueryOptions; @@ -481,7 +481,7 @@ declare module 'mongoose' { getPopulatedPaths(): Array; /** Returns the current query filter. Equivalent to `getFilter()`. */ - getQuery(): FilterQuery; + getQuery(): FilterQuery; /** Returns the current update operations as a JSON object. */ getUpdate(): UpdateQuery | UpdateWithAggregationPipeline | null; @@ -551,7 +551,7 @@ declare module 'mongoose' { maxTimeMS(ms: number): this; /** Merges another Query or conditions object into this one. */ - merge(source: Query | FilterQuery): this; + merge(source: Query | FilterQuery): this; /** Specifies a `$mod` condition, filters documents for documents whose `path` property is a number that is equal to `remainder` modulo `divisor`. */ mod(path: K, val: number): this; @@ -579,10 +579,10 @@ declare module 'mongoose' { nin(val: Array): this; /** Specifies arguments for an `$nor` condition. */ - nor(array: Array>): this; + nor(array: Array>): this; /** Specifies arguments for an `$or` condition. */ - or(array: Array>): this; + or(array: Array>): this; /** * Make this query throw an error if no documents match the given `filter`. @@ -639,7 +639,7 @@ declare module 'mongoose' { * not accept any [atomic](https://www.mongodb.com/docs/manual/tutorial/model-data-for-atomic-operations/#pattern) operators (`$set`, etc.) */ replaceOne( - filter?: FilterQuery, + filter?: FilterQuery, replacement?: DocType | AnyObject, options?: QueryOptions | null ): QueryWithHelpers; @@ -698,9 +698,9 @@ declare module 'mongoose' { setOptions(options: QueryOptions, overwrite?: boolean): this; /** Sets the query conditions to the provided JSON object. */ - setQuery(val: FilterQuery | null): void; + setQuery(val: FilterQuery | null): void; - setUpdate(update: UpdateQuery | UpdateWithAggregationPipeline): void; + setUpdate(update: UpdateQuery | UpdateWithAggregationPipeline): void; /** Specifies an `$size` query condition. When called with one argument, the most recent path passed to `where()` is used. */ size(path: K, val: number): this; @@ -738,8 +738,8 @@ declare module 'mongoose' { * the `multi` option. */ updateMany( - filter?: FilterQuery, - update?: UpdateQuery | UpdateWithAggregationPipeline, + filter?: FilterQuery, + update?: UpdateQuery | UpdateWithAggregationPipeline, options?: QueryOptions | null ): QueryWithHelpers; @@ -748,8 +748,8 @@ declare module 'mongoose' { * `update()`, except it does not support the `multi` or `overwrite` options. */ updateOne( - filter?: FilterQuery, - update?: UpdateQuery | UpdateWithAggregationPipeline, + filter?: FilterQuery, + update?: UpdateQuery | UpdateWithAggregationPipeline, options?: QueryOptions | null ): QueryWithHelpers; From 6def405aa87d5267bdd85a0f514edc1472d147d6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 Nov 2023 10:46:21 -0500 Subject: [PATCH 098/191] fix(populate): set populated docs in correct order when populating virtual underneath doc array with justOne (#14105) Fix #14018 --- .../populate/assignRawDocsToIdStructure.js | 8 +- lib/helpers/populate/assignVals.js | 3 +- test/model.populate.test.js | 82 +++++++++++++++++++ 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/lib/helpers/populate/assignRawDocsToIdStructure.js b/lib/helpers/populate/assignRawDocsToIdStructure.js index c08671e5918..95b84e0bb03 100644 --- a/lib/helpers/populate/assignRawDocsToIdStructure.js +++ b/lib/helpers/populate/assignRawDocsToIdStructure.js @@ -32,9 +32,13 @@ const kHasArray = Symbol('assignRawDocsToIdStructure.hasArray'); */ function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, recursed) { - // honor user specified sort order + // honor user specified sort order, unless we're populating a single + // virtual underneath an array (e.g. populating `employees.mostRecentShift` where + // `mostRecentShift` is a virtual with `justOne`) const newOrder = []; - const sorting = options.sort && rawIds.length > 1; + const sorting = options.isVirtual && options.justOne && rawIds.length > 1 + ? false : + options.sort && rawIds.length > 1; const nullIfNotFound = options.$nullIfNotFound; let doc; let sid; diff --git a/lib/helpers/populate/assignVals.js b/lib/helpers/populate/assignVals.js index 9a30ce28299..017066add17 100644 --- a/lib/helpers/populate/assignVals.js +++ b/lib/helpers/populate/assignVals.js @@ -19,7 +19,8 @@ module.exports = function assignVals(o) { // `o.options` contains options explicitly listed in `populateOptions`, like // `match` and `limit`. const populateOptions = Object.assign({}, o.options, userOptions, { - justOne: o.justOne + justOne: o.justOne, + isVirtual: o.isVirtual }); populateOptions.$nullIfNotFound = o.isVirtual; const populatedModel = o.populatedModel; diff --git a/test/model.populate.test.js b/test/model.populate.test.js index d8db2a67124..4e638a90178 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -10694,4 +10694,86 @@ describe('model: populate:', function() { [company._id.toString(), company2._id.toString()] ); }); + + it('sets populated docs in correct order when populating virtual underneath document array with justOne (gh-14018)', async function() { + const shiftSchema = new mongoose.Schema({ + employeeId: mongoose.Types.ObjectId, + startedAt: Date, + endedAt: Date + }); + const Shift = db.model('Shift', shiftSchema); + + const employeeSchema = new mongoose.Schema({ + name: String + }); + employeeSchema.virtual('mostRecentShift', { + ref: Shift, + localField: '_id', + foreignField: 'employeeId', + options: { + sort: { startedAt: -1 } + }, + justOne: true + }); + const storeSchema = new mongoose.Schema({ + location: String, + employees: [employeeSchema] + }); + const Store = db.model('Store', storeSchema); + + const store = await Store.create({ + location: 'Tashbaan', + employees: [ + { _id: '0'.repeat(24), name: 'Aravis' }, + { _id: '1'.repeat(24), name: 'Shasta' } + ] + }); + + const employeeAravis = store.employees + .find(({ name }) => name === 'Aravis'); + const employeeShasta = store.employees + .find(({ name }) => name === 'Shasta'); + + await Shift.insertMany([ + { employeeId: employeeAravis._id, startedAt: new Date('2011-06-01'), endedAt: new Date('2011-06-02') }, + { employeeId: employeeAravis._id, startedAt: new Date('2013-06-01'), endedAt: new Date('2013-06-02') }, + { employeeId: employeeShasta._id, startedAt: new Date('2015-06-01'), endedAt: new Date('2015-06-02') } + ]); + + const storeWithMostRecentShifts = await Store.findOne({ location: 'Tashbaan' }) + .populate('employees.mostRecentShift') + .select('-__v') + .exec(); + assert.equal( + storeWithMostRecentShifts.employees[0].mostRecentShift.employeeId.toHexString(), + '0'.repeat(24) + ); + assert.equal( + storeWithMostRecentShifts.employees[1].mostRecentShift.employeeId.toHexString(), + '1'.repeat(24) + ); + + await Shift.findOne({ employeeId: employeeAravis._id }).sort({ startedAt: 1 }).then((s) => s.deleteOne()); + + const storeWithMostRecentShiftsNew = await Store.findOne({ location: 'Tashbaan' }) + .populate('employees.mostRecentShift') + .select('-__v') + .exec(); + assert.equal( + storeWithMostRecentShiftsNew.employees[0].mostRecentShift.employeeId.toHexString(), + '0'.repeat(24) + ); + assert.equal( + storeWithMostRecentShiftsNew.employees[0].mostRecentShift.startedAt.toString(), + new Date('2013-06-01').toString() + ); + assert.equal( + storeWithMostRecentShiftsNew.employees[1].mostRecentShift.employeeId.toHexString(), + '1'.repeat(24) + ); + assert.equal( + storeWithMostRecentShiftsNew.employees[1].mostRecentShift.startedAt.toString(), + new Date('2015-06-01').toString() + ); + }); }); From e3cccc30f17a8781d344c112f6b3935620b18161 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 Nov 2023 12:11:30 -0500 Subject: [PATCH 099/191] perf(array): use push() instead of concat() for $push atomics re: #11380 --- lib/types/array/methods/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js index deb4e801f29..50691aedaee 100644 --- a/lib/types/array/methods/index.js +++ b/lib/types/array/methods/index.js @@ -374,7 +374,7 @@ const methods = { if (val != null && utils.hasUserDefinedProperty(val, '$each')) { atomics.$push = val; } else { - atomics.$push.$each = atomics.$push.$each.concat(val); + atomics.$push.$each.push(val); } } else { atomics[op] = val; From 62d5302ed75adcc92b4fe0eb40631c2910312a68 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 Nov 2023 12:22:16 -0500 Subject: [PATCH 100/191] perf: correct way to avoid calling concat() re: #11380 --- lib/types/array/methods/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js index 50691aedaee..28ed50ddc95 100644 --- a/lib/types/array/methods/index.js +++ b/lib/types/array/methods/index.js @@ -374,7 +374,13 @@ const methods = { if (val != null && utils.hasUserDefinedProperty(val, '$each')) { atomics.$push = val; } else { - atomics.$push.$each.push(val); + if (val.length < 10000) { + atomics.$push.$each.push(...val); + } else { + for (const v of val) { + atomics.$push.$each.push(v); + } + } } } else { atomics[op] = val; @@ -711,7 +717,7 @@ const methods = { 'with different `$position`'); } atomic = values; - ret = [].push.apply(arr, values); + ret = _basePush.apply(arr, values); } this._registerAtomic('$push', atomic); From 865a8f2dc846c23a1fe1e200909b13301472729f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 Nov 2023 12:25:39 -0500 Subject: [PATCH 101/191] perf: one more quick improvement --- lib/types/array/methods/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js index 28ed50ddc95..f29b31ed32b 100644 --- a/lib/types/array/methods/index.js +++ b/lib/types/array/methods/index.js @@ -374,7 +374,9 @@ const methods = { if (val != null && utils.hasUserDefinedProperty(val, '$each')) { atomics.$push = val; } else { - if (val.length < 10000) { + if (val.length === 1) { + atomics.$push.$each.push(val); + } else if (val.length < 10000) { atomics.$push.$each.push(...val); } else { for (const v of val) { From b0476908f46a75eff7e85eb812aad2224fca3fd1 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 Nov 2023 12:35:57 -0500 Subject: [PATCH 102/191] perf: avoid mutating internal state if setting state to current re: #11380 --- lib/statemachine.js | 6 +++++- lib/types/array/methods/index.js | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/statemachine.js b/lib/statemachine.js index 70b1beca695..02fbc03e0fc 100644 --- a/lib/statemachine.js +++ b/lib/statemachine.js @@ -65,7 +65,11 @@ StateMachine.ctor = function() { */ StateMachine.prototype._changeState = function _changeState(path, nextState) { - const prevBucket = this.states[this.paths[path]]; + const prevState = this.paths[path]; + if (prevState === nextState) { + return; + } + const prevBucket = this.states[prevState]; if (prevBucket) delete prevBucket[path]; this.paths[path] = nextState; diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js index f29b31ed32b..f9c1e2b80ba 100644 --- a/lib/types/array/methods/index.js +++ b/lib/types/array/methods/index.js @@ -375,7 +375,7 @@ const methods = { atomics.$push = val; } else { if (val.length === 1) { - atomics.$push.$each.push(val); + atomics.$push.$each.push(val[0]); } else if (val.length < 10000) { atomics.$push.$each.push(...val); } else { From 7d26ad2024f4f113971b0668b5ddfa16698c504e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 Nov 2023 21:24:50 -0500 Subject: [PATCH 103/191] perf: avoid double-calling setters when pushing onto an array Fix #11380 Re: #13456 --- lib/types/array/methods/index.js | 9 ++------- test/types.array.test.js | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js index f9c1e2b80ba..c798464b6d8 100644 --- a/lib/types/array/methods/index.js +++ b/lib/types/array/methods/index.js @@ -411,8 +411,7 @@ const methods = { addToSet() { _checkManualPopulation(this, arguments); - let values = [].map.call(arguments, this._mapCast, this); - values = this[arraySchemaSymbol].applySetters(values, this[arrayParentSymbol]); + const values = [].map.call(arguments, this._mapCast, this); const added = []; let type = ''; if (values[0] instanceof ArraySubdocument) { @@ -423,7 +422,7 @@ const methods = { type = 'ObjectId'; } - const rawValues = utils.isMongooseArray(values) ? values.__array : this; + const rawValues = utils.isMongooseArray(values) ? values.__array : values; const rawArray = utils.isMongooseArray(this) ? this.__array : this; rawValues.forEach(function(v) { @@ -690,10 +689,7 @@ const methods = { _checkManualPopulation(this, values); - const parent = this[arrayParentSymbol]; values = [].map.call(values, this._mapCast, this); - values = this[arraySchemaSymbol].applySetters(values, parent, undefined, - undefined, { skipDocumentArrayCast: true }); let ret; const atomics = this[arrayAtomicsSymbol]; this._markModified(); @@ -925,7 +921,6 @@ const methods = { values = arguments; } else { values = [].map.call(arguments, this._cast, this); - values = this[arraySchemaSymbol].applySetters(values, this[arrayParentSymbol]); } const arr = utils.isMongooseArray(this) ? this.__array : this; diff --git a/test/types.array.test.js b/test/types.array.test.js index f1a9192dc97..ccc83e02558 100644 --- a/test/types.array.test.js +++ b/test/types.array.test.js @@ -1915,4 +1915,26 @@ describe('types array', function() { } }); }); + + it('calls array setters (gh-11380)', function() { + let called = 0; + const Test = db.model('Test', new Schema({ + intArr: [{ + type: Number, + set: v => { + ++called; + return Math.floor(v); + } + }] + })); + + assert.equal(called, 0); + const doc = new Test({ intArr: [3.14] }); + assert.deepStrictEqual(doc.intArr, [3]); + assert.equal(called, 1); + + doc.intArr.push(2.718); + assert.deepStrictEqual(doc.intArr, [3, 2]); + assert.equal(called, 2); + }); }); From c041378539dfda181fee0590c232b02686064a8d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 26 Nov 2023 07:53:29 -0500 Subject: [PATCH 104/191] chore: add extra check to prevent copying non-versioned deploy to tmp, handle ENOENT error, consistently check for truthy DOCS_DEPLOY --- scripts/website.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/website.js b/scripts/website.js index c3063df5807..98e112bf008 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -111,7 +111,16 @@ function deleteAllHtmlFiles() { } function moveDocsToTemp() { - fs.rmSync('./tmp', { recursive: true }); + if (!versionObj.versionedPath) { + throw new Error('Cannot move unversioned deploy to /tmp'); + } + try { + fs.rmSync('./tmp', { recursive: true }); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } const folder = versionObj.versionedPath.replace(/^\//, ''); const directory = fs.readdirSync(folder); for (const file of directory) { @@ -258,7 +267,7 @@ const versionObj = (() => { getLatestVersionOf(5), ] }; - const versionedDeploy = process.env.DOCS_DEPLOY === "true" ? !(base.currentVersion.listed === base.latestVersion.listed) : false; + const versionedDeploy = !!process.env.DOCS_DEPLOY ? !(base.currentVersion.listed === base.latestVersion.listed) : false; const versionedPath = versionedDeploy ? `/docs/${base.currentVersion.path}` : ''; From c0c408c8ac011cd15f483337ae2ec0b3add477fa Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 26 Nov 2023 16:33:10 +0100 Subject: [PATCH 105/191] chore(scripts/website): change "getTests" to be sync as per review comment --- scripts/website.js | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/scripts/website.js b/scripts/website.js index 626a64d8407..9213311b6ca 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -70,34 +70,26 @@ const ignoredTestFiles = [ * Load all test file contents with acquit * @returns {Object[]} acquit ast array */ -async function getTests() { - const promiseArray = []; - - for (const file of additionalTestFiles) { - const filePath = path.join(testPath, file); - promiseArray.push(fs.promises.readFile(filePath).then(v => ({value: v.toString(), path: filePath}))); - } - +function getTests() { const testDocs = path.resolve(testPath, 'docs'); + const filesToScan = [ + ...additionalTestFiles.map(v => path.join(testPath, v)), + ...fs.readdirSync(testDocs).filter(v => !ignoredTestFiles.includes(v)).map(v => path.join(testDocs, v)) + ]; - for (const file of await fs.promises.readdir(testDocs)) { - if (ignoredTestFiles.includes(file)) { - continue; - } + const retArray = []; - const filePath = path.join(testDocs, file); - promiseArray.push(fs.promises.readFile(filePath).then(v => ({value: v.toString(), path: filePath}))); - } - - return (await Promise.all(promiseArray)).flatMap(v => { + for (const file of filesToScan) { try { - return acquit.parse(v.value); + retArray.push(acquit.parse(fs.readFileSync(file).toString())); } catch (err) { // add a file path to a acquit error, for better debugging - err.filePath = v.path; + err.filePath = file; throw err; } - }) + } + + return retArray.flat(); } /** @@ -376,7 +368,7 @@ async function pugify(filename, options, isReload = false) { let contents = fs.readFileSync(path.resolve(cwd, inputFile)).toString(); if (options.acquit) { - contents = transform(contents, await getTests()); + contents = transform(contents, getTests()); contents = contents.replaceAll(/^```acquit$/gmi, "```javascript"); } From 47bd314c10aedf0d34049bd6a9a385f6cb0b8437 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 27 Nov 2023 14:32:58 -0500 Subject: [PATCH 106/191] Update scripts/website.js Co-authored-by: hasezoey --- scripts/website.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/website.js b/scripts/website.js index 98e112bf008..eaabea581bc 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -562,7 +562,7 @@ if (isMain) { await deleteAllHtmlFiles(); await pugifyAllFiles(); await copyAllRequiredFiles(); - if (process.env.DOCS_DEPLOY) { + if (!!process.env.DOCS_DEPLOY && !!versionObj.versionedPath) { await moveDocsToTemp(); } From 80495ae3461d4ddd2da170482e0fb03d7f2b74f7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 27 Nov 2023 14:35:00 -0500 Subject: [PATCH 107/191] Update package.json Co-authored-by: hasezoey --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d3954404199..454a66e175d 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "docs:prepare:publish:stable": "npm run docs:checkout:gh-pages && npm run docs:merge:stable && npm run docs:clean:stable && npm run docs:generate && npm run docs:generate:search", "docs:prepare:publish:5x": "npm run docs:checkout:5x && npm run docs:merge:5x && npm run docs:clean:stable && npm run docs:generate && npm run docs:copy:tmp && npm run docs:checkout:gh-pages && npm run docs:copy:tmp:5x", "docs:prepare:publish:6x": "npm run docs:checkout:6x && npm run docs:merge:6x && npm run docs:clean:stable && env DOCS_DEPLOY=true npm run docs:generate && npm run docs:move:6x:tmp && npm run docs:checkout:gh-pages && npm run docs:copy:tmp:6x", - "docs:prepare:publish:7x": "env DOCS_DEPLOY=true npm run docs:generate && npm run docs:checkout:gh-pages && rimraf ./docs/7.x && ncp ./tmp ./docs/7.x", + "docs:prepare:publish:7x": "env DOCS_DEPLOY=true npm run docs:generate && npm run docs:checkout:gh-pages && rimraf ./docs/7.x && mv ./tmp ./docs/7.x", "docs:check-links": "blc http://127.0.0.1:8089 -ro", "lint": "eslint .", "lint-js": "eslint . --ext .js --ext .cjs", From a26d3048069bddbb404fc234bc93d97f35e15b2b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 27 Nov 2023 14:36:53 -0500 Subject: [PATCH 108/191] Update package.json Co-authored-by: hasezoey --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 454a66e175d..dd8138eca3d 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "docs:merge:6x": "git merge 6.x", "docs:test": "npm run docs:generate && npm run docs:generate:search", "docs:view": "node ./scripts/static.js", - "docs:prepare:publish:stable": "npm run docs:checkout:gh-pages && npm run docs:merge:stable && npm run docs:clean:stable && npm run docs:generate && npm run docs:generate:search", + "docs:prepare:publish:stable": "npm run docs:checkout:gh-pages && npm run docs:merge:stable && npm run docs:generate && npm run docs:generate:search", "docs:prepare:publish:5x": "npm run docs:checkout:5x && npm run docs:merge:5x && npm run docs:clean:stable && npm run docs:generate && npm run docs:copy:tmp && npm run docs:checkout:gh-pages && npm run docs:copy:tmp:5x", "docs:prepare:publish:6x": "npm run docs:checkout:6x && npm run docs:merge:6x && npm run docs:clean:stable && env DOCS_DEPLOY=true npm run docs:generate && npm run docs:move:6x:tmp && npm run docs:checkout:gh-pages && npm run docs:copy:tmp:6x", "docs:prepare:publish:7x": "env DOCS_DEPLOY=true npm run docs:generate && npm run docs:checkout:gh-pages && rimraf ./docs/7.x && mv ./tmp ./docs/7.x", From 140a118dda67cbf9d9ce62ba0eac1ad0efd13154 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 27 Nov 2023 14:53:43 -0500 Subject: [PATCH 109/191] chore: correctly clean relative to version path --- scripts/website.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/scripts/website.js b/scripts/website.js index eaabea581bc..50cfaf4d767 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -79,22 +79,24 @@ const tests = [ function deleteAllHtmlFiles() { try { - fs.unlinkSync('./index.html'); + console.log('Delete', path.join(versionObj.versionedPath, 'index.html')); + fs.unlinkSync(path.join(versionObj.versionedPath, 'index.html')); } catch (err) { if (err.code !== 'ENOENT') { throw err; } } const foldersToClean = [ - './docs', - './docs/tutorials', - './docs/typescript', - './docs/api', - './docs/source/_docs', + path.join('.', versionObj.versionedPath, 'docs'), + path.join('.', versionObj.versionedPath, 'docs', 'tutorials'), + path.join('.', versionObj.versionedPath, 'docs', 'typescript'), + path.join('.', versionObj.versionedPath, 'docs', 'api'), + path.join('.', versionObj.versionedPath, 'docs', 'source', '_docs'), './tmp' ]; for (const folder of foldersToClean) { let files = []; + try { files = fs.readdirSync(folder); } catch (err) { @@ -104,7 +106,8 @@ function deleteAllHtmlFiles() { } for (const file of files) { if (file.endsWith('.html')) { - fs.unlinkSync(`${folder}/${file}`); + console.log('Delete', path.join(folder, file)); + fs.unlinkSync(path.join(folder, file)); } } } From 280bd4a2c59c9d149a9bf0f0b13709d89450b4c4 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 25 Nov 2023 15:52:23 -0500 Subject: [PATCH 110/191] types: make property names show up in intellisense for UpdateQuery Fix #14090 --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index d8d9e0c629a..05578ccc3ca 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -634,7 +634,7 @@ declare module 'mongoose' { * { age: 30 } * ``` */ - export type UpdateQuery = _UpdateQuery & AnyObject; + export type UpdateQuery = AnyKeys & _UpdateQuery & AnyObject; /** * A more strict form of UpdateQuery that enforces updating only From 79aab0c0d357fc2bafb428c4ab18d2d246473892 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 27 Nov 2023 17:04:04 -0500 Subject: [PATCH 111/191] chore: release 7.6.6 --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 282262f803b..0da10349286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +7.6.6 / 2023-11-27 +================== + * perf: avoid double-running setter logic when calling `push()` #14120 #11380 + * fix(populate): set populated docs in correct order when populating virtual underneath doc array with justOne #14105 #14018 + * fix: bump mongodb driver -> 5.9.1 #14084 #13829 [lorand-horvath](https://github.com/lorand-horvath) + * types: allow defining document array using [{ prop: String }] syntax #14095 #13424 + * types: correct types for when includeResultMetadata: true is set #14078 #13987 [prathamVaidya](https://github.com/prathamVaidya) + * types(query): base filters and projections off of RawDocType instead of DocType so autocomplete doesn't show populate #14118 #14077 + * types: make property names show up in intellisense for UpdateQuery #14123 #14090 + * types(model): support calling Model.validate() with pathsToSkip option #14088 #14003 + * docs: remove "DEPRECATED" warning mistakenly added to read() tags param #13980 + 7.6.5 / 2023-11-14 ================== * fix: handle update validators and single nested doc with numeric paths #14066 #13977 diff --git a/package.json b/package.json index dd8138eca3d..05507004181 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.5", + "version": "7.6.6", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 4437d8edc93451e7e302f99aada56c0ee0a3f7af Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 29 Nov 2023 10:49:45 -0500 Subject: [PATCH 112/191] fix: allow adding discriminators using `Schema.prototype.discriminator()` to subdocuments after defining parent schema Fix #14109 --- lib/index.js | 10 +++++++ lib/schema/SubdocumentPath.js | 5 ---- lib/schema/documentarray.js | 8 +----- test/document.test.js | 51 +++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 12 deletions(-) diff --git a/lib/index.js b/lib/index.js index 3e7bf3709ef..c6cba262b8e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -629,6 +629,16 @@ Mongoose.prototype._model = function(name, schema, collection, options) { } } + for (const path of Object.keys(schema.paths)) { + const schemaType = schema.paths[path]; + if (!schemaType.schema || !schemaType.schema._applyDiscriminators) { + continue; + } + for (const disc of schemaType.schema._applyDiscriminators.keys()) { + schemaType.discriminator(disc, schemaType.schema._applyDiscriminators.get(disc)); + } + } + return model; }; diff --git a/lib/schema/SubdocumentPath.js b/lib/schema/SubdocumentPath.js index 2edba587321..7e77d71a0d9 100644 --- a/lib/schema/SubdocumentPath.js +++ b/lib/schema/SubdocumentPath.js @@ -55,11 +55,6 @@ function SubdocumentPath(schema, path, options) { this.$isSingleNested = true; this.base = schema.base; SchemaType.call(this, path, options, 'Embedded'); - if (schema._applyDiscriminators != null && !options?._skipApplyDiscriminators) { - for (const disc of schema._applyDiscriminators.keys()) { - this.discriminator(disc, schema._applyDiscriminators.get(disc)); - } - } } /*! diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js index b0cef6401a2..7f8a39a04d6 100644 --- a/lib/schema/documentarray.js +++ b/lib/schema/documentarray.js @@ -88,12 +88,6 @@ function DocumentArrayPath(key, schema, options, schemaOptions) { this.$embeddedSchemaType.caster = this.Constructor; this.$embeddedSchemaType.schema = this.schema; - - if (schema._applyDiscriminators != null && !options?._skipApplyDiscriminators) { - for (const disc of schema._applyDiscriminators.keys()) { - this.discriminator(disc, schema._applyDiscriminators.get(disc)); - } - } } /** @@ -528,7 +522,7 @@ DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) { DocumentArrayPath.prototype.clone = function() { const options = Object.assign({}, this.options); - const schematype = new this.constructor(this.path, this.schema, { ...options, _skipApplyDiscriminators: true }, this.schemaOptions); + const schematype = new this.constructor(this.path, this.schema, options, this.schemaOptions); schematype.validators = this.validators.slice(); if (this.requiredValidator !== undefined) { schematype.requiredValidator = this.requiredValidator; diff --git a/test/document.test.js b/test/document.test.js index b000de90fd8..16b565e38c0 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12657,6 +12657,57 @@ describe('document', function() { ); }); + it('handles embedded discriminators defined using Schema.prototype.discriminator after defining schema (gh-14109) (gh-13898)', async function() { + const baseNestedDiscriminated = new Schema({ + type: { type: Number, required: true } + }, { discriminatorKey: 'type' }); + + class BaseClass { + whoAmI() { + return 'I am baseNestedDiscriminated'; + } + } + BaseClass.type = 1; + + baseNestedDiscriminated.loadClass(BaseClass); + + class NumberTyped extends BaseClass { + whoAmI() { + return 'I am NumberTyped'; + } + } + NumberTyped.type = 3; + + class StringTyped extends BaseClass { + whoAmI() { + return 'I am StringTyped'; + } + } + StringTyped.type = 4; + + const containsNestedSchema = new Schema({ + nestedDiscriminatedTypes: { type: [baseNestedDiscriminated], required: true } + }); + + // After `containsNestedSchema`, in #13898 test these were before `containsNestedSchema` + baseNestedDiscriminated.discriminator(1, new Schema({}).loadClass(NumberTyped)); + baseNestedDiscriminated.discriminator('3', new Schema({}).loadClass(StringTyped)); + + class ContainsNested { + whoAmI() { + return 'I am ContainsNested'; + } + } + containsNestedSchema.loadClass(ContainsNested); + + const Test = db.model('Test', containsNestedSchema); + const instance = await Test.create({ type: 1, nestedDiscriminatedTypes: [{ type: 1 }, { type: '3' }] }); + assert.deepStrictEqual( + instance.nestedDiscriminatedTypes.map(i => i.whoAmI()), + ['I am NumberTyped', 'I am StringTyped'] + ); + }); + it('can use `collection` as schema name (gh-13956)', async function() { const schema = new mongoose.Schema({ name: String, collection: String }); const Test = db.model('Test', schema); From 159d70581743c186ca6ac22f45db48d4780c3d1c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 29 Nov 2023 11:28:28 -0500 Subject: [PATCH 113/191] fix: apply embedded discriminators recursively so fix for #14109 works on discriminators underneath subdocs --- .../applyEmbeddedDiscriminators.js | 23 ++++++++ lib/index.js | 11 +--- test/document.test.js | 52 +++++++++++++++++++ 3 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 lib/helpers/discriminator/applyEmbeddedDiscriminators.js diff --git a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js new file mode 100644 index 00000000000..930e1c86c13 --- /dev/null +++ b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js @@ -0,0 +1,23 @@ +'use strict'; + +module.exports = applyEmbeddedDiscriminators; + +function applyEmbeddedDiscriminators(schema, seen = new WeakSet()) { + if (seen.has(schema)) { + return; + } + seen.add(schema); + for (const path of Object.keys(schema.paths)) { + const schemaType = schema.paths[path]; + if (!schemaType.schema) { + continue; + } + applyEmbeddedDiscriminators(schemaType.schema, seen); + if (!schemaType.schema._applyDiscriminators) { + continue; + } + for (const disc of schemaType.schema._applyDiscriminators.keys()) { + schemaType.discriminator(disc, schemaType.schema._applyDiscriminators.get(disc)); + } + } +} diff --git a/lib/index.js b/lib/index.js index c6cba262b8e..90547f1e95b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -32,6 +32,7 @@ const sanitizeFilter = require('./helpers/query/sanitizeFilter'); const isBsonType = require('./helpers/isBsonType'); const MongooseError = require('./error/mongooseError'); const SetOptionError = require('./error/setOptionError'); +const applyEmbeddedDiscriminators = require('./helpers/discriminator/applyEmbeddedDiscriminators'); const defaultMongooseSymbol = Symbol.for('mongoose:default'); @@ -629,15 +630,7 @@ Mongoose.prototype._model = function(name, schema, collection, options) { } } - for (const path of Object.keys(schema.paths)) { - const schemaType = schema.paths[path]; - if (!schemaType.schema || !schemaType.schema._applyDiscriminators) { - continue; - } - for (const disc of schemaType.schema._applyDiscriminators.keys()) { - schemaType.discriminator(disc, schemaType.schema._applyDiscriminators.get(disc)); - } - } + applyEmbeddedDiscriminators(schema); return model; }; diff --git a/test/document.test.js b/test/document.test.js index 16b565e38c0..0e7ee27a510 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12708,6 +12708,58 @@ describe('document', function() { ); }); + it('handles embedded discriminators on nested path defined using Schema.prototype.discriminator (gh-14109) (gh-13898)', async function() { + const baseNestedDiscriminated = new Schema({ + type: { type: Number, required: true } + }, { discriminatorKey: 'type' }); + + class BaseClass { + whoAmI() { + return 'I am baseNestedDiscriminated'; + } + } + BaseClass.type = 1; + + baseNestedDiscriminated.loadClass(BaseClass); + + class NumberTyped extends BaseClass { + whoAmI() { + return 'I am NumberTyped'; + } + } + NumberTyped.type = 3; + + class StringTyped extends BaseClass { + whoAmI() { + return 'I am StringTyped'; + } + } + StringTyped.type = 4; + + const containsNestedSchema = new Schema({ + nestedDiscriminatedTypes: { type: [baseNestedDiscriminated], required: true } + }); + + baseNestedDiscriminated.discriminator(1, new Schema({}).loadClass(NumberTyped)); + baseNestedDiscriminated.discriminator('3', new Schema({}).loadClass(StringTyped)); + + const l2Schema = new Schema({ l3: containsNestedSchema }); + const l1Schema = new Schema({ l2: l2Schema }); + + const Test = db.model('Test', l1Schema); + const instance = await Test.create({ + l2: { + l3: { + nestedDiscriminatedTypes: [{ type: 1 }, { type: '3' }] + } + } + }); + assert.deepStrictEqual( + instance.l2.l3.nestedDiscriminatedTypes.map(i => i.whoAmI()), + ['I am NumberTyped', 'I am StringTyped'] + ); + }); + it('can use `collection` as schema name (gh-13956)', async function() { const schema = new mongoose.Schema({ name: String, collection: String }); const Test = db.model('Test', schema); From e1426caf44a3ab089339af9f13792398030e1634 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 29 Nov 2023 12:28:56 -0500 Subject: [PATCH 114/191] docs: update version support now that Mongoose 6 is past its legacy support date --- docs/version-support.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/docs/version-support.md b/docs/version-support.md index 512c3736def..eff39bbdb21 100644 --- a/docs/version-support.md +++ b/docs/version-support.md @@ -5,17 +5,11 @@ We ship all new bug fixes and features to 7.x. ## Mongoose 6 -Mongoose 6.x (released August 24, 2021) is currently in legacy support. -We will continue to ship bug fixes to Mongoose 6 until August 24, 2023. -After August 24, 2023, we will only ship security fixes, and backport requested fixes to Mongoose 6. +Mongoose 6.x (released August 24, 2021) is currently only receiving security fixes and requested bug fixes as of August 24, 2023. Please open a [bug report on GitHub](https://github.com/Automattic/mongoose/issues/new?assignees=&labels=&template=bug.yml) to request backporting a fix to Mongoose 6. -We are **not** actively backporting any new features from Mongoose 7 into Mongoose 6. -Until August 24, 2023, we will backport requested features into Mongoose 6; please open a [feature request on GitHub](https://github.com/Automattic/mongoose/issues/new?assignees=&labels=enhancement%2Cnew+feature&template=feature.yml) to request backporting a feature into Mongoose 6. -After August 24, 2023, we will not backport any new features into Mongoose 6. - We do not currently have a formal end of life (EOL) date for Mongoose 6. -However, we will not end support for Mongoose 6 until at least January 1, 2024. +However, we will not end support for Mongoose 6 until at least April 1, 2024. ## Mongoose 5 From 16e53086f85f0915fd4f72460f40a072c9babd34 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 30 Nov 2023 21:24:25 -0500 Subject: [PATCH 115/191] test: remove unnecessary code in tests for #14109 --- test/document.test.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/document.test.js b/test/document.test.js index 0e7ee27a510..f6862905519 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12667,7 +12667,6 @@ describe('document', function() { return 'I am baseNestedDiscriminated'; } } - BaseClass.type = 1; baseNestedDiscriminated.loadClass(BaseClass); @@ -12676,14 +12675,12 @@ describe('document', function() { return 'I am NumberTyped'; } } - NumberTyped.type = 3; class StringTyped extends BaseClass { whoAmI() { return 'I am StringTyped'; } } - StringTyped.type = 4; const containsNestedSchema = new Schema({ nestedDiscriminatedTypes: { type: [baseNestedDiscriminated], required: true } @@ -12718,7 +12715,6 @@ describe('document', function() { return 'I am baseNestedDiscriminated'; } } - BaseClass.type = 1; baseNestedDiscriminated.loadClass(BaseClass); @@ -12727,14 +12723,12 @@ describe('document', function() { return 'I am NumberTyped'; } } - NumberTyped.type = 3; class StringTyped extends BaseClass { whoAmI() { return 'I am StringTyped'; } } - StringTyped.type = 4; const containsNestedSchema = new Schema({ nestedDiscriminatedTypes: { type: [baseNestedDiscriminated], required: true } From 7584ffdfccfcfd3501a4ca07afdd0cf397ca4f24 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 28 Nov 2023 21:02:17 -0500 Subject: [PATCH 116/191] fix(schema): avoid creating unnecessary clone of schematype in nested array so nested document arrays use correct constructor Fix #14101 --- lib/schema.js | 7 +++---- test/types.documentarray.test.js | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index 1e234064446..23e097a375e 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1122,15 +1122,14 @@ Schema.prototype.path = function(path, obj) { if (_schemaType.$isMongooseDocumentArray) { _schemaType.$embeddedSchemaType._arrayPath = arrayPath; _schemaType.$embeddedSchemaType._arrayParentPath = path; - _schemaType = _schemaType.$embeddedSchemaType.clone(); + _schemaType = _schemaType.$embeddedSchemaType; } else { _schemaType.caster._arrayPath = arrayPath; _schemaType.caster._arrayParentPath = path; - _schemaType = _schemaType.caster.clone(); + _schemaType = _schemaType.caster; } - _schemaType.path = arrayPath; - toAdd.push(_schemaType); + this.subpaths[arrayPath] = _schemaType; } for (const _schemaType of toAdd) { diff --git a/test/types.documentarray.test.js b/test/types.documentarray.test.js index 073f34e4096..80f8b943b48 100644 --- a/test/types.documentarray.test.js +++ b/test/types.documentarray.test.js @@ -754,4 +754,28 @@ describe('types.documentarray', function() { assert.equal(doc.myMap.get('foo').$path(), 'myMap.foo'); }); + + it('bubbles up validation errors from doubly nested doc arrays (gh-14101)', async function() { + const optionsSchema = new mongoose.Schema({ + val: { + type: Number, + required: true + } + }); + + const testSchema = new mongoose.Schema({ + name: String, + options: { + type: [[optionsSchema]], + required: true + } + }); + + const Test = db.model('Test', testSchema); + + await assert.rejects( + Test.create({ name: 'test', options: [[{ val: null }]] }), + /options.0.0.val: Path `val` is required./ + ); + }); }); From 965b7d596d3c62e1a6917dcc29bbd748e404dfed Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 1 Dec 2023 14:53:22 -0500 Subject: [PATCH 117/191] fix(populate): call `transform` object with single id instead of array when populating a justOne path under an array Fix #14073 --- lib/helpers/populate/assignVals.js | 2 ++ test/model.populate.test.js | 55 ++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/lib/helpers/populate/assignVals.js b/lib/helpers/populate/assignVals.js index 017066add17..858846b42c0 100644 --- a/lib/helpers/populate/assignVals.js +++ b/lib/helpers/populate/assignVals.js @@ -80,6 +80,8 @@ module.exports = function assignVals(o) { return valueFilter(val[0], options, populateOptions, _allIds); } else if (o.justOne === false && !Array.isArray(val)) { return valueFilter([val], options, populateOptions, _allIds); + } else if (o.justOne === true && !Array.isArray(val) && Array.isArray(_allIds)) { + return valueFilter(val, options, populateOptions, val == null ? val : _allIds[o.rawOrder[val._id]]); } return valueFilter(val, options, populateOptions, _allIds); } diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 4e638a90178..4072a2a081a 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -10776,4 +10776,59 @@ describe('model: populate:', function() { new Date('2015-06-01').toString() ); }); + + it('calls transform with single ObjectId when populating justOne path underneath array (gh-14073)', async function() { + const mySchema = mongoose.Schema({ + name: { type: String }, + items: [{ + _id: false, + name: { type: String }, + brand: { type: mongoose.Schema.Types.ObjectId, ref: 'Brand' } + }] + }); + + const brandSchema = mongoose.Schema({ + name: 'String', + quantity: Number + }); + + const myModel = db.model('MyModel', mySchema); + const brandModel = db.model('Brand', brandSchema); + const { _id: id1 } = await brandModel.create({ + name: 'test', + quantity: 1 + }); + const { _id: id2 } = await brandModel.create({ + name: 'test1', + quantity: 1 + }); + const { _id: id3 } = await brandModel.create({ + name: 'test2', + quantity: 2 + }); + const brands = await brandModel.find(); + const test = new myModel({ name: 'Test Model' }); + for (let i = 0; i < brands.length; i++) { + test.items.push({ name: `${i}`, brand: brands[i]._id }); + } + await test.save(); + + const ids = []; + await myModel + .findOne() + .populate([ + { + path: 'items.brand', + transform: (doc, id) => { + ids.push(id); + return doc; + } + } + ]); + assert.equal(ids.length, 3); + assert.deepStrictEqual( + ids.map(id => id.toHexString()), + [id1.toString(), id2.toString(), id3.toString()] + ); + }); }); From 0282f50b5fe696ca9ae869c07f3cec41f2b40cb6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 1 Dec 2023 15:13:07 -0500 Subject: [PATCH 118/191] fix(populate): make sure to call `transform` with the correct index, even if no document found Re: #14073 --- lib/helpers/populate/assignVals.js | 5 ++++- test/model.populate.test.js | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/helpers/populate/assignVals.js b/lib/helpers/populate/assignVals.js index 858846b42c0..44de64fbc39 100644 --- a/lib/helpers/populate/assignVals.js +++ b/lib/helpers/populate/assignVals.js @@ -39,8 +39,10 @@ module.exports = function assignVals(o) { const options = o.options; const count = o.count && o.isVirtual; let i; + let setValueIndex = 0; function setValue(val) { + ++setValueIndex; if (count) { return val; } @@ -81,12 +83,13 @@ module.exports = function assignVals(o) { } else if (o.justOne === false && !Array.isArray(val)) { return valueFilter([val], options, populateOptions, _allIds); } else if (o.justOne === true && !Array.isArray(val) && Array.isArray(_allIds)) { - return valueFilter(val, options, populateOptions, val == null ? val : _allIds[o.rawOrder[val._id]]); + return valueFilter(val, options, populateOptions, val == null ? val : _allIds[setValueIndex - 1]); } return valueFilter(val, options, populateOptions, _allIds); } for (i = 0; i < docs.length; ++i) { + setValueIndex = 0; const _path = o.path.endsWith('.$*') ? o.path.slice(0, -3) : o.path; const existingVal = mpath.get(_path, docs[i], lookupLocalFields); if (existingVal == null && !getVirtual(o.originalModel.schema, _path)) { diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 4072a2a081a..9f629f28844 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -10811,6 +10811,9 @@ describe('model: populate:', function() { for (let i = 0; i < brands.length; i++) { test.items.push({ name: `${i}`, brand: brands[i]._id }); } + + const id4 = new mongoose.Types.ObjectId(); + test.items.push({ name: '4', brand: id4 }); await test.save(); const ids = []; @@ -10825,10 +10828,10 @@ describe('model: populate:', function() { } } ]); - assert.equal(ids.length, 3); + assert.equal(ids.length, 4); assert.deepStrictEqual( - ids.map(id => id.toHexString()), - [id1.toString(), id2.toString(), id3.toString()] + ids.map(id => id?.toHexString()), + [id1.toString(), id2.toString(), id3.toString(), id4.toString()] ); }); }); From 88db8aaaf8446025448115dd44e6a4005e863270 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 1 Dec 2023 15:40:33 -0500 Subject: [PATCH 119/191] types: add back mistakenly removed findByIdAndRemove() function signature Fix #14132 --- types/models.d.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/types/models.d.ts b/types/models.d.ts index 837e1e3ac2b..49cd4ccffce 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -567,6 +567,10 @@ declare module 'mongoose' { TRawDocType, 'findOneAndDelete' >; + findByIdAndRemove( + id?: mongodb.ObjectId | any, + options?: QueryOptions | null + ): QueryWithHelpers; /** Creates a `findOneAndUpdate` query, filtering by the given `_id`. */ findByIdAndUpdate( From c752f40a9e502f402b520ee764c3fa7f4c5251ce Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 4 Dec 2023 12:59:33 -0500 Subject: [PATCH 120/191] fix: avoid minimizing single nested subdocs if they are required Fix #14058 --- lib/helpers/clone.js | 2 +- lib/schema/SubdocumentPath.js | 5 +++-- test/document.test.js | 11 +++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/helpers/clone.js b/lib/helpers/clone.js index 8aac7163422..b83858acdf7 100644 --- a/lib/helpers/clone.js +++ b/lib/helpers/clone.js @@ -54,7 +54,7 @@ function clone(obj, options, isArrayChild) { ret = obj.toObject(options); } - if (options && options.minimize && isSingleNested && Object.keys(ret).length === 0) { + if (options && options.minimize && !obj.constructor.$__required && isSingleNested && Object.keys(ret).length === 0) { return undefined; } diff --git a/lib/schema/SubdocumentPath.js b/lib/schema/SubdocumentPath.js index 7e77d71a0d9..ae45b7ab259 100644 --- a/lib/schema/SubdocumentPath.js +++ b/lib/schema/SubdocumentPath.js @@ -48,7 +48,7 @@ function SubdocumentPath(schema, path, options) { schema = handleIdOption(schema, options); - this.caster = _createConstructor(schema); + this.caster = _createConstructor(schema, null, options); this.caster.path = path; this.caster.prototype.$basePath = path; this.schema = schema; @@ -69,7 +69,7 @@ SubdocumentPath.prototype.OptionsConstructor = SchemaSubdocumentOptions; * ignore */ -function _createConstructor(schema, baseClass) { +function _createConstructor(schema, baseClass, options) { // lazy load Subdocument || (Subdocument = require('../types/subdocument')); @@ -89,6 +89,7 @@ function _createConstructor(schema, baseClass) { _embedded.prototype = Object.create(proto); _embedded.prototype.$__setSchema(schema); _embedded.prototype.constructor = _embedded; + _embedded.$__required = options?.required; _embedded.base = schema.base; _embedded.schema = schema; _embedded.$isSingleNested = true; diff --git a/test/document.test.js b/test/document.test.js index f6862905519..fff52d1e7b1 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -793,6 +793,17 @@ describe('document', function() { assert.strictEqual(myModel.toObject().foo, void 0); }); + it('does not minimize single nested subdocs if they are required (gh-14058) (gh-11247)', async function() { + const nestedSchema = Schema({ bar: String }, { _id: false }); + const schema = Schema({ foo: { type: nestedSchema, required: true } }); + + const MyModel = db.model('Test', schema); + + const myModel = await MyModel.create({ foo: {} }); + + assert.deepStrictEqual(myModel.toObject().foo, {}); + }); + it('should propogate toObject to implicitly created schemas (gh-13599) (gh-13325)', async function() { const transformCalls = []; const userSchema = Schema({ From 1f1f897922fc7eb2046ca2dceea7ef72525e9d6b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 5 Dec 2023 10:49:06 -0500 Subject: [PATCH 121/191] fix(populate): allow deselecting discriminator key when populating Fix #3230 Re: #13760 Re: #13679 --- lib/helpers/projection/isExclusive.js | 5 ++--- lib/model.js | 2 +- lib/query.js | 9 +++------ lib/queryhelpers.js | 5 +++-- lib/utils.js | 6 ------ test/model.populate.test.js | 25 +++++++++++++++++++++++++ 6 files changed, 34 insertions(+), 18 deletions(-) diff --git a/lib/helpers/projection/isExclusive.js b/lib/helpers/projection/isExclusive.js index a232857d601..b55cf468458 100644 --- a/lib/helpers/projection/isExclusive.js +++ b/lib/helpers/projection/isExclusive.js @@ -12,13 +12,12 @@ module.exports = function isExclusive(projection) { } const keys = Object.keys(projection); - let ki = keys.length; let exclude = null; - if (ki === 1 && keys[0] === '_id') { + if (keys.length === 1 && keys[0] === '_id') { exclude = !projection._id; } else { - while (ki--) { + for (let ki = 0; ki < keys.length; ++ki) { // Does this projection explicitly define inclusion/exclusion? // Explicitly avoid `$meta` and `$slice` const key = keys[ki]; diff --git a/lib/model.js b/lib/model.js index 8b0a99e05b6..e97407429aa 100644 --- a/lib/model.js +++ b/lib/model.js @@ -4408,7 +4408,7 @@ function populate(model, docs, options, callback) { select = select.replace(excludeIdRegGlobal, ' '); } else { // preserve original select conditions by copying - select = utils.object.shallowCopy(select); + select = { ...select }; delete select._id; } } diff --git a/lib/query.js b/lib/query.js index 98112653776..a6063cc5fc4 100644 --- a/lib/query.js +++ b/lib/query.js @@ -4932,16 +4932,14 @@ Query.prototype._castFields = function _castFields(fields) { elemMatchKeys, keys, key, - out, - i; + out; if (fields) { keys = Object.keys(fields); elemMatchKeys = []; - i = keys.length; // collect $elemMatch args - while (i--) { + for (let i = 0; i < keys.length; ++i) { key = keys[i]; if (fields[key].$elemMatch) { selected || (selected = {}); @@ -4960,8 +4958,7 @@ Query.prototype._castFields = function _castFields(fields) { } // apply the casted field args - i = elemMatchKeys.length; - while (i--) { + for (let i = 0; i < elemMatchKeys.length; ++i) { key = elemMatchKeys[i]; fields[key] = out[key]; } diff --git a/lib/queryhelpers.js b/lib/queryhelpers.js index 8d5f3aea0c0..90d8739ef8b 100644 --- a/lib/queryhelpers.js +++ b/lib/queryhelpers.js @@ -180,11 +180,12 @@ exports.applyPaths = function applyPaths(fields, schema) { if (!isDefiningProjection(field)) { continue; } - // `_id: 1, name: 0` is a mixed inclusive/exclusive projection in - // MongoDB 4.0 and earlier, but not in later versions. if (keys[keyIndex] === '_id' && keys.length > 1) { continue; } + if (keys[keyIndex] === schema.options.discriminatorKey && keys.length > 1 && field != null && !field) { + continue; + } exclude = !field; break; } diff --git a/lib/utils.js b/lib/utils.js index 4fca9502d56..fa17f2f2d48 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -687,12 +687,6 @@ exports.object.vals = function vals(o) { return ret; }; -/** - * @see exports.options - */ - -exports.object.shallowCopy = exports.options; - const hop = Object.prototype.hasOwnProperty; /** diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 9f629f28844..1c6029853de 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -10834,4 +10834,29 @@ describe('model: populate:', function() { [id1.toString(), id2.toString(), id3.toString(), id4.toString()] ); }); + + it('allows deselecting discriminator key when populating (gh-3230) (gh-13760) (gh-13679)', async function() { + const Test = db.model( + 'Test', + Schema({ name: String, arr: [{ testRef: { type: 'ObjectId', ref: 'Test2' } }] }) + ); + + const schema = Schema({ name: String }); + const Test2 = db.model('Test2', schema); + const D = Test2.discriminator('D', Schema({ prop: String })); + + + await Test.deleteMany({}); + await Test2.deleteMany({}); + const { _id } = await D.create({ name: 'foo', prop: 'bar' }); + const test = await Test.create({ name: 'test', arr: [{ testRef: _id }] }); + + const doc = await Test + .findById(test._id) + .populate('arr.testRef', { name: 1, prop: 1, _id: 0, __t: 0 }); + assert.deepStrictEqual( + doc.toObject().arr[0].testRef, + { name: 'foo', prop: 'bar' } + ); + }); }); From 78d3438c1cdf7ca46818aabd6742083796b9df23 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Tue, 5 Dec 2023 19:13:54 +0100 Subject: [PATCH 122/191] refactor(utils): remove "options" function it was for shallow-copying objects, which is now solved by spread-operators --- lib/document.js | 6 +++--- lib/model.js | 2 +- lib/schema.js | 7 ++++--- lib/schema/bigint.js | 6 +++--- lib/schema/boolean.js | 4 +--- lib/schema/buffer.js | 22 +++++++++++----------- lib/schema/date.js | 14 +++++++------- lib/schema/decimal128.js | 15 +++++++-------- lib/schema/number.js | 24 ++++++++++++------------ lib/schema/objectid.js | 14 +++++++------- lib/schema/string.js | 5 +++-- lib/schema/uuid.js | 6 +++--- lib/utils.js | 33 --------------------------------- test/utils.test.js | 23 ----------------------- 14 files changed, 62 insertions(+), 119 deletions(-) diff --git a/lib/document.js b/lib/document.js index 08ba3a58c44..6ed5d116057 100644 --- a/lib/document.js +++ b/lib/document.js @@ -3690,8 +3690,8 @@ Document.prototype.$toObject = function(options, json) { const schemaOptions = this.$__schema && this.$__schema.options || {}; // merge base default options with Schema's set default options if available. // `clone` is necessary here because `utils.options` directly modifies the second input. - defaultOptions = utils.options(defaultOptions, clone(baseOptions)); - defaultOptions = utils.options(defaultOptions, clone(schemaOptions[path] || {})); + defaultOptions = { ...defaultOptions, ...clone(baseOptions) }; + defaultOptions = { ...defaultOptions, ...clone(schemaOptions[path] || {}) }; // If options do not exist or is not an object, set it to empty object options = utils.isPOJO(options) ? { ...options } : {}; @@ -3753,7 +3753,7 @@ Document.prototype.$toObject = function(options, json) { } // merge default options with input options. - options = utils.options(defaultOptions, options); + options = { ...defaultOptions, ...options }; options._isNested = true; options.json = json; options.minimize = _minimize; diff --git a/lib/model.js b/lib/model.js index 8b0a99e05b6..e97407429aa 100644 --- a/lib/model.js +++ b/lib/model.js @@ -4408,7 +4408,7 @@ function populate(model, docs, options, callback) { select = select.replace(excludeIdRegGlobal, ' '); } else { // preserve original select conditions by copying - select = utils.object.shallowCopy(select); + select = { ...select }; delete select._id; } } diff --git a/lib/schema.js b/lib/schema.js index 23e097a375e..d94bcb0e00b 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -560,7 +560,7 @@ Schema.prototype.defaultOptions = function(options) { const strict = 'strict' in baseOptions ? baseOptions.strict : true; const strictQuery = 'strictQuery' in baseOptions ? baseOptions.strictQuery : false; const id = 'id' in baseOptions ? baseOptions.id : true; - options = utils.options({ + options = { strict, strictQuery, bufferCommands: true, @@ -577,8 +577,9 @@ Schema.prototype.defaultOptions = function(options) { // the following are only applied at construction time _id: true, id: id, - typeKey: 'type' - }, clone(options)); + typeKey: 'type', + ...clone(options) + }; if (options.versionKey && typeof options.versionKey !== 'string') { throw new MongooseError('`versionKey` must be falsy or string, got `' + (typeof options.versionKey) + '`'); diff --git a/lib/schema/bigint.js b/lib/schema/bigint.js index 4c7dcb77039..00e65070db7 100644 --- a/lib/schema/bigint.js +++ b/lib/schema/bigint.js @@ -7,7 +7,6 @@ const CastError = require('../error/cast'); const SchemaType = require('../schematype'); const castBigInt = require('../cast/bigint'); -const utils = require('../utils'); /** * BigInt SchemaType constructor. @@ -177,12 +176,13 @@ SchemaBigInt.prototype.cast = function(value) { * ignore */ -SchemaBigInt.$conditionalHandlers = utils.options(SchemaType.prototype.$conditionalHandlers, { +SchemaBigInt.$conditionalHandlers = { + ...SchemaType.prototype.$conditionalHandlers, $gt: handleSingle, $gte: handleSingle, $lt: handleSingle, $lte: handleSingle -}); +}; /*! * ignore diff --git a/lib/schema/boolean.js b/lib/schema/boolean.js index 316c825df45..51065176ace 100644 --- a/lib/schema/boolean.js +++ b/lib/schema/boolean.js @@ -7,7 +7,6 @@ const CastError = require('../error/cast'); const SchemaType = require('../schematype'); const castBoolean = require('../cast/boolean'); -const utils = require('../utils'); /** * Boolean SchemaType constructor. @@ -235,8 +234,7 @@ SchemaBoolean.prototype.cast = function(value) { } }; -SchemaBoolean.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, {}); +SchemaBoolean.$conditionalHandlers = { ...SchemaType.prototype.$conditionalHandlers }; /** * Casts contents for queries. diff --git a/lib/schema/buffer.js b/lib/schema/buffer.js index 5bfaabcd2f6..77c6779fe6e 100644 --- a/lib/schema/buffer.js +++ b/lib/schema/buffer.js @@ -250,17 +250,17 @@ function handleSingle(val, context) { return this.castForQuery(null, val, context); } -SchemaBuffer.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - $bitsAllClear: handleBitwiseOperator, - $bitsAnyClear: handleBitwiseOperator, - $bitsAllSet: handleBitwiseOperator, - $bitsAnySet: handleBitwiseOperator, - $gt: handleSingle, - $gte: handleSingle, - $lt: handleSingle, - $lte: handleSingle - }); +SchemaBuffer.prototype.$conditionalHandlers = { + ...SchemaType.prototype.$conditionalHandlers, + $bitsAllClear: handleBitwiseOperator, + $bitsAnyClear: handleBitwiseOperator, + $bitsAllSet: handleBitwiseOperator, + $bitsAnySet: handleBitwiseOperator, + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle +}; /** * Casts contents for queries. diff --git a/lib/schema/date.js b/lib/schema/date.js index 61928bb457d..6c60b835d0f 100644 --- a/lib/schema/date.js +++ b/lib/schema/date.js @@ -388,13 +388,13 @@ function handleSingle(val) { return this.cast(val); } -SchemaDate.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - $gt: handleSingle, - $gte: handleSingle, - $lt: handleSingle, - $lte: handleSingle - }); +SchemaDate.prototype.$conditionalHandlers = { + ...SchemaType.prototype.$conditionalHandlers, + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle +}; /** diff --git a/lib/schema/decimal128.js b/lib/schema/decimal128.js index 650d78c2571..2b93aee580b 100644 --- a/lib/schema/decimal128.js +++ b/lib/schema/decimal128.js @@ -7,7 +7,6 @@ const SchemaType = require('../schematype'); const CastError = SchemaType.CastError; const castDecimal128 = require('../cast/decimal128'); -const utils = require('../utils'); const isBsonType = require('../helpers/isBsonType'); /** @@ -214,13 +213,13 @@ function handleSingle(val) { return this.cast(val); } -Decimal128.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - $gt: handleSingle, - $gte: handleSingle, - $lt: handleSingle, - $lte: handleSingle - }); +Decimal128.prototype.$conditionalHandlers = { + ...SchemaType.prototype.$conditionalHandlers, + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle +}; /*! * Module exports. diff --git a/lib/schema/number.js b/lib/schema/number.js index b2695cd94a6..4aa12d01d48 100644 --- a/lib/schema/number.js +++ b/lib/schema/number.js @@ -399,18 +399,18 @@ function handleArray(val) { }); } -SchemaNumber.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - $bitsAllClear: handleBitwiseOperator, - $bitsAnyClear: handleBitwiseOperator, - $bitsAllSet: handleBitwiseOperator, - $bitsAnySet: handleBitwiseOperator, - $gt: handleSingle, - $gte: handleSingle, - $lt: handleSingle, - $lte: handleSingle, - $mod: handleArray - }); +SchemaNumber.prototype.$conditionalHandlers = { + ...SchemaType.prototype.$conditionalHandlers, + $bitsAllClear: handleBitwiseOperator, + $bitsAnyClear: handleBitwiseOperator, + $bitsAllSet: handleBitwiseOperator, + $bitsAnySet: handleBitwiseOperator, + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle, + $mod: handleArray +}; /** * Casts contents for queries. diff --git a/lib/schema/objectid.js b/lib/schema/objectid.js index f0a0b6be747..bdd4ce2e5b7 100644 --- a/lib/schema/objectid.js +++ b/lib/schema/objectid.js @@ -259,13 +259,13 @@ function handleSingle(val) { return this.cast(val); } -ObjectId.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - $gt: handleSingle, - $gte: handleSingle, - $lt: handleSingle, - $lte: handleSingle - }); +ObjectId.prototype.$conditionalHandlers = { + ...SchemaType.prototype.$conditionalHandlers, + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle +}; /*! * ignore diff --git a/lib/schema/string.js b/lib/schema/string.js index 5caee2e3cfc..5971af6af51 100644 --- a/lib/schema/string.js +++ b/lib/schema/string.js @@ -641,7 +641,8 @@ function handleSingleNoSetters(val) { return this.cast(val, this); } -const $conditionalHandlers = utils.options(SchemaType.prototype.$conditionalHandlers, { +const $conditionalHandlers = { + ...SchemaType.prototype.$conditionalHandlers, $all: handleArray, $gt: handleSingle, $gte: handleSingle, @@ -656,7 +657,7 @@ const $conditionalHandlers = utils.options(SchemaType.prototype.$conditionalHand return handleSingleNoSetters.call(this, val); }, $not: handleSingle -}); +}; Object.defineProperty(SchemaString.prototype, '$conditionalHandlers', { configurable: false, diff --git a/lib/schema/uuid.js b/lib/schema/uuid.js index 9de95f6cd4d..2f415e37dd7 100644 --- a/lib/schema/uuid.js +++ b/lib/schema/uuid.js @@ -313,8 +313,8 @@ function handleArray(val) { }); } -SchemaUUID.prototype.$conditionalHandlers = -utils.options(SchemaType.prototype.$conditionalHandlers, { +SchemaUUID.prototype.$conditionalHandlers = { + ...SchemaType.prototype.$conditionalHandlers, $bitsAllClear: handleBitwiseOperator, $bitsAnyClear: handleBitwiseOperator, $bitsAllSet: handleBitwiseOperator, @@ -327,7 +327,7 @@ utils.options(SchemaType.prototype.$conditionalHandlers, { $lte: handleSingle, $ne: handleSingle, $nin: handleArray -}); +}; /** * Casts contents for queries. diff --git a/lib/utils.js b/lib/utils.js index 4fca9502d56..512b7728a3e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -227,33 +227,6 @@ exports.omit = function omit(obj, keys) { return ret; }; - -/** - * Shallow copies defaults into options. - * - * @param {Object} defaults - * @param {Object} [options] - * @return {Object} the merged object - * @api private - */ - -exports.options = function(defaults, options) { - const keys = Object.keys(defaults); - let i = keys.length; - let k; - - options = options || {}; - - while (i--) { - k = keys[i]; - if (!(k in options)) { - options[k] = defaults[k]; - } - } - - return options; -}; - /** * Merges `from` into `to` without overwriting existing properties. * @@ -687,12 +660,6 @@ exports.object.vals = function vals(o) { return ret; }; -/** - * @see exports.options - */ - -exports.object.shallowCopy = exports.options; - const hop = Object.prototype.hasOwnProperty; /** diff --git a/test/utils.test.js b/test/utils.test.js index fe321b424c6..85b76bb49cb 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -121,29 +121,6 @@ describe('utils', function() { }); }); - it('utils.options', function() { - const o = { a: 1, b: 2, c: 3, 0: 'zero1' }; - const defaults = { b: 10, d: 20, 0: 'zero2' }; - const result = utils.options(defaults, o); - assert.equal(result.a, 1); - assert.equal(result.b, 2); - assert.equal(result.c, 3); - assert.equal(result.d, 20); - assert.deepEqual(o.d, result.d); - assert.equal(result['0'], 'zero1'); - - const result2 = utils.options(defaults); - assert.equal(result2.b, 10); - assert.equal(result2.d, 20); - assert.equal(result2['0'], 'zero2'); - - // same properties/vals - assert.deepEqual(defaults, result2); - - // same object - assert.notEqual(defaults, result2); - }); - it('deepEquals on ObjectIds', function() { const s = (new ObjectId()).toString(); From 006740a78e412abe7956177211cd7d4acd19aebb Mon Sep 17 00:00:00 2001 From: hasezoey Date: Tue, 5 Dec 2023 19:14:19 +0100 Subject: [PATCH 123/191] refactor(document): remove "clone" where previously necessary because of "utils.options" --- lib/document.js | 3 +-- lib/schema.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/document.js b/lib/document.js index 6ed5d116057..e7cc4e00175 100644 --- a/lib/document.js +++ b/lib/document.js @@ -3690,8 +3690,7 @@ Document.prototype.$toObject = function(options, json) { const schemaOptions = this.$__schema && this.$__schema.options || {}; // merge base default options with Schema's set default options if available. // `clone` is necessary here because `utils.options` directly modifies the second input. - defaultOptions = { ...defaultOptions, ...clone(baseOptions) }; - defaultOptions = { ...defaultOptions, ...clone(schemaOptions[path] || {}) }; + defaultOptions = { ...defaultOptions, ...baseOptions, ...schemaOptions[path] }; // If options do not exist or is not an object, set it to empty object options = utils.isPOJO(options) ? { ...options } : {}; diff --git a/lib/schema.js b/lib/schema.js index d94bcb0e00b..3087f157d3f 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -578,7 +578,7 @@ Schema.prototype.defaultOptions = function(options) { _id: true, id: id, typeKey: 'type', - ...clone(options) + ...options }; if (options.versionKey && typeof options.versionKey !== 'string') { From 4a3851760dc1f13558601a57e9f60de1c40d4b1c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 6 Dec 2023 12:52:44 -0500 Subject: [PATCH 124/191] chore: release 7.6.7 --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0da10349286..362e96cd859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +7.6.7 / 2023-12-06 +================== + * fix: avoid minimizing single nested subdocs if they are required #14151 #14058 + * fix(populate): allow deselecting discriminator key when populating #14155 #3230 + * fix: allow adding discriminators using Schema.prototype.discriminator() to subdocuments after defining parent schema #14131 #14109 + * fix(schema): avoid creating unnecessary clone of schematype in nested array so nested document arrays use correct constructor #14128 #14101 + * fix(populate): call transform object with single id instead of array when populating a justOne path under an array #14135 #14073 + * types: add back mistakenly removed findByIdAndRemove() function signature #14136 #14132 + 7.6.6 / 2023-11-27 ================== * perf: avoid double-running setter logic when calling `push()` #14120 #11380 diff --git a/package.json b/package.json index 05507004181..efbcc7a2b50 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.6", + "version": "7.6.7", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 637b63e05b7ac05099479999032338678eabfc6a Mon Sep 17 00:00:00 2001 From: Rohan Kothapalli Date: Mon, 11 Dec 2023 19:12:57 +0530 Subject: [PATCH 125/191] null check --- lib/document.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index 1143a1d37cc..7de74b86395 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1689,7 +1689,7 @@ Document.prototype.$__set = function(pathToMark, path, options, constructing, pa val[arrayAtomicsSymbol] = priorVal[arrayAtomicsSymbol]; val[arrayAtomicsBackupSymbol] = priorVal[arrayAtomicsBackupSymbol]; if (utils.isMongooseDocumentArray(val)) { - val.forEach(doc => { doc.isNew = false; }); + val.forEach(doc => { doc && (doc.isNew = false); }); } } From 30315ce1e60407185761303409e8efd382f5d750 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 11 Dec 2023 17:21:52 -0500 Subject: [PATCH 126/191] fix(document): avoid treating nested projection as inclusive when applying defaults Fix #14115 --- lib/helpers/document/applyDefaults.js | 4 +++- lib/helpers/projection/hasIncludedChildren.js | 1 + lib/helpers/projection/isNestedProjection.js | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 lib/helpers/projection/isNestedProjection.js diff --git a/lib/helpers/document/applyDefaults.js b/lib/helpers/document/applyDefaults.js index 10a4e1ffefd..631d5151e5b 100644 --- a/lib/helpers/document/applyDefaults.js +++ b/lib/helpers/document/applyDefaults.js @@ -1,5 +1,7 @@ 'use strict'; +const isNestedProjection = require('../projection/isNestedProjection'); + module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildren, isBeforeSetters, pathsToSkip) { const paths = Object.keys(doc.$__schema.paths); const plen = paths.length; @@ -32,7 +34,7 @@ module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildre } } else if (exclude === false && fields && !included) { const hasSubpaths = type.$isSingleNested || type.$isMongooseDocumentArray; - if (curPath in fields || (j === len - 1 && hasSubpaths && hasIncludedChildren != null && hasIncludedChildren[curPath])) { + if ((curPath in fields && !isNestedProjection(fields[curPath])) || (j === len - 1 && hasSubpaths && hasIncludedChildren != null && hasIncludedChildren[curPath])) { included = true; } else if (hasIncludedChildren != null && !hasIncludedChildren[curPath]) { break; diff --git a/lib/helpers/projection/hasIncludedChildren.js b/lib/helpers/projection/hasIncludedChildren.js index d757b2c677e..50afc5adfb7 100644 --- a/lib/helpers/projection/hasIncludedChildren.js +++ b/lib/helpers/projection/hasIncludedChildren.js @@ -21,6 +21,7 @@ module.exports = function hasIncludedChildren(fields) { const keys = Object.keys(fields); for (const key of keys) { + if (key.indexOf('.') === -1) { hasIncludedChildren[key] = 1; continue; diff --git a/lib/helpers/projection/isNestedProjection.js b/lib/helpers/projection/isNestedProjection.js new file mode 100644 index 00000000000..f53d9cddf3a --- /dev/null +++ b/lib/helpers/projection/isNestedProjection.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function isNestedProjection(val) { + if (val == null || typeof val !== 'object') { + return false; + } + return val.$slice == null && val.$elemMatch == null && val.$meta == null && val.$ == null; +}; From 2a78e255fa2684f8895cce29fd5e450a45ef73c4 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 13 Dec 2023 15:35:05 -0500 Subject: [PATCH 127/191] refactor: use fix from #13883 for #14172 --- lib/document.js | 6 +++++- test/document.test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index 7de74b86395..8acb6103a4c 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1689,7 +1689,11 @@ Document.prototype.$__set = function(pathToMark, path, options, constructing, pa val[arrayAtomicsSymbol] = priorVal[arrayAtomicsSymbol]; val[arrayAtomicsBackupSymbol] = priorVal[arrayAtomicsBackupSymbol]; if (utils.isMongooseDocumentArray(val)) { - val.forEach(doc => { doc && (doc.isNew = false); }); + val.forEach(doc => { + if (doc) { + doc.isNew = false; + } + }); } } diff --git a/test/document.test.js b/test/document.test.js index e6b343dba2f..a0ba629d72f 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12383,6 +12383,37 @@ describe('document', function() { ['__stateBeforeSuspension', '__stateBeforeSuspension.jsonField'] ); }); + + it('should allow null values in list in self assignment (gh-14172) (gh-13859)', async function() { + const objSchema = new Schema({ + date: Date, + value: Number + }); + + const testSchema = new Schema({ + intArray: [Number], + strArray: [String], + objArray: [objSchema] + }); + const Test = db.model('Test', testSchema); + + const doc = new Test({ + intArray: [1, 2, 3, null], + strArray: ['b', null, 'c'], + objArray: [ + { date: new Date(1000), value: 1 }, + null, + { date: new Date(3000), value: 3 } + ] + }); + await doc.save(); + doc.intArray = doc.intArray; + doc.strArray = doc.strArray; + doc.objArray = doc.objArray; // this is the trigger for the error + assert.ok(doc); + await doc.save(); + assert.ok(doc); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() { From dadac4e582a8ff3bb2cb3924e3a895c7a8eaa2d6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 13 Dec 2023 17:33:17 -0500 Subject: [PATCH 128/191] test: add test case for #14115 --- test/query.test.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/query.test.js b/test/query.test.js index 163bc1352f3..13b2b032662 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -4383,4 +4383,33 @@ describe('Query', function() { await Error.find().sort('-'); }, { message: 'Invalid field "" passed to sort()' }); }); + + it('does not apply sibling path defaults if using nested projection (gh-14115)', async function() { + const userSchema = new mongoose.Schema({ + name: String, + account: { + amount: Number, + owner: { type: String, default: () => 'OWNER' }, + taxIds: [Number] + } + }); + const User = db.model('User', userSchema); + + const { _id } = await User.create({ + name: 'test', + account: { + amount: 25, + owner: 'test', + taxIds: [42] + } + }); + + const doc = await User + .findOne({ _id }, { name: 1, account: { amount: 1 } }) + .orFail(); + assert.strictEqual(doc.name, 'test'); + assert.strictEqual(doc.account.amount, 25); + assert.strictEqual(doc.account.owner, undefined); + assert.strictEqual(doc.account.taxIds, undefined); + }); }); From 95d917bf79aa1fe1558d11f2dafaf4ca17d9628e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 13 Dec 2023 17:37:33 -0500 Subject: [PATCH 129/191] test: skip tests for #14115 if MongoDB < 5 --- test/query.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/query.test.js b/test/query.test.js index 13b2b032662..5c5e70e8062 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -4385,6 +4385,11 @@ describe('Query', function() { }); it('does not apply sibling path defaults if using nested projection (gh-14115)', async function() { + const version = await start.mongodVersion(); + if (version[0] < 5) { + return this.skip(); + } + const userSchema = new mongoose.Schema({ name: String, account: { From ff75c75d03cb7e812044035dc9388c0434abcf30 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 27 Dec 2023 15:24:04 -0500 Subject: [PATCH 130/191] fix: upgrade mongodb driver -> 4.17.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 17d4fb86464..c1ac7a18e4a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "bson": "^4.7.2", "kareem": "2.5.1", - "mongodb": "4.17.1", + "mongodb": "4.17.2", "mpath": "0.9.0", "mquery": "4.0.3", "ms": "2.1.3", From 44f391beb7c9167d80a7a7fda4d195b1ca8399a5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 27 Dec 2023 15:27:36 -0500 Subject: [PATCH 131/191] chore: release 6.12.4 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70d84ba4379..569fe469d74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +6.12.4 / 2023-12-27 +=================== + * fix: upgrade mongodb driver -> 4.17.2 + * fix(document): avoid treating nested projection as inclusive when applying defaults #14173 #14115 + * fix: account for null values when assigning isNew property #14172 #13883 + 6.12.3 / 2023-11-07 =================== * fix(ChangeStream): correctly handle hydrate option when using change stream as stream instead of iterator #14052 diff --git a/package.json b/package.json index c1ac7a18e4a..7fba35dfbe4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.12.3", + "version": "6.12.4", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 9725d93f6d49c13eaf49547b849e82d9879b942d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 27 Dec 2023 17:38:32 -0500 Subject: [PATCH 132/191] fix(document): avoid flattening dotted paths in mixed path underneath nested path Fix #14178 --- lib/document.js | 2 -- test/document.test.js | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/document.js b/lib/document.js index 8acb6103a4c..432e4105ee5 100644 --- a/lib/document.js +++ b/lib/document.js @@ -472,8 +472,6 @@ function $applyDefaultsToNested(val, path, doc) { return; } - flattenObjectWithDottedPaths(val); - const paths = Object.keys(doc.$__schema.paths); const plen = paths.length; diff --git a/test/document.test.js b/test/document.test.js index a0ba629d72f..8a95159c2e0 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12414,6 +12414,29 @@ describe('document', function() { await doc.save(); assert.ok(doc); }); + + it('avoids overwriting dotted paths in mixed path underneath nested path (gh-14178)', async function() { + const testSchema = new Schema({ + __stateBeforeSuspension: { + field1: String, + field3: { type: Schema.Types.Mixed }, + } + }); + const Test = db.model('Test', testSchema); + let eventObj = new Test({ + __stateBeforeSuspension: { field1: 'test' } + }) + await eventObj.save(); + const newO = eventObj.toObject(); + newO.__stateBeforeSuspension.field3 = {'.ippo': 5}; + eventObj.set(newO); + await eventObj.save(); + + assert.strictEqual(eventObj.__stateBeforeSuspension.field3['.ippo'], 5); + + const fromDb = await Test.findById(eventObj._id).lean().orFail(); + assert.strictEqual(fromDb.__stateBeforeSuspension.field3['.ippo'], 5); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() { From 4def30212ab0ac708a3ba1c03bc0e58cb9e4388d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 27 Dec 2023 17:40:17 -0500 Subject: [PATCH 133/191] style: fix lint and remove unused file --- lib/document.js | 1 - .../path/flattenObjectWithDottedPaths.js | 39 ------------------- 2 files changed, 40 deletions(-) delete mode 100644 lib/helpers/path/flattenObjectWithDottedPaths.js diff --git a/lib/document.js b/lib/document.js index 432e4105ee5..4b8cc160942 100644 --- a/lib/document.js +++ b/lib/document.js @@ -22,7 +22,6 @@ const cleanModifiedSubpaths = require('./helpers/document/cleanModifiedSubpaths' const compile = require('./helpers/document/compile').compile; const defineKey = require('./helpers/document/compile').defineKey; const flatten = require('./helpers/common').flatten; -const flattenObjectWithDottedPaths = require('./helpers/path/flattenObjectWithDottedPaths'); const get = require('./helpers/get'); const getEmbeddedDiscriminatorPath = require('./helpers/document/getEmbeddedDiscriminatorPath'); const getKeysInSchemaOrder = require('./helpers/schema/getKeysInSchemaOrder'); diff --git a/lib/helpers/path/flattenObjectWithDottedPaths.js b/lib/helpers/path/flattenObjectWithDottedPaths.js deleted file mode 100644 index 2771796d082..00000000000 --- a/lib/helpers/path/flattenObjectWithDottedPaths.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -const MongooseError = require('../../error/mongooseError'); -const isMongooseObject = require('../isMongooseObject'); -const setDottedPath = require('../path/setDottedPath'); -const util = require('util'); - -/** - * Given an object that may contain dotted paths, flatten the paths out. - * For example: `flattenObjectWithDottedPaths({ a: { 'b.c': 42 } })` => `{ a: { b: { c: 42 } } }` - */ - -module.exports = function flattenObjectWithDottedPaths(obj) { - if (obj == null || typeof obj !== 'object' || Array.isArray(obj)) { - return; - } - // Avoid Mongoose docs, like docs and maps, because these may cause infinite recursion - if (isMongooseObject(obj)) { - return; - } - const keys = Object.keys(obj); - for (const key of keys) { - const val = obj[key]; - if (key.indexOf('.') !== -1) { - try { - delete obj[key]; - setDottedPath(obj, key, val); - } catch (err) { - if (!(err instanceof TypeError)) { - throw err; - } - throw new MongooseError(`Conflicting dotted paths when setting document path, key: "${key}", value: ${util.inspect(val)}`); - } - continue; - } - - flattenObjectWithDottedPaths(obj[key]); - } -}; From e4f4c3236a180c97e0c57c27361a82ac05525ffc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 28 Dec 2023 18:31:50 -0500 Subject: [PATCH 134/191] fix(discriminator): handle reusing schema with embedded discriminators defined using Schema.prototype.discriminator Fix #14162 --- .../applyEmbeddedDiscriminators.js | 9 +++++++- test/document.test.js | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js index 930e1c86c13..840a4dc628e 100644 --- a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js +++ b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js @@ -16,8 +16,15 @@ function applyEmbeddedDiscriminators(schema, seen = new WeakSet()) { if (!schemaType.schema._applyDiscriminators) { continue; } + if (schemaType._appliedDiscriminators) { + continue; + } for (const disc of schemaType.schema._applyDiscriminators.keys()) { - schemaType.discriminator(disc, schemaType.schema._applyDiscriminators.get(disc)); + schemaType.discriminator( + disc, + schemaType.schema._applyDiscriminators.get(disc) + ); } + schemaType._appliedDiscriminators = true; } } diff --git a/test/document.test.js b/test/document.test.js index fff52d1e7b1..d3757c661a4 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12765,6 +12765,28 @@ describe('document', function() { ); }); + it('handles reusing schema with embedded discriminators defined using Schema.prototype.discriminator (gh-14162)', async function() { + const discriminated = new Schema({ + type: { type: Number, required: true } + }, { discriminatorKey: 'type' }); + + discriminated.discriminator(1, new Schema({ prop1: String })); + discriminated.discriminator(3, new Schema({ prop2: String })); + + const containerSchema = new Schema({ items: [discriminated] }); + const containerModel = db.model('Test', containerSchema); + const containerModel2 = db.model('Test1', containerSchema); // Error: Discriminator with name "1" already exists + const doc1 = new containerModel({ items: [{ type: 1, prop1: 'foo' }, { type: 3, prop2: 'bar' }] }); + const doc2 = new containerModel2({ items: [{ type: 1, prop1: 'baz' }, { type: 3, prop2: 'qux' }] }); + await doc1.save(); + await doc2.save(); + + doc1.items.push({ type: 3, prop2: 'test1' }); + doc2.items.push({ type: 3, prop2: 'test1' }); + await doc1.save(); + await doc2.save(); + }); + it('can use `collection` as schema name (gh-13956)', async function() { const schema = new mongoose.Schema({ name: String, collection: String }); const Test = db.model('Test', schema); From dabb2cf004978838a9397713ed0c40f39c0fc583 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 28 Dec 2023 18:36:59 -0500 Subject: [PATCH 135/191] style: fix lint --- test/document.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/document.test.js b/test/document.test.js index 8a95159c2e0..c3443d8d489 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12419,16 +12419,16 @@ describe('document', function() { const testSchema = new Schema({ __stateBeforeSuspension: { field1: String, - field3: { type: Schema.Types.Mixed }, + field3: { type: Schema.Types.Mixed } } }); const Test = db.model('Test', testSchema); - let eventObj = new Test({ + const eventObj = new Test({ __stateBeforeSuspension: { field1: 'test' } - }) + }); await eventObj.save(); const newO = eventObj.toObject(); - newO.__stateBeforeSuspension.field3 = {'.ippo': 5}; + newO.__stateBeforeSuspension.field3 = { '.ippo': 5 }; eventObj.set(newO); await eventObj.save(); From 0199c8b3a3f0843e0ff16d4e315e119a6813dca2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 29 Dec 2023 17:00:45 -0500 Subject: [PATCH 136/191] fix(ChangeStream): avoid suppressing errors in closed change stream Fix #14177 --- lib/cursor/ChangeStream.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/cursor/ChangeStream.js b/lib/cursor/ChangeStream.js index 36dc19aeb21..64fcbf22ab9 100644 --- a/lib/cursor/ChangeStream.js +++ b/lib/cursor/ChangeStream.js @@ -60,12 +60,6 @@ class ChangeStream extends EventEmitter { driverChangeStreamEvents.forEach(ev => { this.driverChangeStream.on(ev, data => { - // Sometimes Node driver still polls after close, so - // avoid any uncaught exceptions due to closed change streams - // See tests for gh-7022 - if (ev === 'error' && this.closed) { - return; - } if (data != null && data.fullDocument != null && this.options && this.options.hydrate) { data.fullDocument = this.options.model.hydrate(data.fullDocument); } @@ -83,12 +77,6 @@ class ChangeStream extends EventEmitter { driverChangeStreamEvents.forEach(ev => { this.driverChangeStream.on(ev, data => { - // Sometimes Node driver still polls after close, so - // avoid any uncaught exceptions due to closed change streams - // See tests for gh-7022 - if (ev === 'error' && this.closed) { - return; - } if (data != null && data.fullDocument != null && this.options && this.options.hydrate) { data.fullDocument = this.options.model.hydrate(data.fullDocument); } From 0e7ec7f4b4c657d31cb4c6adc643aa196b5c146a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 29 Dec 2023 17:32:35 -0500 Subject: [PATCH 137/191] test: try closing change stream to avoid test failure --- test/model.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/model.test.js b/test/model.test.js index 49d01fd9dc2..3f2d8a35507 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -3546,6 +3546,7 @@ describe('Model', function() { assert.equal(changeData.operationType, 'insert'); assert.equal(changeData.fullDocument.name, 'Ned Stark'); + await changeStream.close(); await db.close(); }); From 485f15561f4d344036ca6543612a7947da3947ae Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 24 Dec 2023 09:07:38 -0500 Subject: [PATCH 138/191] test: fix deno tests --- test/schema.select.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/schema.select.test.js b/test/schema.select.test.js index a6a2ae29a32..33de8b281e7 100644 --- a/test/schema.select.test.js +++ b/test/schema.select.test.js @@ -53,7 +53,7 @@ describe('schema select option', function() { assert.equal(findByIdDocAgain.isSelected('name'), false); assert.equal(findByIdDocAgain.isSelected('docs.name'), false); assert.strictEqual(undefined, findByIdDocAgain.name); - const findUpdateDoc = await Test.findOneAndUpdate({ _id: doc._id }); + const findUpdateDoc = await Test.findOneAndUpdate({ _id: doc._id }, { name: 'the excluded' }); assert.equal(findUpdateDoc.isSelected('name'), false); assert.equal(findUpdateDoc.isSelected('docs.name'), false); assert.strictEqual(undefined, findUpdateDoc.name); From 50b00783827c80a3b07bdaec9cfaa04c28c0b79d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 29 Dec 2023 17:40:05 -0500 Subject: [PATCH 139/191] style: remove unnecessary comment --- test/document.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/document.test.js b/test/document.test.js index d3757c661a4..e3bb407e9fd 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12775,7 +12775,7 @@ describe('document', function() { const containerSchema = new Schema({ items: [discriminated] }); const containerModel = db.model('Test', containerSchema); - const containerModel2 = db.model('Test1', containerSchema); // Error: Discriminator with name "1" already exists + const containerModel2 = db.model('Test1', containerSchema); const doc1 = new containerModel({ items: [{ type: 1, prop1: 'foo' }, { type: 3, prop2: 'bar' }] }); const doc2 = new containerModel2({ items: [{ type: 1, prop1: 'baz' }, { type: 3, prop2: 'qux' }] }); await doc1.save(); From 8e141d1002c2f7356595157c7bd3b6379ae84b3b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 30 Dec 2023 17:22:18 -0500 Subject: [PATCH 140/191] perf(schema): remove unnecessary lookahead in numeric subpath check --- lib/schema.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/schema.js b/lib/schema.js index 791a1b9253c..88e427d417d 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -24,6 +24,8 @@ const utils = require('./utils'); const validateRef = require('./helpers/populate/validateRef'); const util = require('util'); +const hasNumericSubpathRegex = /\.\d+(\.|$)/; + let MongooseTypes; const queryHooks = require('./helpers/query/applyQueryMiddleware'). @@ -973,7 +975,7 @@ Schema.prototype.path = function(path, obj) { } // subpaths? - return /\.\d+\.?.*$/.test(path) + return hasNumericSubpathRegex.test(path) ? getPositionalPath(this, path) : undefined; } From 3e601454112dbef3b8221627ba531647ba79ed6e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 30 Dec 2023 17:22:18 -0500 Subject: [PATCH 141/191] perf(schema): remove unnecessary lookahead in numeric subpath check --- lib/schema.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/schema.js b/lib/schema.js index 3087f157d3f..8855ad58fd1 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -25,6 +25,8 @@ const utils = require('./utils'); const validateRef = require('./helpers/populate/validateRef'); const util = require('util'); +const hasNumericSubpathRegex = /\.\d+(\.|$)/; + let MongooseTypes; const queryHooks = require('./helpers/query/applyQueryMiddleware'). @@ -1008,7 +1010,7 @@ Schema.prototype.path = function(path, obj) { } // subpaths? - return /\.\d+\.?.*$/.test(path) + return hasNumericSubpathRegex.test(path) ? getPositionalPath(this, path, cleanPath) : undefined; } From 403a28ee02b1a82cf9a34f4693fc02b23cad78e8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 1 Jan 2024 15:53:19 -0500 Subject: [PATCH 142/191] fix: add ignoreAtomics option to isModified() for better backwards compatibility with Mongoose 5 Re: #14024 --- lib/document.js | 16 ++++++++++++--- lib/types/subdocument.js | 6 +++--- test/document.modified.test.js | 37 ++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/lib/document.js b/lib/document.js index 4b8cc160942..fdc49e79742 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2223,8 +2223,9 @@ Document.prototype[documentModifiedPaths] = Document.prototype.modifiedPaths; * @api public */ -Document.prototype.isModified = function(paths, modifiedPaths) { +Document.prototype.isModified = function(paths, options, modifiedPaths) { if (paths) { + const ignoreAtomics = options && options.ignoreAtomics; const directModifiedPathsObj = this.$__.activePaths.states.modify; if (directModifiedPathsObj == null) { return false; @@ -2245,7 +2246,16 @@ Document.prototype.isModified = function(paths, modifiedPaths) { return !!~modified.indexOf(path); }); - const directModifiedPaths = Object.keys(directModifiedPathsObj); + let directModifiedPaths = Object.keys(directModifiedPathsObj); + if (ignoreAtomics) { + directModifiedPaths = directModifiedPaths.filter(path => { + const value = this.$__getValue(path); + if (value != null && value[arrayAtomicsSymbol] != null && value[arrayAtomicsSymbol].$set === undefined) { + return false; + } + return true; + }); + } return isModifiedChild || paths.some(function(path) { return directModifiedPaths.some(function(mod) { return mod === path || path.startsWith(mod + '.'); @@ -2679,7 +2689,7 @@ function _getPathsToValidate(doc) { paths.delete(fullPathToSubdoc + '.' + modifiedPath); } - if (doc.$isModified(fullPathToSubdoc, modifiedPaths) && + if (doc.$isModified(fullPathToSubdoc, null, modifiedPaths) && !doc.isDirectModified(fullPathToSubdoc) && !doc.$isDefault(fullPathToSubdoc)) { paths.add(fullPathToSubdoc); diff --git a/lib/types/subdocument.js b/lib/types/subdocument.js index d282f892400..48a7fc71215 100644 --- a/lib/types/subdocument.js +++ b/lib/types/subdocument.js @@ -178,7 +178,7 @@ Subdocument.prototype.markModified = function(path) { * ignore */ -Subdocument.prototype.isModified = function(paths, modifiedPaths) { +Subdocument.prototype.isModified = function(paths, options, modifiedPaths) { const parent = this.$parent(); if (parent != null) { if (Array.isArray(paths) || typeof paths === 'string') { @@ -188,10 +188,10 @@ Subdocument.prototype.isModified = function(paths, modifiedPaths) { paths = this.$__pathRelativeToParent(); } - return parent.$isModified(paths, modifiedPaths); + return parent.$isModified(paths, options, modifiedPaths); } - return Document.prototype.isModified.call(this, paths, modifiedPaths); + return Document.prototype.isModified.call(this, paths, options, modifiedPaths); }; /** diff --git a/test/document.modified.test.js b/test/document.modified.test.js index 4518b2746cc..b733937d40e 100644 --- a/test/document.modified.test.js +++ b/test/document.modified.test.js @@ -208,6 +208,43 @@ describe('document modified', function() { assert.equal(post.isModified('comments.0.title'), true); assert.equal(post.isDirectModified('comments.0.title'), true); }); + it('with push (gh-14024)', async function() { + const post = new BlogPost(); + post.init({ + title: 'Test', + slug: 'test', + comments: [{ title: 'Test', date: new Date(), body: 'Test' }] + }); + + post.comments.push({ title: 'new comment', body: 'test' }); + + assert.equal(post.isModified('comments.0.title', { ignoreAtomics: true }), false); + assert.equal(post.isModified('comments.0.body', { ignoreAtomics: true }), false); + assert.equal(post.get('comments')[0].isModified('body', { ignoreAtomics: true }), false); + }); + it('with push and set (gh-14024)', async function() { + const post = new BlogPost(); + post.init({ + title: 'Test', + slug: 'test', + comments: [{ title: 'Test', date: new Date(), body: 'Test' }] + }); + + post.comments.push({ title: 'new comment', body: 'test' }); + post.get('comments')[0].set('title', 'Woot'); + + assert.equal(post.isModified('comments', { ignoreAtomics: true }), true); + assert.equal(post.isModified('comments.0.title', { ignoreAtomics: true }), true); + assert.equal(post.isDirectModified('comments.0.title'), true); + assert.equal(post.isDirectModified('comments.0.body'), false); + assert.equal(post.isModified('comments.0.body', { ignoreAtomics: true }), false); + + assert.equal(post.isModified('comments', { ignoreAtomics: true }), true); + assert.equal(post.isModified('comments.0.title', { ignoreAtomics: true }), true); + assert.equal(post.isDirectModified('comments.0.title'), true); + assert.equal(post.isDirectModified('comments.0.body'), false); + assert.equal(post.isModified('comments.0.body', { ignoreAtomics: true }), false); + }); it('with accessors', function() { const post = new BlogPost(); post.init({ From 6d526cd1223c714bd5603e057701305ee6d337ab Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 1 Jan 2024 22:07:18 -0500 Subject: [PATCH 143/191] fix(document): allow setting nested path to `null` Fix #14205 --- lib/document.js | 2 ++ test/document.test.js | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/lib/document.js b/lib/document.js index 4b8cc160942..3bfaa16c20a 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1153,6 +1153,8 @@ Document.prototype.$set = function $set(path, val, type, options) { } else { throw new StrictModeError(key); } + } else if (pathtype === 'nested' && valForKey === null) { + this.$set(pathName, valForKey, constructing, options); } } else if (valForKey !== void 0) { this.$set(pathName, valForKey, constructing, options); diff --git a/test/document.test.js b/test/document.test.js index c3443d8d489..3998343215a 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12437,6 +12437,25 @@ describe('document', function() { const fromDb = await Test.findById(eventObj._id).lean().orFail(); assert.strictEqual(fromDb.__stateBeforeSuspension.field3['.ippo'], 5); }); + + it('handles setting nested path to null (gh-14205)', function() { + const schema = new mongoose.Schema({ + nested: { + key1: String, + key2: String + } + }); + + const Model = db.model('Test', schema); + + const doc = new Model(); + doc.init({ + nested: { key1: 'foo', key2: 'bar' } + }); + + doc.set({ nested: null }); + assert.strictEqual(doc.toObject().nested, null); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() { From b286b02cf18c9cf0af47a74ba8d9153b504ade9e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 2 Jan 2024 11:26:44 -0500 Subject: [PATCH 144/191] fix: also allow setting nested field to undefined re: #14205 --- lib/document.js | 2 +- test/document.test.js | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index 3bfaa16c20a..1c7442ca20f 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1153,7 +1153,7 @@ Document.prototype.$set = function $set(path, val, type, options) { } else { throw new StrictModeError(key); } - } else if (pathtype === 'nested' && valForKey === null) { + } else if (pathtype === 'nested' && valForKey == null) { this.$set(pathName, valForKey, constructing, options); } } else if (valForKey !== void 0) { diff --git a/test/document.test.js b/test/document.test.js index 3998343215a..627a43cf1d7 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12456,6 +12456,25 @@ describe('document', function() { doc.set({ nested: null }); assert.strictEqual(doc.toObject().nested, null); }); + + it('handles setting nested path to undefined (gh-14205)', function() { + const schema = new mongoose.Schema({ + nested: { + key1: String, + key2: String + } + }); + + const Model = db.model('Test', schema); + + const doc = new Model(); + doc.init({ + nested: { key1: 'foo', key2: 'bar' } + }); + + doc.set({ nested: void 0 }); + assert.strictEqual(doc.toObject().nested, void 0); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() { From e3c12cf722b5ba2b016a05bfad14935b0eb9bfc6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 2 Jan 2024 15:58:40 -0500 Subject: [PATCH 145/191] types(model): add missing strict and timestamps options to bulkWrite() re: #8778 --- types/models.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/types/models.d.ts b/types/models.d.ts index 7131a4dfcf7..1a02fd8a202 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -17,6 +17,8 @@ declare module 'mongoose' { interface MongooseBulkWriteOptions { skipValidation?: boolean; + strict?: boolean; + timestamps?: boolean | 'throw'; } interface InsertManyOptions extends From f7e981626e916f87b8e4fa66233a75f162718633 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 3 Jan 2024 15:55:42 -0500 Subject: [PATCH 146/191] docs(document): add ignoreAtomics option to docs --- lib/document.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/document.js b/lib/document.js index fdc49e79742..57d270bc3af 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2219,6 +2219,8 @@ Document.prototype[documentModifiedPaths] = Document.prototype.modifiedPaths; * doc.isDirectModified('documents') // false * * @param {String} [path] optional + * @param {Object} [options] + * @param {Boolean} [options.ignoreAtomics=false] If true, doesn't return true if path is underneath an array that was modified with atomic operations like `push()` * @return {Boolean} * @api public */ From 0960fae4d1b09bf70d2345a43e74cd1b137ab754 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 3 Jan 2024 15:55:59 -0500 Subject: [PATCH 147/191] types(document): add ignoreAtomics option to isModified typedefs re: #14024 --- types/document.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/document.d.ts b/types/document.d.ts index f43db5c34c8..3a831ea017a 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -179,7 +179,7 @@ declare module 'mongoose' { * Returns true if any of the given paths are modified, else false. If no arguments, returns `true` if any path * in this document is modified. */ - isModified(path?: string | Array): boolean; + isModified(path?: string | Array, options?: { ignoreAtomics?: boolean } | null): boolean; /** Boolean flag specifying if the document is new. */ isNew: boolean; From 2d6898307b85d89cf405ce6b28e665b4359daee6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 3 Jan 2024 16:20:29 -0500 Subject: [PATCH 148/191] chore: release 6.12.5 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 569fe469d74..e18dc705af4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +6.12.5 / 2024-01-03 +=================== + * perf(schema): remove unnecessary lookahead in numeric subpath check + * fix(document): allow setting nested path to null #14226 + * fix(document): avoid flattening dotted paths in mixed path underneath nested path #14198 #14178 + * fix: add ignoreAtomics option to isModified() for better backwards compatibility with Mongoose 5 #14213 + 6.12.4 / 2023-12-27 =================== * fix: upgrade mongodb driver -> 4.17.2 diff --git a/package.json b/package.json index 7fba35dfbe4..f4d2b41c199 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.12.4", + "version": "6.12.5", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 6ffb123cc3448cd5aeb9963934d992e6a32f17d6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 8 Jan 2024 15:29:24 -0500 Subject: [PATCH 149/191] chore: release 7.6.8 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 208049308a5..b514b418629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +7.6.8 / 2024-01-08 +================== + * fix(discriminator): handle reusing schema with embedded discriminators defined using Schema.prototype.discriminator #14202 #14162 + * fix(ChangeStream): avoid suppressing errors in closed change stream #14206 #14177 + 6.12.5 / 2024-01-03 =================== * perf(schema): remove unnecessary lookahead in numeric subpath check diff --git a/package.json b/package.json index efbcc7a2b50..3f6a5beb0a8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.7", + "version": "7.6.8", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From ac9af5be89cf190e323ee81f2be49ff5ac754bc5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 8 Jan 2024 15:32:02 -0500 Subject: [PATCH 150/191] docs: add unnecessary lookahead fix to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b514b418629..05ac8299cc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ 7.6.8 / 2024-01-08 ================== + * perf(schema): remove unnecessary lookahead in numeric subpath check * fix(discriminator): handle reusing schema with embedded discriminators defined using Schema.prototype.discriminator #14202 #14162 * fix(ChangeStream): avoid suppressing errors in closed change stream #14206 #14177 From 195b46ccbbe56ac014ad92daafdf0e3dc9bda012 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 12 Jan 2024 10:07:42 -0500 Subject: [PATCH 151/191] fix(document): allow calling `push()` with different `$position` arguments Fix #14244 Re: #4322 --- lib/types/array/methods/index.js | 18 ++++++++---------- test/document.test.js | 13 +++++++------ 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js index 13b3f493c98..45c88a9b2ad 100644 --- a/lib/types/array/methods/index.js +++ b/lib/types/array/methods/index.js @@ -2,7 +2,6 @@ const Document = require('../../../document'); const ArraySubdocument = require('../../ArraySubdocument'); -const MongooseError = require('../../../error/mongooseError'); const cleanModifiedSubpaths = require('../../../helpers/document/cleanModifiedSubpaths'); const internalToObjectOptions = require('../../../options').internalToObjectOptions; const mpath = require('mpath'); @@ -684,22 +683,21 @@ const methods = { if ((atomics.$push && atomics.$push.$each && atomics.$push.$each.length || 0) !== 0 && atomics.$push.$position != atomic.$position) { - throw new MongooseError('Cannot call `Array#push()` multiple times ' + - 'with different `$position`'); - } + if (atomic.$position != null) { + [].splice.apply(arr, [atomic.$position, 0].concat(values)); + ret = arr.length; + } else { + ret = [].push.apply(arr, values); + } - if (atomic.$position != null) { + this._registerAtomic('$set', this); + } else if (atomic.$position != null) { [].splice.apply(arr, [atomic.$position, 0].concat(values)); ret = this.length; } else { ret = [].push.apply(arr, values); } } else { - if ((atomics.$push && atomics.$push.$each && atomics.$push.$each.length || 0) !== 0 && - atomics.$push.$position != null) { - throw new MongooseError('Cannot call `Array#push()` multiple times ' + - 'with different `$position`'); - } atomic = values; ret = [].push.apply(arr, values); } diff --git a/test/document.test.js b/test/document.test.js index 627a43cf1d7..cd2246a1e9b 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -8232,12 +8232,13 @@ describe('document', function() { $each: [0], $position: 0 }); - assert.throws(() => { - doc.nums.push({ $each: [5] }); - }, /Cannot call.*multiple times/); - assert.throws(() => { - doc.nums.push(5); - }, /Cannot call.*multiple times/); + assert.deepStrictEqual(doc.nums.$__getAtomics(), [['$push', { $each: [0], $position: 0 }]]); + + doc.nums.push({ $each: [5] }); + assert.deepStrictEqual(doc.nums.$__getAtomics(), [['$set', [0, 1, 2, 3, 4, 5]]]); + + doc.nums.push({ $each: [0.5], $position: 1 }); + assert.deepStrictEqual(doc.nums.$__getAtomics(), [['$set', [0, 0.5, 1, 2, 3, 4, 5]]]); }); it('setting a path to a single nested document should update the single nested doc parent (gh-8400)', function() { From 6bc42cee9803cc056ecf8a83e697a7f617d60e17 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 12 Jan 2024 10:30:32 -0500 Subject: [PATCH 152/191] test: add missing issue to test title --- test/document.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/document.test.js b/test/document.test.js index cd2246a1e9b..beefe366d1f 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -8208,7 +8208,7 @@ describe('document', function() { assert.deepEqual(Object.keys(err.errors), ['age']); }); - it('array push with $position (gh-4322)', async function() { + it('array push with $position (gh-14244) (gh-4322)', async function() { const schema = Schema({ nums: [Number] }); From d3e22af841abb65ce5366c5fc0fa605d5e8ccb53 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 12 Jan 2024 17:01:39 -0500 Subject: [PATCH 153/191] fix(document): handle embedded recursive discriminators on nested path defined using Schema.prototype.discriminator Fix #14245 --- .../applyEmbeddedDiscriminators.js | 9 ++-- test/document.test.js | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js index 840a4dc628e..9a04ecb072f 100644 --- a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js +++ b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js @@ -19,11 +19,10 @@ function applyEmbeddedDiscriminators(schema, seen = new WeakSet()) { if (schemaType._appliedDiscriminators) { continue; } - for (const disc of schemaType.schema._applyDiscriminators.keys()) { - schemaType.discriminator( - disc, - schemaType.schema._applyDiscriminators.get(disc) - ); + for (const discriminatorKey of schemaType.schema._applyDiscriminators.keys()) { + const discriminatorSchema = schemaType.schema._applyDiscriminators.get(discriminatorKey); + applyEmbeddedDiscriminators(discriminatorSchema, seen); + schemaType.discriminator(discriminatorKey, discriminatorSchema); } schemaType._appliedDiscriminators = true; } diff --git a/test/document.test.js b/test/document.test.js index 7c416967f1b..484294ce0d1 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12787,6 +12787,54 @@ describe('document', function() { await doc2.save(); }); + it('handles embedded recursive discriminators on nested path defined using Schema.prototype.discriminator (gh-14245)', async function() { + const baseSchema = new Schema({ + type: { type: Number, required: true } + }, { discriminatorKey: 'type' }); + + class Base { + whoAmI() { return 'I am Base'; } + } + + baseSchema.loadClass(Base); + + class NumberTyped extends Base { + whoAmI() { return 'I am NumberTyped'; } + } + + class StringTyped extends Base { + whoAmI() { return 'I am StringTyped'; } + } + + const selfRefSchema = new Schema({ + self: { type: [baseSchema], required: true } + }); + + class SelfReferenceTyped extends Base { + whoAmI() { return 'I am SelfReferenceTyped'; } + } + + selfRefSchema.loadClass(SelfReferenceTyped); + baseSchema.discriminator(5, selfRefSchema); + + const numberTypedSchema = new Schema({}).loadClass(NumberTyped); + const stringTypedSchema = new Schema({}).loadClass(StringTyped); + baseSchema.discriminator(1, numberTypedSchema); + baseSchema.discriminator(3, stringTypedSchema); + const containerSchema = new Schema({ items: [baseSchema] }); + const containerModel = db.model('Test', containerSchema); + + const instance = await containerModel.create({ + items: [{ type: 5, self: [{ type: 1 }, { type: 3 }] }] + }); + + assert.equal(instance.items[0].whoAmI(), 'I am SelfReferenceTyped'); + assert.deepStrictEqual(instance.items[0].self.map(item => item.whoAmI()), [ + 'I am NumberTyped', + 'I am StringTyped' + ]); + }); + it('can use `collection` as schema name (gh-13956)', async function() { const schema = new mongoose.Schema({ name: String, collection: String }); const Test = db.model('Test', schema); From 64fa4705c807e1ed285f0c5355e37854a5485f1b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 17 Jan 2024 06:59:00 -0500 Subject: [PATCH 154/191] docs(model+query+findoneandupdate): add more details about `overwriteDiscriminatorKey` option to docs Fix #14246 --- docs/tutorials/findoneandupdate.md | 38 +++++++++++++++++++++++++++++- lib/model.js | 4 ++++ lib/query.js | 5 +++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/findoneandupdate.md b/docs/tutorials/findoneandupdate.md index c589b15b952..5ca0314fff6 100644 --- a/docs/tutorials/findoneandupdate.md +++ b/docs/tutorials/findoneandupdate.md @@ -7,10 +7,18 @@ However, there are some cases where you need to use [`findOneAndUpdate()`](https * [Atomic Updates](#atomic-updates) * [Upsert](#upsert) * [The `rawResult` Option](#raw-result) +* [Updating Discriminator Keys](#updating-discriminator-keys) ## Getting Started -As the name implies, `findOneAndUpdate()` finds the first document that matches a given `filter`, applies an `update`, and returns the document. By default, `findOneAndUpdate()` returns the document as it was **before** `update` was applied. +As the name implies, `findOneAndUpdate()` finds the first document that matches a given `filter`, applies an `update`, and returns the document. +The `findOneAndUpdate()` function has the following signature: + +```javascript +function findOneAndUpdate(filter, update, options) {} +``` + +By default, `findOneAndUpdate()` returns the document as it was **before** `update` was applied. ```acquit [require:Tutorial.*findOneAndUpdate.*basic case] @@ -78,3 +86,31 @@ Here's what the `res` object from the above example looks like: age: 29 }, ok: 1 } ``` + +## Updating Discriminator Keys + +Mongoose prevents updating the [discriminator key](https://mongoosejs.com/docs/discriminators.html#discriminator-keys) using `findOneAndUpdate()` by default. +For example, suppose you have the following discriminator models. + +```javascript +const eventSchema = new mongoose.Schema({ time: Date }); +const Event = db.model('Event', eventSchema); + +const ClickedLinkEvent = Event.discriminator( + 'ClickedLink', + new mongoose.Schema({ url: String }) +); + +const SignedUpEvent = Event.discriminator( + 'SignedUp', + new mongoose.Schema({ username: String }) +); +``` + +Mongoose will remove `__t` (the default discriminator key) from the `update` parameter, if `__t` is set. +This is to prevent unintentional updates to the discriminator key; for example, if you're passing untrusted user input to the `update` parameter. +However, you can tell Mongoose to allow updating the discriminator key by setting the `overwriteDiscriminatorKey` option to `true` as shown below. + +```acquit +[require:use overwriteDiscriminatorKey to change discriminator key] +``` diff --git a/lib/model.js b/lib/model.js index e97407429aa..aee450da45d 100644 --- a/lib/model.js +++ b/lib/model.js @@ -2428,6 +2428,7 @@ Model.$where = function $where() { * @param {Boolean} [options.setDefaultsOnInsert=true] If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @return {Query} * @see Tutorial https://mongoosejs.com/docs/tutorials/findoneandupdate.html * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ @@ -2524,6 +2525,7 @@ Model.findOneAndUpdate = function(conditions, update, options) { * @param {Boolean} [options.new=false] if true, return the modified document rather than the original * @param {Object|String} [options.select] sets the document fields to return. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @return {Query} * @see Model.findOneAndUpdate https://mongoosejs.com/docs/api/model.html#Model.findOneAndUpdate() * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ @@ -3888,6 +3890,7 @@ Model.hydrate = function(obj, projection, options) { * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @return {Query} * @see Query docs https://mongoosejs.com/docs/queries.html * @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output @@ -3927,6 +3930,7 @@ Model.updateMany = function updateMany(conditions, doc, options) { * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @return {Query} * @see Query docs https://mongoosejs.com/docs/queries.html * @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output diff --git a/lib/query.js b/lib/query.js index a6063cc5fc4..5db5c0e7ef7 100644 --- a/lib/query.js +++ b/lib/query.js @@ -3213,6 +3213,7 @@ function prepareDiscriminatorCriteria(query) { * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.returnOriginal=null] An alias for the `new` option. `returnOriginal: false` is equivalent to `new: true`. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @see Tutorial https://mongoosejs.com/docs/tutorials/findoneandupdate.html * @see findAndModify command https://www.mongodb.com/docs/manual/reference/command/findAndModify/ * @see ModifyResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html @@ -4003,6 +4004,7 @@ Query.prototype._replaceOne = async function _replaceOne() { * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @param {Function} [callback] params are (error, writeOpResult) * @return {Query} this * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update() @@ -4071,7 +4073,8 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) { * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set. - @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @param {Function} [callback] params are (error, writeOpResult) * @return {Query} this * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update() From ee0d28f640d99b1fb19ec1ca54a6e016f9880d2f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 17 Jan 2024 13:38:34 -0500 Subject: [PATCH 155/191] Update docs/tutorials/findoneandupdate.md Co-authored-by: hasezoey --- docs/tutorials/findoneandupdate.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/findoneandupdate.md b/docs/tutorials/findoneandupdate.md index 5ca0314fff6..3f34bf3cc6d 100644 --- a/docs/tutorials/findoneandupdate.md +++ b/docs/tutorials/findoneandupdate.md @@ -89,7 +89,7 @@ Here's what the `res` object from the above example looks like: ## Updating Discriminator Keys -Mongoose prevents updating the [discriminator key](https://mongoosejs.com/docs/discriminators.html#discriminator-keys) using `findOneAndUpdate()` by default. +Mongoose prevents updating the [discriminator key](../discriminators.html#discriminator-keys) using `findOneAndUpdate()` by default. For example, suppose you have the following discriminator models. ```javascript From cc80894fccb43cc16d5d3606888437073c479d0b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 17 Jan 2024 13:38:42 -0500 Subject: [PATCH 156/191] Update lib/model.js Co-authored-by: hasezoey --- lib/model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index aee450da45d..967fed3494d 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3890,7 +3890,7 @@ Model.hydrate = function(obj, projection, options) { * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. - * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @return {Query} * @see Query docs https://mongoosejs.com/docs/queries.html * @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output From 73bea51b3f306d1f4553bbb03864b8098aa02ea3 Mon Sep 17 00:00:00 2001 From: Rohan Kothapalli Date: Thu, 18 Jan 2024 17:19:25 +0530 Subject: [PATCH 157/191] null check --- lib/types/ArraySubdocument.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/types/ArraySubdocument.js b/lib/types/ArraySubdocument.js index 55889caa839..af0bdd0d4a1 100644 --- a/lib/types/ArraySubdocument.js +++ b/lib/types/ArraySubdocument.js @@ -137,7 +137,7 @@ ArraySubdocument.prototype.$__fullPath = function(path, skipIndex) { */ ArraySubdocument.prototype.$__pathRelativeToParent = function(path, skipIndex) { - if (this.__index == null) { + if (this.__index == null || (!this.__parentArray || !this.__parentArray.$path)) { return null; } if (skipIndex) { From 900e9fa7e5e0fc1a9e368bd8c1327b4fdf2a9207 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 21 Jan 2024 07:13:00 -0500 Subject: [PATCH 158/191] fix(collection): correctly handle buffer timeouts with `find()` Fix #14184 --- lib/drivers/node-mongodb-native/collection.js | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/drivers/node-mongodb-native/collection.js b/lib/drivers/node-mongodb-native/collection.js index 8cb3cbf8586..8f874542574 100644 --- a/lib/drivers/node-mongodb-native/collection.js +++ b/lib/drivers/node-mongodb-native/collection.js @@ -138,10 +138,23 @@ function iter(i) { let _args = args; let promise = null; let timeout = null; - if (syncCollectionMethods[i]) { - this.addQueue(() => { - lastArg.call(this, null, this[i].apply(this, _args.slice(0, _args.length - 1))); - }, []); + if (syncCollectionMethods[i] && typeof lastArg === 'function') { + this.addQueue(i, _args); + callback = lastArg; + } else if (syncCollectionMethods[i]) { + promise = new this.Promise((resolve, reject) => { + callback = function collectionOperationCallback(err, res) { + if (timeout != null) { + clearTimeout(timeout); + } + if (err != null) { + return reject(err); + } + resolve(res); + }; + _args = args.concat([callback]); + this.addQueue(i, _args); + }); } else if (typeof lastArg === 'function') { callback = function collectionOperationCallback() { if (timeout != null) { From 6b67f9bf65001b36a99a1fb86b55a10706c3b2ee Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 21 Jan 2024 13:09:08 -0500 Subject: [PATCH 159/191] test: add test case for #14184 --- test/collection.test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/collection.test.js b/test/collection.test.js index 8b4fa71ea4c..627050413e3 100644 --- a/test/collection.test.js +++ b/test/collection.test.js @@ -68,6 +68,19 @@ describe('collections:', function() { }); }); + it('returns a promise if buffering and callback with find() (gh-14184)', function(done) { + db = mongoose.createConnection(); + const collection = db.collection('gh14184'); + collection.opts.bufferTimeoutMS = 100; + + collection.find({ foo: 'bar' }, {}, (err, docs) => { + assert.ok(err); + assert.ok(err.message.includes('buffering timed out after 100ms')); + assert.equal(docs, undefined); + done(); + }); + }); + it('methods should that throw (unimplemented)', function() { const collection = new Collection('test', mongoose.connection); let thrown = false; From 775aadf16be64ffc19f86aa05797095ad3d9eca3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 26 Dec 2023 15:53:14 -0500 Subject: [PATCH 160/191] types(model): correct return type for findByIdAndDelete() Fix #14190 --- test/types/queries.test.ts | 8 ++++++++ types/models.d.ts | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index efca12618a5..2b8ecff5504 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -518,3 +518,11 @@ function gh13630() { const x: UpdateQueryKnownOnly = { $set: { name: 'John' } }; expectAssignable>(x); } + +function gh14190() { + const userSchema = new Schema({ name: String, age: Number }); + const UserModel = model('User', userSchema); + + const doc = await UserModel.findByIdAndDelete('0'.repeat(24)); + expectType | null>(doc); +} diff --git a/types/models.d.ts b/types/models.d.ts index 2c7f54b17f8..7cb082bc372 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -550,8 +550,8 @@ declare module 'mongoose' { 'findOneAndDelete' >; findByIdAndDelete( - id?: mongodb.ObjectId | any, - options?: QueryOptions & { includeResultMetadata: true } + id: mongodb.ObjectId | any, + options: QueryOptions & { includeResultMetadata: true } ): QueryWithHelpers, ResultDoc, TQueryHelpers, TRawDocType, 'findOneAndDelete'>; findByIdAndDelete( id?: mongodb.ObjectId | any, From 7f9406ff44c5d8536599060c3ae38009f866ab60 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 26 Dec 2023 16:14:07 -0500 Subject: [PATCH 161/191] types(query): improve findByIdAndDelete return type for query re: #14190 --- test/types/queries.test.ts | 18 +++++++++++++++++- types/query.d.ts | 4 ++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 2b8ecff5504..bf20145e1b7 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -17,7 +17,7 @@ import { ProjectionFields, QueryOptions } from 'mongoose'; -import { ObjectId } from 'mongodb'; +import { ModifyResult, ObjectId } from 'mongodb'; import { expectAssignable, expectError, expectNotAssignable, expectType } from 'tsd'; import { autoTypedModel } from './models.test'; import { AutoTypedSchemaType } from './schema.test'; @@ -525,4 +525,20 @@ function gh14190() { const doc = await UserModel.findByIdAndDelete('0'.repeat(24)); expectType | null>(doc); + + const res = await UserModel.findByIdAndDelete( + '0'.repeat(24), + { includeResultMetadata: true } + ); + expectAssignable< + ModifyResult> + >(res); + + const res2 = await UserModel.find().findByIdAndDelete( + '0'.repeat(24), + { includeResultMetadata: true } + ); + expectAssignable< + ModifyResult> + >(res2); } diff --git a/types/query.d.ts b/types/query.d.ts index 2234e66f656..39ff69de9df 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -435,6 +435,10 @@ declare module 'mongoose' { ): QueryWithHelpers; /** Creates a `findByIdAndDelete` query, filtering by the given `_id`. */ + findByIdAndDelete( + id: mongodb.ObjectId | any, + options: QueryOptions & { includeResultMetadata: true } + ): QueryWithHelpers, DocType, THelpers, RawDocType, 'findOneAndDelete'>; findByIdAndDelete( id?: mongodb.ObjectId | any, options?: QueryOptions | null From a6e4d18ab7bbb015d9f87fd3990b8e5bf4922825 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 21 Jan 2024 14:10:00 -0500 Subject: [PATCH 162/191] style: fix lint --- test/types/queries.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index bf20145e1b7..ed56bb9d7a0 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -532,7 +532,7 @@ function gh14190() { ); expectAssignable< ModifyResult> - >(res); + >(res); const res2 = await UserModel.find().findByIdAndDelete( '0'.repeat(24), @@ -540,5 +540,5 @@ function gh14190() { ); expectAssignable< ModifyResult> - >(res2); + >(res2); } From 09181ef655e1d0360c3b4a60f1ef15c39c56cb15 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 22 Jan 2024 11:03:26 -0500 Subject: [PATCH 163/191] chore: release 6.12.6 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e18dc705af4..1a112597b79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +6.12.6 / 2024-01-22 +=================== + * fix(collection): correctly handle buffer timeouts with find() #14277 + * fix(document): allow calling push() with different $position arguments #14254 + 6.12.5 / 2024-01-03 =================== * perf(schema): remove unnecessary lookahead in numeric subpath check diff --git a/package.json b/package.json index f4d2b41c199..9cb365a38b0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.12.5", + "version": "6.12.6", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 34feac04113fa9b7263d167b0d89cc0c51a010aa Mon Sep 17 00:00:00 2001 From: Brown Date: Tue, 13 Feb 2024 16:42:01 +0900 Subject: [PATCH 164/191] introduce resumeTokenChanged into 6.x --- lib/cursor/ChangeStream.js | 2 +- test/model.test.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/cursor/ChangeStream.js b/lib/cursor/ChangeStream.js index 24c2f55665a..6cee9f8b371 100644 --- a/lib/cursor/ChangeStream.js +++ b/lib/cursor/ChangeStream.js @@ -75,7 +75,7 @@ class ChangeStream extends EventEmitter { this.closed = true; }); - ['close', 'change', 'end', 'error'].forEach(ev => { + ['close', 'change', 'end', 'error', 'resumeTokenChanged'].forEach(ev => { this.driverChangeStream.on(ev, data => { // Sometimes Node driver still polls after close, so // avoid any uncaught exceptions due to closed change streams diff --git a/test/model.test.js b/test/model.test.js index 700dfb87502..00573da4440 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -5406,6 +5406,20 @@ describe('Model', function() { assert.equal(changeData.operationType, 'insert'); assert.equal(changeData.fullDocument.name, 'Child'); }); + + it('bubbles up resumeTokenChanged events (gh-14349)', async function() { + const MyModel = db.model('Test', new Schema({ name: String })); + + const resumeTokenChangedEvent = new Promise(resolve => { + changeStream = MyModel.watch(); + listener = data => resolve(data); + changeStream.once('resumeTokenChanged', listener); + }); + + await MyModel.create({ name: 'test' }); + const { _data } = await resumeTokenChangedEvent; + assert.ok(_data); + }); }); describe('sessions (gh-6362)', function() { From 6950c96b97a7da102444b73b86418d98e527e4e6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 20 Feb 2024 13:16:24 -0500 Subject: [PATCH 165/191] docs(connections): add note about using `asPromise()` with `createConnection()` for error handling Fix #14266 --- docs/connections.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/connections.md b/docs/connections.md index c6a1fdf8cc3..5b65b096293 100644 --- a/docs/connections.md +++ b/docs/connections.md @@ -402,16 +402,24 @@ The `mongoose.createConnection()` function takes the same arguments as const conn = mongoose.createConnection('mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]', options); ``` -This [connection](api/connection.html#connection_Connection) object is then used to -create and retrieve [models](api/model.html#model_Model). Models are -**always** scoped to a single connection. +This [connection](api/connection.html#connection_Connection) object is then used to create and retrieve [models](api/model.html#model_Model). +Models are **always** scoped to a single connection. ```javascript const UserModel = conn.model('User', userSchema); ``` -If you use multiple connections, you should make sure you export schemas, -**not** models. Exporting a model from a file is called the *export model pattern*. +The `createConnection()` function returns a connection instance, not a promise. +If you want to use `await` to make sure Mongoose successfully connects to MongoDB, use the [`asPromise()` function](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.asPromise()): + +```javascript +// `asPromise()` returns a promise that resolves to the connection +// once the connection succeeds, or rejects if connection failed. +const conn = await mongoose.createConnection(connectionString).asPromise(); +``` + +If you use multiple connections, you should make sure you export schemas, **not** models. +Exporting a model from a file is called the *export model pattern*. The export model pattern is limited because you can only use one connection. ```javascript From 81e36d10a3f440081f69d0ccc91b3d2f3d1da995 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 22 Feb 2024 14:55:23 -0500 Subject: [PATCH 166/191] Update docs/connections.md Co-authored-by: hasezoey --- docs/connections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/connections.md b/docs/connections.md index 5b65b096293..2c58cfd2142 100644 --- a/docs/connections.md +++ b/docs/connections.md @@ -410,7 +410,7 @@ const UserModel = conn.model('User', userSchema); ``` The `createConnection()` function returns a connection instance, not a promise. -If you want to use `await` to make sure Mongoose successfully connects to MongoDB, use the [`asPromise()` function](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.asPromise()): +If you want to use `await` to make sure Mongoose successfully connects to MongoDB, use the [`asPromise()` function](api/connection.html#Connection.prototype.asPromise()): ```javascript // `asPromise()` returns a promise that resolves to the connection From 5dfb62f5d2aa6833a20a72ca3a530cae95a7888c Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Wed, 3 May 2023 12:24:35 -0400 Subject: [PATCH 167/191] test: fix issues with cherry-picking #13376 to 6.x --- lib/helpers/processConnectionOptions.js | 7 ++++--- test/connection.test.js | 9 +++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/helpers/processConnectionOptions.js b/lib/helpers/processConnectionOptions.js index a9d862b1030..1dbb767ebee 100644 --- a/lib/helpers/processConnectionOptions.js +++ b/lib/helpers/processConnectionOptions.js @@ -9,11 +9,12 @@ function processConnectionOptions(uri, options) { ? opts.readPreference : getUriReadPreference(uri); + const clonedOpts = clone(opts); const resolvedOpts = (readPreference && readPreference !== 'primary' && readPreference !== 'primaryPreferred') - ? resolveOptsConflicts(readPreference, opts) - : opts; + ? resolveOptsConflicts(readPreference, clonedOpts) + : clonedOpts; - return clone(resolvedOpts); + return resolvedOpts; } function resolveOptsConflicts(pref, opts) { diff --git a/test/connection.test.js b/test/connection.test.js index dcf4cf621c7..49f715a8f4a 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1537,4 +1537,13 @@ describe('connections:', function() { }); assert.deepEqual(m.connections.length, 0); }); + + describe('processConnectionOptions', function() { + it('should not throw an error when attempting to mutate unmutable options object gh-13335', async function() { + const m = new mongoose.Mongoose(); + const opts = Object.preventExtensions({}); + const conn = await m.connect('mongodb://localhost:27017/db?retryWrites=true&w=majority&readPreference=secondaryPreferred', opts); + assert.ok(conn); + }); + }); }); From 45f5c4c6091a47bb0a2761ba0be7a7c18d11a084 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 25 Feb 2024 17:16:13 -0500 Subject: [PATCH 168/191] perf(model): make `insertMany()` `lean` option skip hydrating Mongoose docs Fix #14372 --- lib/model.js | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/model.js b/lib/model.js index c5e54c46f67..d6a3026c7fd 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3430,6 +3430,13 @@ Model.$__insertMany = function(arr, options, callback) { const results = ordered ? null : new Array(arr.length); const toExecute = arr.map((doc, index) => callback => { + // If option `lean` is set to true bypass validation and hydration + if (lean) { + // we have to execute callback at the nextTick to be compatible + // with parallelLimit, as `results` variable has TDZ issue if we + // execute the callback synchronously + return immediate(() => callback(null, doc)); + } if (!(doc instanceof _this)) { try { doc = new _this(doc); @@ -3440,13 +3447,6 @@ Model.$__insertMany = function(arr, options, callback) { if (options.session != null) { doc.$session(options.session); } - // If option `lean` is set to true bypass validation - if (lean) { - // we have to execute callback at the nextTick to be compatible - // with parallelLimit, as `results` variable has TDZ issue if we - // execute the callback synchronously - return immediate(() => callback(null, doc)); - } doc.$validate({ __noPromise: true }, function(error) { if (error) { // Option `ordered` signals that insert should be continued after reaching @@ -3510,7 +3510,7 @@ Model.$__insertMany = function(arr, options, callback) { callback(null, []); return; } - const docObjects = docAttributes.map(function(doc) { + const docObjects = lean ? docAttributes : docAttributes.map(function(doc) { if (doc.$__schema.options.versionKey) { doc[doc.$__schema.options.versionKey] = 0; } @@ -3572,6 +3572,9 @@ Model.$__insertMany = function(arr, options, callback) { return !isErrored; }). map(function setIsNewForInsertedDoc(doc) { + if (lean) { + return doc; + } doc.$__reset(); _setIsNew(doc, false); return doc; @@ -3588,9 +3591,11 @@ Model.$__insertMany = function(arr, options, callback) { return; } - for (const attribute of docAttributes) { - attribute.$__reset(); - _setIsNew(attribute, false); + if (!lean) { + for (const attribute of docAttributes) { + attribute.$__reset(); + _setIsNew(attribute, false); + } } if (rawResult) { From ac9ea0157b42a5c5848705d4827a42bb71a7763c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 25 Feb 2024 17:20:17 -0500 Subject: [PATCH 169/191] test: quick connection string fix --- test/connection.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/connection.test.js b/test/connection.test.js index 49f715a8f4a..36290280b47 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1542,7 +1542,7 @@ describe('connections:', function() { it('should not throw an error when attempting to mutate unmutable options object gh-13335', async function() { const m = new mongoose.Mongoose(); const opts = Object.preventExtensions({}); - const conn = await m.connect('mongodb://localhost:27017/db?retryWrites=true&w=majority&readPreference=secondaryPreferred', opts); + const conn = await m.connect('mongodb://127.0.0.1:27017/db?retryWrites=true&w=majority&readPreference=secondaryPreferred', opts); assert.ok(conn); }); }); From 635c79510160fcf4aab3fd5212bb80f2d649f9d8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 26 Feb 2024 10:59:35 -0500 Subject: [PATCH 170/191] perf(document+schema): small optimizations to make `init()` faster Re: #14113 --- lib/document.js | 25 +++++++++++++------------ lib/schema.js | 3 +++ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/document.js b/lib/document.js index fe636a4c351..757f5101c32 100644 --- a/lib/document.js +++ b/lib/document.js @@ -741,7 +741,7 @@ function init(self, obj, doc, opts, prefix) { if (i === '__proto__' || i === 'constructor') { return; } - path = prefix + i; + path = prefix ? prefix + i : i; schemaType = docSchema.path(path); // Should still work if not a model-level discriminator, but should not be @@ -751,7 +751,8 @@ function init(self, obj, doc, opts, prefix) { return; } - if (!schemaType && utils.isPOJO(obj[i])) { + const value = obj[i]; + if (!schemaType && utils.isPOJO(value)) { // assume nested object if (!doc[i]) { doc[i] = {}; @@ -759,30 +760,30 @@ function init(self, obj, doc, opts, prefix) { self[i] = doc[i]; } } - init(self, obj[i], doc[i], opts, path + '.'); + init(self, value, doc[i], opts, path + '.'); } else if (!schemaType) { - doc[i] = obj[i]; + doc[i] = value; if (!strict && !prefix) { - self[i] = obj[i]; + self[i] = value; } } else { // Retain order when overwriting defaults - if (doc.hasOwnProperty(i) && obj[i] !== void 0) { + if (doc.hasOwnProperty(i) && value !== void 0) { delete doc[i]; } - if (obj[i] === null) { + if (value === null) { doc[i] = schemaType._castNullish(null); - } else if (obj[i] !== undefined) { - const wasPopulated = obj[i].$__ == null ? null : obj[i].$__.wasPopulated; + } else if (value !== undefined) { + const wasPopulated = value.$__ == null ? null : value.$__.wasPopulated; if (schemaType && !wasPopulated) { try { if (opts && opts.setters) { // Call applySetters with `init = false` because otherwise setters are a noop const overrideInit = false; - doc[i] = schemaType.applySetters(obj[i], self, overrideInit); + doc[i] = schemaType.applySetters(value, self, overrideInit); } else { - doc[i] = schemaType.cast(obj[i], self, true); + doc[i] = schemaType.cast(value, self, true); } } catch (e) { self.invalidate(e.path, new ValidatorError({ @@ -794,7 +795,7 @@ function init(self, obj, doc, opts, prefix) { })); } } else { - doc[i] = obj[i]; + doc[i] = value; } } // mark as hydrated diff --git a/lib/schema.js b/lib/schema.js index 88e427d417d..dc0ebbd5003 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -955,6 +955,9 @@ reserved.collection = 1; Schema.prototype.path = function(path, obj) { if (obj === undefined) { + if (this.paths[path] != null) { + return this.paths[path]; + } // Convert to '.$' to check subpaths re: gh-6405 const cleanPath = _pathToPositionalSyntax(path); let schematype = _getPath(this, path, cleanPath); From 834c29f0cb73b8e4e4cfaf5a273831d0f135c15f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 26 Feb 2024 15:11:58 -0500 Subject: [PATCH 171/191] chore: release 7.6.9 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05ac8299cc9..4639ed8d7c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +7.6.9 / 2024-02-26 +================== + * fix(document): handle embedded recursive discriminators on nested path defined using Schema.prototype.discriminator #14256 #14245 + * types(model): correct return type for findByIdAndDelete() #14233 #14190 + * docs(connections): add note about using asPromise() with createConnection() for error handling #14364 #14266 + * docs(model+query+findoneandupdate): add more details about overwriteDiscriminatorKey option to docs #14264 #14246 + 7.6.8 / 2024-01-08 ================== * perf(schema): remove unnecessary lookahead in numeric subpath check diff --git a/package.json b/package.json index 3f6a5beb0a8..d36925cae48 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.8", + "version": "7.6.9", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From a9f661436d6f62bc6fc5ff282179efd7e4daefea Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 27 Feb 2024 16:19:24 -0500 Subject: [PATCH 172/191] test: try fixing tests --- test/connection.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/connection.test.js b/test/connection.test.js index 36290280b47..111432160a6 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1542,7 +1542,7 @@ describe('connections:', function() { it('should not throw an error when attempting to mutate unmutable options object gh-13335', async function() { const m = new mongoose.Mongoose(); const opts = Object.preventExtensions({}); - const conn = await m.connect('mongodb://127.0.0.1:27017/db?retryWrites=true&w=majority&readPreference=secondaryPreferred', opts); + const conn = await m.connect('mongodb://127.0.0.1:27017/db?retryWrites=true&w=majority&readPreference=primaryPreferred', opts); assert.ok(conn); }); }); From 867f7b75cb9b31731ab31d477d7ca67201303d32 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 27 Feb 2024 16:24:41 -0500 Subject: [PATCH 173/191] test: fix #13335 tests --- test/connection.test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/connection.test.js b/test/connection.test.js index 111432160a6..3d65b001870 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1542,7 +1542,12 @@ describe('connections:', function() { it('should not throw an error when attempting to mutate unmutable options object gh-13335', async function() { const m = new mongoose.Mongoose(); const opts = Object.preventExtensions({}); - const conn = await m.connect('mongodb://127.0.0.1:27017/db?retryWrites=true&w=majority&readPreference=primaryPreferred', opts); + + const uri = start.uri.lastIndexOf('?') === -1 ? + start.uri + '?retryWrites=true&w=majority&readPreference=primaryPreferred' : + start.uri.slice(0, start.uri.lastIndexOf('?')) + '?retryWrites=true&w=majority&readPreference=primaryPreferred'; + + const conn = await m.connect(uri, opts); assert.ok(conn); }); }); From 5096630bc236b1406d0f427c02aa1498f5850796 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 27 Feb 2024 16:27:30 -0500 Subject: [PATCH 174/191] style: fix lint --- test/connection.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/connection.test.js b/test/connection.test.js index 3d65b001870..446f4b0ccff 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1546,7 +1546,7 @@ describe('connections:', function() { const uri = start.uri.lastIndexOf('?') === -1 ? start.uri + '?retryWrites=true&w=majority&readPreference=primaryPreferred' : start.uri.slice(0, start.uri.lastIndexOf('?')) + '?retryWrites=true&w=majority&readPreference=primaryPreferred'; - + const conn = await m.connect(uri, opts); assert.ok(conn); }); From 222ad3b2f89f3957f9b87c057f974ea41d7c4da1 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 28 Feb 2024 15:43:02 -0500 Subject: [PATCH 175/191] chore: pin tmp@0.2.1 re: raszi/node-tmp#293 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 9cb365a38b0..fa048ad6cb2 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "q": "1.5.1", "sinon": "15.0.1", "stream-browserify": "3.0.0", + "tmp": "0.2.1", "tsd": "0.25.0", "typescript": "4.9.5", "uuid": "9.0.0", From 29f57c12caee006f2434a0ef9b21aace9b30fdcf Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 1 Mar 2024 13:34:32 -0500 Subject: [PATCH 176/191] chore: release 6.12.7 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a112597b79..e1f7bb87b8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +6.12.7 / 2024-03-01 +=================== + * perf(model): make insertMany() lean option skip hydrating Mongoose docs #14376 #14372 + * perf(document+schema): small optimizations to make init() faster #14383 #14113 + * fix(connection): don't modify passed options object to `openUri()` #14370 #13376 #13335 + * fix(ChangeStream): bubble up resumeTokenChanged changeStream event #14355 #14349 [3150](https://github.com/3150) + 6.12.6 / 2024-01-22 =================== * fix(collection): correctly handle buffer timeouts with find() #14277 diff --git a/package.json b/package.json index fa048ad6cb2..9f3a3889566 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.12.6", + "version": "6.12.7", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From b46652fc84e6a5a4150f1bf51eeaa3371f7ae12d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 5 Mar 2024 18:06:06 -0500 Subject: [PATCH 177/191] docs(model): add extra note about `lean` option for `insertMany()` skipping casting Re: #14376 --- lib/model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index d6a3026c7fd..8c69d441635 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3372,7 +3372,7 @@ Model.startSession = function() { * @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#insertMany) * @param {Boolean} [options.ordered=true] if true, will fail fast on the first error encountered. If false, will insert all the documents it can and report errors later. An `insertMany()` with `ordered = false` is called an "unordered" `insertMany()`. * @param {Boolean} [options.rawResult=false] if false, the returned promise resolves to the documents that passed mongoose document validation. If `true`, will return the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/InsertManyResult.html) with a `mongoose` property that contains `validationErrors` and `results` if this is an unordered `insertMany`. - * @param {Boolean} [options.lean=false] if `true`, skips hydrating and validating the documents. This option is useful if you need the extra performance, but Mongoose won't validate the documents before inserting. + * @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.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 {Function} [callback] callback From 918ae174d44b68f2ec7a6ca34346f5cb9b983169 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 7 Mar 2024 10:44:41 -0500 Subject: [PATCH 178/191] style: fix lint --- lib/types/array/methods/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js index 8276c15d6d1..f192e8c21ea 100644 --- a/lib/types/array/methods/index.js +++ b/lib/types/array/methods/index.js @@ -2,6 +2,7 @@ const Document = require('../../../document'); const ArraySubdocument = require('../../ArraySubdocument'); +const MongooseError = require('../../../error/mongooseError'); const cleanModifiedSubpaths = require('../../../helpers/document/cleanModifiedSubpaths'); const clone = require('../../../helpers/clone'); const internalToObjectOptions = require('../../../options').internalToObjectOptions; From c41ee9a93acbba1f2a9ad35de0d15259933f6afe Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 7 Mar 2024 10:55:26 -0500 Subject: [PATCH 179/191] docs(mongoose): add `options.overwriteModel` details to `mongoose.model()` docs Fix #14387 --- lib/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index 90547f1e95b..641e19fe876 100644 --- a/lib/index.js +++ b/lib/index.js @@ -507,12 +507,14 @@ Mongoose.prototype.pluralize = function(fn) { * * // or * - * const collectionName = 'actor' - * const M = mongoose.model('Actor', schema, collectionName) + * const collectionName = 'actor'; + * const M = mongoose.model('Actor', schema, collectionName); * * @param {String|Function} name model name or class extending Model * @param {Schema} [schema] the schema to use. * @param {String} [collection] name (optional, inferred from model name) + * @param {Object} [options] + * @param {Boolean} [options.overwriteModels=false] If true, overwrite existing models with the same name to avoid `OverwriteModelError` * @return {Model} The model associated with `name`. Mongoose will create the model if it doesn't already exist. * @api public */ From 0d41df3f6807cab60952b46bc3ff7a5c686fbf60 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 7 Mar 2024 16:46:56 -0500 Subject: [PATCH 180/191] Update lib/model.js Co-authored-by: hasezoey --- lib/model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index 8c69d441635..8a9e4a3226f 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3372,7 +3372,7 @@ Model.startSession = function() { * @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#insertMany) * @param {Boolean} [options.ordered=true] if true, will fail fast on the first error encountered. If false, will insert all the documents it can and report errors later. An `insertMany()` with `ordered = false` is called an "unordered" `insertMany()`. * @param {Boolean} [options.rawResult=false] if false, the returned promise resolves to the documents that passed mongoose document validation. If `true`, will return the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/InsertManyResult.html) with a `mongoose` property that contains `validationErrors` and `results` if this is an unordered `insertMany`. - * @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.castObject()). + * @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 {Function} [callback] callback From ffd254e3a2254611583a4b1587d76c9673dbdeb3 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Fri, 8 Mar 2024 13:46:56 +0100 Subject: [PATCH 181/191] style(model): fix link for 7.x documentation and up re 0d41df3f6807cab60952b46bc3ff7a5c686fbf60 --- lib/model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index 7c3572d63f7..7562986aeed 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3058,7 +3058,7 @@ Model.startSession = function() { * @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#insertMany) * @param {Boolean} [options.ordered=true] if true, will fail fast on the first error encountered. If false, will insert all the documents it can and report errors later. An `insertMany()` with `ordered = false` is called an "unordered" `insertMany()`. * @param {Boolean} [options.rawResult=false] if false, the returned promise resolves to the documents that passed mongoose document validation. If `true`, will return the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/InsertManyResult.html) with a `mongoose` property that contains `validationErrors` and `results` if this is an unordered `insertMany`. - * @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 {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()`](https://mongoosejs.com/docs/api/model.html#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. From 9afba5fd43f8c014dd9d4dc50ca1421a4043bd89 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 13 Mar 2024 09:38:14 -0400 Subject: [PATCH 182/191] chore: release 7.6.10 --- CHANGELOG.md | 5 +++++ package.json | 2 +- scripts/tsc-diagnostics-check.js | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca10a38fb79..e064efc1588 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +7.6.10 / 2024-03-13 +=================== + * docs(model): add extra note about lean option for insertMany() skipping casting #14415 + * docs(mongoose): add options.overwriteModel details to mongoose.model() docs #14422 + 6.12.7 / 2024-03-01 =================== * perf(model): make insertMany() lean option skip hydrating Mongoose docs #14376 #14372 diff --git a/package.json b/package.json index d36925cae48..ee64f3c5f74 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.9", + "version": "7.6.10", "author": "Guillermo Rauch ", "keywords": [ "mongodb", diff --git a/scripts/tsc-diagnostics-check.js b/scripts/tsc-diagnostics-check.js index b00bcbb2438..2f74bf39b92 100644 --- a/scripts/tsc-diagnostics-check.js +++ b/scripts/tsc-diagnostics-check.js @@ -3,7 +3,7 @@ const fs = require('fs'); const stdin = fs.readFileSync(0).toString('utf8'); -const maxInstantiations = isNaN(process.argv[2]) ? 120000 : parseInt(process.argv[2], 10); +const maxInstantiations = isNaN(process.argv[2]) ? 125000 : parseInt(process.argv[2], 10); console.log(stdin); From 3e63142323d4ba80c69de8923ad3269a52efb052 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 21 Mar 2024 16:09:10 -0400 Subject: [PATCH 183/191] fix(document): avoid depopulating populated subdocs underneath document arrays when copying to another document Fix #14118 --- lib/document.js | 11 +++++++---- lib/model.js | 2 +- lib/schema/documentarray.js | 16 +++------------- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/lib/document.js b/lib/document.js index 757f5101c32..d85288ac64a 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1087,7 +1087,11 @@ Document.prototype.$set = function $set(path, val, type, options) { if (path.$__isNested) { path = path.toObject(); } else { - path = path._doc; + // This ternary is to support gh-7898 (copying virtuals if same schema) + // while not breaking gh-10819, which for some reason breaks if we use toObject() + path = path.$__schema === this.$__schema + ? applyVirtuals(path, { ...path._doc }) + : path._doc; } } if (path == null) { @@ -4012,11 +4016,11 @@ function applyVirtuals(self, json, options, toObjectOptions) { ? toObjectOptions.aliases : true; + options = options || {}; let virtualsToApply = null; if (Array.isArray(options.virtuals)) { virtualsToApply = new Set(options.virtuals); - } - else if (options.virtuals && options.virtuals.pathsToSkip) { + } else if (options.virtuals && options.virtuals.pathsToSkip) { virtualsToApply = new Set(paths); for (let i = 0; i < options.virtuals.pathsToSkip.length; i++) { if (virtualsToApply.has(options.virtuals.pathsToSkip[i])) { @@ -4029,7 +4033,6 @@ function applyVirtuals(self, json, options, toObjectOptions) { return json; } - options = options || {}; for (i = 0; i < numPaths; ++i) { path = paths[i]; diff --git a/lib/model.js b/lib/model.js index 8a9e4a3226f..313d1f4747f 100644 --- a/lib/model.js +++ b/lib/model.js @@ -5124,7 +5124,7 @@ function _assign(model, vals, mod, assignmentOpts) { } // flag each as result of population if (!lean) { - val.$__.wasPopulated = val.$__.wasPopulated || true; + val.$__.wasPopulated = val.$__.wasPopulated || { value: _val }; } } } diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js index 3867c512aa2..eb63ad759c7 100644 --- a/lib/schema/documentarray.js +++ b/lib/schema/documentarray.js @@ -443,19 +443,9 @@ DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) { const Constructor = getConstructor(this.casterConstructor, rawArray[i]); - // Check if the document has a different schema (re gh-3701) - if (rawArray[i].$__ != null && !(rawArray[i] instanceof Constructor)) { - const spreadDoc = handleSpreadDoc(rawArray[i], true); - if (rawArray[i] !== spreadDoc) { - rawArray[i] = spreadDoc; - } else { - rawArray[i] = rawArray[i].toObject({ - transform: false, - // Special case: if different model, but same schema, apply virtuals - // re: gh-7898 - virtuals: rawArray[i].schema === Constructor.schema - }); - } + const spreadDoc = handleSpreadDoc(rawArray[i], true); + if (rawArray[i] !== spreadDoc) { + rawArray[i] = spreadDoc; } if (rawArray[i] instanceof Subdocument) { From ed355731ab4b5d91e900a4de411ca132d7e56cfa Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 21 Mar 2024 16:14:29 -0400 Subject: [PATCH 184/191] test(document): add test case for #14418 --- test/document.test.js | 62 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test/document.test.js b/test/document.test.js index beefe366d1f..1a5c1809212 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12476,6 +12476,68 @@ describe('document', function() { doc.set({ nested: void 0 }); assert.strictEqual(doc.toObject().nested, void 0); }); + + it('avoids depopulating populated subdocs underneath document arrays when copying to another document (gh-14418)', async function() { + const cartSchema = new mongoose.Schema({ + products: [ + { + product: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Product' + }, + quantity: Number + } + ], + singleProduct: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Product' + } + }); + const purchaseSchema = new mongoose.Schema({ + products: [ + { + product: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Product' + }, + quantity: Number + } + ], + singleProduct: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Product' + } + }); + const productSchema = new mongoose.Schema({ + name: String + }); + + const Cart = db.model('Cart', cartSchema); + const Purchase = db.model('Purchase', purchaseSchema); + const Product = db.model('Product', productSchema); + + const dbProduct = await Product.create({ name: 'Bug' }); + + const dbCart = await Cart.create({ + products: [ + { + product: dbProduct, + quantity: 2 + } + ], + singleProduct: dbProduct + }); + + const foundCart = await Cart.findById(dbCart._id). + populate('products.product singleProduct'); + + const purchaseFromDbCart = new Purchase({ + products: foundCart.products, + singleProduct: foundCart.singleProduct + }); + assert.equal(purchaseFromDbCart.products[0].product.name, 'Bug'); + assert.equal(purchaseFromDbCart.singleProduct.name, 'Bug'); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() { From bc483799e6b7520214b2ce60b5e62ca006674519 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 21 Mar 2024 17:24:44 -0400 Subject: [PATCH 185/191] fix(schematype): consistently set `wasPopulated` to object with `value` property rather than boolean Re: #14418 Re: #6048 --- lib/schematype.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/schematype.js b/lib/schematype.js index 85e0241b5ea..ebd5dbc3c3d 100644 --- a/lib/schematype.js +++ b/lib/schematype.js @@ -1505,7 +1505,7 @@ SchemaType.prototype._castRef = function _castRef(value, doc, init) { } if (value.$__ != null) { - value.$__.wasPopulated = value.$__.wasPopulated || true; + value.$__.wasPopulated = value.$__.wasPopulated || { value: value._id }; return value; } @@ -1531,7 +1531,7 @@ SchemaType.prototype._castRef = function _castRef(value, doc, init) { !doc.$__.populated[path].options.options || !doc.$__.populated[path].options.options.lean) { ret = new pop.options[populateModelSymbol](value); - ret.$__.wasPopulated = true; + ret.$__.wasPopulated = { value: ret._id }; } return ret; From 69a0581079a074876cfcb2ae1d1ae7ed5bb75f2f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 26 Mar 2024 14:55:34 -0400 Subject: [PATCH 186/191] fix(document): handle virtuals that are stored as objects but getter returns string with toJSON Fix #14446 --- lib/document.js | 7 ++++++- test/document.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index d85288ac64a..e82d2e73b71 100644 --- a/lib/document.js +++ b/lib/document.js @@ -4113,7 +4113,12 @@ function applyGetters(self, json, options) { for (let ii = 0; ii < plen; ++ii) { part = parts[ii]; v = cur[part]; - if (ii === last) { + // If we've reached a non-object part of the branch, continuing would + // cause "Cannot create property 'foo' on string 'bar'" error. + // Necessary for mongoose-intl plugin re: gh-14446 + if (branch != null && typeof branch !== 'object') { + break; + } else if (ii === last) { const val = self.$get(path); branch[part] = clone(val, options); } else if (v == null) { diff --git a/test/document.test.js b/test/document.test.js index 1a5c1809212..2aa0b332e2f 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12538,6 +12538,38 @@ describe('document', function() { assert.equal(purchaseFromDbCart.products[0].product.name, 'Bug'); assert.equal(purchaseFromDbCart.singleProduct.name, 'Bug'); }); + + it('handles virtuals that are stored as objects but getter returns string with toJSON (gh-14446)', async function() { + const childSchema = new mongoose.Schema(); + + childSchema.virtual('name') + .set(function(values) { + for (const [lang, val] of Object.entries(values)) { + this.set(`name.${lang}`, val); + } + }) + .get(function() { + return this.$__getValue(`name.${this.lang}`); + }); + + childSchema.add({ name: { en: { type: String }, de: { type: String } } }); + + const ChildModel = db.model('Child', childSchema); + const ParentModel = db.model('Parent', new mongoose.Schema({ + children: [childSchema] + })); + + const child = await ChildModel.create({ name: { en: 'Stephen', de: 'Stefan' } }); + child.lang = 'en'; + assert.equal(child.name, 'Stephen'); + + const parent = await ParentModel.create({ + children: [{ name: { en: 'Stephen', de: 'Stefan' } }] + }); + parent.children[0].lang = 'de'; + const obj = parent.toJSON({ getters: true }); + assert.equal(obj.children[0].name, 'Stefan'); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() { From f845fb22eccbeb033bf99c6b412928cbfd40a764 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 2 Apr 2024 16:23:02 -0400 Subject: [PATCH 187/191] fix(schema): support setting discriminator options in Schema.prototype.discriminator() Fix #14448 --- .../applyEmbeddedDiscriminators.js | 7 +++++-- lib/index.js | 6 +++++- lib/schema.js | 10 ++++++++-- test/schema.test.js | 19 +++++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js index 9a04ecb072f..b7832234cbb 100644 --- a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js +++ b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js @@ -20,9 +20,12 @@ function applyEmbeddedDiscriminators(schema, seen = new WeakSet()) { continue; } for (const discriminatorKey of schemaType.schema._applyDiscriminators.keys()) { - const discriminatorSchema = schemaType.schema._applyDiscriminators.get(discriminatorKey); + const { + schema: discriminatorSchema, + options + } = schemaType.schema._applyDiscriminators.get(discriminatorKey); applyEmbeddedDiscriminators(discriminatorSchema, seen); - schemaType.discriminator(discriminatorKey, discriminatorSchema); + schemaType.discriminator(discriminatorKey, discriminatorSchema, options); } schemaType._appliedDiscriminators = true; } diff --git a/lib/index.js b/lib/index.js index 641e19fe876..38197ec50ae 100644 --- a/lib/index.js +++ b/lib/index.js @@ -628,7 +628,11 @@ Mongoose.prototype._model = function(name, schema, collection, options) { if (schema._applyDiscriminators != null) { for (const disc of schema._applyDiscriminators.keys()) { - model.discriminator(disc, schema._applyDiscriminators.get(disc)); + const { + schema: discriminatorSchema, + options + } = schema._applyDiscriminators.get(disc); + model.discriminator(disc, discriminatorSchema, options); } } diff --git a/lib/schema.js b/lib/schema.js index 600e27208ee..ff6d505f7b0 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -624,12 +624,18 @@ Schema.prototype.defaultOptions = function(options) { * * @param {String} name the name of the discriminator * @param {Schema} schema the discriminated Schema + * @param {Object} [options] discriminator options + * @param {String} [options.value] the string stored in the `discriminatorKey` property. If not specified, Mongoose uses the `name` parameter. + * @param {Boolean} [options.clone=true] By default, `discriminator()` clones the given `schema`. Set to `false` to skip cloning. + * @param {Boolean} [options.overwriteModels=false] by default, Mongoose does not allow you to define a discriminator with the same name as another discriminator. Set this to allow overwriting discriminators with the same name. + * @param {Boolean} [options.mergeHooks=true] By default, Mongoose merges the base schema's hooks with the discriminator schema's hooks. Set this option to `false` to make Mongoose use the discriminator schema's hooks instead. + * @param {Boolean} [options.mergePlugins=true] By default, Mongoose merges the base schema's plugins with the discriminator schema's plugins. Set this option to `false` to make Mongoose use the discriminator schema's plugins instead. * @return {Schema} the Schema instance * @api public */ -Schema.prototype.discriminator = function(name, schema) { +Schema.prototype.discriminator = function(name, schema, options) { this._applyDiscriminators = this._applyDiscriminators || new Map(); - this._applyDiscriminators.set(name, schema); + this._applyDiscriminators.set(name, { schema, options }); return this; }; diff --git a/test/schema.test.js b/test/schema.test.js index 0df19a65790..074c6ddc760 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -72,6 +72,7 @@ describe('schema', function() { }, b: { $type: String } }, { typeKey: '$type' }); + db.deleteModel(/Test/); NestedModel = db.model('Test', NestedSchema); }); @@ -3202,4 +3203,22 @@ describe('schema', function() { const doc = new baseModel({ type: 1, self: [{ type: 1 }] }); assert.equal(doc.self[0].type, 1); }); + + it('handles discriminator options with Schema.prototype.discriminator (gh-14448)', async function() { + const eventSchema = new mongoose.Schema({ + name: String + }, { discriminatorKey: 'kind' }); + const clickedEventSchema = new mongoose.Schema({ element: String }); + eventSchema.discriminator( + 'Test2', + clickedEventSchema, + { value: 'click' } + ); + const Event = db.model('Test', eventSchema); + const ClickedModel = db.model('Test2'); + + const doc = await Event.create({ kind: 'click', element: '#hero' }); + assert.equal(doc.element, '#hero'); + assert.ok(doc instanceof ClickedModel); + }); }); From f1ed8b1415a41f9a7c071c9e6f96bce5e9766537 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 2 Apr 2024 15:28:52 -0400 Subject: [PATCH 188/191] fix(schema): deduplicate idGetter so creating multiple models with same schema doesn't result in multiple id getters Fix #14457 --- lib/schema.js | 3 ++- test/schema.test.js | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/schema.js b/lib/schema.js index 600e27208ee..dab8c273164 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1916,6 +1916,7 @@ Schema.prototype.plugin = function(fn, opts) { 'got "' + (typeof fn) + '"'); } + if (opts && opts.deduplicate) { for (const plugin of this.plugins) { if (plugin.fn === fn) { @@ -2720,7 +2721,7 @@ function isArrayFilter(piece) { */ Schema.prototype._preCompile = function _preCompile() { - idGetter(this); + this.plugin(idGetter, { deduplicate: true }); }; /*! diff --git a/test/schema.test.js b/test/schema.test.js index 0df19a65790..c44452a5754 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -3172,6 +3172,13 @@ describe('schema', function() { const res = await Test.findOne({ _id: { $eq: doc._id, $type: 'objectId' } }); assert.equal(res.name, 'Test Testerson'); }); + it('deduplicates idGetter (gh-14457)', function() { + const schema = new Schema({ name: String }); + schema._preCompile(); + assert.equal(schema.virtual('id').getters.length, 1); + schema._preCompile(); + assert.equal(schema.virtual('id').getters.length, 1); + }); it('handles recursive definitions in discriminators (gh-13978)', function() { const base = new Schema({ From c00a715e97c6437a5ff1a503c2a50ebd0df2ba47 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 10 Apr 2024 17:44:25 -0400 Subject: [PATCH 189/191] chore: release 6.12.8 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1f7bb87b8b..4bdcdeec521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +6.12.8 / 2024-04-10 +=================== + * fix(document): handle virtuals that are stored as objects but getter returns string with toJSON #14468 #14446 + * fix(schematype): consistently set wasPopulated to object with `value` property rather than boolean #14418 + * docs(model): add extra note about lean option for insertMany() skipping casting #14415 #14376 + 6.12.7 / 2024-03-01 =================== * perf(model): make insertMany() lean option skip hydrating Mongoose docs #14376 #14372 diff --git a/package.json b/package.json index 9f3a3889566..6d887a254e2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.12.7", + "version": "6.12.8", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 247d0296902dacf6d24862889b31a8b5e0540745 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 10 Apr 2024 16:40:12 -0400 Subject: [PATCH 190/191] fix(populate): avoid match function filtering out `null` values in populate result Fix #14494 --- lib/helpers/populate/assignVals.js | 4 +-- test/model.populate.test.js | 53 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/lib/helpers/populate/assignVals.js b/lib/helpers/populate/assignVals.js index 44de64fbc39..8f363988f87 100644 --- a/lib/helpers/populate/assignVals.js +++ b/lib/helpers/populate/assignVals.js @@ -101,8 +101,8 @@ module.exports = function assignVals(o) { valueToSet = numDocs(rawIds[i]); } else if (Array.isArray(o.match)) { valueToSet = Array.isArray(rawIds[i]) ? - rawIds[i].filter(sift(o.match[i])) : - [rawIds[i]].filter(sift(o.match[i]))[0]; + rawIds[i].filter(v => v == null || sift(o.match[i])(v)) : + [rawIds[i]].filter(v => v == null || sift(o.match[i])(v))[0]; } else { valueToSet = rawIds[i]; } diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 1c6029853de..148093e5d67 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -10859,4 +10859,57 @@ describe('model: populate:', function() { { name: 'foo', prop: 'bar' } ); }); + + it('avoids filtering out `null` values when applying match function (gh-14494)', async function() { + const gradeSchema = new mongoose.Schema({ + studentId: mongoose.Types.ObjectId, + classId: mongoose.Types.ObjectId, + grade: String + }); + + const Grade = db.model('Test', gradeSchema); + + const studentSchema = new mongoose.Schema({ + name: String + }); + + studentSchema.virtual('grade', { + ref: Grade, + localField: '_id', + foreignField: 'studentId', + match: (doc) => ({ + classId: doc._id + }), + justOne: true + }); + + const classSchema = new mongoose.Schema({ + name: String, + students: [studentSchema] + }); + + const Class = db.model('Test2', classSchema); + + const newClass = await Class.create({ + name: 'History', + students: [{ name: 'Henry' }, { name: 'Robert' }] + }); + + const studentRobert = newClass.students.find( + ({ name }) => name === 'Robert' + ); + + await Grade.create({ + studentId: studentRobert._id, + classId: newClass._id, + grade: 'B' + }); + + const latestClass = await Class.findOne({ name: 'History' }).populate('students.grade'); + + assert.equal(latestClass.students[0].name, 'Henry'); + assert.equal(latestClass.students[0].grade, null); + assert.equal(latestClass.students[1].name, 'Robert'); + assert.equal(latestClass.students[1].grade.grade, 'B'); + }); }); From c97c060119bdc3c52d42d63b7fe6d6af046b6f5b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 11 Apr 2024 12:21:24 -0400 Subject: [PATCH 191/191] chore: release 7.6.11 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7715424ce42..e3805188d97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +7.6.11 / 2024-04-11 +=================== + * fix(populate): avoid match function filtering out null values in populate result #14518 + * fix(schema): support setting discriminator options in Schema.prototype.discriminator() #14493 #14448 + * fix(schema): deduplicate idGetter so creating multiple models with same schema doesn't result in multiple id getters #14492 #14457 + 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 ee64f3c5f74..dcd3039fa3c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.10", + "version": "7.6.11", "author": "Guillermo Rauch ", "keywords": [ "mongodb",