okanjo-app-mysql
Version:
Service for interfacing with MySQL or MariaDB
264 lines (236 loc) • 8.99 kB
JavaScript
;
const debug = require('debug')('mysqlservice');
const MySQL = require('@mysql/xdevapi');
const Column = require('@mysql/xdevapi/lib/DevAPI/Column');
/**
* MYSQL Database service
*/
class MySQLService {
/**
* Constructor
* @param {OkanjoApp} app
* @param {Object} config
*/
constructor(app, config) {
this.app = app;
this.config = config;
// No config = no dice.
if (!this.config) {
throw new Error('MySQLService: `config` must be defined on initialization!');
}
if (!this.config.session) {
throw new Error('MySQLService: `config.session` must be defined on initialization!');
}
// if (!this.config.client) {
// throw new Error('MySQLService: `config.client` must be defined on initialization!');
// }
app.registerServiceConnector(async () => this.connect());
}
/**
* Connects to the MySQL database, and initializes the connection pool
*/
async connect() {
// Luckily, all we have to do here is define the pool
debug('Starting connection pool');
try {
this.client = MySQL.getClient(this.config.session, this.config.client);
} catch(err) {
await this.app.report('Failed to setup connection to MySQL', err);
this.client = null;
throw err;
}
}
/**
* Closes down the connection pool.
* @returns {Promise<void>}
*/
async close() {
debug('Closing connection pool');
if (this.client) await this.client.close();
}
// noinspection JSMethodCanBeStatic
/**
* Escapes a schema or field name
* @param {string} str – Identifier
* @returns {string} Escaped value that can safely go into a quoted string
*/
escapeIdentifier(str) {
return (str || '')
.replace(/\\\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/`/g, '``')
;
}
/**
* Converts query arguments into MySQL format (e.g. Date -> string)
* @param {[*]} args – Query arguments
* @returns {[*]} – Encoded query arguments
*/
encodeParams(args) {
return args.map((val) => {
// Convert Date objects into MySQL date strings
if (val instanceof Date) {
return val.toISOString().replace(/(T|\..*$)/g, ' ').trim();
} else {
return val;
}
});
}
/**
* Converts raw mysql row values back into native JavaScript types (e.g. timestamp -> Date)
* @param {[*]} args – Row values
* @param {[Column]} cols – Table Column objects
* @returns {[*]} – Decoded row values
*/
decodeParams(args, cols) {
return args.map((val, i) => {
if (cols[i].getType() === Column.Type.DATETIME) { // DATETIME
return new Date(val);
} else {
return val;
}
});
}
/**
* Query Execution wrapper
* @param {SqlExecute} query – mysqlx session.sql() query object
* @param {{supress:number?}} options – Execution functionality options
* @returns {Promise<any>}
*/
execute(query, options={}) {
return new Promise(async (resolve, reject) => { // eslint-disable-line no-async-promise-executor
const records = []; // raw records received by the driver
let cols = []; // raw metadata about the records (e.g. columns)
let rows = []; // transformed rows to objects
let res; // query response to get additional info about the query
try {
debug('Executing query:\n%s\nArguments:\n%O',
query.getSQL(),
query
);
res = await query.execute(
row => records.push(row),
colGroup => cols = cols.concat(colGroup)
);
debug('Query completed');
rows = records.map((raw) => {
const row = {};
let col;
this.decodeParams(raw, cols).forEach((val, i) => {
col = cols[i];
row[col.getColumnLabel() || /* istanbul ignore next: old driver compatibility */ col.getColumnName()] = val;
// console.log(col)
});
return row;
});
} catch(err) {
if (!options.suppress || options.suppress !== err.info.code) {
await this.app.report('MySQLService: Error executing query', err, {
query,
sql: query.getSQL ? query.getSQL() : /* istanbul ignore next: idk if this is safe, so checking method is present before firing it */ null,
info: err.info,
});
}
return reject(err);
}
// Expose additional query data, without compromising default functionality
Object.defineProperties(rows, {
raw: {
enumerable: false,
value: records
},
result: {
enumerable: false,
value: res
},
cols: {
enumerable: false,
value: cols
}
});
return resolve(rows);
});
}
/**
* Issues a SQL query with parameterized arguments.
* @param {string} sql – Query string
* @param {Object} [args] – Query ordinal argument values
* @param {function(err:*, res:*?)} [callback] – Optional callback to fire when completed
* @param {{session:*?, suppress:number?}} [options] – Query functionality options
* @returns {Promise<{rows:*}>}
*/
query(sql, args, callback, options) {
return new Promise((resolve, reject) => {
let resolveSession;
// args is optional
if (typeof args === "function") {
options = callback;
callback = args;
args = null;
}
// if a session was given, resolve it otherwise fetch a new session from the pool
if (options && options.session) {
debug('Using supplied session for query');
resolveSession = Promise.resolve(options.session);
} else {
debug('Getting a new session for query');
try {
resolveSession = this.client.getSession();
} catch (err) /* istanbul ignore next: oos */ {
this.app.report('Failed to get session on MySQL client', err);
return reject(err);
}
}
let session;
let error;
let response;
resolveSession
.then(sess => {
// hold session
session = sess;
// generate query
const query = session.sql(sql);
// bind query ordinal placeholders
if (args) query.bind(this.encodeParams(args));
// execute the query
return this.execute(query, options)
.then(res => {
debug('Query succeeded');
response = res;
error = null;
}, /* istanbul ignore next: oos */ err => {
debug('Query failed', err);
error = err;
response = null;
})
;
}, /* istanbul ignore next: oos */ err => {
this.app.report('Failed to resolve session', err);
error = err;
})
.finally(() => {
// close the session if one was pulled for this operation
if (session && (!options || !options.session)) {
debug('Closing query session');
session.close();
}
if (callback) {
callback(error, response);
resolve(); // callback disables rejections
} else {
if (error) return reject(error);
return resolve(response)
}
})
;
});
}
/**
* Gets a fresh session from the pool
* @returns {*|Promise<Session>}
*/
getSession() {
return this.client.getSession();
}
}
module.exports = MySQLService;