diff --git a/.gitignore b/.gitignore index be9871c..148dc53 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ node_modules .nyc_output -logfile +# npm lock +package-lock.json + +logfile \ No newline at end of file diff --git a/dialects/mysql-client.js b/dialects/mysql-client.js index d919e7d..dbc6f0e 100644 --- a/dialects/mysql-client.js +++ b/dialects/mysql-client.js @@ -2,7 +2,7 @@ var mysql = require('mysql') var url = require('url') var querystring = require('querystring') -var connections = {} +var connections = {}; class MysqlClient { constructor (options) { @@ -17,50 +17,199 @@ class MysqlClient { username: auth[0], password: auth[1], database: (info.pathname || '/').substring(1), - host: info.hostname, // host is 'localhost:port' hostname is just 'localhost' - port: port + host: info.hostname // host is 'localhost:port' hostname is just 'localhost' }, more) } + this.options = Object.assign({ user: options.username, + port: port, multipleStatements: true }, options) this.database = options.database var key = `${options.username}:${options.password}@${options.host}:${port}/${options.database}` var conn = connections[key] + if (!conn) { conn = connections[key] = mysql.createConnection(this.options) } this.connection = conn } - dropTables () { - return this.find(` - SELECT concat('DROP TABLE IF EXISTS ', table_name, ';') AS fullSQL - FROM information_schema.tables - WHERE table_schema = ?; - `, [this.options.database]) + dropTables() { + const sql = ` + SELECT concat('DROP TABLE IF EXISTS ', table_name, ';') AS fullSQL + FROM information_schema.tables + WHERE table_schema = ?;`; + const params = [this.options.database]; + return this.query(sql, params) .then((results) => { - var sql = results.map((result) => result.fullSQL).join(' ') - return sql && this.query(sql) - }) + results = results.rows + var sql_result = results.map((result) => result.fullSQL).join(' ') + return sql_result && this.query(sql_result) + }).catch((reason) => { + console.error('dropTables failed ('+JSON.stringify(reason)+').'); + }); } - query (sql, params = []) { + /** + * connect - create a database connection + * resolve(threadId) - returns threadId + * reject(err) - returns connection error + * @return {type} Promise + */ + connect() { + return new Promise((resolve, reject) => { + var conn = mysql.createConnection(this.options); + this.connection = conn + this.connection.connect((err) => { + if (err) { return reject(err); } + return resolve(this.connection.threadId); + }); + }); + } + + + /** + * query - execute a sql query with the given parametrs + * + * @param {type} sql MySQL statment + * @param {type} params = [] query paramenters + * @return {type} Promise: resolve({ rows, fields }), reject(err) + */ + _query(sql, params = []) { return new Promise((resolve, reject) => { this.connection.query(sql, params, (err, rows, fields) => { - err ? reject(err) : resolve({ rows, fields }) + if (err) return reject(err); + return resolve({ rows, fields }); }) }) } - find (sql, params = []) { - return this.query(sql, params).then((result) => result.rows) + find(sql, params = []) { + return this.query(sql, params) + .then((result) => { + return result.rows + }).catch((reason) => { + console.error('find query failed ('+ JSON.stringify(reason)+').'); + }); + } + + findOne(sql, params = []) { + // this is returning a Promise because this.query returns a promise + return this.query(sql, params) + .this((result) => { + return result.rows[0]; + }).catch((error) => { + console.error('findOne query faild - ('+reason+').'); + }); } - findOne (sql, params = []) { - return this.query(sql, params).then((result) => result.rows[0]) + /** + * query - executes a query and closes the connection + * resolve() is called when the query is executed (even with a connection close exception) + * reject() is called when the query can not be executed + * If there is a (connection error or a query error) and a close error, the close error is appended to the first error's supressed Array + * If there is a valid result and a close error, the close error is appended to the results supressed Array + * Result {"rows": {}, "fields": {}} + * @param {type} sql sql statment + * @param {type} params = [] sql parameters + * @return {type} Promise + */ + query(sql, params = []) { + return new Promise((resolve, reject) => { + this.connect().then((threadId) => { + // connected to the database + this._query(sql, params) + .then((queryResult) => { + // query returned a result, attempt to close the connection + this.close(null, queryResult) + .then((result) => { + // connection was closed + resolve(result) + }).catch((res) => { + // could not close the connection + if(res.supressed) console.error('db close error. ' + res.supressed); + resolve(res) + }); + }).catch((error) => { + // there was an error with the query, attempt to close the connection + this.close(error, null) + .then((err) => { + // connection was closed + return reject(err); + }).catch((e) => { + if(e.supressed) console.error('db close error. ' + e.supressed); + // could not close the connection + reject(e); + }) + }); + }).catch((connError) => { + // could not connect to the database // might not need to attempt to close if you don't have a valid connection + this.close(connError, null) + .then((error) => { + // connection was closed + return reject(error); + }).catch((e) => { + // could not close the connection + reject(e); + }); + }); + }); + } + + /** + * close - close database connection + * If the connection is closed without errors, resolve() is called + * If the connection can not be closed, reject() is called + * If the connection can't be closed and a queryError or queryResult is provided, the close arrow is appended to the object as obj.supressed Array + * @param {type} queryError error returned from a query or null + * @param {type} queryResult result returned from a query or null + * @return {type} Promise + */ + close(queryError , queryResult) { + return new Promise((resolve, reject) => { + this.connection.end((dbError) => { + if(dbError && queryResult) { return reject(this._appendError(queryResult, dbError));} // db close error and query result. supress db error + if(dbError && queryError) { return reject(this._appendError(queryError, dbError)); } // db error and query error. supress db error + if(dbError) { return reject(dbErr); } // only db error + if(queryError) { return resolve(queryError); } // only query error (resolve because db close was a success) + if(queryError === null && queryResult === null) { return resolve(); } // normal call to close() without a prior query + return resolve(queryResult); // pass up the query result + // its impossible to get a (queryError && queryResult) + }); + }); + } + + + + /** + * _appendError - appends an error onto an object as a .supressed attribute. + * Will not overwrite an existing .supressed attribute + * + * @param {type} object description + * @param {type} error description + * @return {type} description + */ + _appendError(object, error){ + let errors = []; + for (var prop in object) { + if(prop === 'supressed') { + if(Array.isArray(object[prop])){ + var clone = object[prop].slice(0); + clone.push(error); + errors = clone; + } + else { + throw new Error('can not add to supressed. it already exsists and is not an array'); + } + } + } + if (errors.length <= 0) errors = new Array(error); + // add supressed error as 1st object in array + object = Object.assign({supressed: errors}, object); + return object } }