jsharmony-db-mssql
Version:
jsHarmony Database Connector for SQL Server
485 lines (436 loc) • 17.9 kB
JavaScript
/*
Copyright 2017 apHarmony
This file is part of jsHarmony.
jsHarmony is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
jsHarmony is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this package. If not, see <http://www.gnu.org/licenses/>.
*/
var DB = require('jsharmony-db');
var types = DB.types;
var _ = require('lodash');
var async = require('async');
var moment = require('moment');
function DBdriver(mssql, options) {
if(!mssql) mssql = require('mssql');
this.mssql = function(){
return mssql;
};
this.name = 'mssql';
this.sql = require('./DB.mssql.sql.js');
this.meta = require('./DB.mssql.meta.js');
this.pool = []; /* { dbconfig: xxx, con: yyy, isConnected: false } */
this.silent = false;
this.onInitDBConfig = null; //function(dbconfig){}
//Initialize platform
this.platform = {
Log: function(msg){ console.log(msg); }, // eslint-disable-line no-console
Config: {
debug_params: {
db_log_level: 6, //Bitmask: 2 = WARNING, 4 = NOTICES :: Database messages logged to the console / log
db_error_sql_state: false //Log SQL state during DB error
}
}
};
this.platform.Log.info = function(msg){ console.log(msg); }; // eslint-disable-line no-console
this.platform.Log.warning = function(msg){ console.log(msg); }; // eslint-disable-line no-console
this.platform.Log.error = function(msg){ console.log(msg); }; // eslint-disable-line no-console
_.extend(this, options);
}
DBdriver.prototype.getDefaultSchema = function(){
return 'dbo';
};
DBdriver.prototype.logRawSQL = function(sql){
if (this.platform.Config.debug_params && this.platform.Config.debug_params.db_raw_sql && this.platform.Log) {
this.platform.Log.info(sql, { source: 'database_raw_sql' });
}
};
DBdriver.prototype.initDBConfig = function(dbconfig){
if(!dbconfig) return;
if(!dbconfig.options) dbconfig.options = {};
if(!dbconfig.options.useUTC) dbconfig.options.useUTC = false;
if(!('pooled' in dbconfig.options)) dbconfig.options.pooled = true;
if(!('encrypt' in dbconfig.options)) dbconfig.options.encrypt = true;
if(!('trustServerCertificate' in dbconfig.options)) dbconfig.options.trustServerCertificate = true;
if(!('packetSize' in dbconfig.options)) dbconfig.options.packetSize = 16384;
if(this.onInitDBConfig) this.onInitDBConfig(dbconfig);
};
DBdriver.prototype.getPooledConnection = function (dbconfig, onInitialized, numRetries, onFail) {
if(!dbconfig) throw new Error('dbconfig is required');
if (!numRetries) numRetries = 0;
var _this = this;
var mspool = null;
//Check if pool was already added
for(var i=0;i<this.pool.length;i++){
if(this.pool[i].dbconfig==dbconfig) mspool = this.pool[i];
}
//Add pool if it does not exist
if(!mspool){
_this.pool.push({
dbconfig: dbconfig,
con: null,
isConnected: false
});
mspool = _this.pool[_this.pool.length - 1];
}
//Initialize pool connection if it was not initialized
if(!mspool.con){
mspool.con = new (_this.mssql().ConnectionPool)(_.omit(dbconfig,['_driver']));
mspool.con.on('error', function(err){
_this.platform.Log.error('MSSQL Pool Error: ' + err.toString(), { source: 'database' });
});
mspool.con.connect(function (err) {
if (err){
if (!_this.silent) _this.platform.Log('MSSQL Pool Error: ' + err.toString(), { source: 'database' });
if(onFail) return onFail(err);
}
else {
mspool.isConnected = true;
return onInitialized(mspool.con);
}
});
}
else if (!mspool.isConnected) {
var maxRetries = 100;
if(dbconfig && ('maxConnectRetries' in dbconfig)) maxRetries = dbconfig.maxConnectRetries;
if (numRetries >= maxRetries) {
if(!_this.silent) _this.platform.Log('Timeout waiting for MSSQL Pool', { source: 'database' });
if(onFail) onFail(new Error('Timeout waiting for MSSQL Pool'));
_this.closePool(dbconfig);
return;
}
if(!_this.silent) _this.platform.Log('Retry: '+numRetries, { source: 'database' });
setTimeout(function () { _this.getPooledConnection(dbconfig, onInitialized, numRetries + 1, onFail); }, 100);
}
else return onInitialized(mspool.con);
};
DBdriver.prototype.closePool = function(dbconfig, onClosed){
if(!dbconfig) throw new Error('dbconfig is required');
if(!onClosed) onClosed = function(){ /* Do nothing */ };
var mspool = null;
//Check if dbconfig exists in pool
for(var i=0;i<this.pool.length;i++){
if(this.pool[i].dbconfig==dbconfig) mspool = this.pool[i];
}
if(!mspool) return onClosed();
if(mspool.con && mspool.isConnected){
mspool.isConnected = false;
mspool.con.close(function(){
mspool.con = null;
onClosed();
});
}
else{
mspool.con = null;
if(onClosed) onClosed();
}
};
DBdriver.prototype.Init = function (cb) { if(cb) return cb(); };
DBdriver.prototype.Close = function(onClosed){
var _this = this;
async.each(_this.pool, function(mspool, pool_cb){
_this.closePool(mspool.dbconfig, pool_cb);
}, onClosed);
};
DBdriver.prototype.getDBParam = function (dbtype, val) {
var _this = this;
if (!dbtype) throw new Error('Cannot get dbtype of null object');
if (val === null) return 'NULL';
if (typeof val === 'undefined') return 'NULL';
if ((dbtype.name == 'VarChar') || (dbtype.name == 'Char')) {
var valstr = val.toString();
if ((dbtype.length == types.MAX)||(dbtype.length == -1)) return "N'" + _this.escape(valstr) + "'";
return "N'" + _this.escape(valstr.substring(0, dbtype.length)) + "'";
}
else if (dbtype.name == 'VarBinary') {
var valbin = null;
if (val instanceof Buffer) valbin = val;
else valbin = new Buffer(val.toString());
if (valbin.legth == 0) return "NULL";
return "0x" + valbin.toString('hex').toUpperCase();
}
else if ((dbtype.name == 'BigInt') || (dbtype.name == 'Int') || (dbtype.name == 'SmallInt') || (dbtype.name == 'TinyInt')) {
var valint = parseInt(val);
if (isNaN(valint)) { return "NULL"; }
return valint.toString();
}
else if (dbtype.name == 'Boolean') {
if((val==='')||(typeof val == 'undefined')) return "NULL";
return "'" + _this.escape(val.toString()) + "'";
}
else if (dbtype.name == 'Decimal') {
let valfloat = parseFloat(val);
if (isNaN(valfloat)) { return "NULL"; }
return _this.escape(val.toString());
}
else if (dbtype.name == 'Float') {
let valfloat = parseFloat(val);
if (isNaN(valfloat)) { return "NULL"; }
return _this.escape(val.toString());
}
else if ((dbtype.name == 'Date') || (dbtype.name == 'Time') || (dbtype.name == 'DateTime')) {
var suffix = '';
var valdt = null;
if (val instanceof Date) { valdt = val; }
else if(_.isNumber(val) && !isNaN(val)){
valdt = moment(moment.utc(val).format('YYYY-MM-DDTHH:mm:ss.SSS'), "YYYY-MM-DDTHH:mm:ss.SSS").toDate();
}
else {
if (isNaN(Date.parse(val))) return "NULL";
valdt = new Date(val);
}
var mdate = moment(valdt);
if (!mdate.isValid()) return "NULL";
if(!_.isNumber(val)){
if('jsh_utcOffset' in val){
//Time is in UTC, Offset specifies amount and timezone
var neg = false;
if(val.jsh_utcOffset < 0){ neg = true; }
suffix = moment.utc(new Date(val.jsh_utcOffset*(neg?-1:1)*60*1000)).format('HH:mm');
//Reverse offset
suffix = ' '+(neg?'+':'-')+suffix;
mdate = moment.utc(valdt);
mdate = mdate.add(val.jsh_utcOffset*-1, 'minutes');
}
if('jsh_microseconds' in val){
var ms_str = "000"+(Math.round(val.jsh_microseconds)).toString();
ms_str = ms_str.slice(-3);
suffix = ms_str.replace(/0+$/,'') + suffix;
}
}
var rslt = '';
if (dbtype.name == 'Date') rslt = "'" + mdate.format('YYYY-MM-DD') + "'";
else if (dbtype.name == 'Time') rslt = "'" + mdate.format('HH:mm:ss.SSS') + suffix + "'";
else rslt = "'" + mdate.format('YYYY-MM-DD HH:mm:ss.SSS') + suffix + "'";
return rslt;
}
else if ((dbtype.name == 'Raw')) {
return val.toString();
}
throw new Error('Invalid datatype: ' + JSON.stringify(dbtype));
};
DBdriver.prototype.getDBType = function (dbtype,desc) {
var _this = this;
if (!desc) desc='';
if (!dbtype) throw new Error('Parameter '+desc+' database type invalid or not set');
if (dbtype.name == 'VarChar') {
if ((dbtype.length == types.MAX)||(dbtype.length == -1)) return _this.mssql().NVarChar(_this.mssql().MAX);
return _this.mssql().NVarChar(dbtype.length);
}
else if (dbtype.name == 'Char') {
if ((dbtype.length == types.MAX)||(dbtype.length == -1)) return _this.mssql().Char(_this.mssql().MAX);
return _this.mssql().Char(dbtype.length);
}
else if (dbtype.name == 'VarBinary') {
if ((dbtype.length == types.MAX)||(dbtype.length == -1)) return _this.mssql().VarBinary(_this.mssql().MAX);
return _this.mssql().VarBinary(dbtype.length);
}
else if (dbtype.name == 'BigInt') { return _this.mssql().BigInt(); }
else if (dbtype.name == 'Int') { return _this.mssql().Int(); }
else if (dbtype.name == 'SmallInt') { return _this.mssql().SmallInt(); }
else if (dbtype.name == 'TinyInt') { return _this.mssql().TinyInt(); }
else if (dbtype.name == 'Decimal') {
//return _this.mssql().Decimal(dbtype.prec_h, dbtype.prec_l);
return _this.mssql().VarChar(dbtype.length||50);
}
else if (dbtype.name == 'Float') {
return _this.mssql().VarChar(dbtype.length||128);
}
else if (dbtype.name == 'Date') { return _this.mssql().Date(); }
else if (dbtype.name == 'Time') {
let prec = dbtype.prec;
if(typeof prec=='undefined') prec = 7;
return _this.mssql().Time(prec);
}
else if (dbtype.name == 'DateTime') {
let prec = dbtype.prec;
if(typeof prec=='undefined') prec = 7;
if(dbtype.preserve_timezone) return _this.mssql().DateTimeOffset(prec);
else return _this.mssql().DateTime2(prec);
}
else if (dbtype.name == 'Boolean') { return _this.mssql().Bit(); }
throw new Error('Invalid datatype: ' + JSON.stringify(dbtype));
};
DBdriver.prototype.ExecSession = function (dbtrans, dbconfig, session) {
if(!dbconfig) throw new Error('dbconfig is required');
var _this = this;
if (dbtrans) {
session(null, dbtrans.con, '', function () { /* Do nothing */ });
}
else {
_this.initDBConfig(dbconfig);
if(dbconfig.options.pooled){
_this.getPooledConnection(
dbconfig,
function (con) {
session(null, con, dbconfig._presql || '', function () { /* Do nothing */ });
},
0,
function(err){ return _this.ExecError(err, session, "DB Connect Error: "); }
);
}
else {
_this.initDBConfig(dbconfig);
var con = new (_this.mssql().ConnectionPool)(_.extend({ pool: { min: 0, max: 1 } }, _.omit(dbconfig,['_driver'])), function (err) {
if (err) { return _this.ExecError(err, session, "DB Connect Error: "); }
session(null, con, dbconfig._presql || '', function () { con.close(); });
});
con.on('error', function(err){
_this.platform.Log.error('DB Connection Error: ' + err.toString(), { source: 'database' });
});
}
}
};
DBdriver.prototype.ExecError = function(err, callback, errprefix) {
if (this.platform.Config.debug_params.db_error_sql_state && !this.silent) this.platform.Log((errprefix || '') + err.toString(), { source: 'database' });
if (callback) return callback(err, null);
else throw err;
};
DBdriver.prototype.Exec = function (dbtrans, context, return_type, sql, ptypes, params, callback, dbconfig) {
if(!dbconfig) throw new Error('dbconfig is required');
var _this = this;
_this.ExecSession(dbtrans, dbconfig, function (err, con, presql, conComplete) {
if(dbtrans && (dbtrans.dbconfig != dbconfig)) err = new Error('Transaction cannot span multiple database connections');
if(err) {
if (callback != null) callback(err, null, null);
else throw err;
return;
}
var dbrslt = null;
var stats = { notices: [], warnings: [] };
var no_errors = true;
//Streaming
var r = new (_this.mssql().Request)(con);
r.stream = true;
var execsql = presql + sql;
execsql = _this.applySQLParams(execsql, ptypes, params);
//Add context SQL
execsql = _this.getContextSQL(context) + execsql;
r.on('recordset', function (cols) {
if (dbrslt instanceof Error) return;
if (return_type == 'multirecordset') {
if (dbrslt == null) dbrslt = [];
dbrslt.push([]);
}
else if (return_type == 'recordset') {
dbrslt = [];
}
});
r.on('row', function (row) {
if (dbrslt instanceof Error) return;
if (row) {
for (let key in row) if (row.hasOwnProperty(key)) row[key] = parseResultData(row[key]);
}
if (return_type == 'row') dbrslt = row;
else if (return_type == 'recordset') dbrslt.push(row);
else if (return_type == 'multirecordset') dbrslt[dbrslt.length - 1].push(row);
else if (return_type == 'scalar') {
if (DB.util.Size(row) == 0) dbrslt = null;
for (let key in row) if (row.hasOwnProperty(key)) dbrslt = row[key];
}
});
r.on('info', function (msg) {
if(msg.number) stats.warnings.push(new DB.Message(DB.Message.WARNING, msg.message));
else stats.notices.push(new DB.Message(DB.Message.NOTICE, msg.message));
});
r.on('error', function (err) {
if ((dbrslt != null) && (dbrslt instanceof Error)) {
// Make Application Errors a priority, otherwise concatenate errors
if (dbrslt.message.indexOf('Application Error - ') == 0) { /* Do nothing */ }
else if (dbrslt.message.indexOf('Execute Form - ') == 0) { /* Do nothing */ }
else if (err.message.indexOf('Application Error - ') == 0) { dbrslt = err; }
else if (dbrslt.code == err.code) dbrslt.message += ' - ' + err.message;
}
else dbrslt = err;
if (_this.platform.Config.debug_params.db_error_sql_state && no_errors && !_this.silent){
no_errors = false;
_this.platform.Log('SQL Error: ' + (err.message||'') + ' - ' + sql + ' ' + JSON.stringify(ptypes) + ' ' + JSON.stringify(params), { source: 'database' });
}
setTimeout(function(){
r.pause();
r.cancel();
}, 100);
});
var isDone = false;
r.on('done', function (rslt) {
if(isDone) return;
isDone = true;
setTimeout(function(){
conComplete();
if (dbrslt instanceof Error) {
if (callback != null) callback(dbrslt, null, null);
else throw dbrslt;
return;
}
DB.util.LogDBResult(_this.platform, { sql: execsql, dbrslt: dbrslt, notices: stats.notices, warnings: stats.warnings });
if (callback != null) callback(null, dbrslt, stats);
},1);
});
_this.logRawSQL(execsql);
r.query(execsql);
});
};
function parseResultData(val) {
if (val instanceof Date) {
var mdt = moment(val);
if (!mdt.isValid()) return val;
var rslt = mdt.format("YYYY-MM-DDTHH:mm:ss.SSS");
if(val.nanosecondsDelta){
var ns_str = "0000"+(val.nanosecondsDelta*10000000).toString();
ns_str = ns_str.slice(-4);
rslt += ns_str.replace(/0+$/,'');
}
return rslt;
}
return val;
}
DBdriver.prototype.ExecTransTasks = function (execTasks, callback, dbconfig) {
if(!dbconfig) throw new Error('dbconfig is required');
var _this = this;
_this.ExecSession(null, dbconfig, function (err, con, presql, conComplete) {
if(err) return callback(err, null);
var contrans = new (_this.mssql().Transaction)(con);
var trans = new DB.TransactionConnection(contrans,dbconfig);
trans.con.begin(function (err) {
execTasks(trans, function (dberr, rslt) {
if (dberr != null) { trans.con.rollback(function (err) { conComplete(); callback(dberr, null); }); }
else { trans.con.commit(function (err) { conComplete(); callback(err, rslt); }); }
});
});
});
};
DBdriver.prototype.escape = function (val) { return this.sql.escape(val); };
DBdriver.prototype.getContextSQL = function(context) {
if(!context) return '';
return 'set context_info 0x' + DB.util.str2hex(context) + ';';
};
DBdriver.prototype.applySQLParams = function (sql, ptypes, params) {
var _this = this;
//Apply ptypes, params to SQL
var ptypes_ref = {};
if(ptypes){
let i = 0;
for (let p in params) {
ptypes_ref[p] = ptypes[i];
i++;
}
}
//Sort params by length
var param_keys = _.keys(params);
param_keys.sort(function (a, b) { return b.length - a.length; });
//Replace params in SQL statement
for (let i = 0; i < param_keys.length; i++) {
let p = param_keys[i];
var val = params[p];
if (val === '') val = null;
sql = DB.util.ReplaceAll(sql, '@' + p, _this.getDBParam(ptypes ? ptypes_ref[p] : types.fromValue(val), val));
}
return sql;
};
exports = module.exports = DBdriver;