UNPKG

@themost/mssql

Version:
1,509 lines (1,475 loc) 71.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var sprintfJs = require('sprintf-js'); var query = require('@themost/query'); var mssql = require('mssql'); var async = require('async'); var common = require('@themost/common'); var events = require('@themost/events'); var merge = require('lodash/merge'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var async__default = /*#__PURE__*/_interopDefaultLegacy(async); var merge__default = /*#__PURE__*/_interopDefaultLegacy(merge); // MOST Web Framework Codename Zero Gravity Copyright (c) 2017-2022, THEMOST LP All rights reserved function zeroPad(number, length) { number = number || 0; let res = number.toString(); while (res.length < length) { res = '0' + res; } return res; } /** * @class * @augments {SqlFormatter} */ class MSSqlFormatter extends query.SqlFormatter { /** * @constructor */ constructor() { super(); const offset = new Date().getTimezoneOffset(); this.settings = { nameFormat: '[$1]', timezone: (offset <= 0 ? '+' : '-') + zeroPad(-Math.floor(offset / 60), 2) + ':' + zeroPad(offset % 60, 2) }; } /** * @private * @param {import('@themost/query').QueryExpression|*} query */ formatLimitSelectWithDistinctClause(query) { const take = parseInt(query.$take, 10) || 0; const skip = parseInt(query.$skip, 10) || 0; let sql = this.formatSelect(query); if (query.$order == null) { const [key] = Object.keys(query.$select); if (key == null) { throw new Error('Select clause is missing'); } const firstField = query.$select[key]; sql += ' '; sql += `ORDER BY ${this.format(firstField, '%ff')} ASC`; } sql += ' '; sql += `OFFSET ${skip} ROWS FETCH NEXT ${take} ROWS ONLY`; return sql; } formatLimitSelect(obj) { let sql; const self = this; if (!obj.$take) { sql = self.formatSelect(obj); } else { if (obj.$distinct) { return self.formatLimitSelectWithDistinctClause(obj); } obj.$take = parseInt(obj.$take) || 0; obj.$skip = parseInt(obj.$skip) || 0; //add row_number with order const keys = Object.keys(obj.$select); if (keys.length === 0) throw new Error('Entity is missing'); const queryFields = obj.$select[keys[0]]; const order = obj.$order; // format order expression const rowIndex = Object.assign(new query.QueryField(), { // use alias __RowIndex: { // use row index func $rowIndex: [order // set order or null ] } }); queryFields.push(rowIndex); if (order) delete obj.$order; const subQuery = self.formatSelect(obj); if (order) obj.$order = order; //delete row index field queryFields.pop(); const fields = []; queryFields.forEach(x => { if (typeof x === 'string') { fields.push(new query.QueryField(x)); } else { /** * @type {QueryField} */ const field = Object.assign(new query.QueryField(), x); fields.push(field.as() || field.getName()); } }); sql = sprintfJs.sprintf('SELECT %s FROM (%s) [t0] WHERE [__RowIndex] BETWEEN %s AND %s', fields.map(x => { return self.format(x, '%f'); }).join(', '), subQuery, parseInt(obj.$skip, 10) + 1, parseInt(obj.$skip, 10) + parseInt(obj.$take, 10)); } return sql; } /** * Implements indexOf(str,substr) expression formatter. * @param {*} p0 The source string * @param {*} p1 The string to search for */ $indexof(p0, p1) { return this.$indexOf(p0, p1); } /** * Implements indexOf(str,substr) expression formatter. * @param {*} p0 The source string * @param {*} p1 The string to search for */ $indexOf(p0, p1) { p1 = '%' + p1 + '%'; return '(PATINDEX('.concat(this.escape(p1), ',', this.escape(p0), ')-1)'); } $length(p0) { return sprintfJs.sprintf('LEN(%s)', this.escape(p0)); } /** * Implements simple regular expression formatter. Important Note: MS SQL Server does not provide a core sql function for regular expression matching. * @param {string|*} p0 The source string or field * @param {string|*} p1 The string to search for */ $regex(p0, p1) { let s1; //implement starts with equivalent for PATINDEX T-SQL if (/^\^/.test(p1)) { s1 = p1.replace(/^\^/, ''); } else { s1 = '%' + p1; } //implement ends with equivalent for PATINDEX T-SQL if (/\$$/.test(s1)) { s1 = s1.replace(/\$$/, ''); } else { s1 = s1 + '%'; } //use PATINDEX for text searching return sprintfJs.sprintf('PATINDEX(%s,%s) >= 1', this.escape(s1), this.escape(p0)); } $date(p0) { return this.$toDate(p0, 'date'); } /** * Escapes an object or a value and returns the equivalent sql value. * @param {*} value * @param {boolean=} unquoted */ escape(value, unquoted) { if (typeof value === 'boolean') { return value ? '1' : '0'; } if (value instanceof Date) { return this.escapeDate(value); } if (typeof value === 'string') { const str = value.replace(/'/g, '\'\''); return unquoted ? str : 'N\'' + str + '\''; } return super.escape.bind(this)(value, unquoted); } /** * @param {Date|*} val * @returns {string} */ escapeDate(val) { const year = val.getFullYear(); const month = zeroPad(val.getMonth() + 1, 2); const day = zeroPad(val.getDate(), 2); const hour = zeroPad(val.getHours(), 2); const minute = zeroPad(val.getMinutes(), 2); const second = zeroPad(val.getSeconds(), 2); const millisecond = zeroPad(val.getMilliseconds(), 3); //format timezone const offset = val.getTimezoneOffset(), timezone = (offset <= 0 ? '+' : '-') + zeroPad(-Math.floor(offset / 60), 2) + ':' + zeroPad(offset % 60, 2); return 'CONVERT(datetimeoffset,\'' + year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second + '.' + millisecond + timezone + '\')'; } /** * Implements startsWith(a,b) expression formatter. * @param p0 {*} * @param p1 {*} */ $startswith(p0, p1) { p1 = '%' + p1 + '%'; return sprintfJs.sprintf('PATINDEX (%s,%s)', this.escape(p1), this.escape(p0)); } /** * Implements contains(a,b) expression formatter. * @param p0 {*} * @param p1 {*} */ $text(p0, p1) { return sprintfJs.sprintf('(PATINDEX (%s,%s) - 1)', this.escape('%' + p1 + '%'), this.escape(p0)); } /** * Implements endsWith(a,b) expression formatter. * @param p0 {*} * @param p1 {*} */ $endswith(p0, p1) { p1 = '%' + p1; // (PATINDEX('%S%', UserData.alternateName)) return sprintfJs.sprintf('(CASE WHEN %s LIKE %s THEN 1 ELSE 0 END)', this.escape(p0), this.escape(p1)); } /** * Implements substring(str,pos) expression formatter. * @param {String} p0 The source string * @param {Number} pos The starting position * @param {Number=} length The length of the resulted string * @returns {string} */ $substring(p0, pos, length) { if (length) return sprintfJs.sprintf('SUBSTRING(%s,%s,%s)', this.escape(p0), pos.valueOf() + 1, length.valueOf());else return sprintfJs.sprintf('SUBSTRING(%s,%s,%s)', this.escape(p0), pos.valueOf() + 1, 255); } /** * Implements trim(a) expression formatter. * @param p0 {*} */ $trim(p0) { return sprintfJs.sprintf('LTRIM(RTRIM((%s)))', this.escape(p0)); } /** * @param {*=} order * @returns {string} */ $rowIndex(order) { if (order == null) { return 'ROW_NUMBER() OVER(ORDER BY (SELECT NULL))'; } return sprintfJs.sprintf('ROW_NUMBER() OVER(%s)', this.format(order, '%o')); } $year(p0) { return sprintfJs.sprintf('DATEPART(year, SWITCHOFFSET(%s, \'%s\'))', this.escape(p0), this.settings.timezone); } $month(p0) { return sprintfJs.sprintf('DATEPART(month, SWITCHOFFSET(%s, \'%s\'))', this.escape(p0), this.settings.timezone); } $dayOfMonth(p0) { return sprintfJs.sprintf('DATEPART(day, SWITCHOFFSET(%s, \'%s\'))', this.escape(p0), this.settings.timezone); } $day(p0) { return this.$dayOfMonth(p0); } $hour(p0) { return sprintfJs.sprintf('DATEPART(hour, SWITCHOFFSET(%s, \'%s\'))', this.escape(p0), this.settings.timezone); } $minute(p0) { return sprintfJs.sprintf('DATEPART(minute, SWITCHOFFSET(%s, \'%s\'))', this.escape(p0), this.settings.timezone); } $minutes(p0) { return this.$minute(p0); } $second(p0) { return sprintfJs.sprintf('DATEPART(second, SWITCHOFFSET(%s, \'%s\'))', this.escape(p0), this.settings.timezone); } $seconds(p0) { return this.$second(p0); } $ifnull(p0, p1) { return sprintfJs.sprintf('ISNULL(%s, %s)', this.escape(p0), this.escape(p1)); } $ifNull(p0, p1) { return sprintfJs.sprintf('ISNULL(%s, %s)', this.escape(p0), this.escape(p1)); } $toString(p0) { return sprintfJs.sprintf('CAST(%s AS NVARCHAR)', this.escape(p0)); } isLogical = function (obj) { for (const key in obj) { return /^\$(and|or|not|nor)$/g.test(key); } return false; }; $cond(ifExpr, thenExpr, elseExpr) { // validate ifExpr which should an instance of QueryExpression or a comparison expression let ifExpression; if (ifExpr instanceof query.QueryExpression) { ifExpression = this.formatWhere(ifExpr.$where); } else if (this.isComparison(ifExpr) || this.isLogical(ifExpr)) { ifExpression = this.formatWhere(ifExpr); } else { throw new Error('Condition parameter should be an instance of query or comparison expression'); } return sprintfJs.sprintf('(CASE WHEN %s THEN %s ELSE %s END)', ifExpression, this.escape(thenExpr), this.escape(elseExpr)); } /** * @param {*} expr * @return {string} */ $jsonGet(expr) { if (typeof expr.$name !== 'string') { throw new Error('Invalid json expression. Expected a string'); } const parts = expr.$name.split('.'); const extract = this.escapeName(parts.splice(0, 2).join('.')); return `JSON_VALUE(${extract}, '$.${parts.join('.')}')`; } /** * @param {*} expr * @return {string} */ $jsonEach(expr) { if (typeof expr.$name !== 'string') { throw new Error('Invalid json expression. Expected a string'); } const parts = expr.$name.split('.'); const extract = this.escapeName(parts.splice(0, 2).join('.')); return `JSON_QUERY(${extract}, '$.${parts.join('.')}')`; } $uuid() { return 'NEWID()'; } $toGuid(expr) { return sprintfJs.sprintf('dbo.BIN_TO_UUID(HASHBYTES(\'MD5\',CONVERT(VARCHAR(MAX), %s)))', this.escape(expr)); } $toInt(expr) { return sprintfJs.sprintf('CAST(%s AS INT)', this.escape(expr)); } $toDouble(expr) { return this.$toDecimal(expr, 19, 8); } /** * @param {*} expr * @param {number=} precision * @param {number=} scale * @returns */ $toDecimal(expr, precision, scale) { const p = typeof precision === 'number' ? parseInt(precision, 10) : 19; const s = typeof scale === 'number' ? parseInt(scale, 10) : 8; return sprintfJs.sprintf('CAST(%s as DECIMAL(%s,%s))', this.escape(expr), p, s); } $toLong(expr) { return sprintfJs.sprintf('CAST(%s AS BIGINT)', this.escape(expr)); } /** * * @param {('date'|'datetime'|'timestamp')} type * @returns */ $getDate(type) { switch (type) { case 'date': return 'CAST(GETDATE() AS DATE)'; case 'datetime': return 'CAST(GETDATE() AS DATETIME)'; case 'timestamp': return 'CAST(GETDATE() AS DATETIMEOFFSET)'; default: return 'GETDATE()'; } } /** * @param {...*} expr */ // eslint-disable-next-line no-unused-vars $jsonObject(expr) { // expected an array of QueryField objects const args = Array.from(arguments).reduce((previous, current) => { // get the first key of the current object let [name] = Object.keys(current); let value; // if the name is not a string then throw an error if (typeof name !== 'string') { throw new Error('Invalid json object expression. The attribute name cannot be determined.'); } // if the given name is a dialect function (starts with $) then use the current value as is // otherwise create a new QueryField object if (name.startsWith('$')) { value = new query.QueryField(current[name]); name = value.getName(); } else { value = current instanceof query.QueryField ? new query.QueryField(current[name]) : current[name]; } // escape json attribute name and value previous.push(this.escape(name), this.escape(value)); return previous; }, []); const pairs = args.reduce((previous, current, index) => { if (index % 2 === 0) { return previous; } previous.push(`${args[index - 1]}:${current}`); return previous; }, []); return `json_object(${pairs.join(',')})`; } /** * @param {{ $jsonGet: Array<*> }} expr */ $jsonGroupArray(expr) { const [key] = Object.keys(expr); if (key !== '$jsonObject') { throw new Error('Invalid json group array expression. Expected a json object expression'); } return `JSON_ARRAYAGG(${this.escape(expr)})`; } /** * @param {import('@themost/query').QueryExpression} expr */ $jsonArray(expr) { if (expr == null) { throw new Error('The given query expression cannot be null'); } if (expr instanceof query.QueryField) { // escape expr as field and waiting for parsing results as json array return this.escape(expr); } // trear expr as select expression if (expr.$select) { return `(${this.format(expr)} FOR JSON PATH)`; } // treat expression as query field if (Object.prototype.hasOwnProperty.call(expr, '$name')) { return this.escape(expr); } // treat expression as value if (Object.prototype.hasOwnProperty.call(expr, '$value')) { if (Array.isArray(expr.$value)) { return this.escape(JSON.stringify(expr.$value)); } return this.escape(expr); } if (Object.prototype.hasOwnProperty.call(expr, '$literal')) { if (Array.isArray(expr.$literal)) { return this.escape(JSON.stringify(expr.$literal)); } return this.escape(expr); } throw new Error('Invalid json array expression. Expected a valid select expression'); } /** * Converts the give expression to a date, datetime or timestamp value * @param {*} arg * @param {('date'|'datetime'|'timestamp')} type * @returns */ $toDate(arg, type) { switch (type) { case 'date': return sprintfJs.sprintf('CAST(%s AS DATE)', this.escape(arg)); case 'datetime': return sprintfJs.sprintf('CAST(%s AS DATETIME)', this.escape(arg)); case 'timestamp': return sprintfJs.sprintf('TODATETIMEOFFSET(%s,datepart(TZ,SYSDATETIMEOFFSET()))', this.escape(arg)); default: return sprintfJs.sprintf('CAST(%s AS DATETIMEOFFSET)', this.escape(arg)); } } } function _applyDecoratedDescriptor(i, e, r, n, l) { var a = {}; return Object.keys(n).forEach(function (i) { a[i] = n[i]; }), a.enumerable = !!a.enumerable, a.configurable = !!a.configurable, ("value" in a || a.initializer) && (a.writable = !0), a = r.slice().reverse().reduce(function (r, n) { return n(i, e, r) || r; }, a), l && void 0 !== a.initializer && (a.value = a.initializer ? a.initializer.call(l) : void 0, a.initializer = void 0), void 0 === a.initializer ? (Object.defineProperty(i, e, a), null) : a; } const TransactionIsolationLevelEnum = { readUncommitted: 'READ UNCOMMITTED', readCommitted: 'READ COMMITTED', repeatableRead: 'REPEATABLE READ', snapshot: 'SNAPSHOT', serializable: 'SERIALIZABLE' }; Object.freeze(TransactionIsolationLevelEnum); class TransactionIsolationLevelFormatter { /** * @param {'readUncommitted' | 'readCommitted' | 'repeatableRead' | 'snapshot' | 'serializable'} isolationLevel * @returns {string} */ format(isolationLevel) { if (Object.prototype.hasOwnProperty.call(TransactionIsolationLevelEnum, isolationLevel)) { let sql = 'SET TRANSACTION ISOLATION LEVEL'; sql += ' '; sql += TransactionIsolationLevelEnum[isolationLevel]; return sql; } throw new TypeError('The specified transaction isolation level is invalid'); } } var _dec, _dec2, _class; /** * * @param {{target: SqliteAdapter, query: string|QueryExpression, results: Array<*>}} event */ function onReceivingJsonObject(event) { if (typeof event.query === 'object' && event.query.$select) { // try to identify the usage of a $jsonObject dialect and format result as JSON const { $select: select } = event.query; if (select) { const attrs = Object.keys(select).reduce((previous, current) => { const fields = select[current]; previous.push(...fields); return previous; }, []).filter(x => { const [key] = Object.keys(x); if (typeof key !== 'string') { return false; } return x[key].$jsonObject != null || x[key].$jsonArray != null || x[key].$jsonGroupArray != null; }).map(x => { return Object.keys(x)[0]; }); if (attrs.length > 0) { if (Array.isArray(event.results)) { for (const result of event.results) { attrs.forEach(attr => { if (Object.prototype.hasOwnProperty.call(result, attr) && typeof result[attr] === 'string') { result[attr] = JSON.parse(result[attr]); } }); } } } } } } class ConnectionStateError extends Error { constructor() { super('The connection has an invalid state. It seems that the current operation was cancelled by the user or the socket has been closed.'); this.name = 'ConnectionStateError'; } } /** * @type {Map<string, ConnectionPool>} */ const pools = new Map(); class MSSqlConnectionPoolManager { /** * @type {Map<string, ConnectionPool>} */ get pools() { return pools; } /** * Gets a connection pool for the given connection options * @param {*} connectionOptions * @returns Promise<ConnectionPool> */ async getAsync(connectionOptions) { return new Promise((resolve, reject) => { return this.get(connectionOptions, (err, pool) => { if (err) { return reject(err); } return resolve(pool); }); }); } /** * * @param {*} connectOptions * @param {function(err: Error=, pool: ConnectionPool)} callback * @returns */ get(connectOptions, callback) { if (connectOptions.id == null) { return callback(new Error('Invalid connection options. The configuration is missing a unique identifier')); } const key = connectOptions.id; if (pools.has(key)) { return callback(null, pools.get(key)); } const pool = new mssql.ConnectionPool(connectOptions); const close = pool.close.bind(pool); pool.close = (...args) => { pools.delete(key); return close(...args); }; pool.connect(err => { if (err) { return callback(err); } pools.set(key, pool); return callback(null, pool); }); } /** * Finalizes all connection pools * @param {function(err: Error=)} callback */ finalize(callback) { async__default["default"].each(pools.values(), (pool, cb) => { pool.close(cb); }, err => { pools.clear(); if (typeof callback === 'function') { return callback(err); } }); } /** * Finalizes all connection pools * @returns Promise<void> */ async finalizeAsync() { return new Promise((resolve, reject) => { this.finalize(err => { if (err) { return reject(err); } return resolve(); }); }); } } class RetryQuery { /** * Creates a new instance of RetryQuery * @param {string|import('@themost/query').QueryExpression} query * @param {number=} retry */ constructor(query, retry) { /** * Gets or sets the query to be retried * @type {string|import('@themost/query').QueryExpression} */ this.query = query; /** * Gets or sets the retry count * @type {number} */ this.retry = retry || 0; } } /** * @class */ let MSSqlAdapter = (_dec = events.after(({ target, args, result: results }, callback) => { const [query, params] = args; const event = { target, query, params, results }; void target.executed.emit(event).then(() => { return callback(null, { value: results }); }).catch(err => { return callback(err); }); }), _dec2 = events.before(({ target, args }, callback) => { const [query, params] = args; void target.executing.emit({ target, query, params }).then(() => { return callback(); }).catch(err => { return callback(err); }); }), _class = class MSSqlAdapter { /** * @constructor * @param {*} options */ constructor(options) { /** * @private * @type {ConnectionPool} */ this.rawConnection = null; /** * Gets or sets database connection string * @type {*} */ this.options = options; /** * Gets or sets a boolean that indicates whether connection pooling is enabled or not. * @type {boolean} */ this.connectionPooling = false; const self = this; // get retry options if (typeof this.options.retry === 'undefined') { this.options.retry = 4; this.options.retryInterval = 1000; } /** * Gets connection string from options. * @type {string} */ Object.defineProperty(this, 'connectionString', { get: function () { const keys = Object.keys(self.options); return keys.map(function (x) { return x.concat('=', self.options[x]); }).join(';'); }, configurable: false, enumerable: false }); this.id = common.Guid.from(this.connectionString).toString(); this.executing = new events.AsyncSeriesEventEmitter(); this.executed = new events.AsyncSeriesEventEmitter(); this.executed.subscribe(onReceivingJsonObject); this.committed = new events.AsyncSeriesEventEmitter(); this.rollbacked = new events.AsyncSeriesEventEmitter(); } prepare(query$1, values) { return query.SqlUtils.format(query$1, values); } /** * Opens database connection */ open(callback) { callback = callback || function () {}; const self = this; if (self.rawConnection) { return callback(); } // important note: validate the connection state against transaction state // if the connection is closed and a transaction is still active then throw error if (self.disposed === true) { common.TraceUtils.debug('The connection has been already closed.'); return callback(new ConnectionStateError()); } common.TraceUtils.debug('Opening database connection'); // clone connection options const connectionOptions = merge__default["default"]({ id: this.id, options: { encrypt: false, trustServerCertificate: true } }, self.options); // create connection //let callbackAlreadyCalled = false; const connectionManager = new MSSqlConnectionPoolManager(); let transactionIsolationLevel = null; if (connectionOptions && connectionOptions.options) { if (Object.prototype.hasOwnProperty.call(connectionOptions.options, 'transactionIsolationLevel')) { const level = connectionOptions.options.transactionIsolationLevel; transactionIsolationLevel = new TransactionIsolationLevelFormatter().format(level); } } connectionManager.get(connectionOptions, function (err, connection) { //callbackAlreadyCalled = true; if (err) { // destroy connection self.rawConnection = null; common.TraceUtils.error('An error occurred while connecting to database server'); common.TraceUtils.error(err); return callback(err); } // set connection self.rawConnection = connection; if (transactionIsolationLevel == null) { return callback(); } return self.execute(transactionIsolationLevel, [], function (err) { if (err) { return callback(err); } return callback(); }); }); } /** * Opens a database connection */ openAsync() { return new Promise((resolve, reject) => { return this.open(err => { if (err) { return reject(err); } return resolve(); }); }); } /** * * @param {Function=} callback */ close(callback) { const self = this; if (self.rawConnection != null) { common.TraceUtils.debug('Closing database connection'); } self.rawConnection = null; // auto-rollback transaction /** * @type {Transaction} */ const transaction = self.transaction; if (transaction != null) { common.TraceUtils.warn('A connection is being closed while a transaction is still active. The transaction will be rolled back.'); // if transaction has an active request, transaction rollback is disabled if (transaction._activeRequest) { // exit callback return callback(); } common.TraceUtils.debug('MSSqlAdapter.close()', 'Rolling back transaction'); // otherwise, rollback transaction try { return transaction.rollback(function (err) { if (err) { common.TraceUtils.error('An error occurred while rolling back the transaction.'); common.TraceUtils.error(err); } return callback(); }); } catch (err) { return callback(err); } finally { self.transaction = null; common.TraceUtils.debug('MSSqlAdapter.close()', 'Transaction has been destroyed'); } } // close connection and return return callback(); } /** * Closes the current database connection */ closeAsync() { return new Promise((resolve, reject) => { return this.close(err => { if (err) { return reject(err); } return resolve(); }); }); } /** * Begins a data transaction and executes the given function * @param fn {Function} * @param callback {Function} */ executeInTransaction(fn, callback) { const self = this; //ensure callback callback = callback || function () {}; //ensure that database connection is open if (self.disposed === true) { if (self.transaction) { try { return self.transaction.rollback(function (rollbackErr) { if (rollbackErr) { return callback(rollbackErr); } common.TraceUtils.debug('Transaction has been rolled back'); return callback(new ConnectionStateError()); }); } catch (err) { return callback(err); } finally { self.transaction = null; common.TraceUtils.debug('MSSqlAdapter.executeInTransaction()', 'Transaction has been destroyed'); } } return callback(new ConnectionStateError()); } self.open(function (err) { if (err) { callback.call(self, err); return; } //check if transaction is already defined (as object) if (self.transaction) { //so invoke method fn.call(self, function (err) { //call callback callback.call(self, err); }); } else { //create transaction self.transaction = new mssql.Transaction(self.rawConnection); //begin transaction common.TraceUtils.debug('MSSqlAdapter.executeInTransaction()', 'Beginning transaction'); self.transaction.begin(function (err) { //error check (?) let rolledBack = false; if (self.transaction) { self.transaction.on('rollback', aborted => { common.TraceUtils.debug('transaction.on("rollback")', 'Transaction has been rolled back'); rolledBack = true; }); } if (err) { common.TraceUtils.error(err); return callback(err); } else { try { fn.call(self, function (err) { try { if (err) { if (self.transaction) { if (rolledBack) { common.TraceUtils.warn('The transaction has been already rolled back. The operation will exit with error.'); return callback(err); } common.TraceUtils.debug('MSSqlAdapter.executeInTransaction()', 'Rolling back transaction'); try { return self.transaction.rollback(function (rollbackErr) { if (rollbackErr) { return callback(rollbackErr); } common.TraceUtils.debug('MSSqlAdapter.executeInTransaction()', 'Transaction has been rolled back'); return callback(err); }); } catch (err) { return callback(err); } finally { self.transaction = null; common.TraceUtils.debug('MSSqlAdapter.executeInTransaction()', 'Transaction has been destroyed'); } } return callback(err); } else { if (typeof self.transaction === 'undefined' || self.transaction === null) { return callback(new Error('Database transaction cannot be empty on commit.')); } common.TraceUtils.debug('MSSqlAdapter.executeInTransaction()', 'Committing transaction'); return self.transaction.commit(function (err) { if (err) { common.TraceUtils.debug('An error occurred while committing the transaction'); try { return self.transaction.rollback(function (rollbackErr) { if (rollbackErr) { return callback(rollbackErr); } common.TraceUtils.debug('MSSqlAdapter.executeInTransaction()', 'Transaction has been rolled back'); return callback(err); }); } catch (err) { return callback(err); } finally { self.transaction = null; common.TraceUtils.debug('MSSqlAdapter.executeInTransaction()', 'Transaction has been destroyed'); } } self.transaction = null; return self.committed.emit({ target: self }).then(() => { return callback(); }).catch(err => { return callback(err); }); }); } } catch (e) { return callback(e); } }); } catch (e) { return callback(e); } } }); } }); } /** * Begins a data transaction and executes the given function * @param func {Function} */ executeInTransactionAsync(func) { return new Promise((resolve, reject) => { return this.executeInTransaction(callback => { return func.call(this).then(res => { return callback(null, res); }).catch(err => { return callback(err); }); }, (err, res) => { if (err) { return reject(err); } return resolve(res); }); }); } /** * Produces a new identity value for the given entity and attribute. * @param entity {String} The target entity name * @param attribute {String} The target attribute * @param callback {Function} */ selectIdentity(entity, attribute, callback) { // create a dedicated connection or use current connection if transaction is empty const db = this; const sequenceName = `${entity}_${attribute}_seq`; /** * @type {MSSqlFormatter} */ const formatter = db.getFormatter(); const nextValueSql = `SELECT NEXT VALUE FOR ${formatter.escapeName(sequenceName)} AS [value];`; const entityAndSchema = entity.match(new RegExp(query.ObjectNameValidator.validator.pattern, 'g')); let schema = 'dbo'; let table = entity; if (entityAndSchema && entityAndSchema.length > 1) { [schema, table] = entityAndSchema.slice(-2); } // get max value for the given entity and attribute if sequence does not exist return db.executeAsync(` IF NOT EXISTS (SELECT * FROM [sysobjects] WHERE [name] = ${formatter.escape(sequenceName)} AND [type] = 'SO') IF EXISTS(SELECT [c0].* FROM [syscolumns] AS [c0] INNER JOIN sysobjects s0 ON c0.[id]=s0.[id] AND [s0].[type]='U' WHERE [c0].[name]=${formatter.escape(attribute)} AND [s0].name = ${formatter.escape(table)} AND SCHEMA_NAME(s0.[uid]) = ${formatter.escape(schema)}) EXEC sp_executesql N'SELECT ISNULL(MAX(${formatter.escapeName(attribute)}), 0) AS [value] FROM ${formatter.escapeName(entity)}'`, null).then(results => { const startValue = results && results.length > 0 ? results[0].value : 1; // create sequence if it does not exist return db.executeAsync(` IF NOT EXISTS (SELECT * FROM [sysobjects] WHERE [name] = ${formatter.escape(sequenceName)} AND [type] = 'SO') CREATE SEQUENCE ${formatter.escapeName(sequenceName)} START WITH ${startValue} INCREMENT BY 1;`, null).then(() => { // get next value for sequence return db.executeAsync(nextValueSql, null).then(([result]) => { // return result[0] return callback(null, parseInt(result.value, 10) + 1); }); }); }).catch(err => { return callback(err); }); } /** * @param {string} entity * @param {string} attribute * @returns Promise<any> */ selectIdentityAsync(entity, attribute) { return new Promise((resolve, reject) => { return this.selectIdentity(entity, attribute, (err, res) => { if (err) { return reject(err); } return resolve(res); }); }); } /** * @param {*} query * @param {*} values * @param {function} callback */ execute(query, values, callback) { const self = this; let sql = null; try { if (typeof query === 'string') { //get raw sql statement sql = query; } else { //format query expression or any object that may act as query expression const formatter = new MSSqlFormatter(); if (query instanceof RetryQuery) { sql = typeof query.query === 'string' ? query.query : formatter.format(query.query); } else { sql = formatter.format(query); } } //validate sql statement if (typeof sql !== 'string') { callback.call(self, new Error('The executing command is of the wrong type or empty.')); return; } if (self.disposed === true) { return callback(new ConnectionStateError()); } //ensure connection self.open(function (err) { if (err) { callback.call(self, err); } else { // log statement (optional) let startTime; if (process.env.NODE_ENV === 'development') { startTime = new Date().getTime(); } // execute raw command const request = self.transaction ? new mssql.Request(self.transaction) : new mssql.Request(self.rawConnection); let preparedSql = self.prepare(sql, values); if (typeof query.$insert !== 'undefined') preparedSql += ';SELECT SCOPE_IDENTITY() as insertId'; request.query(preparedSql, function (err, result) { if (err) { if (err.code === 'ESOCKET' || err.code === 'ETIMEOUT') { // connection is closed or timeout const shouldRetry = typeof self.options.retry === 'number' && self.options.retry > 0; if (shouldRetry) { const retry = self.options.retry; let retryInterval = 1000; if (typeof self.options.retryInterval === 'number' && self.options.retryInterval > 0) { retryInterval = self.options.retryInterval; } const retryQuery = query instanceof RetryQuery === false ? new RetryQuery(query) : query; // validate retry option if (typeof retryQuery.retry === 'number' && retryQuery.retry >= retry * retryInterval) { // the retries have been exhausted delete retryQuery.retry; // trace error common.TraceUtils.error(`SQL (Execution Error):${err.message}, ${preparedSql}`); // return callback with error return callback(err); } // retry retryQuery.retry += retryInterval; common.TraceUtils.warn(`'SQL Error:${preparedSql}. Retrying in ${retryQuery.retry} ms.'`); return setTimeout(function () { return self.execute(retryQuery, values, callback); }, retryQuery.retry); } } // otherwise, return callback with error common.TraceUtils.error(`SQL (Execution Error):${err.message}, ${preparedSql}`); return callback(err); } if (process.env.NODE_ENV === 'development') { common.TraceUtils.debug(sprintfJs.sprintf('SQL (Execution Time:%sms):%s, Parameters:%s', new Date().getTime() - startTime, sql, JSON.stringify(values))); } if (typeof query.$insert === 'undefined') { if (result.recordsets.length === 1) { return callback(err, Array.from(result.recordset)); } return callback(err, result.recordsets.map(function (recordset) { return Array.from(recordset); })); } else { if (result && result.recordset) { const insertId = result.recordset[0] && result.recordset[0].insertId; if (insertId != null) { return callback(err, { insertId }); } } return callback(err, result); } }); } }); } catch (err) { callback.bind(self)(err); } } /** * @param query {*} * @param values {*} * @returns Promise<any> */ executeAsync(query, values) { return new Promise((resolve, reject) => { return this.execute(query, values, (err, res) => { if (err) { return reject(err); } return resolve(res); }); }); } /** * Formats an object based on the format string provided. Valid formats are: * %t : Formats a field and returns field type definition * %f : Formats a field and returns field name * @param format {string} * @param obj {*} */ format(format, obj) { let result = format; if (/%t/.test(format)) result = result.replace(/%t/g, this.formatType(obj)); if (/%f/.test(format)) result = result.replace(/%f/g, obj.name); return result; } /** * @deprecated * @param {string} format * @param {*} obj */ static format(format, obj) { new MSSqlAdapter().format(format, obj); } formatType(field) { const size = parseInt(field.size); const scale = parseInt(field.scale); let s = 'varchar(512) NULL'; const type = field.type; switch (type) { case 'Boolean': s = 'bit'; break; case 'Byte': s = 'tinyint'; break; case 'Number': case 'Float': s = 'float'; break; case 'Counter': return 'int IDENTITY (1,1) NOT NULL'; case 'Currency': s = size > 0 ? size <= 10 ? 'smallmoney' : 'money' : 'money'; break; case 'Decimal': s = sprintfJs.sprintf('decimal(%s,%s)', size > 0 ? size : 19, scale > 0 ? scale : 4); break; case 'Date': s = 'date'; break; case 'DateTime': s = 'datetimeoffset'; break; case 'Time': s = 'time'; break; case 'Integer': s = 'int'; break; case 'Duration': s = size > 0 ? sprintfJs.sprintf('varchar(%s)', size) : 'varchar(48)'; break; case 'URL': if (size > 0) s = sprintfJs.sprintf('varchar(%s)', size);else s = 'varchar(512)'; break; case 'Text': if (size > 0) s = sprintfJs.sprintf('varchar(%s)', size);else s = 'varchar(512)'; break; case 'Note': if (size > 0) s = sprintfJs.sprintf('varchar(%s)', size);else s = 'text'; break; case 'Json': s = 'nvarchar(max)'; break; case 'Image': case 'Binary': s = 'binary'; break; case 'Guid': s = 'varchar(36)'; break; case 'Short': s = 'smallint'; break; default: s = 'int'; break; } s += field.nullable === undefined ? ' null' : field.nullable ? ' null' : ' not null'; return s; } /** * @param {string} name * @param {QueryExpression} query * @param {Function} callback */ /** * @deprecated * @param {*} field */ static formatType(field) { new MSSqlAdapter().formatType(field); } createView(name, query, callback) { return this.view(name).create(query, callback); } /** * Initializes database table helper. * @param {string} name - The table name * @returns {{exists: Function, version: Function, columns: Function, create: Function, add: Function, change: Function}} */ table(name) { const self = this; let owner; let table; const matches = /(\w+)\.(\w+)/.exec(name); if (matches) { //get schema owner owner = matches[1]; //get table name table = matches[2]; } else { //get view name table = name; //get default owner owner = 'dbo'; } return { /** * @param {Function} callback */ exists: function (callback) { callback = callback || function () {}; self.execute('SELECT COUNT(*) AS [count] FROM sysobjects WHERE [name]=? AND [type]=\'U\' AND SCHEMA_NAME([uid])=?', [table, owner], function (err, result) { if (err) { return callback(err); } callback(null, result[0].count === 1); }); }, existsAsync: function () { return new Promise((resolve, reject) => { this.exists((err, value) => { if (err) { return reject(err); } return resolve(value); }); }); }, /** * @param {function(Error,string=)} callback */ version: function (callback) { callback = callback || function () {}; self.execute('SELECT MAX([version]) AS [version] FROM [migrations] WHERE [appliesTo]=?', [table], function (err, result) { if (err) { return callback(err); } if (result.length === 0) callback(null, '0.0');else callback(null, result[0].version || '0.0'); }); }, versionAsync: function () { return new Promise((resolve, reject) => { this.version((err, value) => { if (err) { return reject(err); } return resolve(value); }); }); }, /** * @param {function(Error=,Array=)} callback */ columns: function (callback) { callback = callback || function () {}; self.execute('SELECT c0.[name] AS [name], c0.[isnullable] AS [nullable], c0.[length] AS [size], c0.[prec] AS [precision], ' + 'c0.[scale] AS [scale], t0.[name] AS type, t0.[name] + CASE WHEN t0.[variable]=0 THEN \'\' ELSE \'(\' + CONVERT(varchar,c0.[length]) + \')\' END AS [type1], ' + 'CASE WHEN p0.[indid]>0 THEN 1 ELSE 0 END [primary] FROM syscolumns c0 INNER JOIN systypes t0 ON c0.[xusertype] = t0.[xusertype] ' + 'INNER JOIN sysobjects s0 ON c0.[id]=s0.[id] LEFT JOIN (SELECT k0.* FROM sysindexkeys k0 INNER JOIN (SELECT i0.* FROM sysindexes i0 ' + 'INNER JOIN sysobjects s0 ON i0.[id]=s0.[id] WHERE i0.[status]=2066) x0 ON k0.[id]=x0.[id] AND k0.[indid]=x0.[indid] ) p0 ON c0.[id]=p0.[id] ' + 'AND c0.[colid]=p0.[colid] WHERE s0.[name]=? AND s0.[xtype]=\'U\' AND SCHEMA_NAME(s0.[uid])=?', [table, owner], function (err, result) { if (err) { return callback(err); } callback(null, result); }); }, columnsAsync: function () { return new Promise((resolve, reject) => { this.columns((err, res) => { if (err) { return reject(err); } return resolve(res); }); }); }, /** * @param {{name:string,type:string,primary:boolean|number,nullable:boolean|number,size:number, scale:number,precision:number,oneToMany:boolean}[]|*} fields * @param callback */ create: function (fields, callback) { callback = callback || function () {}; fields = fields || []; if (!Array.isArray(fields)) { return callback(new Error('Invalid argument type. Expected Array.')); } if (fields.length === 0) { return callback(new Error('Invalid argument. Fields collection cannot be empty.')); } let strFields = fields.filter(x => { return !x.oneToMany; }).map(x => { return self.format('[%f] %t', x); }).join(', '); //add primary key constraint const strPKFields = fields.filter(x => { return x.primary === true || x.primary === 1; }).map(x => { return self.format('[%f]', x); }).join(', '); if (strPKFields.length > 0) { strFields += ', ' + sprintfJs.sprintf('PRIMARY KEY (%s)', strPKFields); } const strTable = sprintfJs.sprintf('[%s].[%s]', owner, table); const sql = sprintfJs.sprintf('CREATE TABLE %s (%s)', strTable, strFields); self.execute(sql, null, function (err) { callback(err); }); }, createAsync: function (fields) { return new Promise((resolve, reject) => { this.create(fields, (err, res) => { if (err) { return reject(err); } return resolve(res); }); }); }, /** * Alters the table by adding an array of fields * @param {{name:string,type:string,primary:boolean|number,nullable:boolean|number,size:number,oneToMany:boolean}[]|*} fields * @param callback */ add: function (fields, callback) { callback = callback || function () {}; callback = callback || function () {}; fields = fields || []; if (!Array.isArray(fields)) { //invalid argument exception return callback(new Error('Invalid argument type. Expected Array.')); } if (fields.length === 0) { //do nothing return callback(); } const strTable = sprintfJs.sprintf('[%s].[%s]', owner, table); //generate SQL statement const sql = fields.map(x => { return self.format('ALTER TABLE ' + strTable + ' ADD [%f] %t', x); }).join(';'); self.execute(sql, [], function (err) { callback(err); }); }, addAsync: function (fields) { return new Promise((resolve, reject) => { this.add(fields, (err, res) => { if (err) { return reject(err); } return resolve(res); }); }); }, /** * Alters the table by modifying an array of fields * @param {{name:string,type:string,primary:boolean|number,nullable:boolean|number,size:number,oneToMany:boolean}[]|*} fields * @param callback */ change: function (fields, callback) { callback = callback || function () {}; callback = callback || function () {}; fields = fields || []; if (!Array.isArray(fields)) { //invalid argument exception return callback(new Error('Invalid argument type. Expected Array.')); } if (fields.length === 0) { //do nothing return callback(); } const strTable = sprintfJs.sprintf('[%s].[%s]', owner, table); //generate SQL statement const sql = fields.map(x => { return self.format('ALTER TABLE ' + strTable + ' ALTER COLUMN [%f] %t', x); }).join(';'); self.execute(sql, [], function (err) { callback(err); }); }, changeAsync: function (fields) { return new Promise((resolve, reject) => { this.change(field