mysql2-db
Version:
utility wrapper for Node.js mysql2 library
514 lines (461 loc) • 20.5 kB
JavaScript
const mysql2 = require('mysql2');
module.exports = {
/**
* Attaches to database with specified connection information, and immediately
* returns a stage object that you can use to queue up SQL statements
* with query() and execute().
*/
stage: (connectionInfo) => {
return new DbStage(connectionInfo);
},
/**
* Gracefully closes all connections to all pools made through this
* instance of the library. Pass an (optional) callback if you want to know
* when it's all over.
*/
curtains: (callback) => {
return POOL_FUNCTIONS.doCurtains(callback ? callback : () => {});
}
};
function DbStage(cfg) {
var ops = [];
/**
* Specifies a database action for which the result will indicate the number
* of rows modified, rather than a resultset.
* Pass in the SQL to execute and any parameters to bind when executing.
*/
this.execute = (sql, params) => {
ops.push(doAction('e', sql, params, null));
return this;
};
/**
* Specifies a database query action, which returns an array of objects (one per row returned by your query).
* Pass in the SQL to execute and any parameters to bind when executing.
*/
this.query = (sql, params) => {
ops.push(doAction('q', sql, params, null));
return this;
};
/**
* Specifies a database query action that returns one integer.
* Pass in the SQL to execute and any parameters to bind when executing.
* You can also pass in a default value that will be returned
* if the result set is empty, or if the first value returned
* isn't an integer.
*/
this.queryInt = (sql, params, dflt) => {
ops.push(doAction('qi', sql, params, dflt));
return this;
};
/**
* Specifies a database query action that returns one floating point number.
* Pass in the SQL to execute and any parameters to bind when executing.
* You can also pass in a default value that will be returned
* if the result set is empty, or if the first value returned
* isn't a number.
*/
this.queryFloat = (sql, params, dflt) => {
ops.push(doAction('qf', sql, params, dflt));
return this;
};
/**
* Specifies a database query action that returns one string.
* Pass in the SQL to execute and any parameters to bind when executing.
* You can also pass in a default value that will be returned
* if the result set is empty, or if the first value returned
* is null.
*/
this.queryString = (sql, params, dflt) => {
ops.push(doAction('qs', sql, params, dflt));
return this;
};
/**
* Acts out queued SQL statements.
* Call this after you queue up statements with execute() and query() methods.
* If you queue up multiple statements, you'll get back an array of results.
* If you queue up a single statement, you'll get back a single result.
* If you pass autocommit=true, then no transaction will be used.
* If you pass autocommit=false, then a transaction will be started
* and committed on success, or rolled back (as much as possible)
* on failure.
*/
this.finale = (callback, autocommit) => {
doFinale(cfg, !autocommit, ops, callback);
};
/**
* Await-friendly wrapper for finale: Calls finale (with autocommit=false) and returns a promise.
*/
this.perform = () => {
return new Promise((resolve, reject) => {
try {
this.finale((err, results) => {
if (err) return reject(err);
if (results) return resolve(results);
return reject(new Error("No results returned"));
}, false, true);
} catch (e) {
return reject(e);
}
});
}
}
function doAction(opcode, sql, params, dflt) {
params = JSON.parse(JSON.stringify(params ? params : null));
if (!opcode) throw new Error("Internal error: missing opcode");
if (!sql) throw new Error("The SQL provided is blank or missing.");
if (typeof (sql) != "string") throw new Error("The SQL provided is not a string.");
var op = parseSql();
op.opcode = opcode;
op.dflt = dflt;
op.paramVals = params;
op.paramShape = computeParamShape();
if (op.bindStyles[':'] && !op.bindStyles['?'] && op.paramShape == "array" && params.length === 0) {
op.paramShape = 'array.object';
}
op.isMulti = (op.paramShape == "array.array" || op.paramShape == "array.object");
if (op.bindStyles[':'] && op.bindStyles['?'])
throw new Error("The SQL statement \"" + sql + "\" uses ? placeholders and : named placeholders. Pick one. It won't work to use both in the same SQL statement.");
if (op.bindStyles['?'] && op.paramShape != "array" && op.paramShape != "array.array")
throw new Error("The SQL statement \"" + sql + "\" uses ? placeholders, but params is " + op.paramShape + " instead of a single array, or an array of arrays. " + (op.paramShape == "scalar" ? "You probably mean to wrap your param with [] to form an array?" : ""));
if (op.bindStyles[':'] && op.paramShape != "object" && op.paramShape != "array.object")
throw new Error("The SQL statement \"" + sql + "\" uses : placeholders, but params is " + op.paramShape + " instead of a single object, or an array of objects.");
if (params !== null && params !== undefined) {
if (Array.isArray(params))
params.forEach((param) => {
if (param === null || param === undefined)
console.log("Warning: You have at least one null value in your parameters.");
if (Array.isArray(param))
param.forEach((p) => {
if (p === null || p === undefined)
console.log("Warning: You have at least one null value in your parameters.");
});
else if (typeof param == "object")
for (var nm in param)
if (param.hasOwnProperty(nm)) {
var p = param[nm];
if (p === null || p === undefined)
console.log("Warning: You have at least one null value in your parameters.");
}
});
else if (typeof params == "object")
for (var nm in params)
if (params.hasOwnProperty(nm)) {
var p = params[nm];
if (p === null || p === undefined)
console.log("Warning: You have at least one null value in your parameters (" + nm + ").");
}
}
return op;
function parseSql() {
const re = /(:[a-zA-Z0-9_]+)|(\?)|(\$[0-9]+)/g;
var matches = sql.match(re);
var matchRefs = [];
var matchTypes = [];
var bindStyles = {};
if (!matches) matches = [];
else {
var qcounter = 0;
for (var i = 0; i < matches.length; i++) {
var match = matches[i];
var mtype = match.charAt(0);
var mref;
bindStyles[mtype] = true;
switch (mtype) {
case ':':
mref = match.substring(1);
break;
case '$':
mref = parseInt(match.substring(1), 10);
break;
case '?':
mref = qcounter++;
break;
default:
throw new Error("parse error");
}
matchRefs.push(mref);
matchTypes.push(mtype);
}
}
return {
rawSql: sql,
sql: sql.replace(re, "?"),
paramRefs: matchRefs,
paramTypes: matchTypes,
bindStyles: bindStyles
};
}
function computeParamShape() {
var rv = "";
if (params === null || params === undefined) return false;
rv += Array.isArray(params) ? "array" : (typeof params == "object" ? "object" : "scalar");
if (rv != "array") return rv;
if (params.length == 0) return rv;
if (params[0] == null) return rv;
rv += Array.isArray(params[0]) ? ".array" : (typeof params[0] == "object" ? ".object" : "");
return rv;
}
}
var POOL_FUNCTIONS = {
_pools: {},
_closing: false,
getPool: function (dbcfg, callback) {
try {
if (!dbcfg) throw new Error("Null database configuration info; be sure to provide username, password, etc.");
var key = ((typeof dbcfg) == "string") ? dbcfg : JSON.stringify(dbcfg);
var pool = POOL_FUNCTIONS._pools[key];
if (!pool) {
pool = mysql2.createPool(dbcfg);
POOL_FUNCTIONS._pools[key] = pool;
}
process.nextTick(callback, null, pool);
} catch (err) {
process.nextTick(callback, err, null);
}
},
isClosing: function () {
return POOL_FUNCTIONS._closing;
},
doCurtains: function (callback) {
POOL_FUNCTIONS._closing = true;
var poolsToClose = [];
for (var p in POOL_FUNCTIONS._pools)
if (POOL_FUNCTIONS._pools.hasOwnProperty(p))
poolsToClose.push(POOL_FUNCTIONS._pools[p]);
closeNextPool();
function closeNextPool() {
if (!poolsToClose.length) return callback();
var pool = poolsToClose.pop();
pool.end(closeNextPool);
}
},
getConnection: function (pool, callback) {
try {
if (!pool) throw new Error("Null database connection pool returned by driver");
pool.getConnection((err, conn) => {
process.nextTick(callback, err, conn);
});
} catch (err) {
process.nextTick(callback, err, null);
}
},
releaseConnection: function (conn, callback) {
try {
conn.release();
process.nextTick(callback, null);
} catch (err) {
process.nextTick(callback, err);
}
}
};
function doFinale(dbcfg, bTransact, ops, cb) {
if (POOL_FUNCTIONS.isClosing()) return cb(new Error("Databases are closing down."));
if (!dbcfg) return cb(new Error("Internal error: config not passed through"));
if (!ops) return cb(new Error("Internal error: ops not passed through"));
if (!cb || typeof cb != "function") return cb(new Error("Oops, you forgot to provide a function to call back after the finale."));
if (ops.finaleComplete) return cb(new Error("You already had your finale on this stage. Go get a new stage."));
ops.finaleComplete = true;
var singularOperation = (ops.length == 1);
var results = [];
var transactionHasStarted = false;
// 'twould be nice to use async library, but don't want to add another dependency
// gonna generally use process.nextTick(callback) to avoid polluting the call stack of client
POOL_FUNCTIONS.getPool(dbcfg, (err, pool) => {
if (err) return finalize(null, err);
POOL_FUNCTIONS.getConnection(pool, (err, conn) => {
if (err) return finalize(conn, err);
var setCommitOp = doAction('e', "SET autocommit=" + (bTransact ? 0 : 1));
doExecute(conn, dbcfg, setCommitOp, [], (err) => {
if (err) return finalize(conn, err);
if (bTransact) {
conn.beginTransaction((err) => {
if (err) return finalize(conn, err);
transactionHasStarted = true;
fillInResults(conn, dbcfg, ops, results, (err) => {
if (err) return finalize(conn, err);
conn.commit((err) => {
return finalize(conn, err);
});
});
});
} else {
fillInResults(conn, dbcfg, ops, results, (err) => {
return finalize(conn, err);
});
}
});
});
});
function fillInResults(conn, dbcfg, ops, results, callback) {
if (ops.length == results.length) // done!
return process.nextTick(callback, null);
try { // else, do next
var nextOp = ops[results.length];
var fn = (nextOp.opcode == 'e' ? doExecute : doQuery);
fn(conn, dbcfg, nextOp, results, (err, result) => {
if (err)
return process.nextTick(callback, err);
else {
results.push(result);
process.nextTick(fillInResults, conn, dbcfg, ops, results, callback);
}
});
} catch (e) {
return process.nextTick(callback, e);
}
}
function finalize(conn, err) {
var rv = singularOperation ? results[0] : results;
if (conn) {
if (transactionHasStarted) {
conn.rollback(() => {
POOL_FUNCTIONS.releaseConnection(conn, () => {
return process.nextTick(cb, err, rv);
});
});
} else {
POOL_FUNCTIONS.releaseConnection(conn, () => {
return process.nextTick(cb, err, rv);
});
}
} else
return process.nextTick(cb, err, rv);
}
function makeArgs(paramRefs, paramTypes, explicitParams, paramsFromPriorResults) {
var rv = [];
if (paramRefs == null) paramRefs = [];
if (paramTypes == null) paramTypes = [];
if (paramRefs.length != paramTypes.length) throw new Error("paramRefs.length != paramTypes.length: " + paramRefs.length + " vs " + paramTypes.length);
if (explicitParams == null) explicitParams = [];
//console.log(JSON.stringify(paramRefs) + ";" + JSON.stringify(paramTypes) + ";" + JSON.stringify(explicitParams) + ";" + JSON.stringify(paramsFromPriorResults));
for (var i = 0; i < paramRefs.length; i++) {
var paramRef = paramRefs[i];
var paramType = paramTypes[i];
var paramVal;
switch (paramType) {
case ':':
paramVal = explicitParams[paramRef];
break;
case '$':
paramVal = paramsFromPriorResults[paramRef];
break;
case '?':
paramVal = explicitParams[paramRef];
break;
default:
throw new Error("Internal error: unrecognized param type");
}
rv.push(paramVal);
}
return rv;
}
function doExecute(conn, dbcfg, op, resultsSoFar, callback) {
try {
var paramVals = op.paramVals;
if (!op.isMulti)
paramVals = [paramVals];
var totalNumberOfRowsAffected = 0;
var i = 0;
doNextExec();
function doNextExec() {
if (i >= paramVals.length) {
// all done, return result
return process.nextTick(callback, null, totalNumberOfRowsAffected);
} else {
var sql = op.sql;
var args = makeArgs(op.paramRefs, op.paramTypes, paramVals[i], resultsSoFar);
if (dbcfg.echo)
if (sql.indexOf("insert into log") == -1 && sql.indexOf("SET autocommit=") == -1)
console.log("executing \"" + sql + "\" with " + JSON.stringify(args));
conn.execute(sql, args, (err, resultsFromThisExec) => {
if (err) return process.nextTick(callback, err, null);
var nrowsAffected = (resultsFromThisExec && resultsFromThisExec.affectedRows ? resultsFromThisExec.affectedRows : 0);
totalNumberOfRowsAffected += nrowsAffected;
i++;
return process.nextTick(doNextExec);
});
}
}
} catch (e) {
return process.nextTick(callback, e);
}
}
function doQuery(conn, dbcfg, op, resultsSoFar, callback) {
try {
var paramVals = op.paramVals;
var singularExecution = !op.isMulti;
if (singularExecution) paramVals = [paramVals];
var resultsFromThisOp = [];
var i = 0;
doNextExec();
function doNextExec() {
if (i >= paramVals.length) {
// all done, return result; unbundle from array if user passed in singular params
//console.log("out:" + JSON.stringify(resultsFromThisOp) + ";" + singularExecution + "..." + JSON.stringify(singularExecution ? resultsFromThisOp[0] : resultsFromThisOp))
return process.nextTick(callback, null,
singularExecution ? resultsFromThisOp[0] : resultsFromThisOp
);
} else {
var sql = op.sql;
var args = makeArgs(op.paramRefs, op.paramTypes, paramVals[i], resultsSoFar);
if (dbcfg.echo)
console.log("querying \"" + sql + "\" with " + JSON.stringify(args));
conn.execute(sql, args, (err, resultsFromThisExec) => {
if (err) return process.nextTick(callback, err, null);
resultsFromThisOp.push(formatResult(op, resultsFromThisExec));
i++;
return process.nextTick(doNextExec);
});
}
}
} catch (e) {
process.nextTick(callback, e);
}
}
function formatResult(op, resultSet) {
if (op.opcode == 'q') { // dump all results
var rv = [];
resultSet.forEach(function (row) {
var rvRow = {};
for (var p in row) {
if (row.hasOwnProperty(p))
rvRow[p] = row[p];
}
rv.push(rvRow);
});
return rv;
} else {
var dflt = op.dflt;
if (!resultSet) return dflt;
if (!resultSet.length) return dflt;
var resultRow = resultSet[0];
if (!resultRow) return dflt;
for (var p in resultRow) {
var vl = resultRow[p];
if (vl === null || vl === undefined) return dflt;
switch (op.opcode) {
case 'qi':
if ((typeof vl) != "number" || !Number.isInteger(vl)) {
vl = parseFloat("" + vl);
if (Number.isNaN(vl) || !Number.isInteger(vl))
return dflt;
}
break;
case 'qf':
if ((typeof vl) != "number" || Number.isNaN(vl)) {
vl = parseFloat("" + vl);
if (Number.isNaN(vl))
return dflt;
}
break;
case 'qs':
if ((typeof vl) != "string")
vl = vl + "";
break;
}
return vl;
}
}
return dflt;
}
}