From 9afcb1c7499de430d5b75f097475055a65c0a498 Mon Sep 17 00:00:00 2001 From: Omkar Hirve Date: Sat, 16 Apr 2022 09:03:37 +0530 Subject: [PATCH 01/42] Added support for cleanAndClose connections from memory --- lib/connection.js | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 51a9aec7281..0292794faa1 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -905,25 +905,35 @@ function _setClient(conn, client, options, dbName) { * Closes the connection * * @param {Boolean} [force] optional + * @param {Boolean} [closeAndClean] optional * @param {Function} [callback] optional * @return {Promise} * @api public */ -Connection.prototype.close = function(force, callback) { +Connection.prototype.close = function(force, closeAndClean, callback) { if (typeof force === 'function') { callback = force; force = false; } + if (typeof closeAndClean === 'function') { + callback = closeAndClean; + closeAndClean = false; + } + if (force != null && typeof force === 'object') { this.$wasForceClosed = !!force.force; } else { this.$wasForceClosed = !!force; } + if (closeAndClean !== null) { + closeAndClean = !!closeAndClean + } + return promiseOrCallback(callback, cb => { - this._close(force, cb); + this._close(force, closeAndClean, cb); }); }; @@ -931,10 +941,11 @@ Connection.prototype.close = function(force, callback) { * Handles closing the connection * * @param {Boolean} force + * @param {Boolean} closeAndClean * @param {Function} callback * @api private */ -Connection.prototype._close = function(force, callback) { +Connection.prototype._close = function(force, closeAndClean, callback) { const _this = this; const closeCalled = this._closeCalled; this._closeCalled = true; @@ -942,8 +953,14 @@ Connection.prototype._close = function(force, callback) { this.client._closeCalled = true; } + let conn = this; switch (this.readyState) { case STATES.disconnected: + if (closeAndClean && this.base.connections.indexOf(conn) != -1) { + console.log("BASE CONNECTIONS COUNT BEFORE 1---------> ", this.base.connections.length) + this.base.connections.splice(this.base.connections.indexOf(conn), 1); + console.log("BASE CONNECTIONS COUNT AFTER 1---------> ", this.base.connections.length) + } if (closeCalled) { callback(); } else { @@ -963,6 +980,12 @@ Connection.prototype._close = function(force, callback) { if (err) { return callback(err); } + if (closeAndClean && _this.base.connections.indexOf(conn) != -1) { + console.log("BASE CONNECTIONS COUNT BEFORE 2---------> ", _this.base.connections.length) + _this.base.connections.splice(_this.base.connections.indexOf(conn), 1); + console.log("BASE CONNECTIONS COUNT AFTER 2---------> ", _this.base.connections.length) + + } _this.onClose(force); callback(null); }); @@ -970,12 +993,18 @@ Connection.prototype._close = function(force, callback) { break; case STATES.connecting: this.once('open', function() { - _this.close(callback); + console.log("======== connecting ==========") + _this.close(false, true, callback); }); break; case STATES.disconnecting: this.once('close', function() { + if (closeAndClean && _this.base.connections.indexOf(conn) != -1) { + console.log("BASE CONNECTIONS COUNT BEFORE 3---------> ", _this.base.connections.length) + _this.base.connections.splice(_this.base.connections.indexOf(conn), 1); + console.log("BASE CONNECTIONS COUNT AFTER 3---------> ", _this.base.connections.length) + } callback(); }); break; From dc8c589adc1a303f016b9cd100b20e3ce455aadf Mon Sep 17 00:00:00 2001 From: Omkar Hirve <35989385+OmkarHirve@users.noreply.github.com> Date: Sat, 16 Apr 2022 23:22:02 +0530 Subject: [PATCH 02/42] Made sure to check for undefined, removed strict check Co-authored-by: Hafez --- lib/connection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.js b/lib/connection.js index 0292794faa1..8c9a8240b24 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -928,7 +928,7 @@ Connection.prototype.close = function(force, closeAndClean, callback) { this.$wasForceClosed = !!force; } - if (closeAndClean !== null) { + if (closeAndClean != null) { closeAndClean = !!closeAndClean } From 7025c0acb7dac035ebde4d4957032b5cdab5c18f Mon Sep 17 00:00:00 2001 From: Omkar Hirve Date: Mon, 18 Apr 2022 16:21:32 +0530 Subject: [PATCH 03/42] Added unit test, method definition and fixed lint issues --- lib/connection.js | 16 ++++------------ test/connection.test.js | 21 +++++++++++++++++++++ types/connection.d.ts | 2 ++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 8c9a8240b24..9190ecee43a 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -928,8 +928,8 @@ Connection.prototype.close = function(force, closeAndClean, callback) { this.$wasForceClosed = !!force; } - if (closeAndClean != null) { - closeAndClean = !!closeAndClean + if (closeAndClean != null && typeof closeAndClean !== 'function') { + closeAndClean = !!closeAndClean; } return promiseOrCallback(callback, cb => { @@ -953,13 +953,11 @@ Connection.prototype._close = function(force, closeAndClean, callback) { this.client._closeCalled = true; } - let conn = this; + const conn = this; switch (this.readyState) { case STATES.disconnected: if (closeAndClean && this.base.connections.indexOf(conn) != -1) { - console.log("BASE CONNECTIONS COUNT BEFORE 1---------> ", this.base.connections.length) this.base.connections.splice(this.base.connections.indexOf(conn), 1); - console.log("BASE CONNECTIONS COUNT AFTER 1---------> ", this.base.connections.length) } if (closeCalled) { callback(); @@ -981,10 +979,7 @@ Connection.prototype._close = function(force, closeAndClean, callback) { return callback(err); } if (closeAndClean && _this.base.connections.indexOf(conn) != -1) { - console.log("BASE CONNECTIONS COUNT BEFORE 2---------> ", _this.base.connections.length) _this.base.connections.splice(_this.base.connections.indexOf(conn), 1); - console.log("BASE CONNECTIONS COUNT AFTER 2---------> ", _this.base.connections.length) - } _this.onClose(force); callback(null); @@ -993,17 +988,14 @@ Connection.prototype._close = function(force, closeAndClean, callback) { break; case STATES.connecting: this.once('open', function() { - console.log("======== connecting ==========") - _this.close(false, true, callback); + _this.close(force, closeAndClean, callback); }); break; case STATES.disconnecting: this.once('close', function() { if (closeAndClean && _this.base.connections.indexOf(conn) != -1) { - console.log("BASE CONNECTIONS COUNT BEFORE 3---------> ", _this.base.connections.length) _this.base.connections.splice(_this.base.connections.indexOf(conn), 1); - console.log("BASE CONNECTIONS COUNT AFTER 3---------> ", _this.base.connections.length) } callback(); }); diff --git a/test/connection.test.js b/test/connection.test.js index 1d9bde1d8e3..fc725ac19f1 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -9,6 +9,7 @@ const start = require('./common'); const Promise = require('bluebird'); const Q = require('q'); const assert = require('assert'); +const sinon = require('sinon'); const mongodb = require('mongodb'); const MongooseError = require('../lib/error/index'); @@ -360,6 +361,26 @@ describe('connections:', function() { }); }); + it('close connection and remove it permanantly', (done) => { + const opts = {}; + const conn = mongoose.createConnection(start.uri, opts); + const MongoClient = mongodb.MongoClient; + sinon.stub(MongoClient.prototype, 'close').callsFake((force, callback) => { + callback(); + }) + + const db = conn.useDb('test-db') + + const totalConn = mongoose.connections.length + console.log("Total open connections before close are --> ", totalConn) + + conn.close(false, true, ()=> { + console.log("Total open connections after close are --> ", mongoose.connections.length) + assert.equal(mongoose.connections.length, totalConn-1) + done(); + }); + }); + it('force close with connection created after close (gh-5664)', function(done) { const opts = {}; const db = mongoose.createConnection(start.uri, opts); diff --git a/types/connection.d.ts b/types/connection.d.ts index e97cf70a003..d4e86817fda 100644 --- a/types/connection.d.ts +++ b/types/connection.d.ts @@ -44,8 +44,10 @@ declare module 'mongoose' { /** Closes the connection */ close(force: boolean, callback: CallbackWithoutResult): void; + close(force: boolean, closeAndClean: boolean, callback: CallbackWithoutResult): void; close(callback: CallbackWithoutResult): void; close(force?: boolean): Promise; + close(force?: boolean, closeAndClean?: boolean): Promise; /** Retrieves a collection, creating it if not cached. */ collection(name: string, options?: mongodb.CreateCollectionOptions): Collection; From 623efdfc40306b6fc9b0f5ffce183df0863213be Mon Sep 17 00:00:00 2001 From: Omkar Hirve Date: Sat, 23 Apr 2022 12:29:03 +0530 Subject: [PATCH 04/42] Added support for throwing error if openUri is called on cleaned connection --- lib/connection.js | 14 +++++++++++ test/connection.test.js | 53 +++++++++++++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 9190ecee43a..bd01c28dda8 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -697,6 +697,18 @@ Connection.prototype.openUri = function(uri, options, callback) { typeof callback + '"'); } + if (this._closeCalledWithClean && this.readyState === STATES.disconnected) { + const error = 'Connection has been closed and cleaned already, and cannot be used for re-opening the connection. ' + + 'Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'; + if (typeof callback === 'function') { + callback(error); + return; + } + else { + throw new MongooseError(error); + } + } + if (this.readyState === STATES.connecting || this.readyState === STATES.connected) { if (this._connectionString !== uri) { throw new MongooseError('Can\'t call `openUri()` on an active connection with ' + @@ -949,8 +961,10 @@ Connection.prototype._close = function(force, closeAndClean, callback) { const _this = this; const closeCalled = this._closeCalled; this._closeCalled = true; + this._closeCalledWithClean = closeAndClean; if (this.client != null) { this.client._closeCalled = true; + this.client._closeCalledWithClean = closeAndClean; } const conn = this; diff --git a/test/connection.test.js b/test/connection.test.js index fc725ac19f1..48344279caf 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -367,20 +367,59 @@ describe('connections:', function() { const MongoClient = mongodb.MongoClient; sinon.stub(MongoClient.prototype, 'close').callsFake((force, callback) => { callback(); - }) + }); - const db = conn.useDb('test-db') + conn.useDb('test-db'); - const totalConn = mongoose.connections.length - console.log("Total open connections before close are --> ", totalConn) + const totalConn = mongoose.connections.length; + console.log('Total open connections before close are --> ', totalConn); - conn.close(false, true, ()=> { - console.log("Total open connections after close are --> ", mongoose.connections.length) - assert.equal(mongoose.connections.length, totalConn-1) + conn.close(false, true, () => { + console.log('Total open connections after close are --> ', mongoose.connections.length); + assert.equal(mongoose.connections.length, totalConn - 1); done(); }); }); + it('verify that attempt to re-open closedAndCleaned connection throws error, via promise', (done) => { + const opts = {}; + const conn = mongoose.createConnection(start.uri, opts); + const MongoClient = mongodb.MongoClient; + sinon.stub(MongoClient.prototype, 'close').callsFake((force, callback) => { + callback(); + }); + + conn.useDb('test-db'); + + conn.close(false, true, async() => { + try { + await conn.openUri(start.uri); + } catch (error) { + assert.equal(error.message, 'Connection has been closed and cleaned already, and cannot be used for re-opening the connection. Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'); + done(); + } + }); + }); + + it('verify that attempt to re-open closedAndCleaned connection throws error, via callback', (done) => { + const opts = {}; + const conn = mongoose.createConnection(start.uri, opts); + const MongoClient = mongodb.MongoClient; + sinon.stub(MongoClient.prototype, 'close').callsFake((force, callback) => { + callback(); + }); + + conn.useDb('test-db'); + + conn.close(false, true, () => { + conn.openUri(start.uri, function(error, result) { + assert.equal(result, undefined); + assert.equal(error, 'Connection has been closed and cleaned already, and cannot be used for re-opening the connection. Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'); + done(); + }); + }); + }); + it('force close with connection created after close (gh-5664)', function(done) { const opts = {}; const db = mongoose.createConnection(start.uri, opts); From dbcf0dc191f8b038f63c6ac824c5c396e7980bb7 Mon Sep 17 00:00:00 2001 From: Omkar Hirve Date: Sat, 23 Apr 2022 15:20:12 +0530 Subject: [PATCH 05/42] restored stub after use --- test/connection.test.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/connection.test.js b/test/connection.test.js index 48344279caf..915e79da0f1 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -365,7 +365,7 @@ describe('connections:', function() { const opts = {}; const conn = mongoose.createConnection(start.uri, opts); const MongoClient = mongodb.MongoClient; - sinon.stub(MongoClient.prototype, 'close').callsFake((force, callback) => { + const stub = sinon.stub(MongoClient.prototype, 'close').callsFake((force, callback) => { callback(); }); @@ -377,6 +377,7 @@ describe('connections:', function() { conn.close(false, true, () => { console.log('Total open connections after close are --> ', mongoose.connections.length); assert.equal(mongoose.connections.length, totalConn - 1); + stub.restore(); done(); }); }); @@ -385,7 +386,7 @@ describe('connections:', function() { const opts = {}; const conn = mongoose.createConnection(start.uri, opts); const MongoClient = mongodb.MongoClient; - sinon.stub(MongoClient.prototype, 'close').callsFake((force, callback) => { + const stub = sinon.stub(MongoClient.prototype, 'close').callsFake((force, callback) => { callback(); }); @@ -396,6 +397,7 @@ describe('connections:', function() { await conn.openUri(start.uri); } catch (error) { assert.equal(error.message, 'Connection has been closed and cleaned already, and cannot be used for re-opening the connection. Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'); + stub.restore(); done(); } }); @@ -405,7 +407,7 @@ describe('connections:', function() { const opts = {}; const conn = mongoose.createConnection(start.uri, opts); const MongoClient = mongodb.MongoClient; - sinon.stub(MongoClient.prototype, 'close').callsFake((force, callback) => { + const stub = sinon.stub(MongoClient.prototype, 'close').callsFake((force, callback) => { callback(); }); @@ -415,6 +417,7 @@ describe('connections:', function() { conn.openUri(start.uri, function(error, result) { assert.equal(result, undefined); assert.equal(error, 'Connection has been closed and cleaned already, and cannot be used for re-opening the connection. Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'); + stub.restore(); done(); }); }); From 8fa8fecb95b5650817a1a80db0594377128ca4e8 Mon Sep 17 00:00:00 2001 From: Omkar Hirve Date: Fri, 29 Apr 2022 14:24:01 +0530 Subject: [PATCH 06/42] Created a 'destroy' method to clear conns --- lib/connection.js | 49 +++++++++++++++++++++++------------------ test/connection.test.js | 26 +++++++++++----------- types/connection.d.ts | 7 ++++-- 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index bd01c28dda8..f9b177460a2 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -697,7 +697,7 @@ Connection.prototype.openUri = function(uri, options, callback) { typeof callback + '"'); } - if (this._closeCalledWithClean && this.readyState === STATES.disconnected) { + if (this._destroyCalled && this.readyState === STATES.disconnected) { const error = 'Connection has been closed and cleaned already, and cannot be used for re-opening the connection. ' + 'Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'; if (typeof callback === 'function') { @@ -913,39 +913,46 @@ function _setClient(conn, client, options, dbName) { } } +Connection.prototype.destroy = function(force, callback) { + if (typeof force === 'function') { + callback = force; + force = false; + } + + if (force != null && typeof force === 'object') { + this.$wasForceClosed = !!force.force; + } else { + this.$wasForceClosed = !!force; + } + + return promiseOrCallback(callback, cb => { + this._close(force, true, cb); + }); +}; + /** * Closes the connection * * @param {Boolean} [force] optional - * @param {Boolean} [closeAndClean] optional * @param {Function} [callback] optional * @return {Promise} * @api public */ -Connection.prototype.close = function(force, closeAndClean, callback) { +Connection.prototype.close = function(force, callback) { if (typeof force === 'function') { callback = force; force = false; } - if (typeof closeAndClean === 'function') { - callback = closeAndClean; - closeAndClean = false; - } - if (force != null && typeof force === 'object') { this.$wasForceClosed = !!force.force; } else { this.$wasForceClosed = !!force; } - if (closeAndClean != null && typeof closeAndClean !== 'function') { - closeAndClean = !!closeAndClean; - } - return promiseOrCallback(callback, cb => { - this._close(force, closeAndClean, cb); + this._close(force, false, cb); }); }; @@ -953,24 +960,24 @@ Connection.prototype.close = function(force, closeAndClean, callback) { * Handles closing the connection * * @param {Boolean} force - * @param {Boolean} closeAndClean + * @param {Boolean} destroyConnAfterClose * @param {Function} callback * @api private */ -Connection.prototype._close = function(force, closeAndClean, callback) { +Connection.prototype._close = function(force, destroyConnAfterClose, callback) { const _this = this; const closeCalled = this._closeCalled; this._closeCalled = true; - this._closeCalledWithClean = closeAndClean; + this._destroyCalled = destroyConnAfterClose; if (this.client != null) { this.client._closeCalled = true; - this.client._closeCalledWithClean = closeAndClean; + this.client._destroyCalled = destroyConnAfterClose; } const conn = this; switch (this.readyState) { case STATES.disconnected: - if (closeAndClean && this.base.connections.indexOf(conn) != -1) { + if (destroyConnAfterClose && this.base.connections.indexOf(conn) != -1) { this.base.connections.splice(this.base.connections.indexOf(conn), 1); } if (closeCalled) { @@ -992,7 +999,7 @@ Connection.prototype._close = function(force, closeAndClean, callback) { if (err) { return callback(err); } - if (closeAndClean && _this.base.connections.indexOf(conn) != -1) { + if (destroyConnAfterClose && _this.base.connections.indexOf(conn) != -1) { _this.base.connections.splice(_this.base.connections.indexOf(conn), 1); } _this.onClose(force); @@ -1002,13 +1009,13 @@ Connection.prototype._close = function(force, closeAndClean, callback) { break; case STATES.connecting: this.once('open', function() { - _this.close(force, closeAndClean, callback); + _this.close(force, destroyConnAfterClose, callback); }); break; case STATES.disconnecting: this.once('close', function() { - if (closeAndClean && _this.base.connections.indexOf(conn) != -1) { + if (destroyConnAfterClose && _this.base.connections.indexOf(conn) != -1) { _this.base.connections.splice(_this.base.connections.indexOf(conn), 1); } callback(); diff --git a/test/connection.test.js b/test/connection.test.js index 915e79da0f1..74db2730d84 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -361,7 +361,7 @@ describe('connections:', function() { }); }); - it('close connection and remove it permanantly', (done) => { + it('destroy connection and remove it permanantly', (done) => { const opts = {}; const conn = mongoose.createConnection(start.uri, opts); const MongoClient = mongodb.MongoClient; @@ -374,7 +374,7 @@ describe('connections:', function() { const totalConn = mongoose.connections.length; console.log('Total open connections before close are --> ', totalConn); - conn.close(false, true, () => { + conn.destroy(() => { console.log('Total open connections after close are --> ', mongoose.connections.length); assert.equal(mongoose.connections.length, totalConn - 1); stub.restore(); @@ -382,7 +382,7 @@ describe('connections:', function() { }); }); - it('verify that attempt to re-open closedAndCleaned connection throws error, via promise', (done) => { + it('verify that attempt to re-open destroyed connection throws error, via promise', (done) => { const opts = {}; const conn = mongoose.createConnection(start.uri, opts); const MongoClient = mongodb.MongoClient; @@ -392,7 +392,7 @@ describe('connections:', function() { conn.useDb('test-db'); - conn.close(false, true, async() => { + conn.destroy(async() => { try { await conn.openUri(start.uri); } catch (error) { @@ -403,7 +403,7 @@ describe('connections:', function() { }); }); - it('verify that attempt to re-open closedAndCleaned connection throws error, via callback', (done) => { + it('verify that attempt to re-open destroyed connection throws error, via callback', (done) => { const opts = {}; const conn = mongoose.createConnection(start.uri, opts); const MongoClient = mongodb.MongoClient; @@ -413,7 +413,7 @@ describe('connections:', function() { conn.useDb('test-db'); - conn.close(false, true, () => { + conn.destroy(() => { conn.openUri(start.uri, function(error, result) { assert.equal(result, undefined); assert.equal(error, 'Connection has been closed and cleaned already, and cannot be used for re-opening the connection. Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'); @@ -1132,13 +1132,13 @@ describe('connections:', function() { describe('Connection#syncIndexes() (gh-10893) (gh-11039)', () => { let connection; - this.beforeEach(async() => { - const mongooseInstance = new mongoose.Mongoose(); - connection = mongooseInstance.createConnection(start.uri); - }); - this.afterEach(async() => { - await connection.dropDatabase(); - }); + // this.beforeEach(async() => { + // const mongooseInstance = new mongoose.Mongoose(); + // connection = mongooseInstance.createConnection(start.uri); + // }); + // this.afterEach(async() => { + // await connection.dropDatabase(); + // }); it('Allows a syncIndexes option with connection mongoose.connection.syncIndexes (gh-10893)', async function() { const coll = 'tests2'; diff --git a/types/connection.d.ts b/types/connection.d.ts index 73e7f0dbfbe..674ecbc26d0 100644 --- a/types/connection.d.ts +++ b/types/connection.d.ts @@ -44,10 +44,13 @@ declare module 'mongoose' { /** Closes the connection */ close(force: boolean, callback: CallbackWithoutResult): void; - close(force: boolean, closeAndClean: boolean, callback: CallbackWithoutResult): void; close(callback: CallbackWithoutResult): void; close(force?: boolean): Promise; - close(force?: boolean, closeAndClean?: boolean): Promise; + + /** Closes and destroys the connection. Connection once destroyed cannot be reopened */ + destroy(force: boolean, callback: CallbackWithoutResult): void; + destroy(callback: CallbackWithoutResult): void; + destroy(force?: boolean): Promise; /** Retrieves a collection, creating it if not cached. */ collection(name: string, options?: mongodb.CreateCollectionOptions): Collection; From c925d3b9d39a20e0ac3620966c755e547d30d7db Mon Sep 17 00:00:00 2001 From: Omkar Hirve <35989385+OmkarHirve@users.noreply.github.com> Date: Tue, 3 May 2022 14:13:37 +0530 Subject: [PATCH 07/42] Update test/connection.test.js Co-authored-by: Uzlopak --- 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 74db2730d84..300a1d4a79c 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -416,7 +416,7 @@ describe('connections:', function() { conn.destroy(() => { conn.openUri(start.uri, function(error, result) { assert.equal(result, undefined); - assert.equal(error, 'Connection has been closed and cleaned already, and cannot be used for re-opening the connection. Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'); + assert.equal(error, 'Connection has been closed and destroyed, and cannot be used for re-opening the connection. Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'); stub.restore(); done(); }); From bc4571419e0fb3f4d920a3762d758dd723b441a1 Mon Sep 17 00:00:00 2001 From: Omkar Hirve <35989385+OmkarHirve@users.noreply.github.com> Date: Tue, 3 May 2022 14:13:46 +0530 Subject: [PATCH 08/42] Update test/connection.test.js Co-authored-by: Uzlopak --- 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 300a1d4a79c..65945b2d145 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -396,7 +396,7 @@ describe('connections:', function() { try { await conn.openUri(start.uri); } catch (error) { - assert.equal(error.message, 'Connection has been closed and cleaned already, and cannot be used for re-opening the connection. Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'); + assert.equal(error.message, 'Connection has been closed and destroyed, and cannot be used for re-opening the connection. Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'); stub.restore(); done(); } From bbe3bb723e2beb980596d5dcbb8726bcd8f839a4 Mon Sep 17 00:00:00 2001 From: Omkar Hirve <35989385+OmkarHirve@users.noreply.github.com> Date: Tue, 3 May 2022 14:13:53 +0530 Subject: [PATCH 09/42] Update lib/connection.js Co-authored-by: Uzlopak --- lib/connection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.js b/lib/connection.js index 06d8502459e..5507ca9062d 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -1015,7 +1015,7 @@ Connection.prototype._close = function(force, destroyConnAfterClose, callback) { case STATES.disconnecting: this.once('close', function() { - if (destroyConnAfterClose && _this.base.connections.indexOf(conn) != -1) { + if (destroy && _this.base.connections.indexOf(conn) !== -1) { _this.base.connections.splice(_this.base.connections.indexOf(conn), 1); } callback(); From c665c4ec0ae5ba42ef2c4f976502fc657c10c59e Mon Sep 17 00:00:00 2001 From: Omkar Hirve <35989385+OmkarHirve@users.noreply.github.com> Date: Tue, 3 May 2022 14:13:59 +0530 Subject: [PATCH 10/42] Update lib/connection.js Co-authored-by: Uzlopak --- lib/connection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.js b/lib/connection.js index 5507ca9062d..702238e90f0 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -1009,7 +1009,7 @@ Connection.prototype._close = function(force, destroyConnAfterClose, callback) { break; case STATES.connecting: this.once('open', function() { - _this.close(force, destroyConnAfterClose, callback); + _this.close(force, destroy, callback); }); break; From aa5f9347cd3ad523ce317fd58074ebd2b76de954 Mon Sep 17 00:00:00 2001 From: Omkar Hirve <35989385+OmkarHirve@users.noreply.github.com> Date: Tue, 3 May 2022 14:14:06 +0530 Subject: [PATCH 11/42] Update lib/connection.js Co-authored-by: Uzlopak --- lib/connection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.js b/lib/connection.js index 702238e90f0..a6021befb41 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -999,7 +999,7 @@ Connection.prototype._close = function(force, destroyConnAfterClose, callback) { if (err) { return callback(err); } - if (destroyConnAfterClose && _this.base.connections.indexOf(conn) != -1) { + if (destroy && _this.base.connections.indexOf(conn) !== -1) { _this.base.connections.splice(_this.base.connections.indexOf(conn), 1); } _this.onClose(force); From 227c15f3a3060506ef95130d9a11baa2f88f24c6 Mon Sep 17 00:00:00 2001 From: Omkar Hirve <35989385+OmkarHirve@users.noreply.github.com> Date: Tue, 3 May 2022 14:14:29 +0530 Subject: [PATCH 12/42] Update lib/connection.js Co-authored-by: Uzlopak --- lib/connection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.js b/lib/connection.js index a6021befb41..39b14e9f247 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -698,7 +698,7 @@ Connection.prototype.openUri = function(uri, options, callback) { } if (this._destroyCalled && this.readyState === STATES.disconnected) { - const error = 'Connection has been closed and cleaned already, and cannot be used for re-opening the connection. ' + + const error = 'Connection has been closed and destroyed, and cannot be used for re-opening the connection. ' + 'Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'; if (typeof callback === 'function') { callback(error); From 16df6f6b85ab21db9bd2decb14308420fdaed472 Mon Sep 17 00:00:00 2001 From: Uzlopak Date: Tue, 3 May 2022 11:01:52 +0200 Subject: [PATCH 13/42] Apply suggestions from code review --- lib/connection.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 39b14e9f247..97d4e52a2d6 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -964,20 +964,20 @@ Connection.prototype.close = function(force, callback) { * @param {Function} callback * @api private */ -Connection.prototype._close = function(force, destroyConnAfterClose, callback) { +Connection.prototype._close = function(force, destroy, callback) { const _this = this; const closeCalled = this._closeCalled; this._closeCalled = true; - this._destroyCalled = destroyConnAfterClose; + this._destroyCalled = destroy; if (this.client != null) { this.client._closeCalled = true; - this.client._destroyCalled = destroyConnAfterClose; + this.client._destroyCalled = destroy; } const conn = this; switch (this.readyState) { case STATES.disconnected: - if (destroyConnAfterClose && this.base.connections.indexOf(conn) != -1) { + if (destroyConnAfterClose && this.base.connections.indexOf(conn) !== -1) { this.base.connections.splice(this.base.connections.indexOf(conn), 1); } if (closeCalled) { From cf76140d28848b41596dbc062a2f23782bb52a5e Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 11 May 2022 21:48:27 +0200 Subject: [PATCH 14/42] chore: update mongodb driver --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 66b77945807..798bfc3d79a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "bson": "^4.6.2", "kareem": "2.3.5", - "mongodb": "4.5.0", + "mongodb": "4.6.0", "mpath": "0.9.0", "mquery": "4.0.2", "ms": "2.1.3", From 7b89ea9812d1cd28bd5dc2d95dbee50a44088c1b Mon Sep 17 00:00:00 2001 From: Omkar Hirve Date: Fri, 13 May 2022 17:07:47 +0530 Subject: [PATCH 15/42] Applied suggested changes and fixed unit test failures --- lib/connection.js | 10 +++++----- test/connection.test.js | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 97d4e52a2d6..3e1c652e180 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -697,7 +697,7 @@ Connection.prototype.openUri = function(uri, options, callback) { typeof callback + '"'); } - if (this._destroyCalled && this.readyState === STATES.disconnected) { + if (this._destroyCalled) { const error = 'Connection has been closed and destroyed, and cannot be used for re-opening the connection. ' + 'Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'; if (typeof callback === 'function') { @@ -960,7 +960,7 @@ Connection.prototype.close = function(force, callback) { * Handles closing the connection * * @param {Boolean} force - * @param {Boolean} destroyConnAfterClose + * @param {Boolean} destroy * @param {Function} callback * @api private */ @@ -977,7 +977,7 @@ Connection.prototype._close = function(force, destroy, callback) { const conn = this; switch (this.readyState) { case STATES.disconnected: - if (destroyConnAfterClose && this.base.connections.indexOf(conn) !== -1) { + if (destroy && this.base.connections.indexOf(conn) !== -1) { this.base.connections.splice(this.base.connections.indexOf(conn), 1); } if (closeCalled) { @@ -1009,7 +1009,7 @@ Connection.prototype._close = function(force, destroy, callback) { break; case STATES.connecting: this.once('open', function() { - _this.close(force, destroy, callback); + destroy ? _this.destroy(force, callback) : _this.close(force, callback); }); break; @@ -1046,7 +1046,7 @@ Connection.prototype.onClose = function(force) { this.emit('close', force); for (const db of this.otherDbs) { - db.close({ force: force, skipCloseClient: true }); + this._destroyCalled ? db.destroy({ force: force, skipCloseClient: true }) : db.close({ force: force, skipCloseClient: true }); } }; diff --git a/test/connection.test.js b/test/connection.test.js index 65945b2d145..95662a52639 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1132,13 +1132,13 @@ describe('connections:', function() { describe('Connection#syncIndexes() (gh-10893) (gh-11039)', () => { let connection; - // this.beforeEach(async() => { - // const mongooseInstance = new mongoose.Mongoose(); - // connection = mongooseInstance.createConnection(start.uri); - // }); - // this.afterEach(async() => { - // await connection.dropDatabase(); - // }); + this.beforeEach(async() => { + const mongooseInstance = new mongoose.Mongoose(); + connection = mongooseInstance.createConnection(start.uri); + }); + this.afterEach(async() => { + await connection.dropDatabase(); + }); it('Allows a syncIndexes option with connection mongoose.connection.syncIndexes (gh-10893)', async function() { const coll = 'tests2'; From 36329d4e187f37902326b176b672d37cfa7d693e Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 13 May 2022 14:32:02 -0400 Subject: [PATCH 16/42] on is no longer a reserved word --- lib/schema.js | 1 - test/schema.test.js | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index 78b2e386753..993b5683791 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -736,7 +736,6 @@ reserved['prototype'] = // EventEmitter reserved.emit = reserved.listeners = -reserved.on = reserved.removeListener = // document properties and functions diff --git a/test/schema.test.js b/test/schema.test.js index c47cdd4b11c..c0441e23afc 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -1438,7 +1438,7 @@ describe('schema', function() { describe('reserved keys are log a warning (gh-9010)', () => { this.afterEach(() => sinon.restore()); const reservedProperties = [ - 'emit', 'listeners', 'on', 'removeListener', /* 'collection', */ // TODO: add `collection` + 'emit', 'listeners', 'removeListener', /* 'collection', */ // TODO: add `collection` 'errors', 'get', 'init', 'isModified', 'isNew', 'populated', 'remove', 'save', 'toObject', 'validate' ]; @@ -2763,4 +2763,17 @@ describe('schema', function() { }); assert(batch.message); }); + it('can use on as a schema property (gh-11580)', async () => { + const testSchema = new mongoose.Schema({ + on: String + }); + const Test = db.model('Test', testSchema) + await Test.create({ + on: 'Test' + }); + const result = await Test.findOne() + assert.ok(result); + assert.ok(result.on); + + }) }); From b3242b7e6515ae71238f479b6a49fb678f31b121 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 13 May 2022 14:36:30 -0400 Subject: [PATCH 17/42] fixed model compilation failure --- 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 c0441e23afc..06de06e15a3 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -2767,7 +2767,7 @@ describe('schema', function() { const testSchema = new mongoose.Schema({ on: String }); - const Test = db.model('Test', testSchema) + const Test = db.model('gh11580', testSchema) await Test.create({ on: 'Test' }); From 5b7348c337ef799c11d855c334ef36fad810e2b5 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 13 May 2022 14:37:37 -0400 Subject: [PATCH 18/42] lint fix --- test/schema.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/schema.test.js b/test/schema.test.js index 06de06e15a3..a861aa83016 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -2763,17 +2763,17 @@ describe('schema', function() { }); assert(batch.message); }); - it('can use on as a schema property (gh-11580)', async () => { + it('can use on as a schema property (gh-11580)', async() => { const testSchema = new mongoose.Schema({ on: String }); - const Test = db.model('gh11580', testSchema) + const Test = db.model('gh11580', testSchema); await Test.create({ on: 'Test' }); - const result = await Test.findOne() + const result = await Test.findOne(); assert.ok(result); assert.ok(result.on); - }) + }); }); From 0044913813610767da07d9f9874731eed137d5a4 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 13 May 2022 16:50:25 -0400 Subject: [PATCH 19/42] added test case --- test/query.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/query.test.js b/test/query.test.js index ab65be08332..0dd0f4e7775 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -3962,4 +3962,16 @@ describe('Query', function() { assert.equal(result.length, 1); assert.equal(result[0].name, '@foo.com'); }); + it('allows a transform option for lean on a query gh-10423', async function() { + const testSchema = new mongoose.Schema({ + name: String + }); + const Test = db.model('gh10423', testSchema); + await Test.create({name: 'foo'}); + const result = await Test.findOne().lean({ transform: (doc, ret) => { + delete ret._id; + return ret; + }}); + assert.equal(result._id, undefined); + }); }); From 2c9b41718cf649642bd8f81e4446c9e01bc490be Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Mon, 16 May 2022 17:28:12 -0400 Subject: [PATCH 20/42] `completeOneLean()` done --- lib/query.js | 48 ++++++++++++++++++++++++++++++++++++++++++++-- test/query.test.js | 30 ++++++++++++++++++++++++----- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/lib/query.js b/lib/query.js index d444c1e0725..cef5182cd68 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2217,7 +2217,8 @@ Query.prototype._find = wrapThunk(function(callback) { // Separate options to pass down to `completeMany()` in case we need to // set a session on the document const completeManyOptions = Object.assign({}, { - session: this && this.options && this.options.session || null + session: this && this.options && this.options.session || null, + transform: mongooseOptions.lean.transform || null }); const cb = (err, docs) => { @@ -2241,7 +2242,9 @@ Query.prototype._find = wrapThunk(function(callback) { }); } return mongooseOptions.lean ? - callback(null, docs) : + // call _completeManyLean here? + _completeManyLean(_this.model, docs, fields, userProvidedFields, completeManyOptions, callback) : + // callback(null, docs) : completeMany(_this.model, docs, fields, userProvidedFields, completeManyOptions, callback); } @@ -2415,6 +2418,9 @@ Query.prototype._completeOne = function(doc, res, callback) { const mongooseOptions = this._mongooseOptions; // `rawResult` const options = this.options; + if (!options.lean && mongooseOptions.lean) { + options.lean = mongooseOptions.lean; + } if (options.explain) { return callback(null, doc); @@ -4007,12 +4013,50 @@ Query.prototype._findAndModify = function(type, callback) { */ function _completeOneLean(doc, res, opts, callback) { + // need to recurse on subdocs like arrays and what not. + if (opts.lean && opts.lean.transform) { + for (const key of Object.keys(doc)) { + if (Array.isArray(doc[key])) { + // call completeOneLean or completeManyLean? + // if staying in completeOneLean + doc[key].forEach((item) => { + console.log(item); + for (const k in item) { + console.log(k); + if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { + _completeOneLean(item[k], res, opts); + } + opts.lean.transform({}, item); + } + }); + } else if (typeof doc[key] === 'object' && doc[key] != null && Object.keys(doc[key]).length) { + _completeOneLean(doc[key], res, opts); + } + } + opts.lean.transform({}, doc); + if (callback) { + return callback(null, doc); + } else { + return; + } + } if (opts.rawResult) { return callback(null, res); } return callback(null, doc); } +/*! + * ignore + */ + +function _completeManyLean(model, docs, fields, userProvidedFields, opts, callback) { + docs.forEach(doc => { + opts.transform({}, doc); + }); + return callback(null, docs); +} + /*! * Override mquery.prototype._mergeUpdate to handle mongoose objects in * updates. diff --git a/test/query.test.js b/test/query.test.js index 0dd0f4e7775..765de6e8032 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -3963,15 +3963,35 @@ describe('Query', function() { assert.equal(result[0].name, '@foo.com'); }); it('allows a transform option for lean on a query gh-10423', async function() { + const arraySchema = new mongoose.Schema({ + sub: String + }); + const subDoc = new mongoose.Schema({ + nickName: String + }); const testSchema = new mongoose.Schema({ - name: String + name: String, + foo: [arraySchema], + otherName: subDoc }); const Test = db.model('gh10423', testSchema); - await Test.create({name: 'foo'}); - const result = await Test.findOne().lean({ transform: (doc, ret) => { + await Test.create({ name: 'foo', foo: [{ sub: 'Test' }, { sub: 'Testerson' }], otherName: { nickName: 'Bar' } }); + /* + const result = await Test.find().lean({ transform: (doc, ret) => { + delete ret._id; + return ret; + } }); + console.log('The result', result); + assert.equal(result[0]._id, undefined); + */ + const single = await Test.findOne().lean({ transform: (doc, ret) => { delete ret._id; return ret; - }}); - assert.equal(result._id, undefined); + } }); + console.log('The single document', single); + assert.equal(single._id, undefined); + assert.equal(single.otherName._id, undefined); + assert.equal(single.foo[0]._id, undefined); + assert.equal(single.foo[0]._id, undefined); }); }); From b8dbcd8381851b4ca69c3dc56bea139f8ab293e8 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Mon, 16 May 2022 18:02:00 -0400 Subject: [PATCH 21/42] removed logs --- lib/query.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/query.js b/lib/query.js index cef5182cd68..be7d296d66e 100644 --- a/lib/query.js +++ b/lib/query.js @@ -4020,9 +4020,7 @@ function _completeOneLean(doc, res, opts, callback) { // call completeOneLean or completeManyLean? // if staying in completeOneLean doc[key].forEach((item) => { - console.log(item); for (const k in item) { - console.log(k); if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { _completeOneLean(item[k], res, opts); } From f6be5e0e23a6fe1b007f2d448f2909dca8a89101 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 16 May 2022 21:48:42 -0400 Subject: [PATCH 22/42] test: remove unnecessary log statements re: #11674 --- test/connection.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/connection.test.js b/test/connection.test.js index 95662a52639..c586f589982 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -372,10 +372,8 @@ describe('connections:', function() { conn.useDb('test-db'); const totalConn = mongoose.connections.length; - console.log('Total open connections before close are --> ', totalConn); conn.destroy(() => { - console.log('Total open connections after close are --> ', mongoose.connections.length); assert.equal(mongoose.connections.length, totalConn - 1); stub.restore(); done(); From 6cbbaef5ef18c280b5eeb7112f9bdea68beef3fc Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Tue, 17 May 2022 12:04:12 -0400 Subject: [PATCH 23/42] `_completeManyLean()` done, I think --- lib/query.js | 30 ++++++++++++++++++++++++------ test/query.test.js | 7 +++---- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/lib/query.js b/lib/query.js index be7d296d66e..5018118787f 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2218,7 +2218,7 @@ Query.prototype._find = wrapThunk(function(callback) { // set a session on the document const completeManyOptions = Object.assign({}, { session: this && this.options && this.options.session || null, - transform: mongooseOptions.lean.transform || null + lean: mongooseOptions.lean || null }); const cb = (err, docs) => { @@ -4017,8 +4017,6 @@ function _completeOneLean(doc, res, opts, callback) { if (opts.lean && opts.lean.transform) { for (const key of Object.keys(doc)) { if (Array.isArray(doc[key])) { - // call completeOneLean or completeManyLean? - // if staying in completeOneLean doc[key].forEach((item) => { for (const k in item) { if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { @@ -4049,9 +4047,29 @@ function _completeOneLean(doc, res, opts, callback) { */ function _completeManyLean(model, docs, fields, userProvidedFields, opts, callback) { - docs.forEach(doc => { - opts.transform({}, doc); - }); + if (opts.lean && opts.lean.transform) { + docs.forEach(doc => { + for (const key of Object.keys(doc)) { + if (Array.isArray(doc[key])) { + doc[key].forEach((item) => { + for (const k in item) { + if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { + _completeOneLean(item[k], null, opts); + } + opts.lean.transform({}, item); + } + }); + } else if (typeof doc[key] === 'object' && doc[key] != null && Object.keys(doc[key]).length) { + _completeOneLean(doc[key], null, opts); + } + } + opts.lean.transform({}, doc); + }); + } + + if (!callback) { + return; + } return callback(null, docs); } diff --git a/test/query.test.js b/test/query.test.js index 765de6e8032..6d0a989f819 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -3976,19 +3976,18 @@ describe('Query', function() { }); const Test = db.model('gh10423', testSchema); await Test.create({ name: 'foo', foo: [{ sub: 'Test' }, { sub: 'Testerson' }], otherName: { nickName: 'Bar' } }); - /* const result = await Test.find().lean({ transform: (doc, ret) => { delete ret._id; return ret; } }); - console.log('The result', result); assert.equal(result[0]._id, undefined); - */ + assert.equal(result[0].otherName._id, undefined); + assert.equal(result[0].foo[0]._id, undefined); + assert.equal(result[0].foo[1]._id, undefined); const single = await Test.findOne().lean({ transform: (doc, ret) => { delete ret._id; return ret; } }); - console.log('The single document', single); assert.equal(single._id, undefined); assert.equal(single.otherName._id, undefined); assert.equal(single.foo[0]._id, undefined); From 73cc7420dc7dbb3bf9031716f3a45afc44007a64 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 20 May 2022 16:40:22 -0400 Subject: [PATCH 24/42] reworking solution --- lib/query.js | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/lib/query.js b/lib/query.js index 5018118787f..5ad0d3a7134 100644 --- a/lib/query.js +++ b/lib/query.js @@ -4048,23 +4048,43 @@ function _completeOneLean(doc, res, opts, callback) { function _completeManyLean(model, docs, fields, userProvidedFields, opts, callback) { if (opts.lean && opts.lean.transform) { - docs.forEach(doc => { - for (const key of Object.keys(doc)) { - if (Array.isArray(doc[key])) { - doc[key].forEach((item) => { + if (Array.isArray(docs)) { + docs.forEach(doc => { + for (const key of Object.keys(doc)) { + if (Array.isArray(doc[key])) { + doc[key].forEach((item) => { + for (const k in item) { + if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { + _completeManyLean(model, item[k], fields, userProvidedFields, opts); + } + console.log('item', item); + opts.lean.transform({}, item); + } + }); + } else if (typeof doc[key] === 'object' && doc[key] != null && Object.keys(doc[key]).length) { + _completeManyLean(model, doc[key], fields, userProvidedFields, opts); + } + } + console.log('doc', doc) + opts.lean.transform({}, doc); + }); + } else { + for (const key of Object.keys(docs)) { + if (Array.isArray(docs[key])) { + docs[key].forEach((item) => { for (const k in item) { if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { - _completeOneLean(item[k], null, opts); + _completeManyLean(model, item[k], fields, userProvidedFields, opts); } + console.log('else item', item) opts.lean.transform({}, item); } }); - } else if (typeof doc[key] === 'object' && doc[key] != null && Object.keys(doc[key]).length) { - _completeOneLean(doc[key], null, opts); + } else if (typeof docs[key] === 'object' && docs[key] != null && Object.keys(docs[key]).length) { + _completeManyLean(model, docs[key], fields, userProvidedFields, opts); } } - opts.lean.transform({}, doc); - }); + } } if (!callback) { From da0d878b82e841d31c0f0187b56af1f150e805cf Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Mon, 23 May 2022 11:51:44 -0400 Subject: [PATCH 25/42] independent `completeManyLean()` --- lib/query.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/query.js b/lib/query.js index 5ad0d3a7134..fed500f228d 100644 --- a/lib/query.js +++ b/lib/query.js @@ -4013,7 +4013,6 @@ Query.prototype._findAndModify = function(type, callback) { */ function _completeOneLean(doc, res, opts, callback) { - // need to recurse on subdocs like arrays and what not. if (opts.lean && opts.lean.transform) { for (const key of Object.keys(doc)) { if (Array.isArray(doc[key])) { @@ -4057,16 +4056,14 @@ function _completeManyLean(model, docs, fields, userProvidedFields, opts, callba if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { _completeManyLean(model, item[k], fields, userProvidedFields, opts); } - console.log('item', item); opts.lean.transform({}, item); } }); } else if (typeof doc[key] === 'object' && doc[key] != null && Object.keys(doc[key]).length) { _completeManyLean(model, doc[key], fields, userProvidedFields, opts); } - } - console.log('doc', doc) opts.lean.transform({}, doc); + } }); } else { for (const key of Object.keys(docs)) { @@ -4076,13 +4073,13 @@ function _completeManyLean(model, docs, fields, userProvidedFields, opts, callba if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { _completeManyLean(model, item[k], fields, userProvidedFields, opts); } - console.log('else item', item) opts.lean.transform({}, item); } }); } else if (typeof docs[key] === 'object' && docs[key] != null && Object.keys(docs[key]).length) { _completeManyLean(model, docs[key], fields, userProvidedFields, opts); } + opts.lean.transform({}, docs); } } } From 9babf9ccefb3be2107024d9ccdc237370619e1db Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Mon, 23 May 2022 12:01:23 -0400 Subject: [PATCH 26/42] lint fix --- lib/query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/query.js b/lib/query.js index fed500f228d..6e355f0b6ee 100644 --- a/lib/query.js +++ b/lib/query.js @@ -4062,7 +4062,7 @@ function _completeManyLean(model, docs, fields, userProvidedFields, opts, callba } else if (typeof doc[key] === 'object' && doc[key] != null && Object.keys(doc[key]).length) { _completeManyLean(model, doc[key], fields, userProvidedFields, opts); } - opts.lean.transform({}, doc); + opts.lean.transform({}, doc); } }); } else { From fb71984c349a635a398886cdf6e6287f9dfd7df1 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 27 May 2022 21:37:59 -0400 Subject: [PATCH 27/42] feat(document): add `$assertPopulated()` for working with manually populated paths in TypeScript Fix #11758 --- lib/document.js | 39 +++++++++++++++++++++++++++++++++---- test/types/populate.test.ts | 27 +++++++++++++++++++++++++ types/document.d.ts | 3 +++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/lib/document.js b/lib/document.js index ad216686859..57b2d77e639 100644 --- a/lib/document.js +++ b/lib/document.js @@ -4251,10 +4251,10 @@ Document.prototype.$getPopulatedDocs = function $getPopulatedDocs() { * * #### Example: * - * Model.findOne().populate('author').exec(function (err, doc) { - * console.log(doc.author.name) // Dr.Seuss - * console.log(doc.populated('author')) // '5144cf8050f071d979c118a7' - * }) + * const doc = await Model.findOne().populate('author'); + * + * console.log(doc.author.name); // Dr.Seuss + * console.log(doc.populated('author')); // '5144cf8050f071d979c118a7' * * If the path was not populated, returns `undefined`. * @@ -4308,6 +4308,37 @@ Document.prototype.populated = function(path, val, options) { Document.prototype.$populated = Document.prototype.populated; +/** + * Throws an error if a given path is not populated + * + * #### Example: + * + * const doc = await Model.findOne().populate('author'); + * + * doc.$assertPopulated('author'); // ok + * doc.$assertPopulated('other path'); + * + * + * @param {String} path + * @return {Document} this + * @memberOf Document + * @instance + * @api public + */ + +Document.prototype.$assertPopulated = function $assertPopulated(paths) { + if (Array.isArray(paths)) { + paths.forEach(path => this.$assertPopulated(path)); + return this; + } + + if (!this.$populated(paths)) { + throw new MongooseError(`Expected path "${paths}" to be populated`); + } + + return this; +}; + /** * Takes a populated field and returns it to its unpopulated state. * diff --git a/test/types/populate.test.ts b/test/types/populate.test.ts index e122cf7f47c..46e2021946e 100644 --- a/test/types/populate.test.ts +++ b/test/types/populate.test.ts @@ -228,4 +228,31 @@ async function _11532() { if (!leanResult) return; expectType(leanResult.child.name); expectError(leanResult?.__v); +} + +function gh11758() { + interface NestedChild { + name: string + _id: Types.ObjectId + } + const nestedChildSchema: Schema = new Schema({ name: String }); + + interface Parent { + nestedChild: Types.ObjectId + name?: string + } + + const ParentModel = model('Parent', new Schema({ + nestedChild: { type: Schema.Types.ObjectId, ref: 'NestedChild' }, + name: String + })); + + const NestedChildModel = model('NestedChild', nestedChildSchema); + + const parent = new ParentModel({ + nestedChild: new NestedChildModel({ name: 'test' }), + name: 'Parent' + }).$assertPopulated<{ nestedChild: NestedChild }>('nestedChild'); + + expectType(parent.nestedChild.name); } \ No newline at end of file diff --git a/types/document.d.ts b/types/document.d.ts index bb4d2541d83..cc699658115 100644 --- a/types/document.d.ts +++ b/types/document.d.ts @@ -19,6 +19,9 @@ declare module 'mongoose' { /** This documents __v. */ __v?: any; + /** Assert that a given path or paths is populated. Throws an error if not populated. */ + $assertPopulated(paths: string | string[]): Omit & Paths; + /* Get all subdocs (by bfs) */ $getAllSubdocs(): Document[]; From 70ba07af99a61cc30c86a778aa73af275ca54517 Mon Sep 17 00:00:00 2001 From: Hafez Date: Sat, 28 May 2022 17:49:49 +0200 Subject: [PATCH 28/42] Apply suggestions from code review add comments on $assertPopulated docs --- lib/document.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/document.js b/lib/document.js index 57b2d77e639..5b030321bd4 100644 --- a/lib/document.js +++ b/lib/document.js @@ -4315,8 +4315,8 @@ Document.prototype.$populated = Document.prototype.populated; * * const doc = await Model.findOne().populate('author'); * - * doc.$assertPopulated('author'); // ok - * doc.$assertPopulated('other path'); + * doc.$assertPopulated('author'); // does not throw + * doc.$assertPopulated('other path'); // throws an error * * * @param {String} path From 6b084729db0c6977a3922080f5ea8ae4eddc366f Mon Sep 17 00:00:00 2001 From: Hafez Date: Sat, 28 May 2022 17:51:56 +0200 Subject: [PATCH 29/42] fix JSDoc type for $assertPopulated --- lib/document.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index 5b030321bd4..c42673447e5 100644 --- a/lib/document.js +++ b/lib/document.js @@ -4319,7 +4319,7 @@ Document.prototype.$populated = Document.prototype.populated; * doc.$assertPopulated('other path'); // throws an error * * - * @param {String} path + * @param {String | Array} path * @return {Document} this * @memberOf Document * @instance From 640adca6323cd27413235825112b1833c6de3457 Mon Sep 17 00:00:00 2001 From: Hafez Date: Thu, 2 Jun 2022 17:23:09 +0200 Subject: [PATCH 30/42] test(base): add test for immutableCreatedAt --- test/index.test.js | 24 ++++++++++++++++++++++++ test/types/base.test.ts | 4 ++++ 2 files changed, 28 insertions(+) diff --git a/test/index.test.js b/test/index.test.js index e3015496ae2..09b5ab3ac18 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1019,5 +1019,29 @@ describe('mongoose module:', function() { assert.equal(optionsSentToMongo.allowDiskUse, false); }); }); + describe('global `immutableCreatedAt` (gh-10139)', () => { + it('is `true` by default', () => { + // Arrange + const m = new mongoose.Mongoose(); + + // Act + const userSchema = new m.Schema({ name: String }, { timestamps: true }); + + // Assert + assert.equal(userSchema.path('createdAt').options.immutable, true); + }); + + it('can be overridden to `false`', () => { + // Arrange + const m = new mongoose.Mongoose(); + m.set('immutableCreatedAt', false); + + // Act + const userSchema = new m.Schema({ name: String }, { timestamps: true }); + + // Assert + assert.equal(userSchema.path('createdAt').options.immutable, false); + }); + }); }); }); diff --git a/test/types/base.test.ts b/test/types/base.test.ts index ff6d4e56d5c..ed0aa247bae 100644 --- a/test/types/base.test.ts +++ b/test/types/base.test.ts @@ -41,4 +41,8 @@ function connectionStates() { function gh11478() { mongoose.set('allowDiskUse', false); mongoose.set('allowDiskUse', true); +} + +function gh10139() { + mongoose.set('immutableCreatedAt', false); } \ No newline at end of file From 31926c9c841b44be20a93d39eedf651d18e2cb11 Mon Sep 17 00:00:00 2001 From: Hafez Date: Thu, 2 Jun 2022 17:23:24 +0200 Subject: [PATCH 31/42] implement immutableCreatedAt --- lib/helpers/timestamps/setupTimestamps.js | 5 +++- lib/index.js | 29 ++++++++++++----------- lib/validoptions.js | 1 + 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/helpers/timestamps/setupTimestamps.js b/lib/helpers/timestamps/setupTimestamps.js index b4e5e0a07c8..55b08cbba9f 100644 --- a/lib/helpers/timestamps/setupTimestamps.js +++ b/lib/helpers/timestamps/setupTimestamps.js @@ -31,8 +31,11 @@ module.exports = function setupTimestamps(schema, timestamps) { } if (createdAt && !schema.paths[createdAt]) { - schemaAdditions[createdAt] = { [schema.options.typeKey || 'type']: Date, immutable: true }; + const baseImmutableCreatedAt = schema.base.get('immutableCreatedAt'); + const immutable = baseImmutableCreatedAt != null ? baseImmutableCreatedAt : true; + schemaAdditions[createdAt] = { [schema.options.typeKey || 'type']: Date, immutable }; } + schema.add(schemaAdditions); schema.pre('save', function(next) { diff --git a/lib/index.js b/lib/index.js index a9483c1fc03..67ccccb0352 100644 --- a/lib/index.js +++ b/lib/index.js @@ -153,23 +153,24 @@ Mongoose.prototype.driver = driver; * mongoose.set('debug', function(collectionName, methodName, ...methodArgs) {}); // use custom function to log collection methods + arguments * * Currently supported options are: - * - 'debug': If `true`, prints the operations mongoose sends to MongoDB to the console. If a writable stream is passed, it will log to that stream, without colorization. If a callback function is passed, it will receive the collection name, the method name, then all arugments passed to the method. For example, if you wanted to replicate the default logging, you could output from the callback `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`. - * - 'returnOriginal': If `false`, changes the default `returnOriginal` option to `findOneAndUpdate()`, `findByIdAndUpdate`, and `findOneAndReplace()` to false. This is equivalent to setting the `new` option to `true` for `findOneAndX()` calls by default. Read our [`findOneAndUpdate()` tutorial](/docs/tutorials/findoneandupdate.html) for more information. + * - 'applyPluginsToChildSchemas': `true` by default. Set to false to skip applying global plugins to child schemas + * - 'applyPluginsToDiscriminators': `false` by default. Set to true to apply global plugins to discriminator schemas. This typically isn't necessary because plugins are applied to the base schema and discriminators copy all middleware, methods, statics, and properties from the base schema. + * - 'autoCreate': Set to `true` to make Mongoose call [`Model.createCollection()`](/docs/api/model.html#model_Model.createCollection) automatically when you create a model with `mongoose.model()` or `conn.model()`. This is useful for testing transactions, change streams, and other features that require the collection to exist. + * - 'autoIndex': `true` by default. Set to false to disable automatic index creation for all models associated with this Mongoose instance. * - 'bufferCommands': enable/disable mongoose's buffering mechanism for all connections and models - * - 'cloneSchemas': false by default. Set to `true` to `clone()` all schemas before compiling into a model. - * - 'applyPluginsToDiscriminators': false by default. Set to true to apply global plugins to discriminator schemas. This typically isn't necessary because plugins are applied to the base schema and discriminators copy all middleware, methods, statics, and properties from the base schema. - * - 'applyPluginsToChildSchemas': true by default. Set to false to skip applying global plugins to child schemas - * - 'objectIdGetter': true by default. Mongoose adds a getter to MongoDB ObjectId's called `_id` that returns `this` for convenience with populate. Set this to false to remove the getter. - * - 'runValidators': false by default. Set to true to enable [update validators](/docs/validation.html#update-validators) for all validators by default. - * - 'toObject': `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toObject()`](/docs/api.html#document_Document-toObject) - * - 'toJSON': `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toJSON()`](/docs/api.html#document_Document-toJSON), for determining how Mongoose documents get serialized by `JSON.stringify()` - * - 'strict': true by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas. - * - 'strictQuery': same value as 'strict' by default (`true`), may be `false`, `true`, or `'throw'`. Sets the default [strictQuery](/docs/guide.html#strictQuery) mode for schemas. - * - 'selectPopulatedPaths': true by default. Set to false to opt out of Mongoose adding all fields that you `populate()` to your `select()`. The schema-level option `selectPopulatedPaths` overwrites this one. + * - 'cloneSchemas': `false` by default. Set to `true` to `clone()` all schemas before compiling into a model. + * - 'debug': If `true`, prints the operations mongoose sends to MongoDB to the console. If a writable stream is passed, it will log to that stream, without colorization. If a callback function is passed, it will receive the collection name, the method name, then all arugments passed to the method. For example, if you wanted to replicate the default logging, you could output from the callback `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`. + * - 'immutableCreatedAt': `true` by default. If `false`, it will change the `createdAt` field to be [`immutable: false`](https://mongoosejs.com/docs/api/schematype.html#schematype_SchemaType-immutable) which means you can update the `createdAt` * - 'maxTimeMS': If set, attaches [maxTimeMS](https://docs.mongodb.com/manual/reference/operator/meta/maxTimeMS/) to every query - * - 'autoIndex': true by default. Set to false to disable automatic index creation for all models associated with this Mongoose instance. - * - 'autoCreate': Set to `true` to make Mongoose call [`Model.createCollection()`](/docs/api/model.html#model_Model.createCollection) automatically when you create a model with `mongoose.model()` or `conn.model()`. This is useful for testing transactions, change streams, and other features that require the collection to exist. + * - 'objectIdGetter': `true` by default. Mongoose adds a getter to MongoDB ObjectId's called `_id` that returns `this` for convenience with populate. Set this to false to remove the getter. * - 'overwriteModels': Set to `true` to default to overwriting models with the same name when calling `mongoose.model()`, as opposed to throwing an `OverwriteModelError`. + * - 'returnOriginal': If `false`, changes the default `returnOriginal` option to `findOneAndUpdate()`, `findByIdAndUpdate`, and `findOneAndReplace()` to false. This is equivalent to setting the `new` option to `true` for `findOneAndX()` calls by default. Read our [`findOneAndUpdate()` tutorial](/docs/tutorials/findoneandupdate.html) for more information. + * - 'runValidators': `false` by default. Set to true to enable [update validators](/docs/validation.html#update-validators) for all validators by default. + * - 'selectPopulatedPaths': `true` by default. Set to false to opt out of Mongoose adding all fields that you `populate()` to your `select()`. The schema-level option `selectPopulatedPaths` overwrites this one. + * - 'strict': `true` by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas. + * - 'strictQuery': same value as 'strict' by default (`true`), may be `false`, `true`, or `'throw'`. Sets the default [strictQuery](/docs/guide.html#strictQuery) mode for schemas. + * - 'toJSON': `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toJSON()`](/docs/api.html#document_Document-toJSON), for determining how Mongoose documents get serialized by `JSON.stringify()` + * - 'toObject': `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toObject()`](/docs/api.html#document_Document-toObject) * * @param {String} key * @param {String|Function|Boolean} value diff --git a/lib/validoptions.js b/lib/validoptions.js index e83369f86c5..546c6d0abe2 100644 --- a/lib/validoptions.js +++ b/lib/validoptions.js @@ -15,6 +15,7 @@ const VALID_OPTIONS = Object.freeze([ 'bufferTimeoutMS', 'cloneSchemas', 'debug', + 'immutableCreatedAt', 'maxTimeMS', 'objectIdGetter', 'overwriteModels', From 29969f06ef1b678da0bbacb62f4b5eb35f8dce94 Mon Sep 17 00:00:00 2001 From: Hafez Date: Thu, 2 Jun 2022 17:23:38 +0200 Subject: [PATCH 32/42] add type for immutableCreatedAt --- types/mongooseoptions.d.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/types/mongooseoptions.d.ts b/types/mongooseoptions.d.ts index 8faa1845267..433388f0c54 100644 --- a/types/mongooseoptions.d.ts +++ b/types/mongooseoptions.d.ts @@ -79,6 +79,14 @@ declare module 'mongoose' { | stream.Writable | ((collectionName: string, methodName: string, ...methodArgs: any[]) => void); + /** + * If `false`, it will change the `createdAt` field to be [`immutable: false`](https://mongoosejs.com/docs/api/schematype.html#schematype_SchemaType-immutable) + * which means you can update the `createdAt`. + * + * @default true + */ + immutableCreatedAt?: boolean + /** If set, attaches [maxTimeMS](https://docs.mongodb.com/manual/reference/operator/meta/maxTimeMS/) to every query */ maxTimeMS?: number; From 1dc2e4ff888a3500abce7d1ac54f3c0a09229a2e Mon Sep 17 00:00:00 2001 From: Hafez Date: Thu, 2 Jun 2022 17:27:20 +0200 Subject: [PATCH 33/42] change option from immutableCreatedAt => timestamps.createdAt.immutable --- lib/helpers/timestamps/setupTimestamps.js | 2 +- lib/index.js | 2 +- lib/validoptions.js | 2 +- test/index.test.js | 4 ++-- types/mongooseoptions.d.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/helpers/timestamps/setupTimestamps.js b/lib/helpers/timestamps/setupTimestamps.js index 55b08cbba9f..7cd4514f02d 100644 --- a/lib/helpers/timestamps/setupTimestamps.js +++ b/lib/helpers/timestamps/setupTimestamps.js @@ -31,7 +31,7 @@ module.exports = function setupTimestamps(schema, timestamps) { } if (createdAt && !schema.paths[createdAt]) { - const baseImmutableCreatedAt = schema.base.get('immutableCreatedAt'); + const baseImmutableCreatedAt = schema.base.get('timestamps.createdAt.immutable'); const immutable = baseImmutableCreatedAt != null ? baseImmutableCreatedAt : true; schemaAdditions[createdAt] = { [schema.options.typeKey || 'type']: Date, immutable }; } diff --git a/lib/index.js b/lib/index.js index 67ccccb0352..5afee4a899e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -160,7 +160,7 @@ Mongoose.prototype.driver = driver; * - 'bufferCommands': enable/disable mongoose's buffering mechanism for all connections and models * - 'cloneSchemas': `false` by default. Set to `true` to `clone()` all schemas before compiling into a model. * - 'debug': If `true`, prints the operations mongoose sends to MongoDB to the console. If a writable stream is passed, it will log to that stream, without colorization. If a callback function is passed, it will receive the collection name, the method name, then all arugments passed to the method. For example, if you wanted to replicate the default logging, you could output from the callback `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`. - * - 'immutableCreatedAt': `true` by default. If `false`, it will change the `createdAt` field to be [`immutable: false`](https://mongoosejs.com/docs/api/schematype.html#schematype_SchemaType-immutable) which means you can update the `createdAt` + * - 'timestamps.createdAt.immutable': `true` by default. If `false`, it will change the `createdAt` field to be [`immutable: false`](https://mongoosejs.com/docs/api/schematype.html#schematype_SchemaType-immutable) which means you can update the `createdAt` * - 'maxTimeMS': If set, attaches [maxTimeMS](https://docs.mongodb.com/manual/reference/operator/meta/maxTimeMS/) to every query * - 'objectIdGetter': `true` by default. Mongoose adds a getter to MongoDB ObjectId's called `_id` that returns `this` for convenience with populate. Set this to false to remove the getter. * - 'overwriteModels': Set to `true` to default to overwriting models with the same name when calling `mongoose.model()`, as opposed to throwing an `OverwriteModelError`. diff --git a/lib/validoptions.js b/lib/validoptions.js index 546c6d0abe2..0f7b697c415 100644 --- a/lib/validoptions.js +++ b/lib/validoptions.js @@ -15,7 +15,7 @@ const VALID_OPTIONS = Object.freeze([ 'bufferTimeoutMS', 'cloneSchemas', 'debug', - 'immutableCreatedAt', + 'timestamps.createdAt.immutable', 'maxTimeMS', 'objectIdGetter', 'overwriteModels', diff --git a/test/index.test.js b/test/index.test.js index 09b5ab3ac18..c0b1cc483e6 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1019,7 +1019,7 @@ describe('mongoose module:', function() { assert.equal(optionsSentToMongo.allowDiskUse, false); }); }); - describe('global `immutableCreatedAt` (gh-10139)', () => { + describe('global `timestamps.createdAt.immutable` (gh-10139)', () => { it('is `true` by default', () => { // Arrange const m = new mongoose.Mongoose(); @@ -1034,7 +1034,7 @@ describe('mongoose module:', function() { it('can be overridden to `false`', () => { // Arrange const m = new mongoose.Mongoose(); - m.set('immutableCreatedAt', false); + m.set('timestamps.createdAt.immutable', false); // Act const userSchema = new m.Schema({ name: String }, { timestamps: true }); diff --git a/types/mongooseoptions.d.ts b/types/mongooseoptions.d.ts index 433388f0c54..a84e5a1545a 100644 --- a/types/mongooseoptions.d.ts +++ b/types/mongooseoptions.d.ts @@ -85,7 +85,7 @@ declare module 'mongoose' { * * @default true */ - immutableCreatedAt?: boolean + 'timestamps.createdAt.immutable'?: boolean /** If set, attaches [maxTimeMS](https://docs.mongodb.com/manual/reference/operator/meta/maxTimeMS/) to every query */ maxTimeMS?: number; From 403766a9f380997dc5ea93a4a2e486994f612dc6 Mon Sep 17 00:00:00 2001 From: Hafez Date: Thu, 2 Jun 2022 17:28:45 +0200 Subject: [PATCH 34/42] fix failing TS test --- test/types/base.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/base.test.ts b/test/types/base.test.ts index ed0aa247bae..fbf345ada30 100644 --- a/test/types/base.test.ts +++ b/test/types/base.test.ts @@ -44,5 +44,5 @@ function gh11478() { } function gh10139() { - mongoose.set('immutableCreatedAt', false); + mongoose.set('timestamps.createdAt.immutable', false); } \ No newline at end of file From d082047d98bbd2e6138d30ccc7ca5c5600524652 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Thu, 2 Jun 2022 18:01:56 -0400 Subject: [PATCH 35/42] refining the logic to requests made --- lib/query.js | 42 ++++++++++++++++++++++++++++++++++-------- test/query.test.js | 16 +++++++++------- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/lib/query.js b/lib/query.js index 6e355f0b6ee..f26930d424e 100644 --- a/lib/query.js +++ b/lib/query.js @@ -27,6 +27,7 @@ const immediate = require('./helpers/immediate'); const isExclusive = require('./helpers/projection/isExclusive'); const isInclusive = require('./helpers/projection/isInclusive'); const isSubpath = require('./helpers/projection/isSubpath'); +const mpath = require('mpath'); const mquery = require('mquery'); const parseProjection = require('./helpers/projection/parseProjection'); const removeUnusedArrayFilters = require('./helpers/update/removeUnusedArrayFilters'); @@ -2243,9 +2244,9 @@ Query.prototype._find = wrapThunk(function(callback) { } return mongooseOptions.lean ? // call _completeManyLean here? - _completeManyLean(_this.model, docs, fields, userProvidedFields, completeManyOptions, callback) : + _completeManyLean(_this.model.schema, docs, null, completeManyOptions, callback) : // callback(null, docs) : - completeMany(_this.model, docs, fields, userProvidedFields, completeManyOptions, callback); + completeMany(_this.model.schema, docs, null, completeManyOptions, callback); } const pop = helpers.preparePopulationOptionsMQ(_this, mongooseOptions); @@ -4021,14 +4022,14 @@ function _completeOneLean(doc, res, opts, callback) { if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { _completeOneLean(item[k], res, opts); } - opts.lean.transform({}, item); + item = opts.lean.transform(item); } }); } else if (typeof doc[key] === 'object' && doc[key] != null && Object.keys(doc[key]).length) { _completeOneLean(doc[key], res, opts); } } - opts.lean.transform({}, doc); + doc = opts.lean.transform(doc); if (callback) { return callback(null, doc); } else { @@ -4045,7 +4046,9 @@ function _completeOneLean(doc, res, opts, callback) { * ignore */ +/* function _completeManyLean(model, docs, fields, userProvidedFields, opts, callback) { + console.log('this', model.schema.childSchemas); if (opts.lean && opts.lean.transform) { if (Array.isArray(docs)) { docs.forEach(doc => { @@ -4056,13 +4059,13 @@ function _completeManyLean(model, docs, fields, userProvidedFields, opts, callba if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { _completeManyLean(model, item[k], fields, userProvidedFields, opts); } - opts.lean.transform({}, item); + item = opts.lean.transform(item); } }); } else if (typeof doc[key] === 'object' && doc[key] != null && Object.keys(doc[key]).length) { _completeManyLean(model, doc[key], fields, userProvidedFields, opts); } - opts.lean.transform({}, doc); + doc = opts.lean.transform(doc); } }); } else { @@ -4073,13 +4076,13 @@ function _completeManyLean(model, docs, fields, userProvidedFields, opts, callba if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { _completeManyLean(model, item[k], fields, userProvidedFields, opts); } - opts.lean.transform({}, item); + item = opts.lean.transform(item); } }); } else if (typeof docs[key] === 'object' && docs[key] != null && Object.keys(docs[key]).length) { _completeManyLean(model, docs[key], fields, userProvidedFields, opts); } - opts.lean.transform({}, docs); + docs = opts.lean.transform(docs); } } } @@ -4089,7 +4092,30 @@ function _completeManyLean(model, docs, fields, userProvidedFields, opts, callba } return callback(null, docs); } +*/ +function _completeManyLean(schema, docs, path, opts, callback) { + if (opts.lean && opts.lean.transform) { + for (let i = 0; i < schema.childSchemas.length; i++) { + const childPath = path ? path + '.' + schema.childSchemas[i].model.path : schema.childSchemas[i].model.path; + const _schema = schema.childSchemas[i].schema; + console.log('childPath', childPath, 'schema', _schema); + let doc = mpath.get(childPath, docs); + console.log('before if', doc) + if (doc == null) { + continue; + } + doc = opts.lean.transform(doc[0]); + console.log('doc', doc); + _completeManyLean(_schema, doc, childPath, opts); + } + } + + if (!callback) { + return; + } + return callback(null, docs); +} /*! * Override mquery.prototype._mergeUpdate to handle mongoose objects in * updates. diff --git a/test/query.test.js b/test/query.test.js index 6d0a989f819..83d0431cba0 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -3976,17 +3976,19 @@ describe('Query', function() { }); const Test = db.model('gh10423', testSchema); await Test.create({ name: 'foo', foo: [{ sub: 'Test' }, { sub: 'Testerson' }], otherName: { nickName: 'Bar' } }); - const result = await Test.find().lean({ transform: (doc, ret) => { - delete ret._id; - return ret; + const result = await Test.find().lean({ transform: (doc) => { + delete doc._id; + return doc; } }); - assert.equal(result[0]._id, undefined); + // only OtherName and foo should have _id stripped + console.log(result[0]) + assert(result[0]._id); assert.equal(result[0].otherName._id, undefined); assert.equal(result[0].foo[0]._id, undefined); assert.equal(result[0].foo[1]._id, undefined); - const single = await Test.findOne().lean({ transform: (doc, ret) => { - delete ret._id; - return ret; + const single = await Test.findOne().lean({ transform: (doc) => { + delete doc._id; + return doc; } }); assert.equal(single._id, undefined); assert.equal(single.otherName._id, undefined); From 4c0ecbe945341cec840ca9d208de7139360032ff Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 3 Jun 2022 10:57:29 -0400 Subject: [PATCH 36/42] reworked `completeManyLean()` to be in line with requested change --- lib/query.js | 56 ++++------------------------------------------ test/query.test.js | 2 +- 2 files changed, 5 insertions(+), 53 deletions(-) diff --git a/lib/query.js b/lib/query.js index f26930d424e..65c2f696a67 100644 --- a/lib/query.js +++ b/lib/query.js @@ -4046,67 +4046,19 @@ function _completeOneLean(doc, res, opts, callback) { * ignore */ -/* -function _completeManyLean(model, docs, fields, userProvidedFields, opts, callback) { - console.log('this', model.schema.childSchemas); - if (opts.lean && opts.lean.transform) { - if (Array.isArray(docs)) { - docs.forEach(doc => { - for (const key of Object.keys(doc)) { - if (Array.isArray(doc[key])) { - doc[key].forEach((item) => { - for (const k in item) { - if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { - _completeManyLean(model, item[k], fields, userProvidedFields, opts); - } - item = opts.lean.transform(item); - } - }); - } else if (typeof doc[key] === 'object' && doc[key] != null && Object.keys(doc[key]).length) { - _completeManyLean(model, doc[key], fields, userProvidedFields, opts); - } - doc = opts.lean.transform(doc); - } - }); - } else { - for (const key of Object.keys(docs)) { - if (Array.isArray(docs[key])) { - docs[key].forEach((item) => { - for (const k in item) { - if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { - _completeManyLean(model, item[k], fields, userProvidedFields, opts); - } - item = opts.lean.transform(item); - } - }); - } else if (typeof docs[key] === 'object' && docs[key] != null && Object.keys(docs[key]).length) { - _completeManyLean(model, docs[key], fields, userProvidedFields, opts); - } - docs = opts.lean.transform(docs); - } - } - } - - if (!callback) { - return; - } - return callback(null, docs); -} -*/ - function _completeManyLean(schema, docs, path, opts, callback) { if (opts.lean && opts.lean.transform) { for (let i = 0; i < schema.childSchemas.length; i++) { const childPath = path ? path + '.' + schema.childSchemas[i].model.path : schema.childSchemas[i].model.path; const _schema = schema.childSchemas[i].schema; - console.log('childPath', childPath, 'schema', _schema); let doc = mpath.get(childPath, docs); - console.log('before if', doc) if (doc == null) { continue; } - doc = opts.lean.transform(doc[0]); - console.log('doc', doc); + doc = doc.flat(); + for (let i = 0; i < doc.length; i++) { + opts.lean.transform(doc[i]); + } _completeManyLean(_schema, doc, childPath, opts); } } diff --git a/test/query.test.js b/test/query.test.js index 83d0431cba0..c92111e2040 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -3981,7 +3981,7 @@ describe('Query', function() { return doc; } }); // only OtherName and foo should have _id stripped - console.log(result[0]) + console.log(result[0]); assert(result[0]._id); assert.equal(result[0].otherName._id, undefined); assert.equal(result[0].foo[0]._id, undefined); From d11c511cb4ad88792b91b6b2cdc0fa7dee075b73 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 3 Jun 2022 11:19:02 -0400 Subject: [PATCH 37/42] reworked `_completeOneLean()` to be in line with requested changes --- lib/query.js | 33 +++++++++++++++++---------------- test/query.test.js | 4 +--- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/query.js b/lib/query.js index 65c2f696a67..c3e426298a5 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2435,7 +2435,7 @@ Query.prototype._completeOne = function(doc, res, callback) { } } return mongooseOptions.lean ? - _completeOneLean(doc, res, options, callback) : + _completeOneLean(model.schema, doc, null, res, options, callback) : completeOne(model, doc, res, options, projection, userProvidedFields, null, callback); } @@ -2446,7 +2446,7 @@ Query.prototype._completeOne = function(doc, res, callback) { if (err != null) { return callback(err); } - _completeOneLean(doc, res, options, callback); + _completeOneLean(model.schema, doc, null, res, options, callback); }); } @@ -4013,23 +4013,24 @@ Query.prototype._findAndModify = function(type, callback) { * ignore */ -function _completeOneLean(doc, res, opts, callback) { +function _completeOneLean(schema, doc, path, res, opts, callback) { if (opts.lean && opts.lean.transform) { - for (const key of Object.keys(doc)) { - if (Array.isArray(doc[key])) { - doc[key].forEach((item) => { - for (const k in item) { - if (typeof item[k] === 'object' && item[k] != null && Object.keys(item[k]).length) { - _completeOneLean(item[k], res, opts); - } - item = opts.lean.transform(item); - } - }); - } else if (typeof doc[key] === 'object' && doc[key] != null && Object.keys(doc[key]).length) { - _completeOneLean(doc[key], res, opts); + for (let i = 0; i < schema.childSchemas.length; i++) { + const childPath = path ? path + '.' + schema.childSchemas[i].model.path : schema.childSchemas[i].model.path; + const _schema = schema.childSchemas[i].schema; + const obj = mpath.get(childPath, doc); + if (obj == null) { + continue; + } + if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; i++) { + opts.lean.transform(obj[i]); + } + } else { + opts.lean.transform(obj); } + _completeOneLean(_schema, obj, childPath, res, opts); } - doc = opts.lean.transform(doc); if (callback) { return callback(null, doc); } else { diff --git a/test/query.test.js b/test/query.test.js index c92111e2040..d09a4135bdc 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -3980,8 +3980,6 @@ describe('Query', function() { delete doc._id; return doc; } }); - // only OtherName and foo should have _id stripped - console.log(result[0]); assert(result[0]._id); assert.equal(result[0].otherName._id, undefined); assert.equal(result[0].foo[0]._id, undefined); @@ -3990,7 +3988,7 @@ describe('Query', function() { delete doc._id; return doc; } }); - assert.equal(single._id, undefined); + assert(single._id); assert.equal(single.otherName._id, undefined); assert.equal(single.foo[0]._id, undefined); assert.equal(single.foo[0]._id, undefined); From 1c7696e84e07267a91e94d9f1aa181a980c7fed0 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 3 Jun 2022 12:28:52 -0400 Subject: [PATCH 38/42] should work now --- lib/query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/query.js b/lib/query.js index c3e426298a5..ace76a7f9b6 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2246,7 +2246,7 @@ Query.prototype._find = wrapThunk(function(callback) { // call _completeManyLean here? _completeManyLean(_this.model.schema, docs, null, completeManyOptions, callback) : // callback(null, docs) : - completeMany(_this.model.schema, docs, null, completeManyOptions, callback); + completeMany(_this.model, docs, fields, userProvidedFields, completeManyOptions, callback); } const pop = helpers.preparePopulationOptionsMQ(_this, mongooseOptions); From e98073792337a7feb94c66c7ef60859c0b10f4c9 Mon Sep 17 00:00:00 2001 From: Mike Noseworthy Date: Fri, 3 Jun 2022 15:31:11 -0230 Subject: [PATCH 39/42] Fix ObjectId `instanceof` checks PR #11841 changed a whole bunch of checks for `instanceof ObjectId` to use the `isBsonType()` function instead. Unfortunately some were missed in the `./lib/schema/objectid.js` file. Convert the remaining `instanceof` checks in `objectid.js` to use the `isBsonType()` check instead so that all `ObjectId`s created from any `bson` modules should validate/cast properly. This was found because my nested document with required ObjectIds started failing validation when I loaded the documents from JSON files in my tests using `bson.EJSON` after the release of 6.3.4 -> 6.3.5. --- lib/schema/objectid.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/schema/objectid.js b/lib/schema/objectid.js index 2f1f90e323e..09a11f716ac 100644 --- a/lib/schema/objectid.js +++ b/lib/schema/objectid.js @@ -9,6 +9,7 @@ const SchemaType = require('../schematype'); const castObjectId = require('../cast/objectid'); const getConstructorName = require('../helpers/getConstructorName'); const oid = require('../types/objectid'); +const isBsonType = require('../helpers/isBsonType'); const utils = require('../utils'); const CastError = SchemaType.CastError; @@ -113,7 +114,7 @@ ObjectId.prototype.auto = function(turnOn) { * ignore */ -ObjectId._checkRequired = v => v instanceof oid; +ObjectId._checkRequired = v => isBsonType(v, 'ObjectID'); /*! * ignore @@ -161,7 +162,7 @@ ObjectId.cast = function cast(caster) { */ ObjectId._defaultCaster = v => { - if (!(v instanceof oid)) { + if (!(isBsonType(v, 'ObjectID'))) { throw new Error(v + ' is not an instance of ObjectId'); } return v; @@ -221,7 +222,7 @@ ObjectId.prototype.checkRequired = function checkRequired(value, doc) { */ ObjectId.prototype.cast = function(value, doc, init) { - if (!(value instanceof oid) && SchemaType._isRef(this, value, doc, init)) { + if (!(isBsonType(value, 'ObjectID')) && SchemaType._isRef(this, value, doc, init)) { // wait! we may need to cast this to a document if ((getConstructorName(value) || '').toLowerCase() === 'objectid') { return new oid(value.toHexString()); From 44e4c88fa8de02899e6e5ee358f7a87a160de12c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 4 Jun 2022 15:06:56 -0400 Subject: [PATCH 40/42] refactor(query): avoid depending on mquery duck-typing for instantiating query collection --- lib/query.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/query.js b/lib/query.js index d444c1e0725..4a4477a343a 100644 --- a/lib/query.js +++ b/lib/query.js @@ -124,7 +124,10 @@ function Query(conditions, options, model, collection) { } // inherit mquery - mquery.call(this, this.mongooseCollection, options); + mquery.call(this, null, options); + if (collection) { + this.collection(collection); + } if (conditions) { this.find(conditions); From f5605ed36bc5c9840abf8b3f21fa81e8be534788 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 4 Jun 2022 21:53:30 -0400 Subject: [PATCH 41/42] fix(connection+index): correctly handle setting driver --- lib/connection.js | 3 ++- lib/driver.js | 10 ++++++++++ lib/index.js | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/connection.js b/lib/connection.js index f03f42a67c4..54d98cec28e 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -7,13 +7,13 @@ const ChangeStream = require('./cursor/ChangeStream'); const EventEmitter = require('events').EventEmitter; const Schema = require('./schema'); -const Collection = require('./driver').get().Collection; const STATES = require('./connectionstate'); const MongooseError = require('./error/index'); const SyncIndexesError = require('./error/syncIndexes'); const PromiseProvider = require('./promise_provider'); const ServerSelectionError = require('./error/serverSelection'); const applyPlugins = require('./helpers/schema/applyPlugins'); +const driver = require('./driver'); const promiseOrCallback = require('./helpers/promiseOrCallback'); const get = require('./helpers/get'); const immediate = require('./helpers/immediate'); @@ -1026,6 +1026,7 @@ Connection.prototype.collection = function(name, options) { }; options = Object.assign({}, defaultOptions, options ? utils.clone(options) : {}); options.$wasForceClosed = this.$wasForceClosed; + const Collection = driver.get().Collection; if (!(name in this.collections)) { this.collections[name] = new Collection(name, this, options); } diff --git a/lib/driver.js b/lib/driver.js index cf7ca3d7b25..38269132686 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -6,10 +6,20 @@ let driver = null; +const _mongooseInstances = []; +module.exports._mongooseInstances = _mongooseInstances; + module.exports.get = function() { return driver; }; module.exports.set = function(v) { driver = v; + + for (const mongoose of _mongooseInstances) { + const Connection = driver.getConnection(); + mongoose.Connection = Connection; + mongoose.connections = [new Connection(mongoose)]; + mongoose.Collection = driver.Collection; + } }; diff --git a/lib/index.js b/lib/index.js index 0fb1d7329ed..a6af69db233 100644 --- a/lib/index.js +++ b/lib/index.js @@ -70,6 +70,7 @@ function Mongoose(options) { }, options); const conn = this.createConnection(); // default connection conn.models = this.models; + driver._mongooseInstances.push(this); if (this.options.pluralization) { this._pluralize = legacyPluralize; @@ -275,6 +276,7 @@ Mongoose.prototype.get = Mongoose.prototype.set; Mongoose.prototype.createConnection = function(uri, options, callback) { const _mongoose = this instanceof Mongoose ? this : mongoose; + const Connection = driver.get().getConnection(); const conn = new Connection(_mongoose); if (typeof options === 'function') { callback = options; From 238431e76189b65dc0ddb9dce712916f48e1ebb3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 5 Jun 2022 12:57:33 -0400 Subject: [PATCH 42/42] fix: avoid adding every Mongoose instance to global driver --- lib/driver.js | 10 ---------- lib/index.js | 1 - 2 files changed, 11 deletions(-) diff --git a/lib/driver.js b/lib/driver.js index 38269132686..cf7ca3d7b25 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -6,20 +6,10 @@ let driver = null; -const _mongooseInstances = []; -module.exports._mongooseInstances = _mongooseInstances; - module.exports.get = function() { return driver; }; module.exports.set = function(v) { driver = v; - - for (const mongoose of _mongooseInstances) { - const Connection = driver.getConnection(); - mongoose.Connection = Connection; - mongoose.connections = [new Connection(mongoose)]; - mongoose.Collection = driver.Collection; - } }; diff --git a/lib/index.js b/lib/index.js index a6af69db233..3b102fbcd45 100644 --- a/lib/index.js +++ b/lib/index.js @@ -70,7 +70,6 @@ function Mongoose(options) { }, options); const conn = this.createConnection(); // default connection conn.models = this.models; - driver._mongooseInstances.push(this); if (this.options.pluralization) { this._pluralize = legacyPluralize;