@themost/mssql
Version:
MOST Web Framework MSSQL Data Adapter
1,509 lines (1,475 loc) • 71.4 kB
JavaScript
'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