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

416 lines
14 KiB

"use strict";
var parse = require('./url_parser')
, Server = require('./server')
, Mongos = require('./mongos')
, ReplSet = require('./replset')
, Define = require('./metadata')
, ReadPreference = require('./read_preference')
, Db = require('./db')
, shallowClone = require('./utils').shallowClone;
/**
* @fileOverview The **MongoClient** class is a class that allows for making Connections to MongoDB.
*
* @example
* var MongoClient = require('mongodb').MongoClient,
* test = require('assert');
* // Connection url
* var url = 'mongodb://localhost:27017/test';
* // Connect using MongoClient
* MongoClient.connect(url, function(err, db) {
* // Get an additional db
* db.close();
* });
*/
/**
* Creates a new MongoClient instance
* @class
* @return {MongoClient} a MongoClient instance.
*/
function MongoClient() {
/**
* The callback format for results
* @callback MongoClient~connectCallback
* @param {MongoError} error An error instance representing the error during the execution.
* @param {Db} db The connected database.
*/
/**
* Connect to MongoDB using a url as documented at
*
* docs.mongodb.org/manual/reference/connection-string/
*
* Note that for replicasets the replicaSet query parameter is required in the 2.0 driver
*
* @method
* @param {string} url The connection URI string
* @param {object} [options=null] Optional settings.
* @param {boolean} [options.uri_decode_auth=false] Uri decode the user name and password for authentication
* @param {object} [options.db=null] A hash of options to set on the db object, see **Db constructor**
* @param {object} [options.server=null] A hash of options to set on the server objects, see **Server** constructor**
* @param {object} [options.replSet=null] A hash of options to set on the replSet object, see **ReplSet** constructor**
* @param {object} [options.mongos=null] A hash of options to set on the mongos object, see **Mongos** constructor**
* @param {object} [options.promiseLibrary=null] A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible
* @param {MongoClient~connectCallback} [callback] The command result callback
* @return {Promise} returns Promise if no callback passed
*/
this.connect = MongoClient.connect;
}
var define = MongoClient.define = new Define('MongoClient', MongoClient, false);
/**
* Connect to MongoDB using a url as documented at
*
* docs.mongodb.org/manual/reference/connection-string/
*
* Note that for replicasets the replicaSet query parameter is required in the 2.0 driver
*
* @method
* @static
* @param {string} url The connection URI string
* @param {object} [options=null] Optional settings.
* @param {boolean} [options.uri_decode_auth=false] Uri decode the user name and password for authentication
* @param {object} [options.db=null] A hash of options to set on the db object, see **Db constructor**
* @param {object} [options.server=null] A hash of options to set on the server objects, see **Server** constructor**
* @param {object} [options.replSet=null] A hash of options to set on the replSet object, see **ReplSet** constructor**
* @param {object} [options.mongos=null] A hash of options to set on the mongos object, see **Mongos** constructor**
* @param {object} [options.promiseLibrary=null] A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible
* @param {MongoClient~connectCallback} [callback] The command result callback
* @return {Promise} returns Promise if no callback passed
*/
MongoClient.connect = function(url, options, callback) {
var args = Array.prototype.slice.call(arguments, 1);
callback = typeof args[args.length - 1] == 'function' ? args.pop() : null;
options = args.length ? args.shift() : null;
options = options || {};
// Get the promiseLibrary
var promiseLibrary = options.promiseLibrary;
// No promise library selected fall back
if(!promiseLibrary) {
promiseLibrary = typeof global.Promise == 'function' ?
global.Promise : require('es6-promise').Promise;
}
// Return a promise
if(typeof callback != 'function') {
return new promiseLibrary(function(resolve, reject) {
connect(url, options, function(err, db) {
if(err) return reject(err);
resolve(db);
});
});
}
// Fallback to callback based connect
connect(url, options, callback);
}
define.staticMethod('connect', {callback: true, promise:true});
var mergeOptions = function(target, source, flatten) {
for(var name in source) {
if(source[name] && typeof source[name] == 'object' && flatten) {
target = mergeOptions(target, source[name], flatten);
} else {
target[name] = source[name];
}
}
return target;
}
var createUnifiedOptions = function(finalOptions, options) {
var childOptions = ['mongos', 'server', 'db'
, 'replset', 'db_options', 'server_options', 'rs_options', 'mongos_options'];
for(var name in options) {
if(childOptions.indexOf(name.toLowerCase()) != -1) {
finalOptions = mergeOptions(finalOptions, options[name], false);
} else {
if(options[name] && typeof options[name] == 'object') {
finalOptions = mergeOptions(finalOptions, options[name], true);
} else {
finalOptions[name] = options[name];
}
}
}
return finalOptions;
}
/*
* Connect using MongoClient
*/
var connect = function(url, options, callback) {
options = options || {};
options = shallowClone(options);
// If callback is null throw an exception
if(callback == null) {
throw new Error("no callback function provided");
}
// Parse the string
var object = parse(url, options);
var _finalOptions = createUnifiedOptions({}, object);
_finalOptions = createUnifiedOptions(_finalOptions, options);
// Check if we have connection and socket timeout set
if(!_finalOptions.socketTimeoutMS) _finalOptions.socketTimeoutMS = 120000;
if(!_finalOptions.connectTimeoutMS) _finalOptions.connectTimeoutMS = 120000;
// We need to ensure that the list of servers are only either direct members or mongos
// they cannot be a mix of monogs and mongod's
var totalNumberOfServers = object.servers.length;
var totalNumberOfMongosServers = 0;
var totalNumberOfMongodServers = 0;
var serverConfig = null;
var errorServers = {};
// Failure modes
if(object.servers.length == 0) {
throw new Error("connection string must contain at least one seed host");
}
// If we have more than a server, it could be replicaset or mongos list
// need to verify that it's one or the other and fail if it's a mix
// Connect to all servers and run ismaster
for(var i = 0; i < object.servers.length; i++) {
// Set up socket options
var _server_options = {
poolSize:1
, socketOptions: {
connectTimeoutMS: _finalOptions.connectTimeoutMS || (1000 * 120)
, socketTimeoutMS: _finalOptions.socketTimeoutMS || (1000 * 120)
}
, auto_reconnect:false};
// Ensure we have ssl setup for the servers
if(_finalOptions.ssl) {
_server_options.ssl = _finalOptions.ssl;
_server_options.sslValidate = _finalOptions.sslValidate;
_server_options.checkServerIdentity = _finalOptions.checkServerIdentity;
_server_options.sslCA = _finalOptions.sslCA;
_server_options.sslCert = _finalOptions.sslCert;
_server_options.sslKey = _finalOptions.sslKey;
_server_options.sslPass = _finalOptions.sslPass;
}
// Error
var error = null;
// Set up the Server object
var _server = object.servers[i].domain_socket
? new Server(object.servers[i].domain_socket, _server_options)
: new Server(object.servers[i].host, object.servers[i].port, _server_options);
var connectFunction = function(__server) {
// Attempt connect
new Db(object.dbName, __server, {w:1, native_parser:false}).open(function(err, db) {
// Update number of servers
totalNumberOfServers = totalNumberOfServers - 1;
// If no error do the correct checks
if(!err) {
// Close the connection
db.close();
// Get the last ismaster document
var isMasterDoc = db.serverConfig.isMasterDoc;
// Check what type of server we have
if(isMasterDoc.setName) {
totalNumberOfMongodServers++;
}
if(isMasterDoc.msg && isMasterDoc.msg == "isdbgrid") totalNumberOfMongosServers++;
} else {
error = err;
errorServers[__server.host + ":" + __server.port] = __server;
}
if(totalNumberOfServers == 0) {
// Error out
if(totalNumberOfMongodServers == 0 && totalNumberOfMongosServers == 0 && error) {
return callback(error, null);
}
// If we have a mix of mongod and mongos, throw an error
if(totalNumberOfMongosServers > 0 && totalNumberOfMongodServers > 0) {
if(db) db.close();
return process.nextTick(function() {
try {
callback(new Error("cannot combine a list of replicaset seeds and mongos seeds"));
} catch (err) {
throw err
}
})
}
if(totalNumberOfMongodServers == 0
&& totalNumberOfMongosServers == 0
&& object.servers.length == 1
&& (!_finalOptions.replicaSet || !_finalOptions.rs_name)) {
var obj = object.servers[0];
serverConfig = obj.domain_socket ?
new Server(obj.domain_socket, _finalOptions)
: new Server(obj.host, obj.port, _finalOptions);
} else if(totalNumberOfMongodServers > 0
|| totalNumberOfMongosServers > 0
|| _finalOptions.replicaSet || _finalOptions.rs_name) {
var finalServers = object.servers
.filter(function(serverObj) {
return errorServers[serverObj.host + ":" + serverObj.port] == null;
})
.map(function(serverObj) {
return serverObj.domain_socket ?
new Server(serverObj.domain_socket, 27017, _finalOptions)
: new Server(serverObj.host, serverObj.port, _finalOptions);
});
// Clean out any error servers
errorServers = {};
// Set up the final configuration
if(totalNumberOfMongodServers > 0) {
try {
// If no replicaset name was provided, we wish to perform a
// direct connection
if(totalNumberOfMongodServers == 1
&& (!_finalOptions.replicaSet && !_finalOptions.rs_name)) {
serverConfig = finalServers[0];
} else if(totalNumberOfMongodServers == 1) {
_finalOptions.replicaSet = _finalOptions.replicaSet || _finalOptions.rs_name;
serverConfig = new ReplSet(finalServers, _finalOptions);
} else {
serverConfig = new ReplSet(finalServers, _finalOptions);
}
} catch(err) {
return callback(err, null);
}
} else {
serverConfig = new Mongos(finalServers, _finalOptions);
}
}
if(serverConfig == null) {
return process.nextTick(function() {
try {
callback(new Error("Could not locate any valid servers in initial seed list"));
} catch (err) {
if(db) db.close();
throw err
}
});
}
// Ensure no firing of open event before we are ready
serverConfig.emitOpen = false;
// Set up all options etc and connect to the database
_finishConnecting(serverConfig, object, _finalOptions, callback)
}
});
}
// Wrap the context of the call
connectFunction(_server);
}
}
var _finishConnecting = function(serverConfig, object, options, callback) {
// If we have a readPreference passed in by the db options
if(typeof options.readPreference == 'string') {
options.readPreference = new ReadPreference(options.readPreference);
} else if(typeof options.read_preference == 'string') {
options.readPreference = new ReadPreference(options.read_preference);
}
// Do we have readPreference tags
if(options.readPreference && options.readPreferenceTags) {
options.readPreference.tags = options.readPreferenceTags;
} else if(options.readPreference && options.read_preference_tags) {
options.readPreference.tags = options.read_preference_tags;
}
// Get the socketTimeoutMS
var socketTimeoutMS = options.socketTimeoutMS || 0;
// If we have a replset, override with replicaset socket timeout option if available
if(serverConfig instanceof ReplSet) {
socketTimeoutMS = options.socketTimeoutMS || socketTimeoutMS;
}
// Set socketTimeout to the same as the connectTimeoutMS or 30 sec
serverConfig.connectTimeoutMS = serverConfig.connectTimeoutMS || 30000;
serverConfig.socketTimeoutMS = serverConfig.connectTimeoutMS;
// Set up the db options
var db = new Db(object.dbName, serverConfig, options);
// Open the db
db.open(function(err, db) {
if(err) {
return process.nextTick(function() {
try {
callback(err, null);
} catch (err) {
if(db) db.close();
throw err
}
});
}
// Reset the socket timeout
serverConfig.socketTimeoutMS = socketTimeoutMS || 0;
// Return object
if(err == null && object.auth){
// What db to authenticate against
var authentication_db = db;
if(options.authSource) {
authentication_db = db.db(options.authSource);
}
// Authenticate
authentication_db.authenticate(options.user, options.password, options, function(err, success){
if(success){
process.nextTick(function() {
try {
callback(null, db);
} catch (err) {
if(db) db.close();
throw err
}
});
} else {
if(db) db.close();
process.nextTick(function() {
try {
callback(err ? err : new Error('Could not authenticate user ' + object.auth[0]), null);
} catch (err) {
if(db) db.close();
throw err
}
});
}
});
} else {
process.nextTick(function() {
try {
callback(err, db);
} catch (err) {
if(db) db.close();
throw err
}
})
}
});
}
module.exports = MongoClient