diff --git a/lib/mongodb.js b/lib/mongodb.js index f1b44bf67..85e67fcdf 100644 --- a/lib/mongodb.js +++ b/lib/mongodb.js @@ -437,13 +437,11 @@ MongoDB.prototype.connect = function(callback) { if (callback) callback(err); } - const urlObj = new URL(self.settings.url); - - if ((urlObj.pathname === '' || - urlObj.pathname.split('/')[1] === '') && - typeof self.settings.database === 'string') { - urlObj.pathname = self.settings.database; - self.settings.url = urlObj.toString(); + // This is special processing if database is not part of url, but is in settings + if (self.settings.url && self.settings.database) { + if (self.settings.url.indexOf('/' + self.settings.database) === -1) { + self.settings.url = processMongoDBURL(self.settings.database, self.settings.url); + } } const mongoClient = new mongodb.MongoClient(self.settings.url, validOptions); @@ -2586,3 +2584,101 @@ function buildOptions(requiredOptions, connectorOptions) { return Object.assign({}, connectorOptions, requiredOptions); } } + +/** + * This method parses a Mongo connection url string and refers the formats + * specified at: https://www.mongodb.com/docs/manual/reference/connection-string/. + * Since there are cases where database is not specified in the url, but as a settings property, + * the code has to reflect that in the url otherwise the MongoDB driver defaults to 'admin' db. + * @param {string} settingsDatabase - the database that will be added if url doesn't have a db specified + * @param {string} mongoUrl - the url to be processed for database manipulation + */ +function processMongoDBURL(settingsDatabase, mongoUrl) { + // Reference: https://www.mongodb.com/docs/manual/reference/connection-string/ + // Standard format::: mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]] + // DNS SeedList format::: mongodb+srv://server.example.com/?connectTimeoutMS=300000&authSource=aDifferentAuthDB + // Actual replicaset example::: mongodb://mongodb1.example.com:27317,mongodb2.example.com:27017/?connectTimeoutMS=300000&replicaSet=mySet&authSource=aDifferentAuthDB + + if (mongoUrl) { + // 1. Know protocol + let baseUrl = ''; + if (mongoUrl.startsWith('mongodb:')) + baseUrl = 'mongodb://'; + else if (mongoUrl.startsWith('mongodb+srv:')) + baseUrl = 'mongodb+srv://'; + else if (mongoUrl.startsWith('loopback-connector-mongodb:')) + baseUrl = 'loopback-connector-mongodb://'; + else if (mongoUrl.startsWith('loopback-connector-mongodb+srv:')) + baseUrl = 'loopback-connector-mongodb+srv://'; + else + return mongoUrl; // Not a MongoURL that we can process + + let remainderUrl = mongoUrl.substring(baseUrl.length); + // 2. Check if userId/password is present + let uidPassword = ''; + if (remainderUrl.indexOf('@') !== -1) { + const parts = remainderUrl.split('@'); + uidPassword = parts[0]; + if (parts.length === 2) + remainderUrl = parts[1]; + else + remainderUrl = ''; + } + let hosts = ''; + let dbName = ''; + let options = ''; + let hostsArray = []; + // 3. Check if comma separated replicas are specified + if (remainderUrl.indexOf(',') !== -1) { + hostsArray = remainderUrl.split(','); + remainderUrl = hostsArray[hostsArray.length - 1]; + } + + // 4. Check if authDB is specified in the URL + const slashIndex = remainderUrl.indexOf('/'); + if ((slashIndex !== -1)) { + if (slashIndex !== (remainderUrl.length - 1)) { + const optionsIndex = remainderUrl.indexOf('?'); + if (optionsIndex !== -1) { + options = remainderUrl.substring(optionsIndex + 1); + dbName = remainderUrl.substring(slashIndex + 1, optionsIndex); + } else { + // No DB options specified + dbName = remainderUrl.substring(slashIndex + 1); + } + } + + if (hostsArray.length > 1) { + const newHosts = hostsArray; + newHosts.pop(); + newHosts.push(remainderUrl.substring(0, slashIndex)); + hosts = newHosts.join(','); + } else { + hosts = remainderUrl.substring(0, slashIndex); + } + } else { + // No database specified + if (hostsArray.length > 1) + hosts = hostsArray.join(','); + else + hosts = remainderUrl; + } + + // 5. Reconstruct url, but this time add database from settings if URL didn't have it + // The below code has an overlap with generateMongoDBURL() + let modifiedUrl = baseUrl; + + if (uidPassword) + modifiedUrl += uidPassword + '@'; + if (hosts) + modifiedUrl += hosts; + + modifiedUrl += '/' + (dbName ? dbName : settingsDatabase); + + if (options) + modifiedUrl += '?' + options; + + return modifiedUrl; + } + return mongoUrl; +} diff --git a/test/mongodb.test.js b/test/mongodb.test.js index 909161ffa..0ffa347ed 100644 --- a/test/mongodb.test.js +++ b/test/mongodb.test.js @@ -141,6 +141,24 @@ describe('connect', function() { }, ); }); + + it('should accept cluster url connection string', function(done) { + const clusterUrl = 'mongodb://mongos0.example.com:27017,mongos1.example.com:27017,mongos2.example.com:27017/test_db'; + const ds = global.getDataSource({ + url: clusterUrl, + serverSelectionTimeoutMS: 2000, + lazyConnect: false, + }); + + ds.once('connected', function() { + ds.disconnect(done); + }); + ds.on('error', function(err) { + // If you don't have a real cluster, just check error is connection-related + err.name.should.match(/Mongo.*Error/); + done(); + }); + }); }); describe('mongodb connector', function() {