UNPKG

jsharmony-db-mssql

Version:
485 lines (436 loc) 17.9 kB
/* 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;