@vuept/loopback-connector-mssql
Version:
Microsoft SQL Server connector for LoopBack
1,831 lines (1,517 loc) • 48 kB
JavaScript
const EventEmitter = require('events').EventEmitter
const debug = require('debug')('mssql:base')
const gp = require('generic-pool')
const TYPES = require('./datatypes').TYPES
const declare = require('./datatypes').declare
const ISOLATION_LEVEL = require('./isolationlevel')
const Table = require('./table')
const ConnectionString = require('./connectionstring')
const IDS = require('./utils').IDS
let globalConnection = null
let PromiseLibrary = Promise
const globalConnectionHandlers = {}
const map = []
const driver = {}
/**
* Register you own type map.
*
* @path module.exports.map
* @param {*} jstype JS data type.
* @param {*} sqltype SQL data type.
*/
map.register = function (jstype, sqltype) {
for (let index = 0; index < this.length; index++) {
let item = this[index]
if (item.js === jstype) {
this.splice(index, 1)
break
}
}
this.push({
js: jstype,
sql: sqltype
})
return null
}
map.register(String, TYPES.NVarChar)
map.register(Number, TYPES.Int)
map.register(Boolean, TYPES.Bit)
map.register(Date, TYPES.DateTime)
map.register(Buffer, TYPES.VarBinary)
map.register(Table, TYPES.TVP)
/**
* @ignore
*/
let getTypeByValue = function (value) {
if ((value === null) || (value === undefined)) { return TYPES.NVarChar }
switch (typeof value) {
case 'string':
for (var item of Array.from(map)) {
if (item.js === String) {
return item.sql
}
}
return TYPES.NVarChar
case 'number':
if (value % 1 === 0) {
return TYPES.Int
} else {
return TYPES.Float
}
case 'boolean':
for (item of Array.from(map)) {
if (item.js === Boolean) {
return item.sql
}
}
return TYPES.Bit
case 'object':
for (item of Array.from(map)) {
if (value instanceof item.js) {
return item.sql
}
}
return TYPES.NVarChar
default:
return TYPES.NVarChar
}
}
/**
* Class ConnectionPool.
*
* Internally, each `Connection` instance is a separate pool of TDS connections. Once you create a new `Request`/`Transaction`/`Prepared Statement`, a new TDS connection is acquired from the pool and reserved for desired action. Once the action is complete, connection is released back to the pool.
*
* @property {Boolean} connected If true, connection is established.
* @property {Boolean} connecting If true, connection is being established.
*
* @fires ConnectionPool#connect
* @fires ConnectionPool#close
*/
class ConnectionPool extends EventEmitter {
/**
* Create new Connection.
*
* @param {Object|String} config Connection configuration object or connection string.
* @param {basicCallback} [callback] A callback which is called after connection has established, or an error has occurred.
*/
constructor (config, callback) {
super()
IDS.add(this, 'ConnectionPool')
debug('pool(%d): created', IDS.get(this))
this.config = config
this._connected = false
this._connecting = false
if (typeof this.config === 'string') {
try {
this.config = ConnectionString.resolve(this.config, driver.name)
} catch (ex) {
if (typeof callback === 'function') {
return callback(ex)
}
throw ex
}
}
// set defaults
this.config.port = this.config.port || 1433
this.config.options = this.config.options || {}
this.config.stream = this.config.stream || false
this.config.parseJSON = this.config.parseJSON || false
if (/^(.*)\\(.*)$/.exec(this.config.server)) {
this.config.server = RegExp.$1
this.config.options.instanceName = RegExp.$2
}
if (typeof callback === 'function') {
this.connect(callback)
}
}
get connected () {
return this._connected
}
get connecting () {
return this._connecting
}
/**
* Acquire connection from this connection pool.
*
* @param {ConnectionPool|Transaction|PreparedStatement} requester Requester.
* @param {acquireCallback} [callback] A callback which is called after connection has been acquired, or an error has occurred. If omited, method returns Promise.
* @return {ConnectionPool|Promise}
*/
acquire (requester, callback) {
if (typeof callback === 'function') {
this._acquire().then(connection => callback(null, connection, this.config)).catch(callback)
return this
}
return this._acquire()
}
_acquire () {
if (!this.pool) {
return Promise.reject(new ConnectionError('Connection not yet open.', 'ENOTOPEN'))
}
return this.pool.acquire()
}
/**
* Release connection back to the pool.
*
* @param {Connection} connection Previously acquired connection.
* @return {ConnectionPool}
*/
release (connection) {
debug('connection(%d): released', IDS.get(connection))
if (this.pool) {
this.pool.release(connection)
}
return this
}
/**
* Creates a new connection pool with one active connection. This one initial connection serves as a probe to find out whether the configuration is valid.
*
* @param {basicCallback} [callback] A callback which is called after connection has established, or an error has occurred. If omited, method returns Promise.
* @return {ConnectionPool|Promise}
*/
connect (callback) {
if (typeof callback === 'function') {
this._connect(callback)
return this
}
return new PromiseLibrary((resolve, reject) => {
return this._connect(err => {
if (err) return reject(err)
resolve(this)
})
})
}
/**
* @private
* @param {basicCallback} callback
*/
_connect (callback) {
if (this._connected) {
return callback(new ConnectionError('Database is already connected! Call close before connecting to different database.', 'EALREADYCONNECTED'))
}
if (this._connecting) {
return callback(new ConnectionError('Already connecting to database! Call close before connecting to different database.', 'EALREADYCONNECTING'))
}
this._connecting = true
debug('pool(%d): connecting', IDS.get(this))
// create one testing connection to check if everything is ok
this._poolCreate().then((connection) => {
debug('pool(%d): connected', IDS.get(this))
this._poolDestroy(connection)
if (!this._connecting) {
// close was called before connection was established
return // exit silently
}
// prepare pool
this.pool = gp.createPool({
create: this._poolCreate.bind(this),
validate: this._poolValidate.bind(this),
destroy: this._poolDestroy.bind(this)
}, Object.assign({
max: 10,
min: 0,
evictionRunIntervalMillis: 1000,
idleTimeoutMillis: 30000,
testOnBorrow: true
}, this.config.pool))
this.pool.on('factoryCreateError', this.emit.bind(this, 'error'))
this.pool.on('factoryDestroyError', this.emit.bind(this, 'error'))
this._connecting = false
this._connected = true
callback(null)
}).catch(err => {
this._connecting = false
callback(err)
})
}
/**
* Close all active connections in the pool.
*
* @param {basicCallback} [callback] A callback which is called after connection has closed, or an error has occurred. If omited, method returns Promise.
* @return {ConnectionPool|Promise}
*/
close (callback) {
if (typeof callback === 'function') {
this._close(callback)
return this
}
return new PromiseLibrary((resolve, reject) => {
this._close(err => {
if (err) return reject(err)
resolve(this)
})
})
}
/**
* @private
* @param {basicCallback} callback
*/
_close (callback) {
this._connecting = this._connected = false
if (!this.pool) return setImmediate(callback, null)
const pool = this.pool
this.pool.drain().then(() => {
pool.clear()
callback(null)
})
this.pool = null
}
/**
* Returns new request using this connection.
*
* @return {Request}
*/
request () {
return new driver.Request(this)
}
/**
* Returns new transaction using this connection.
*
* @return {Transaction}
*/
transaction () {
return new driver.Transaction(this)
}
/**
* Creates a new query using this connection from a tagged template string.
*
* @variation 1
* @param {Array} strings Array of string literals.
* @param {...*} keys Values.
* @return {Request}
*/
/**
* Execute the SQL command.
*
* @variation 2
* @param {String} command T-SQL command to be executed.
* @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise.
* @return {Request|Promise}
*/
query () {
if (typeof arguments[0] === 'string') { return new driver.Request(this).query(arguments[0], arguments[1]) }
const values = Array.prototype.slice.call(arguments)
const strings = values.shift()
return new driver.Request(this)._template(strings, values, 'query')
}
/**
* Creates a new batch using this connection from a tagged template string.
*
* @variation 1
* @param {Array} strings Array of string literals.
* @param {...*} keys Values.
* @return {Request}
*/
/**
* Execute the SQL command.
*
* @variation 2
* @param {String} command T-SQL command to be executed.
* @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise.
* @return {Request|Promise}
*/
batch () {
if (typeof arguments[0] === 'string') { return new driver.Request(this).batch(arguments[0], arguments[1]) }
const values = Array.prototype.slice.call(arguments)
const strings = values.shift()
return new driver.Request(this)._template(strings, values, 'batch')
}
}
/**
* Class PreparedStatement.
*
* IMPORTANT: Rememeber that each prepared statement means one reserved connection from the pool. Don't forget to unprepare a prepared statement!
*
* @property {String} statement Prepared SQL statement.
*/
class PreparedStatement extends EventEmitter {
/**
* Creates a new Prepared Statement.
*
* @param {ConnectionPool|Transaction} [holder]
*/
constructor (parent) {
super()
IDS.add(this, 'PreparedStatement')
debug('ps(%d): created', IDS.get(this))
this.parent = parent || globalConnection
this._handle = 0
this.prepared = false
this.parameters = {}
}
get connected () {
return this.parent.connected
}
/**
* Acquire connection from connection pool.
*
* @param {Request} request Request.
* @param {ConnectionPool~acquireCallback} [callback] A callback which is called after connection has established, or an error has occurred. If omited, method returns Promise.
* @return {PreparedStatement|Promise}
*/
acquire (request, callback) {
if (!this._acquiredConnection) {
setImmediate(callback, new PreparedStatementError('Statement is not prepared. Call prepare() first.', 'ENOTPREPARED'))
return this
}
if (this._activeRequest) {
setImmediate(callback, new TransactionError("Can't acquire connection for the request. There is another request in progress.", 'EREQINPROG'))
return this
}
this._activeRequest = request
setImmediate(callback, null, this._acquiredConnection, this._acquiredConfig)
return this
}
/**
* Release connection back to the pool.
*
* @param {Connection} connection Previously acquired connection.
* @return {PreparedStatement}
*/
release (connection) {
if (connection === this._acquiredConnection) {
this._activeRequest = null
}
return this
}
/**
* Add an input parameter to the prepared statement.
*
* @param {String} name Name of the input parameter without @ char.
* @param {*} type SQL data type of input parameter.
* @return {PreparedStatement}
*/
input (name, type) {
if ((/(--| |\/\*|\*\/|')/).test(name)) {
throw new PreparedStatementError(`SQL injection warning for param '${name}'`, 'EINJECT')
}
if (arguments.length < 2) {
throw new PreparedStatementError('Invalid number of arguments. 2 arguments expected.', 'EARGS')
}
if (type instanceof Function) {
type = type()
}
this.parameters[name] = {
name,
type: type.type,
io: 1,
length: type.length,
scale: type.scale,
precision: type.precision,
tvpType: type.tvpType
}
return this
}
/**
* Add an output parameter to the prepared statement.
*
* @param {String} name Name of the output parameter without @ char.
* @param {*} type SQL data type of output parameter.
* @return {PreparedStatement}
*/
output (name, type) {
if (/(--| |\/\*|\*\/|')/.test(name)) {
throw new PreparedStatementError(`SQL injection warning for param '${name}'`, 'EINJECT')
}
if (arguments.length < 2) {
throw new PreparedStatementError('Invalid number of arguments. 2 arguments expected.', 'EARGS')
}
if (type instanceof Function) type = type()
this.parameters[name] = {
name,
type: type.type,
io: 2,
length: type.length,
scale: type.scale,
precision: type.precision
}
return this
}
/**
* Prepare a statement.
*
* @param {String} statement SQL statement to prepare.
* @param {basicCallback} [callback] A callback which is called after preparation has completed, or an error has occurred. If omited, method returns Promise.
* @return {PreparedStatement|Promise}
*/
prepare (statement, callback) {
if (typeof callback === 'function') {
this._prepare(statement, callback)
return this
}
return new PromiseLibrary((resolve, reject) => {
this._prepare(statement, err => {
if (err) return reject(err)
resolve(this)
})
})
}
/**
* @private
* @param {String} statement
* @param {basicCallback} callback
*/
_prepare (statement, callback) {
debug('ps(%d): prepare', IDS.get(this))
if (typeof statement === 'function') {
callback = statement
statement = undefined
}
if (this.prepared) {
return setImmediate(callback, new PreparedStatementError('Statement is already prepared.', 'EALREADYPREPARED'))
}
this.statement = statement || this.statement
this.parent.acquire(this, (err, connection, config) => {
if (err) return callback(err)
this._acquiredConnection = connection
this._acquiredConfig = config
const req = new driver.Request(this)
req.stream = false
req.output('handle', TYPES.Int)
req.input('params', TYPES.NVarChar, ((() => {
let result = []
for (let name in this.parameters) {
let param = this.parameters[name]
result.push(`@${name} ${declare(param.type, param)}${param.io === 2 ? ' output' : ''}`)
}
return result
})()).join(','))
req.input('stmt', TYPES.NVarChar, this.statement)
req.execute('sp_prepare', (err, result) => {
if (err) {
this.parent.release(this._acquiredConnection)
this._acquiredConnection = null
this._acquiredConfig = null
return callback(err)
}
debug('ps(%d): prepared', IDS.get(this))
this._handle = result.output.handle
this.prepared = true
callback(null)
})
})
}
/**
* Execute a prepared statement.
*
* @param {Object} values An object whose names correspond to the names of parameters that were added to the prepared statement before it was prepared.
* @param {basicCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise.
* @return {Request|Promise}
*/
execute (values, callback) {
if (this.stream || (typeof callback === 'function')) {
return this._execute(values, callback)
}
return new PromiseLibrary((resolve, reject) => {
this._execute(values, (err, recordset) => {
if (err) return reject(err)
resolve(recordset)
})
})
}
/**
* @private
* @param {Object} values
* @param {basicCallback} callback
*/
_execute (values, callback) {
const req = new driver.Request(this)
req.stream = this.stream
req.input('handle', TYPES.Int, this._handle)
// copy parameters with new values
for (let name in this.parameters) {
let param = this.parameters[name]
req.parameters[name] = {
name,
type: param.type,
io: param.io,
value: values[name],
length: param.length,
scale: param.scale,
precision: param.precision
}
}
req.execute('sp_execute', (err, result) => {
if (err) return callback(err)
callback(null, result)
})
return req
}
/**
* Unprepare a prepared statement.
*
* @param {basicCallback} [callback] A callback which is called after unpreparation has completed, or an error has occurred. If omited, method returns Promise.
* @return {PreparedStatement|Promise}
*/
unprepare (callback) {
if (typeof callback === 'function') {
this._unprepare(callback)
return this
}
return new PromiseLibrary((resolve, reject) => {
this._unprepare(err => {
if (err) return reject(err)
resolve()
})
})
}
/**
* @private
* @param {basicCallback} callback
*/
_unprepare (callback) {
debug('ps(%d): unprepare', IDS.get(this))
if (!this.prepared) {
return setImmediate(callback, new PreparedStatementError('Statement is not prepared. Call prepare() first.', 'ENOTPREPARED'))
}
if (this._activeRequest) {
return setImmediate(callback, new TransactionError("Can't unprepare the statement. There is a request in progress.", 'EREQINPROG'))
}
const req = new driver.Request(this)
req.stream = false
req.input('handle', TYPES.Int, this._handle)
req.execute('sp_unprepare', err => {
if (err) return callback(err)
this.parent.release(this._acquiredConnection)
this._acquiredConnection = null
this._acquiredConfig = null
this._handle = 0
this.prepared = false
debug('ps(%d): unprepared', IDS.get(this))
return callback(null)
})
}
}
/**
* Class Transaction.
*
* @property {Number} isolationLevel Controls the locking and row versioning behavior of TSQL statements issued by a connection. READ_COMMITTED by default.
* @property {String} name Transaction name. Empty string by default.
*
* @fires Transaction#begin
* @fires Transaction#commit
* @fires Transaction#rollback
*/
class Transaction extends EventEmitter {
/**
* Create new Transaction.
*
* @param {Connection} [holder] If ommited, global connection is used instead.
*/
constructor (parent) {
super()
IDS.add(this, 'Transaction')
debug('transaction(%d): created', IDS.get(this))
this.parent = parent || globalConnection
this.isolationLevel = ISOLATION_LEVEL.READ_COMMITTED
this.name = ''
}
get connected () {
return this.parent.connected
}
/**
* Acquire connection from connection pool.
*
* @param {Request} request Request.
* @param {ConnectionPool~acquireCallback} [callback] A callback which is called after connection has established, or an error has occurred. If omited, method returns Promise.
* @return {Transaction|Promise}
*/
acquire (request, callback) {
if (!this._acquiredConnection) {
setImmediate(callback, new TransactionError('Transaction has not begun. Call begin() first.', 'ENOTBEGUN'))
return this
}
if (this._activeRequest) {
setImmediate(callback, new TransactionError("Can't acquire connection for the request. There is another request in progress.", 'EREQINPROG'))
return this
}
this._activeRequest = request
setImmediate(callback, null, this._acquiredConnection, this._acquiredConfig)
return this
}
/**
* Release connection back to the pool.
*
* @param {Connection} connection Previously acquired connection.
* @return {Transaction}
*/
release (connection) {
if (connection === this._acquiredConnection) {
this._activeRequest = null
}
return this
}
/**
* Begin a transaction.
*
* @param {Number} [isolationLevel] Controls the locking and row versioning behavior of TSQL statements issued by a connection.
* @param {basicCallback} [callback] A callback which is called after transaction has began, or an error has occurred. If omited, method returns Promise.
* @return {Transaction|Promise}
*/
begin (isolationLevel, callback) {
if (isolationLevel instanceof Function) {
callback = isolationLevel
isolationLevel = undefined
}
if (typeof callback === 'function') {
this._begin(isolationLevel, err => {
if (!err) {
this.emit('begin')
}
callback(err)
})
return this
}
return new PromiseLibrary((resolve, reject) => {
this._begin(isolationLevel, err => {
if (err) return reject(err)
this.emit('begin')
resolve(this)
})
})
}
/**
* @private
* @param {Number} [isolationLevel]
* @param {basicCallback} [callback]
* @return {Transaction}
*/
_begin (isolationLevel, callback) {
if (this._acquiredConnection) {
return setImmediate(callback, new TransactionError('Transaction has already begun.', 'EALREADYBEGUN'))
}
this._aborted = false
this._rollbackRequested = false
this.isolationLevel = isolationLevel || this.isolationLevel
setImmediate(callback)
}
/**
* Commit a transaction.
*
* @param {basicCallback} [callback] A callback which is called after transaction has commited, or an error has occurred. If omited, method returns Promise.
* @return {Transaction|Promise}
*/
commit (callback) {
if (typeof callback === 'function') {
this._commit(err => {
if (!err) {
this.emit('commit')
}
callback(err)
})
return this
}
return new PromiseLibrary((resolve, reject) => {
this._commit(err => {
if (err) return reject(err)
this.emit('commit')
resolve()
})
})
}
/**
* @private
* @param {basicCallback} [callback]
* @return {Transaction}
*/
_commit (callback) {
if (this._aborted) {
return setImmediate(callback, new TransactionError('Transaction has been aborted.', 'EABORT'))
}
if (!this._acquiredConnection) {
return setImmediate(callback, new TransactionError('Transaction has not begun. Call begin() first.', 'ENOTBEGUN'))
}
if (this._activeRequest) {
return setImmediate(callback, new TransactionError("Can't commit transaction. There is a request in progress.", 'EREQINPROG'))
}
setImmediate(callback)
}
/**
* Returns new request using this transaction.
*
* @return {Request}
*/
request () {
return new driver.Request(this)
}
/**
* Rollback a transaction.
*
* @param {basicCallback} [callback] A callback which is called after transaction has rolled back, or an error has occurred. If omited, method returns Promise.
* @return {Transaction|Promise}
*/
rollback (callback) {
if (typeof callback === 'function') {
this._rollback(err => {
if (!err) {
this.emit('rollback', this._aborted)
}
callback(err)
})
return this
}
return new PromiseLibrary((resolve, reject) => {
return this._rollback(err => {
if (err) return reject(err)
this.emit('rollback', this._aborted)
resolve()
})
}
)
}
/**
* @private
* @param {basicCallback} [callback]
* @return {Transaction}
*/
_rollback (callback) {
if (this._aborted) {
return setImmediate(callback, new TransactionError('Transaction has been aborted.', 'EABORT'))
}
if (!this._acquiredConnection) {
return setImmediate(callback, new TransactionError('Transaction has not begun. Call begin() first.', 'ENOTBEGUN'))
}
if (this._activeRequest) {
return setImmediate(callback, new TransactionError("Can't rollback transaction. There is a request in progress.", 'EREQINPROG'))
}
this._rollbackRequested = true
setImmediate(callback)
}
}
/**
* Class Request.
*
* @property {Transaction} transaction Reference to transaction when request was created in transaction.
* @property {*} parameters Collection of input and output parameters.
* @property {Boolean} canceled `true` if request was canceled.
*
* @fires Request#recordset
* @fires Request#row
* @fires Request#done
* @fires Request#error
*/
class Request extends EventEmitter {
/**
* Create new Request.
*
* @param {Connection|ConnectionPool|Transaction|PreparedStatement} parent If ommited, global connection is used instead.
*/
constructor (parent) {
super()
IDS.add(this, 'Request')
debug('request(%d): created', IDS.get(this))
this.canceled = false
this.parent = parent || globalConnection
this.parameters = {}
}
/**
* Fetch request from tagged template string.
*
* @private
* @param {Array} strings
* @param {Array} values
* @param {String} [method] If provided, method is automtically called with serialized command on this object.
* @return {Request}
*/
_template (strings, values, method) {
let command = [strings[0]]
for (let index = 0; index < values.length; index++) {
let value = values[index]
this.input(`param${index + 1}`, value)
command.push(`@param${index + 1}`, strings[index + 1])
}
if (method) {
return this[method](command.join(''))
} else {
return command.join('')
}
}
/**
* Add an input parameter to the request.
*
* @param {String} name Name of the input parameter without @ char.
* @param {*} [type] SQL data type of input parameter. If you omit type, module automaticaly decide which SQL data type should be used based on JS data type.
* @param {*} value Input parameter value. `undefined` and `NaN` values are automatically converted to `null` values.
* @return {Request}
*/
input (name, type, value) {
if ((/(--| |\/\*|\*\/|')/).test(name)) {
throw new RequestError(`SQL injection warning for param '${name}'`, 'EINJECT')
}
if (arguments.length === 1) {
throw new RequestError('Invalid number of arguments. At least 2 arguments expected.', 'EARGS')
} else if (arguments.length === 2) {
value = type
type = getTypeByValue(value)
}
// support for custom data types
if (value && typeof value.valueOf === 'function' && !(value instanceof Date)) value = value.valueOf()
if (value === undefined) value = null // undefined to null
if (typeof value === 'number' && isNaN(value)) value = null // NaN to null
if (type instanceof Function) type = type()
this.parameters[name] = {
name,
type: type.type,
io: 1,
value,
length: type.length,
scale: type.scale,
precision: type.precision,
tvpType: type.tvpType
}
return this
}
/**
* Add an output parameter to the request.
*
* @param {String} name Name of the output parameter without @ char.
* @param {*} type SQL data type of output parameter.
* @param {*} [value] Output parameter value initial value. `undefined` and `NaN` values are automatically converted to `null` values. Optional.
* @return {Request}
*/
output (name, type, value) {
if (!type) { type = TYPES.NVarChar }
if ((/(--| |\/\*|\*\/|')/).test(name)) {
throw new RequestError(`SQL injection warning for param '${name}'`, 'EINJECT')
}
if ((type === TYPES.Text) || (type === TYPES.NText) || (type === TYPES.Image)) {
throw new RequestError('Deprecated types (Text, NText, Image) are not supported as OUTPUT parameters.', 'EDEPRECATED')
}
// support for custom data types
if (value && typeof value.valueOf === 'function' && !(value instanceof Date)) value = value.valueOf()
if (value === undefined) value = null // undefined to null
if (typeof value === 'number' && isNaN(value)) value = null // NaN to null
if (type instanceof Function) type = type()
this.parameters[name] = {
name,
type: type.type,
io: 2,
value,
length: type.length,
scale: type.scale,
precision: type.precision
}
return this
}
/**
* Execute the SQL batch.
*
* @param {String} batch T-SQL batch to be executed.
* @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise.
* @return {Request|Promise}
*/
batch (batch, callback) {
if (this.stream == null && this.connection) this.stream = this.connection.config.stream
this.rowsAffected = 0
if (typeof callback === 'function') {
this._batch(batch, (err, recordsets, output, rowsAffected) => {
if (this.stream) {
if (err) this.emit('error', err)
err = null
this.emit('done', {
output,
rowsAffected
})
}
if (err) return callback(err)
callback(null, {
recordsets,
recordset: recordsets && recordsets[0],
output,
rowsAffected
})
})
return this
}
// Check is method was called as tagged template
if (typeof batch === 'object') {
const values = Array.prototype.slice.call(arguments)
const strings = values.shift()
batch = this._template(strings, values)
}
return new PromiseLibrary((resolve, reject) => {
this._batch(batch, (err, recordsets, output, rowsAffected) => {
if (this.stream) {
if (err) this.emit('error', err)
err = null
this.emit('done', {
output,
rowsAffected
})
}
if (err) return reject(err)
resolve({
recordsets,
recordset: recordsets && recordsets[0],
output,
rowsAffected
})
})
})
}
/**
* @private
* @param {String} batch
* @param {Request~requestCallback} callback
*/
_batch (batch, callback) {
if (!this.connection) {
return setImmediate(callback, new RequestError('No connection is specified for that request.', 'ENOCONN'))
}
if (!this.connection.connected) {
return setImmediate(callback, new ConnectionError('Connection is closed.', 'ECONNCLOSED'))
}
this.canceled = false
setImmediate(callback)
}
/**
* Bulk load.
*
* @param {Table} table SQL table.
* @param {Request~bulkCallback} [callback] A callback which is called after bulk load has completed, or an error has occurred. If omited, method returns Promise.
* @return {Request|Promise}
*/
bulk (table, callback) {
if (this.stream == null && this.connection) this.stream = this.connection.config.stream
if (this.stream || typeof callback === 'function') {
this._bulk(table, (err, rowsAffected) => {
if (this.stream) {
if (err) this.emit('error', err)
return this.emit('done', {
rowsAffected
})
}
if (err) return callback(err)
callback(null, {
rowsAffected
})
})
return this
}
return new PromiseLibrary((resolve, reject) => {
this._bulk(table, (err, rowsAffected) => {
if (err) return reject(err)
resolve({
rowsAffected
})
})
})
}
/**
* @private
* @param {Table} table
* @param {Request~bulkCallback} callback
*/
_bulk (table, callback) {
if (!this.parent) {
return setImmediate(callback, new RequestError('No connection is specified for that request.', 'ENOCONN'))
}
if (!this.parent.connected) {
return setImmediate(callback, new ConnectionError('Connection is closed.', 'ECONNCLOSED'))
}
this.canceled = false
setImmediate(callback)
}
/**
* Sets request to `stream` mode and pulls all rows from all recordsets to a given stream.
*
* @param {Stream} stream Stream to pipe data into.
* @return {Stream}
*/
pipe (stream) {
this.stream = true
this.on('row', stream.write.bind(stream))
this.on('error', stream.emit.bind(stream, 'error'))
this.on('done', () => {
setImmediate(() => stream.end())
})
stream.emit('pipe', this)
return stream
}
/**
* Execute the SQL command.
*
* @param {String} command T-SQL command to be executed.
* @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise.
* @return {Request|Promise}
*/
query (command, callback) {
if (this.stream == null && this.connection) this.stream = this.connection.config.stream
this.rowsAffected = 0
if (typeof callback === 'function') {
this._query(command, (err, recordsets, output, rowsAffected) => {
if (this.stream) {
if (err) this.emit('error', err)
err = null
this.emit('done', {
output,
rowsAffected
})
}
if (err) return callback(err)
callback(null, {
recordsets,
recordset: recordsets && recordsets[0],
output,
rowsAffected
})
})
return this
}
// Check is method was called as tagged template
if (typeof command === 'object') {
const values = Array.prototype.slice.call(arguments)
const strings = values.shift()
command = this._template(strings, values)
}
return new PromiseLibrary((resolve, reject) => {
this._query(command, (err, recordsets, output, rowsAffected) => {
if (this.stream) {
if (err) this.emit('error', err)
err = null
this.emit('done', {
output,
rowsAffected
})
}
if (err) return reject(err)
resolve({
recordsets,
recordset: recordsets && recordsets[0],
output,
rowsAffected
})
})
})
}
/**
* @private
* @param {String} command
* @param {Request~bulkCallback} callback
*/
_query (command, callback) {
if (!this.parent) {
return setImmediate(callback, new RequestError('No connection is specified for that request.', 'ENOCONN'))
}
if (!this.parent.connected) {
return setImmediate(callback, new ConnectionError('Connection is closed.', 'ECONNCLOSED'))
}
this.canceled = false
setImmediate(callback)
}
/**
* Call a stored procedure.
*
* @param {String} procedure Name of the stored procedure to be executed.
* @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise.
* @return {Request|Promise}
*/
execute (command, callback) {
if (this.stream == null && this.connection) this.stream = this.connection.config.stream
this.rowsAffected = 0
if (typeof callback === 'function') {
this._execute(command, (err, recordsets, output, returnValue, rowsAffected) => {
if (this.stream) {
if (err) this.emit('error', err)
err = null
this.emit('done', {
output,
rowsAffected,
returnValue
})
}
if (err) return callback(err)
callback(null, {
recordsets,
recordset: recordsets && recordsets[0],
output,
rowsAffected,
returnValue
})
})
return this
}
return new PromiseLibrary((resolve, reject) => {
this._execute(command, (err, recordsets, output, returnValue, rowsAffected) => {
if (this.stream) {
if (err) this.emit('error', err)
err = null
this.emit('done', {
output,
rowsAffected,
returnValue
})
}
if (err) return reject(err)
resolve({
recordsets,
recordset: recordsets && recordsets[0],
output,
rowsAffected,
returnValue
})
})
})
}
/**
* @private
* @param {String} procedure
* @param {Request~bulkCallback} callback
*/
_execute (procedure, callback) {
if (!this.parent) {
return setImmediate(callback, new RequestError('No connection is specified for that request.', 'ENOCONN'))
}
if (!this.parent.connected) {
return setImmediate(callback, new ConnectionError('Connection is closed.', 'ECONNCLOSED'))
}
this.canceled = false
setImmediate(callback)
}
/**
* Cancel currently executed request.
*
* @return {Boolean}
*/
cancel () {
this._cancel()
return true
}
/**
* @private
*/
_cancel () {
this.canceled = true
}
}
/**
* Class ConnectionError.
*/
class ConnectionError extends Error {
/**
* Creates a new ConnectionError.
*
* @param {String} message Error message.
* @param {String} [code] Error code.
*/
constructor (message, code) {
if (message instanceof Error) {
super(message.message)
this.code = message.code || code
Error.captureStackTrace(this, this.constructor)
Object.defineProperty(this, 'originalError', {enumerable: true, value: message})
} else {
super(message)
this.code = code
}
this.name = 'ConnectionError'
}
}
/**
* Class TransactionError.
*/
class TransactionError extends Error {
/**
* Creates a new TransactionError.
*
* @param {String} message Error message.
* @param {String} [code] Error code.
*/
constructor (message, code) {
if (message instanceof Error) {
super(message.message)
this.code = message.code || code
Error.captureStackTrace(this, this.constructor)
Object.defineProperty(this, 'originalError', {enumerable: true, value: message})
} else {
super(message)
this.code = code
}
this.name = 'TransactionError'
}
}
/**
* Class RequestError.
*
* @property {String} number Error number.
* @property {Number} lineNumber Line number.
* @property {String} state Error state.
* @property {String} class Error class.
* @property {String} serverName Server name.
* @property {String} procName Procedure name.
*/
class RequestError extends Error {
/**
* Creates a new RequestError.
*
* @param {String} message Error message.
* @param {String} [code] Error code.
*/
constructor (message, code) {
if (message instanceof Error) {
super(message.message)
this.code = message.code || code
if (message.info) {
this.number = message.info.number || message.code // err.code is returned by msnodesql driver
this.lineNumber = message.info.lineNumber
this.state = message.info.state || message.sqlstate // err.sqlstate is returned by msnodesql driver
this.class = message.info.class
this.serverName = message.info.serverName
this.procName = message.info.procName
} else {
this.number = message.code // err.code is returned by msnodesql driver
this.state = message.sqlstate // err.sqlstate is returned by msnodesql driver
}
Error.captureStackTrace(this, this.constructor)
Object.defineProperty(this, 'originalError', {enumerable: true, value: message})
} else {
super(message)
this.code = code
}
this.name = 'RequestError'
if ((/^\[Microsoft\]\[SQL Server Native Client 11\.0\](?:\[SQL Server\])?([\s\S]*)$/).exec(this.message)) {
this.message = RegExp.$1
}
}
}
/**
* Class PreparedStatementError.
*/
class PreparedStatementError extends Error {
/**
* Creates a new PreparedStatementError.
*
* @param {String} message Error message.
* @param {String} [code] Error code.
*/
constructor (message, code) {
if (message instanceof Error) {
super(message.message)
this.code = message.code || code
Error.captureStackTrace(this, this.constructor)
Object.defineProperty(this, 'originalError', {enumerable: true, value: message})
} else {
super(message)
this.code = code
}
this.name = 'PreparedStatementError'
}
}
module.exports = {
ConnectionPool,
Transaction,
Request,
PreparedStatement,
ConnectionError,
TransactionError,
RequestError,
PreparedStatementError,
driver,
exports: {
ConnectionError,
TransactionError,
RequestError,
PreparedStatementError,
Table,
ISOLATION_LEVEL,
TYPES,
MAX: 65535, // (1 << 16) - 1
map,
getTypeByValue
}
}
Object.defineProperty(module.exports, 'Promise', {
get: () => {
return PromiseLibrary
},
set: (value) => {
PromiseLibrary = value
}
})
// append datatypes to this modules export
for (let key in TYPES) {
let value = TYPES[key]
module.exports.exports[key] = value
module.exports.exports[key.toUpperCase()] = value
}
/**
* Open global connection pool.
*
* @param {Object|String} config Connection configuration object or connection string.
* @param {basicCallback} [callback] A callback which is called after connection has established, or an error has occurred. If omited, method returns Promise.
* @return {ConnectionPool|Promise}
*/
module.exports.exports.connect = function connect (config, callback) {
if (globalConnection) throw new Error('Global connection already exists. Call sql.close() first.')
globalConnection = new driver.ConnectionPool(config)
for (let event in globalConnectionHandlers) {
for (let i = 0, l = globalConnectionHandlers[event].length; i < l; i++) {
globalConnection.on(event, globalConnectionHandlers[event][i])
}
}
return globalConnection.connect(callback)
}
/**
* Close all active connections in the global pool.
*
* @param {basicCallback} [callback] A callback which is called after connection has closed, or an error has occurred. If omited, method returns Promise.
* @return {ConnectionPool|Promise}
*/
module.exports.exports.close = function close (callback) {
if (globalConnection) {
// remove event handlers from the global connection
for (let event in globalConnectionHandlers) {
for (let i = 0, l = globalConnectionHandlers[event].length; i < l; i++) {
globalConnection.removeListener(event, globalConnectionHandlers[event][i])
}
}
// attach error handler to prevent process crash in case of error
globalConnection.on('error', err => {
if (globalConnectionHandlers['error']) {
for (let i = 0, l = globalConnectionHandlers['error'].length; i < l; i++) {
globalConnectionHandlers['error'][i].call(globalConnection, err)
}
}
})
const gc = globalConnection
globalConnection = null
return gc.close(callback)
}
if (typeof callback === 'function') {
setImmediate(callback)
return null
}
return new PromiseLibrary((resolve, reject) => {
resolve(globalConnection)
})
}
/**
* Attach event handler to global connection pool.
*
* @param {String} event Event name.
* @param {Function} handler Event handler.
* @return {ConnectionPool}
*/
module.exports.exports.on = function on (event, handler) {
if (!globalConnectionHandlers[event]) globalConnectionHandlers[event] = []
globalConnectionHandlers[event].push(handler)
if (globalConnection) globalConnection.on(event, handler)
return globalConnection
}
/**
* Detach event handler from global connection.
*
* @param {String} event Event name.
* @param {Function} handler Event handler.
* @return {ConnectionPool}
*/
module.exports.exports.removeListener = module.exports.exports.off = function removeListener (event, handler) {
if (!globalConnectionHandlers[event]) return globalConnection
const index = globalConnectionHandlers[event].indexOf(handler)
if (index === -1) return globalConnection
globalConnectionHandlers[event].splice(index, 1)
if (globalConnectionHandlers[event].length === 0) globalConnectionHandlers[event] = undefined
if (globalConnection) globalConnection.removeListener(event, handler)
return globalConnection
}
/**
* Creates a new query using global connection from a tagged template string.
*
* @variation 1
* @param {Array|String} strings Array of string literals or sql command.
* @param {...*} keys Values.
* @return {Request}
*/
/**
* Execute the SQL command.
*
* @variation 2
* @param {String} command T-SQL command to be executed.
* @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise.
* @return {Request|Promise}
*/
module.exports.exports.query = function query () {
if (typeof arguments[0] === 'string') { return new driver.Request().query(arguments[0], arguments[1]) }
const values = Array.prototype.slice.call(arguments)
const strings = values.shift()
return new driver.Request()._template(strings, values, 'query')
}
/**
* Creates a new batch using global connection from a tagged template string.
*
* @variation 1
* @param {Array} strings Array of string literals.
* @param {...*} keys Values.
* @return {Request}
*/
/**
* Execute the SQL command.
*
* @variation 2
* @param {String} command T-SQL command to be executed.
* @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise.
* @return {Request|Promise}
*/
module.exports.exports.batch = function batch () {
if (typeof arguments[0] === 'string') { return new driver.Request().batch(arguments[0], arguments[1]) }
const values = Array.prototype.slice.call(arguments)
const strings = values.shift()
return new driver.Request()._template(strings, values, 'batch')
}
/**
* @callback Request~requestCallback
* @param {Error} err Error on error, otherwise null.
* @param {Object} result Request result.
*/
/**
* @callback Request~bulkCallback
* @param {Error} err Error on error, otherwise null.
* @param {Number} rowsAffected Number of affected rows.
*/
/**
* @callback basicCallback
* @param {Error} err Error on error, otherwise null.
* @param {Connection} connection Acquired connection.
*/
/**
* @callback acquireCallback
* @param {Error} err Error on error, otherwise null.
* @param {Connection} connection Acquired connection.
*/
/**
* Dispatched after connection has established.
* @event ConnectionPool#connect
*/
/**
* Dispatched after connection has closed a pool (by calling close).
* @event ConnectionPool#close
*/
/**
* Dispatched when transaction begin.
* @event Transaction#begin
*/
/**
* Dispatched on successful commit.
* @event Transaction#commit
*/
/**
* Dispatched on successful rollback.
* @event Transaction#rollback
*/
/**
* Dispatched when metadata for new recordset are parsed.
* @event Request#recordset
*/
/**
* Dispatched when new row is parsed.
* @event Request#row
*/
/**
* Dispatched when request is complete.
* @event Request#done
*/
/**
* Dispatched on error.
* @event Request#error
*/