You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

410 lines
12 KiB
JavaScript

/*!
* Module dependencies.
*/
'use strict';
const MongooseConnection = require('../../connection');
const MongooseError = require('../../error/index');
const STATES = require('../../connectionstate');
const mongodb = require('mongodb');
const pkg = require('../../../package.json');
const processConnectionOptions = require('../../helpers/processConnectionOptions');
const setTimeout = require('../../helpers/timers').setTimeout;
const utils = require('../../utils');
/**
* A [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) connection implementation.
*
* @inherits Connection
* @api private
*/
function NativeConnection() {
MongooseConnection.apply(this, arguments);
this._listening = false;
}
/**
* Expose the possible connection states.
* @api public
*/
NativeConnection.STATES = STATES;
/*!
* Inherits from Connection.
*/
Object.setPrototypeOf(NativeConnection.prototype, MongooseConnection.prototype);
/**
* Switches to a different database using the same connection pool.
*
* Returns a new connection object, with the new db. If you set the `useCache`
* option, `useDb()` will cache connections by `name`.
*
* **Note:** Calling `close()` on a `useDb()` connection will close the base connection as well.
*
* @param {String} name The database name
* @param {Object} [options]
* @param {Boolean} [options.useCache=false] If true, cache results so calling `useDb()` multiple times with the same name only creates 1 connection object.
* @param {Boolean} [options.noListener=false] If true, the new connection object won't listen to any events on the base connection. This is better for memory usage in cases where you're calling `useDb()` for every request.
* @return {Connection} New Connection Object
* @api public
*/
NativeConnection.prototype.useDb = function(name, options) {
// Return immediately if cached
options = options || {};
if (options.useCache && this.relatedDbs[name]) {
return this.relatedDbs[name];
}
// we have to manually copy all of the attributes...
const newConn = new this.constructor();
newConn.name = name;
newConn.base = this.base;
newConn.collections = {};
newConn.models = {};
newConn.replica = this.replica;
newConn.config = Object.assign({}, this.config, newConn.config);
newConn.name = this.name;
newConn.options = this.options;
newConn._readyState = this._readyState;
newConn._closeCalled = this._closeCalled;
newConn._hasOpened = this._hasOpened;
newConn._listening = false;
newConn._parent = this;
newConn.host = this.host;
newConn.port = this.port;
newConn.user = this.user;
newConn.pass = this.pass;
// First, when we create another db object, we are not guaranteed to have a
// db object to work with. So, in the case where we have a db object and it
// is connected, we can just proceed with setting everything up. However, if
// we do not have a db or the state is not connected, then we need to wait on
// the 'open' event of the connection before doing the rest of the setup
// the 'connected' event is the first time we'll have access to the db object
const _this = this;
newConn.client = _this.client;
if (this.db && this._readyState === STATES.connected) {
wireup();
} else {
this.once('connected', wireup);
}
function wireup() {
newConn.client = _this.client;
const _opts = {};
if (options.hasOwnProperty('noListener')) {
_opts.noListener = options.noListener;
}
newConn.db = _this.client.db(name, _opts);
newConn.onOpen();
}
newConn.name = name;
// push onto the otherDbs stack, this is used when state changes
if (options.noListener !== true) {
this.otherDbs.push(newConn);
}
newConn.otherDbs.push(this);
// push onto the relatedDbs cache, this is used when state changes
if (options && options.useCache) {
this.relatedDbs[newConn.name] = newConn;
newConn.relatedDbs = this.relatedDbs;
}
return newConn;
};
/**
* Removes the database connection with the given name created with `useDb()`.
*
* Throws an error if the database connection was not found.
*
* #### Example:
*
* // Connect to `initialdb` first
* const conn = await mongoose.createConnection('mongodb://127.0.0.1:27017/initialdb').asPromise();
*
* // Creates an un-cached connection to `mydb`
* const db = conn.useDb('mydb');
*
* // Closes `db`, and removes `db` from `conn.relatedDbs` and `conn.otherDbs`
* await conn.removeDb('mydb');
*
* @method removeDb
* @memberOf Connection
* @param {String} name The database name
* @return {Connection} this
*/
NativeConnection.prototype.removeDb = function removeDb(name) {
const dbs = this.otherDbs.filter(db => db.name === name);
if (!dbs.length) {
throw new MongooseError(`No connections to database "${name}" found`);
}
for (const db of dbs) {
db._closeCalled = true;
db._destroyCalled = true;
db._readyState = STATES.disconnected;
db.$wasForceClosed = true;
}
delete this.relatedDbs[name];
this.otherDbs = this.otherDbs.filter(db => db.name !== name);
};
/**
* Closes the connection
*
* @param {Boolean} [force]
* @return {Connection} this
* @api private
*/
NativeConnection.prototype.doClose = async function doClose(force) {
if (this.client == null) {
return this;
}
let skipCloseClient = false;
if (force != null && typeof force === 'object') {
skipCloseClient = force.skipCloseClient;
force = force.force;
}
if (skipCloseClient) {
return this;
}
await this.client.close(force);
// Defer because the driver will wait at least 1ms before finishing closing
// the pool, see https://github.com/mongodb-js/mongodb-core/blob/a8f8e4ce41936babc3b9112bf42d609779f03b39/lib/connection/pool.js#L1026-L1030.
// If there's queued operations, you may still get some background work
// after the callback is called.
await new Promise(resolve => setTimeout(resolve, 1));
return this;
};
/*!
* ignore
*/
NativeConnection.prototype.createClient = async function createClient(uri, options) {
if (typeof uri !== 'string') {
throw new MongooseError('The `uri` parameter to `openUri()` must be a ' +
`string, got "${typeof uri}". Make sure the first parameter to ` +
'`mongoose.connect()` or `mongoose.createConnection()` is a string.');
}
if (this._destroyCalled) {
throw new MongooseError(
'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 (this.readyState === STATES.connecting || this.readyState === STATES.connected) {
if (this._connectionString !== uri) {
throw new MongooseError('Can\'t call `openUri()` on an active connection with ' +
'different connection strings. Make sure you aren\'t calling `mongoose.connect()` ' +
'multiple times. See: https://mongoosejs.com/docs/connections.html#multiple_connections');
}
}
options = processConnectionOptions(uri, options);
if (options) {
const autoIndex = options.config && options.config.autoIndex != null ?
options.config.autoIndex :
options.autoIndex;
if (autoIndex != null) {
this.config.autoIndex = autoIndex !== false;
delete options.config;
delete options.autoIndex;
}
if ('autoCreate' in options) {
this.config.autoCreate = !!options.autoCreate;
delete options.autoCreate;
}
if ('sanitizeFilter' in options) {
this.config.sanitizeFilter = options.sanitizeFilter;
delete options.sanitizeFilter;
}
// Backwards compat
if (options.user || options.pass) {
options.auth = options.auth || {};
options.auth.username = options.user;
options.auth.password = options.pass;
this.user = options.user;
this.pass = options.pass;
}
delete options.user;
delete options.pass;
if (options.bufferCommands != null) {
this.config.bufferCommands = options.bufferCommands;
delete options.bufferCommands;
}
} else {
options = {};
}
this._connectionOptions = options;
const dbName = options.dbName;
if (dbName != null) {
this.$dbName = dbName;
}
delete options.dbName;
if (!utils.hasUserDefinedProperty(options, 'driverInfo')) {
options.driverInfo = {
name: 'Mongoose',
version: pkg.version
};
}
this.readyState = STATES.connecting;
this._connectionString = uri;
let client;
try {
client = new mongodb.MongoClient(uri, options);
} catch (error) {
this.readyState = STATES.disconnected;
throw error;
}
this.client = client;
client.setMaxListeners(0);
await client.connect();
_setClient(this, client, options, dbName);
for (const db of this.otherDbs) {
_setClient(db, client, {}, db.name);
}
return this;
};
/*!
* ignore
*/
NativeConnection.prototype.setClient = function setClient(client) {
if (!(client instanceof mongodb.MongoClient)) {
throw new MongooseError('Must call `setClient()` with an instance of MongoClient');
}
if (this.readyState !== STATES.disconnected) {
throw new MongooseError('Cannot call `setClient()` on a connection that is already connected.');
}
if (client.topology == null) {
throw new MongooseError('Cannot call `setClient()` with a MongoClient that you have not called `connect()` on yet.');
}
this._connectionString = client.s.url;
_setClient(this, client, {}, client.s.options.dbName);
for (const model of Object.values(this.models)) {
// Errors handled internally, so safe to ignore error
model.init().catch(function $modelInitNoop() {});
}
return this;
};
/*!
* ignore
*/
function _setClient(conn, client, options, dbName) {
const db = dbName != null ? client.db(dbName) : client.db();
conn.db = db;
conn.client = client;
conn.host = client &&
client.s &&
client.s.options &&
client.s.options.hosts &&
client.s.options.hosts[0] &&
client.s.options.hosts[0].host || void 0;
conn.port = client &&
client.s &&
client.s.options &&
client.s.options.hosts &&
client.s.options.hosts[0] &&
client.s.options.hosts[0].port || void 0;
conn.name = dbName != null ? dbName : db.databaseName;
conn._closeCalled = client._closeCalled;
const _handleReconnect = () => {
// If we aren't disconnected, we assume this reconnect is due to a
// socket timeout. If there's no activity on a socket for
// `socketTimeoutMS`, the driver will attempt to reconnect and emit
// this event.
if (conn.readyState !== STATES.connected) {
conn.readyState = STATES.connected;
conn.emit('reconnect');
conn.emit('reconnected');
conn.onOpen();
}
};
const type = client &&
client.topology &&
client.topology.description &&
client.topology.description.type || '';
if (type === 'Single') {
client.on('serverDescriptionChanged', ev => {
const newDescription = ev.newDescription;
if (newDescription.type === 'Unknown') {
conn.readyState = STATES.disconnected;
} else {
_handleReconnect();
}
});
} else if (type.startsWith('ReplicaSet')) {
client.on('topologyDescriptionChanged', ev => {
// Emit disconnected if we've lost connectivity to the primary
const description = ev.newDescription;
if (conn.readyState === STATES.connected && description.type !== 'ReplicaSetWithPrimary') {
// Implicitly emits 'disconnected'
conn.readyState = STATES.disconnected;
} else if (conn.readyState === STATES.disconnected && description.type === 'ReplicaSetWithPrimary') {
_handleReconnect();
}
});
}
conn.onOpen();
for (const i in conn.collections) {
if (utils.object.hasOwnProperty(conn.collections, i)) {
conn.collections[i].onOpen();
}
}
}
/*!
* Module exports.
*/
module.exports = NativeConnection;