@itentialopensource/adapter-db_mysql
Version:
Itential adapter to connect to mysql
542 lines (474 loc) • 17 kB
JavaScript
/* @copyright Itential, LLC 2019 (pre-modifications) */
// Set globals
/* global log */
/* eslint no-underscore-dangle: warn */
/* eslint no-loop-func: warn */
/* eslint no-cond-assign: warn */
/* eslint no-unused-vars: warn */
/* eslint consistent-return: warn */
/* Required libraries. */
const fs = require('fs-extra');
const path = require('path');
// libraries to import
const mysql = require('mysql');
// Itential framework event
const EventEmitter = require('events');
let myid = null;
let errors = [];
/**
* @summary Build a standard error object from the data provided
*
* @function formatErrorObject
* @param {String} origin - the originator of the error (optional).
* @param {String} type - the internal error type (optional).
* @param {String} variables - the variables to put into the error message (optional).
* @param {Integer} sysCode - the error code from the other system (optional).
* @param {Object} sysRes - the raw response from the other system (optional).
* @param {Exception} stack - any available stack trace from the issue (optional).
*
* @return {Object} - the error object, null if missing pertinent information
*/
function formatErrorObject(origin, type, variables, sysCode, sysRes, stack) {
log.trace(`${myid}-adapter-formatErrorObject`);
// add the required fields
const errorObject = {
icode: 'AD.999',
IAPerror: {
origin: `${myid}-unidentified`,
displayString: 'error not provided',
recommendation: 'report this issue to the adapter team!'
}
};
if (origin) {
errorObject.IAPerror.origin = origin;
}
if (type) {
errorObject.IAPerror.displayString = type;
}
// add the messages from the error.json
for (let e = 0; e < errors.length; e += 1) {
if (errors[e].key === type) {
errorObject.icode = errors[e].icode;
errorObject.IAPerror.displayString = errors[e].displayString;
errorObject.IAPerror.recommendation = errors[e].recommendation;
} else if (errors[e].icode === type) {
errorObject.icode = errors[e].icode;
errorObject.IAPerror.displayString = errors[e].displayString;
errorObject.IAPerror.recommendation = errors[e].recommendation;
}
}
// replace the variables
let varCnt = 0;
while (errorObject.IAPerror.displayString.indexOf('$VARIABLE$') >= 0) {
let curVar = '';
// get the current variable
if (variables && Array.isArray(variables) && variables.length >= varCnt + 1) {
curVar = variables[varCnt];
}
varCnt += 1;
errorObject.IAPerror.displayString = errorObject.IAPerror.displayString.replace('$VARIABLE$', curVar);
}
// add all of the optional fields
if (sysCode) {
errorObject.IAPerror.code = sysCode;
}
if (sysRes) {
errorObject.IAPerror.raw_response = sysRes;
}
if (stack) {
errorObject.IAPerror.stack = stack;
}
// return the object
return errorObject;
}
class MySQL extends EventEmitter {
constructor(prongid, properties) {
log.trace('adapter mysql loading');
// Instantiate the EventEmitter super class
super();
this.props = properties;
this.id = prongid;
myid = prongid;
// get the path for the specific error file
const errorFile = path.join(__dirname, '/error.json');
// if the file does not exist - error
if (!fs.existsSync(errorFile)) {
const origin = `${this.id}-adapter-constructor`;
log.warn(`${origin}: Could not locate ${errorFile} - errors will be missing details`);
}
// Read the action from the file system
const errorData = JSON.parse(fs.readFileSync(errorFile, 'utf-8'));
({ errors } = errorData);
this.setConfiguration();
}
setConfiguration() {
this.config = {
host: this.props.host,
port: this.props.port,
user: this.props.authentication.username,
password: this.props.authentication.password,
database: this.props.database,
...this.props.connectionOptions
};
this.healthcheckType = this.props.healthcheck.type;
this.healthcheckInterval = this.props.healthcheck.frequency;
if (this.props.ssl) {
if (this.props.ssl.enabled === true) {
log.info('Connecting to MySQL with SSL.');
// validate the server's certificate
if (this.props.ssl.accept_invalid_cert === false) {
log.info('Certificate based SSL MySQL connections will be used.');
this.config.ssl = {
rejectUnauthorized: true
};
// if validation is enabled, we need to read the CA file
if (this.props.ssl.ca_file) {
try {
this.config.ssl.ca = fs.readFileSync(this.props.ssl.ca_file);
} catch (err) {
log.error(`Error: Unable to load MySQL CA file: ${err}`);
this.emit('OFFLINE', {
id: this.id
});
}
} else {
log.error('Error: Certificate validation'
+ 'is enabled but a CA is not specified.');
this.emit('OFFLINE', {
id: this.id
});
}
} else {
log.info('SSL MySQL connection without CA certificate validation.');
this.config.ssl = {
rejectUnauthorized: false
};
}
} else {
log.warn('WARNING: Connecting to MySQL without SSL.');
}
} else {
log.warn('WARNING: Connecting to MySQL without SSL.');
}
}
/**
* Itential connect call for system green/red light
*
* @param {function} callback
*/
connect() {
const meth = 'adapter-connect';
const origin = `${this.id}-${meth}`;
log.trace(origin);
// initially set as off
this.emit('OFFLINE', { id: this.id });
this.dbconnect();
if (this.healthcheckType === 'none') {
log.error(`${origin}: Waiting 1 Seconds to emit Online`);
setTimeout(() => {
this.emit('ONLINE', { id: this.id });
this.healthy = true;
}, 1000);
}
if (this.healthcheckType === 'startup' || this.healthcheckType === 'intermittent') {
this.healthCheck();
}
if (this.healthcheckType === 'intermittent') {
setInterval(() => {
this.healthCheck();
}, this.healthcheckInterval);
}
}
/**
* Database connect call
*
* @param {function} callback
*/
dbconnect() {
const meth = 'adapter-dbconnect';
const origin = `${this.id}-${meth}`;
log.trace(origin);
log.info('Creating connection pool to MySQL');
this.pool = mysql.createPool(this.config);
this.pool.on('acquire', (connection) => log.debug('Connection acquired'));
this.pool.on('connection', (connection) => log.debug('New connection made within the pool'));
this.pool.on('enqueue', () => log.debug('Waiting for available connection slot'));
this.pool.on('release', (connection) => log.debug('Connection released'));
}
/**
* Call to run a healthcheck on the mysql database
*
* @function healthCheck
*/
healthCheck() {
const meth = 'adapter-healthCheck';
const origin = `${this.id}-${meth}`;
log.trace(origin);
try {
if (!this.pool) {
log.error('Error during healthcheck: Not connected to MySQL Database');
return this.emit('OFFLINE', { id: this.id });
}
this.pool.query('SELECT 1;', (error, results, fields) => {
if (error) {
log.error(`HEALTHCHECK FAIL, ${JSON.stringify(error)}`);
return this.emit('OFFLINE', { id: this.id });
}
log.debug('HEALTHCHECK SUCCESSFUL');
return this.emit('ONLINE', { id: this.id });
});
} catch (ex) {
log.error(`HEALTHCHECK EXCEPTION: ${JSON.stringify(ex)}`);
return this.emit('OFFLINE', { id: this.id });
}
}
/**
* getAllFunctions is used to get all of the exposed function in the adapter
*
* @function getAllFunctions
*/
getAllFunctions() {
let myfunctions = [];
let obj = this;
// find the functions in this class
do {
const l = Object.getOwnPropertyNames(obj)
.concat(Object.getOwnPropertySymbols(obj).map((s) => s.toString()))
.sort()
.filter((p, i, arr) => typeof obj[p] === 'function' && p !== 'constructor' && (i === 0 || p !== arr[i - 1]) && myfunctions.indexOf(p) === -1);
myfunctions = myfunctions.concat(l);
}
while (
(obj = Object.getPrototypeOf(obj)) && Object.getPrototypeOf(obj)
);
return myfunctions;
}
/**
* getWorkflowFunctions is used to get all of the workflow function in the adapter
*
* @function getWorkflowFunctions
*/
getWorkflowFunctions() {
const myfunctions = this.getAllFunctions();
const wffunctions = [];
// remove the functions that should not be in a Workflow
for (let m = 0; m < myfunctions.length; m += 1) {
if (myfunctions[m] === 'addListener') {
// got to the second tier (adapterBase)
break;
}
if (myfunctions[m] !== 'connect' && myfunctions[m] !== 'healthCheck' && myfunctions[m] !== 'dbconnect'
&& myfunctions[m] !== 'getAllFunctions' && myfunctions[m] !== 'getWorkflowFunctions' && myfunctions[m] !== 'setConfiguration') {
wffunctions.push(myfunctions[m]);
}
}
return wffunctions;
}
/**
* Call to query the MySQL server.
* @function query
* @param sql - a sql string (required)
* @param callback - a callback function to return a result
*/
query(sql, callback) {
const meth = 'adapter-query';
const origin = `${this.id}-${meth}`;
log.trace(origin);
log.trace(`mysql query started with sql: ${sql}`);
if (!sql) {
const errorObj = formatErrorObject(origin, 'Missing Data', ['sql'], null, null, null);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
try {
this.pool.query(sql, (error, results, fields) => {
log.debug(`result from query: ${JSON.stringify(results)}`);
log.debug(`fields from query: ${JSON.stringify(fields)}`);
if (error) {
const errorObj = formatErrorObject(origin, 'Database Error', [error], null, null, null);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
return callback({
status: 'success',
code: 200,
response: results
});
});
} catch (ex) {
const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
}
/**
* Call to create a table into MySQL server.
* @function create
* @param sql - a sql string (required)
* @param callback - a callback function to return a result
*/
create(sql, callback) {
const meth = 'adapter-create';
const origin = `${this.id}-${meth}`;
log.trace(origin);
log.trace(`mysql create started with sql: ${sql}`);
try {
// verify the required data has been provided
if (!sql) {
const errorObj = formatErrorObject(origin, 'Missing Data', ['sql'], null, null, null);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
if (!sql.toLowerCase().startsWith('create')) {
return callback(null, 'SQL statement must start with "CREATE"');
}
this.query(sql, callback);
} catch (ex) {
const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
}
/**
* Call to select item from MySQL server.
* @function select
* @param sql - a sql string (required)
* @param callback - a callback function to return a result
*/
select(sql, callback) {
const meth = 'adapter-select';
const origin = `${this.id}-${meth}`;
log.trace(origin);
log.trace(`mysql insert started with sql: ${sql}`);
try {
// verify the required data has been provided
if (!sql) {
const errorObj = formatErrorObject(origin, 'Missing Data', ['sql'], null, null, null);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
if (!sql.toLowerCase().startsWith('select')) {
return callback(null, 'SQL statement must start with "SELECT"');
}
this.query(sql, callback);
} catch (ex) {
const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
}
/**
* Call to insert item into MySQL server.
* @function insert
* @param sql - a sql string (required)
* @param callback - a callback function to return a result
*/
insert(sql, callback) {
const meth = 'adapter-insert';
const origin = `${this.id}-${meth}`;
log.trace(origin);
log.trace(`mysql insert started with sql: ${sql}`);
try {
// verify the required data has been provided
if (!sql) {
const errorObj = formatErrorObject(origin, 'Missing Data', ['sql'], null, null, null);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
if (!sql.toLowerCase().startsWith('insert')) {
return callback(null, 'SQL statement must start with "INSERT"');
}
this.query(sql, callback);
} catch (ex) {
const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
}
/**
* Call to update item into MySQL server.
* @function update
* @param sql - a sql string (required)
* @param callback - a callback function to return a result
*/
update(sql, callback) {
const meth = 'adapter-update';
const origin = `${this.id}-${meth}`;
log.trace(origin);
log.trace(`mysql update started with sql: ${sql}`);
try {
// verify the required data has been provided
if (!sql) {
const errorObj = formatErrorObject(origin, 'Missing Data', ['sql'], null, null, null);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
if (!sql.toLowerCase().startsWith('update')) {
return callback(null, 'SQL statement must start with "UPDATE"');
}
this.query(sql, callback);
} catch (ex) {
const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
}
/**
* Call to delete item from MySQL server.
* @function delete
* @param sql - a sql string (required)
* @param callback - a callback function to return a result
*/
delete(sql, callback) {
const meth = 'adapter-delete';
const origin = `${this.id}-${meth}`;
log.trace(origin);
log.trace(`mysql delete started with sql: ${sql}`);
try {
// verify the required data has been provided
if (!sql) {
const errorObj = formatErrorObject(origin, 'Missing Data', ['sql'], null, null, null);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
if (!sql.toLowerCase().startsWith('delete')) {
return callback(null, 'SQL statement must start with "DELETE"');
}
this.query(sql, callback);
} catch (ex) {
const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
}
/**
* Call to drop table from MySQL server.
* @function drop
* @param sql - a sql string (required)
* @param callback - a callback function to return a result
*/
drop(sql, callback) {
const meth = 'adapter-drop';
const origin = `${this.id}-${meth}`;
log.trace(origin);
log.trace(`mysql drop started with sql: ${sql}`);
try {
// verify the required data has been provided
if (!sql) {
const errorObj = formatErrorObject(origin, 'Missing Data', ['sql'], null, null, null);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
if (!sql.toLowerCase().startsWith('drop')) {
return callback(null, 'SQL statement must start with "DROP"');
}
this.query(sql, callback);
} catch (ex) {
const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
}
}
// export to Itential
module.exports = MySQL;