diff --git a/package-lock.json b/package-lock.json index 6b4b9cf..9819d2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@mojaloop/central-services-database", - "version": "8.2.0", + "version": "8.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 6255289..a3c52d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mojaloop/central-services-database", - "version": "8.2.0", + "version": "8.2.1", "description": "Shared database code for central services", "main": "src/index.js", "license": "Apache-2.0", diff --git a/src/database.js b/src/database.js index ccba84d..2911664 100644 --- a/src/database.js +++ b/src/database.js @@ -2,6 +2,26 @@ const Knex = require('knex') const Table = require('./table') +const Utils = require('./utils.js') + +/* Default config to fall back to when using deprecated URI connection string */ +const defaultConfig = { + connection: { + host: 'localhost', + port: 3306 + }, + pool: { + min: 2, + max: 10, + acquireTimeoutMillis: 30000, + createTimeoutMillis: 3000, + destroyTimeoutMillis: 5000, + idleTimeoutMillis: 30000, + reapIntervalMillis: 1000, + createRetryIntervalMillis: 200 + }, + debug: false +} class Database { constructor () { @@ -26,22 +46,36 @@ class Database { return this._knex } + /** + * @function connect + * + * @description Connect to the database given the config object. Returns null if database is already connected. + * + * @params {Object} config - The knex connection object. For more information see: http://knexjs.org/#Installation-client + * + * @returns null - if database is already connected. + * @returns void + * + * @throws {Error} - if Database scheme is invalid + */ async connect (config) { - if (!this._knex) { - if (config.connection.database) { - this._schema = config.connection.database - return configureKnex(config).then(knex => { - this._knex = knex - return this._listTables().then(tables => { - this._tables = tables - this._setTableProperties() - }) - }) - } else { - throw new Error('Invalid database schema in database config') - } + if (this._knex) { + return null } - return null + + if (typeof config === 'string') { + console.warn('`Database.connect()` called using deprecated string config. Please ugrade this to use the knex config object.') + config = Utils.buildDefaultConfig(defaultConfig, config) + } + + if (!config || !config.connection || !config.connection.database) { + throw new Error('Invalid database schema in database config') + } + + this._schema = config.connection.database + this._knex = await configureKnex(config) + this._tables = await this._listTables() + await this._setTableProperties() } async disconnect () { diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..bcd987e --- /dev/null +++ b/src/utils.js @@ -0,0 +1,39 @@ +'use strict' + +/** + * @function buildDefaultConfig + * + * @desciption Converts a deprecated Database Connection URI based config to the newer, Knex compatible version + * + * @returns {Object} - Knex compatible config + */ +const buildDefaultConfig = (defaultConfig, configStr) => { + const connectionRegex = /^(mysql|psql)(?::\/\/)(.*)(?::)(.*)@(.*)(?::)(\d*)(?:\/)(.*)$/ + const matches = configStr.match(connectionRegex) + + // Check that we got the expected amount: 6 capture groups + 1 js defaults = 7 + if (!matches || matches.length !== 7) { + throw new Error(`Invalid database config string: ${configStr}`) + } + + // skip the first, .match() puts the original string at index 0 + matches.shift() + const [clientStr, user, password, host, port, database] = matches + + return { + ...defaultConfig, + client: clientStr, + connection: { + ...defaultConfig.connection, + user, + password, + host, + port, + database + } + } +} + +module.exports = { + buildDefaultConfig +} diff --git a/test/unit/database.test.js b/test/unit/database.test.js index 5663529..0f028a5 100644 --- a/test/unit/database.test.js +++ b/test/unit/database.test.js @@ -87,6 +87,41 @@ Test('database', databaseTest => { } }) + getKnexTest.test('handle connection with a deprecated URI', async test => { + // Arrange + const URI = 'mysql://central_ledger:password@mysql-cl:3307/central_ledger_db' + + // Act + try { + await dbInstance.connect(URI) + const knex = await dbInstance.getKnex() + + // Assert + test.ok(knex) + } catch (e) { + test.fail('Error thrown') + } + + test.end() + }) + + getKnexTest.test('should fail if the config.connection object is undefined', async test => { + // Arrange + const config = {} + + // Act + try { + await dbInstance.connect(config) + + test.fail('Should have thrown error') + } catch (e) { + // Assert + test.equal(e.message, 'Invalid database schema in database config') + } + + test.end() + }) + getKnexTest.test('throw error when database is not connected', test => { try { dbInstance.getKnex() diff --git a/test/unit/utils.test.js b/test/unit/utils.test.js new file mode 100644 index 0000000..b3c7a06 --- /dev/null +++ b/test/unit/utils.test.js @@ -0,0 +1,114 @@ +'use strict' + +const src = '../../src' +const Test = require('tapes')(require('tape')) +const Utils = require(`${src}/utils`) + +Test('utils', utilsTest => { + utilsTest.test('buildDefaultConfig should', defaultTest => { + defaultTest.test('handle a mysql connection string', test => { + // Arrange + const defaultConfig = { + connection: { + host: 'localhost', + port: 3306 + }, + pool: { + field1: true + } + } + const URI = 'mysql://central_ledger:password@mysql-cl:3307/central_ledger_db' + const expected = { + client: 'mysql', + connection: { + host: 'mysql-cl', + port: '3307', + user: 'central_ledger', + password: 'password', + database: 'central_ledger_db' + }, + pool: { + field1: true + } + } + // Act + const config = Utils.buildDefaultConfig(defaultConfig, URI) + + // Assert + test.deepEqual(config, expected) + test.end() + }) + + defaultTest.test('not override all values in defaultConfig.connection', test => { + // Arrange + const defaultConfig = { + connection: { + newFieldNotOverriden: true + }, + pool: { + field1: true + } + } + const URI = 'mysql://central_ledger:password@mysql-cl:3307/central_ledger_db' + const expected = { + client: 'mysql', + connection: { + newFieldNotOverriden: true, + host: 'mysql-cl', + port: '3307', + user: 'central_ledger', + password: 'password', + database: 'central_ledger_db' + }, + pool: { + field1: true + } + } + // Act + const config = Utils.buildDefaultConfig(defaultConfig, URI) + + // Assert + test.deepEqual(config, expected) + test.end() + }) + + defaultTest.test('fails when connections string does not use mysql or psql', test => { + // Arrange + const defaultConfig = {} + const URI = 'mssql://central_ledger:password@mysql-cl:3307/central_ledger_db' + + // Act + try { + Utils.buildDefaultConfig(defaultConfig, URI) + test.fail('Should have thrown error') + } catch (err) { + // Assert + test.equal(err.message, 'Invalid database config string: mssql://central_ledger:password@mysql-cl:3307/central_ledger_db') + } + + test.end() + }) + + defaultTest.test('fail when the connection string is invalid', test => { + // Arrange + const defaultConfig = {} + // Missing a `:` + const URI = 'mssql://central_ledgerpassword@mysql-cl:3307/central_ledger_db' + + // Act + try { + Utils.buildDefaultConfig(defaultConfig, URI) + test.fail('Should have thrown error') + } catch (err) { + // Assert + test.equal(err.message, 'Invalid database config string: mssql://central_ledgerpassword@mysql-cl:3307/central_ledger_db') + } + + test.end() + }) + + defaultTest.end() + }) + + utilsTest.end() +})