any-db-mssql
Version:
The MSSQL adapter for any-db
789 lines (682 loc) • 21 kB
JavaScript
var EventEmitter = require('events').EventEmitter;
var sql = require('tedious');
/**
* Implementation of Adapter as defined by Any-DB API ({@link https://github.com/grncdr/node-any-db}).
*
* @module any-db-mssql
*/
/**
* Database connection options Object.
*
* @external Tedious~ConfigOptions
* @see {@link http://pekim.github.io/tedious/api-connection.html#function_newConnection}
* @property {String} [instanceName] - e.g., 'SQLEXPRESS'
* @property {String} [database] - database name, e.g., 'MyDataBase'
* @property {Number} [port] - server port, e.g., 1433.
*/
/**
* Database connection configuration Object.
*
* @external Tedious~Config
* @see {@link http://pekim.github.io/tedious/api-connection.html#function_newConnection}
* @property {String} server - address, e.g., '10.48.0.1'
* @property {String} userName - user name, e.g., 'MyUserName'
* @property {String} password
* @property {Tedious~ConfigOptions} [options]
*/
/**
* Default configuration for connections.
*
* @private
* @type {Tedious~Config}
*/
var defaultConfig = {
userName: 'sa',
password: 'Password123',
server: 'localhost',
options: {
port: 1433,
instanceName: false,
database: 'myDataBase'
}
};
/**
* Any DB config.
*
* @external any-db~Config
* @see {@link https://github.com/grncdr/node-any-db-adapter-spec#adaptercreateconnection}
*/
/**
* Convert config provided by Any DB to the one used by Tedious.
*
* @private
* @param {any-db~Config} anyConfig
* @return {Tedious~Config}
*/
var parseConfig = function(anyConfig) {
var result = {};
result.userName = anyConfig.user || anyConfig.userName || defaultConfig.userName;
result.password = anyConfig.password || defaultConfig.password;
result.server = anyConfig.host || anyConfig.server || defaultConfig.host;
result.options = anyConfig.options || {};
result.options.database = anyConfig.database || result.options.database || defaultConfig.options.database;
if (anyConfig.instanceName || result.options.instanceName) {
result.options.instanceName = anyConfig.instanceName || result.options.instanceName;
result.options.port = false;
}
else {
result.options.port = anyConfig.port || result.options.port || defaultConfig.options.port;
}
return result;
};
/**
* Each key is a parameter name, and each value is that value.
* Data types will be detected automatically.
*
* Example:
* ```
* {
* first: 1,
* second: 'two',
* third: true
* }
* ```
*
* You can enforce types by setting values to objects with `type`, `value` and
* (optionally) `options` properties, e.g.,
* ```
* {
* first: 1,
* second: {
* type: adapter.getTypeByName('string'),
* value: 'two',
* options: {
* length: 3
* }
* }
* }
* ```
* For more information about type options, see: {@link http://pekim.github.io/tedious/api-request.html#function_addParameter}.
*
* @typedef {Object} namedParameters
*/
/**
* Contains values of parameters. Each index corresponds to the parameter identifier in SQL query string.
* Data types will be detected automatically.
*
* Every value in positionalParameters Array can be either simple value, or object containing type and
* value (to enforce data type). It works same way as with namedParameters ({@link module:any-db-mssql~namedParameters more information}).
*
* @typedef {Array} positionalParameters
*/
/**
* Look through the parameters and "unroll" the ones with Array value, e.g.,
*
* ```sql
* WHERE foo IN (@foo)
* ```
*
* will become:
*
* ```sql
* WHERE foo IN (@foo1, @foo0)
* ```
*
* if `query.values.foo` is an Array.
*
* Since Tedious does not support positional parameters, this function will also convert
* them to named parameters.
*
* This function mutates `query.text` and `query.values`.
*
* @param {any-db~Query} query
* @return {any-db~Query} modified query
*/
exports.prepareQueryParameters = function(query) {
if (!query.values) {
return query;
}
var keys = Object.keys(query.values);
var value;
var i, j, param, temp;
var newParameters = {};
var targetPrefix = exports.namedParameterPrefix;
var sourcePrefix = exports.positionalParameterPrefix;
var positional = query.values instanceof Array;
var sql = query.text;
for (i = 0; i < keys.length; i++) {
value = query.values[keys[i]];
if (!(value instanceof Array)) {
// Tedious does not support positional parameters, so we have to replace them with named parameters.
if (positional) {
newParameters['p'+i] = value;
sql = sql.replace(sourcePrefix, targetPrefix+'p'+i);
}
else {
newParameters[keys[i]] = value;
}
continue;
}
param = [];
temp = targetPrefix+keys[i];
for (j = 0; j < value.length; j++) {
param.push(temp+j);
newParameters[keys[i]+j] = value[j];
}
if (positional) {
sql.replace(sourcePrefix, param.join(', '));
}
else {
temp = new RegExp(temp, 'g');
sql = sql.replace(temp, param.join(', '));
}
}
query.values = newParameters;
query.text = sql;
return query;
};
/**
* RegExp used by `detectParameterType()`.
*
* @private
*/
var _typeCheck = (function(){
var regexTypes = {
'(^-?\\d+$)': sql.TYPES.Int,
'(^-?\\d+\\.\\d+$)': sql.TYPES.Real,
'(^\\d{4}-\\d{2}-\\d{2}$)': sql.TYPES.Date,
'(^\\d{2}\\:\\d{2}(?:\\:\\d{2})?(?:\\+\\d{4})?$)': sql.TYPES.Time,
// MSSQL does not implement full ISO 8601 standard:
// when string contains letter "T", MSSQL rejects time value without seconds specified,
// it also rejects any time value that contains only hour value (omitting minutes and seconds),
// it also rejects formats without colons.
'(^\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}\\:\\d{2}(?:\\:\\d{2}(?:\\.\\d+)?)?$)': sql.TYPES.DateTime2,
'(^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}(?:\\.\\d+)?$)': sql.TYPES.DateTime2,
'(^\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}\\:\\d{2}(?:\\:\\d{2}(?:\\.\\d+)?)?(?:[\\+\\-]\\d{2}\\:\\d{2}|Z)?$)': sql.TYPES.DateTimeOffset,
'(^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}(?:\\.\\d+)?(?:[\\+\\-]\\d{2}\\:\\d{2}|Z)?$)': sql.TYPES.DateTimeOffset,
'(^[\\w\\W]+$)': sql.TYPES.NVarChar
};
var r = new RegExp(Object.keys(regexTypes).join('|'));
r._exec = r.exec;
r.exec = function(str){
var result = this._exec(str);
if (!result) {
return false;
}
for (var i = 1; i < result.length; i++) {
if (!result[i]) {
continue;
}
return regexTypes[(Object.keys(regexTypes))[i-1]];
}
return sql.TYPES.VarBinary;
};
return r;
})();
/**
* Tedious type definition.
* @external Tedious~Type
* @see {@link http://pekim.github.io/tedious/api-datatypes.html}
* @property {number} id - internal identification number
* @property {string} type - internal type name
* @property {string} name - SQL type name
*/
/**
* Check the type of the parameter value and return MSSQL type most suitable for it.
* Defaults to VarBinary type.
*
* @param {*} value
* @return {Tedious~Type} Tedious type definition
*/
exports.detectParameterType = function(value) {
if (value === null || typeof value === 'undefined') {
return sql.TYPES.Null;
}
else if ((value instanceof Boolean) || value === true || value === false) {
return sql.TYPES.Bit;
}
else if (value instanceof Array) {
return (value.length > 0 ? exports.detectParameterType(value[0]) : sql.TYPES.Null);
}
else if (value instanceof Date) {
return sql.TYPES.DateTimeOffset;
}
return _typeCheck.exec(value) || sql.TYPES.VarBinary;
};
/**
* Tedious' Request object.
*
* @external Tedious~Request
* @see {@link http://pekim.github.io/tedious/api-request.html}
*/
/**
* Add parameters to the request.
*
* @private
* @param {Tedious~Request} request
* @param {namedParameters|positionalParameters} [parameters]
*/
var setRequestParameters = function(request, parameters) {
if (!parameters) {
return;
}
var keys = Object.keys(parameters);
var type = false;
var value = null;
var options = null;
for (var i = keys.length - 1; i >= 0; i--) {
value = parameters[keys[i]];
options = null;
if (value instanceof Object && value.type && value.hasOwnProperty('value')) {
if (value.hasOwnProperty('options')) {
options = value.options;
}
type = value.type;
value = value.value;
}
else {
type = exports.detectParameterType(value);
}
if (!(value instanceof Array)) {
request.addParameter(keys[i], type, value, options);
continue;
}
}
};
/**
* Generic callback function.
*
* First argument will allways be error value or empty.
* Second and any next argument are optional and depend on the calling function.
*
* @typedef {Function} genericCallback
* @param {string|Object|null} error - error, if any happened, or null
* @param {Array|Number|string|null} result
*/
/**
* EventEmitter is part of node.js API.
*
* @external events~EventEmitter
* @see {@link http://nodejs.org/api/events.html#events_class_events_eventemitter}
*/
/**
* Readable stream is part of node.js API.
*
* @external stream~Readable
* @see {@link http://nodejs.org/api/stream.html#stream_class_stream_readable}
*/
/**
* Adapter is defined by Any-DB API.
*
* @external any-db~Adapter
* @see {@link https://github.com/grncdr/node-any-db-adapter-spec#adapter}
* @property {string} name
* @property {function} createConnection
* @property {function} createQuery
*/
/**
* Queryable is defined by Any-DB API.
*
* @external any-db~Queryable
* @see {@link https://github.com/grncdr/node-any-db-adapter-spec#queryable}
* @extends {EventEmitter}
* @property {any-db~Adapter} adapter
* @property {function} query
*/
/**
* Query is defined by Any-DB API.
*
* @external any-db~Query
* @see {@link https://github.com/grncdr/node-any-db-adapter-spec#query}
* @extends {stream~Readable}
* @property {string} text
* @property {Array|Object} values
* @property {genericCallback} callback
*/
/**
* Connection is defined by Any-DB API.
*
* @external any-db~Connection
* @see {@link https://github.com/grncdr/node-any-db-adapter-spec#connection}
* @extends {any-db~Queryable}
* @property {function} end
*/
/**
* Do not call this directly, as it has to be bound to a Queryable object.
*
* This function is used by `makeQueryable` function to inject into
* Queryable objects as `query` function.
*
* @private
* @param {string|any-db~Query} query
* @param {Array|Object} [parameters] - used only when query is a string
* @param {genericCallback} [callback] - used only when query is a string
*/
var execQuery = function(query, parameters, callback) {
query = this.adapter.createQuery(query, parameters, callback);
if (query.values) {
exports.prepareQueryParameters(query);
}
query._request = new sql.Request(query.text, function(err, rowCount) {
query._isDone = true;
if (query._resultSet) {
query._resultSet.rowCount = rowCount;
}
if (query._connection) {
query._connection.close();
query._connection = null;
}
if (query._request) {
delete query._request;
query._request = null;
}
query.emit('close');
if (query.callback && !query._emittedError) {
query.callback(err, query._resultSet);
}
// Do not emit `end` event if there was no one listening.
// TODO: this is strange requirement, but it comes from emulating stream.Readable,
// which does not emit `end` untill all data is consumed. If data was not consumed,
// i.e., no callback and no `data` event are used, there should be no `end`.
// This is most probably wrong way, because we're still closing the query
// (remember: Tedious query is not a stream waiting for read - it finishes anyway).
// We should change this once Tedious query is instance of `stream.Readable`, or
// can be paused and resumed.
if (query.callback || EventEmitter.listenerCount(query, 'data')) {
query.emit('end');
}
});
if (query.values) {
setRequestParameters(query._request, query.values);
}
query._request.on('row', function(columns){
var row = {};
for (var i = 0; i < columns.length; i++) {
row[columns[i].metadata.colName] = columns[i].value;
}
query.emit('data', row);
if (query._resultSet && query._resultSet.rows instanceof Array) {
query._resultSet.rows.push(row);
}
});
query._request.on('columnMetadata', function(columns){
var fields = [];
columns.forEach(function(column){
if (!column.colName) {
return;
}
var field = {
name: column.colName
};
Object.keys(column).forEach(function(name){
field[name] = column[name];
});
fields.push(field);
});
query.emit('fields', fields);
if (query._resultSet) {
query._resultSet.fields = fields;
}
});
query._request.on('error', function(err){
// According to Any-DB API, query error event can be emitted only once per query:
// https://github.com/grncdr/node-any-db-adapter-spec#error-event-1
if (!query._emittedError) {
query._emittedError = true;
query.emit('error', err);
}
});
if (query.callback && query.callback instanceof Function) {
query._resultSet = {
fields: [],
rows: [],
rowCount: 0,
lastInsertId: null, // Not supported
// Output parameter values
values: []
};
// We collect parameters only when _resultSet is being used.
query._request.on('returnValue', function(parameterName, value, metadata){
query._resultSet.values.push({
name: parameterName,
value: value,
meta: metadata
});
});
// According to Any-DB API, callback should be subscribed to the error event:
// https://github.com/grncdr/node-any-db-adapter-spec#error-event-1
query.on('error', query.callback);
}
else {
// Add empty error handler, just in case there is none set up by the caller.
// This fixes Any-DB adapter spec tests.
query.on('error', function(){});
}
this.execNextInQueue(query);
return query;
};
/**
* Inject Queryable API into object.
*
* @private
* @param {Object} target
* @return {any-db~Queryable} target object with Queryable API injected
*/
var makeQueryable = function(target) {
target.adapter = exports;
target.query = execQuery.bind(target);
// Tedious cannot execute more than one query at a time, so we have to
// implement a queue for queries, just in case someone tries to set
// multiple queries in a row (like node-any-db-adapter-spec tests do).
target._queue = [];
target._waitingForQueryToFinish = false;
var _execNextInQueue = function(){
if (target._waitingForQueryToFinish) {
return;
}
var query = target._queue.shift();
if (query) {
target._waitingForQueryToFinish = true;
target.emit('query', query);
query.once('close', function(){
target._waitingForQueryToFinish = false;
target.execNextInQueue();
});
target.execSql(query._request);
}
};
target.execNextInQueue = function(query){
if (query) {
target._queue.push(query);
}
if (target._isConnected) {
process.nextTick(_execNextInQueue);
}
};
return target;
};
/**
* Adapter's schema name.
*/
exports.name = 'mssql';
/**
* Extend Any-DB API with namedParameterPrefix flag, that can be used when build SQL query strings
* with named parameters, for example:
*
* ```
* connection.query(
* 'SELECT '+adapter.namedParameterPrefix+'myParam AS test`,
* { myParam: 1 }
* );
* ```
*/
exports.namedParameterPrefix = '@';
Object.defineProperty(exports, 'namedParameterPrefix', { value: '@', writable : false });
/**
* Extend Any-DB API with positionalParameterPrefix flag, that can be used when build SQL query strings
* with positioned parameters, for example:
*
* ```
* connection.query(
* 'SELECT '+adapter.positionalParameterPrefix+' AS test`,
* [ 1 ]
* );
* ``` */
exports.positionalParameterPrefix = '?';
Object.defineProperty(exports, 'positionalParameterPrefix', { value: '?', writable : false });
/**
* Implementation of `Adapter.createConnection` method defined by Any-DB API.
*
* @see {@link https://github.com/grncdr/node-any-db-adapter-spec#adaptercreateconnection}
* @param {any-db~Config} config
* @param {genericCallback} [callback]
* @return {any-db~Connection} connection
*/
exports.createConnection = function(config, callback) {
var result = new sql.Connection(parseConfig(config));
// `id` is added only for testing purposes and should not be counted on
result.id = process.hrtime().join('.');
result._isConnected = false;
makeQueryable(result);
// Tedious Connection has `close()` method, but Any-DB
// expects `end()` method, so we have to build a bridge.
result.end = function(callback){
if (callback instanceof Function) {
result.on('close', callback);
}
if (result._isConnected) {
result.close();
}
else {
// Work around Tedious bug (?) which throws error when connection is closed
// before finishing connecting.
// TODO: remove this if/when Tedious stops throwing error (https://github.com/pekim/tedious/issues/185)?
setImmediate(result.end);
}
};
if (callback && callback instanceof Function) {
callback._done = false;
result.once('error', function(err){
if (callback._done) {
return;
}
callback._done = true;
callback(err, result);
});
}
// Tedious emits `end`, but any-db expects `close` event,
// so we have to fake it.
result.on('end', function(){
result._isConnected = false;
result.emit('close');
result._queue = [];
});
result.on('connect', function(err){
if (err) {
result.emit('error', err);
}
else {
result._isConnected = true;
// This is not mentioned in Any-DB documentation, but connection should
// emit `open` event when it is ready for queries.
result.emit('open');
}
if (callback && callback instanceof Function && !callback._done) {
callback._done = true;
callback(err, result);
}
result.execNextInQueue();
});
result.on('errorMessage', function(err){
if (this.request) {
this.request.emit('error', err);
}
});
return result;
};
/**
* Partial implementation of `Adapter.createQuery` method defined by Any-DB API.
*
* Partial because returned Query DOES NOT inherit Readable stream.
* It inherits EventEmitter only, because Tedious library does not provide Readable
* stream, and faking it does not make much sense.
*
* @see {@link https://github.com/grncdr/node-any-db-adapter-spec#adaptercreatequery}
* @param {string|any-db~Query} query
* @param {Array} [parameters] - used only when query is a string
* @param {genericCallback} [callback] - used only when query is a string
* @return {any-db~Query} query
*/
exports.createQuery = function(query, parameters, callback) {
if (typeof query === 'object') {
return query;
}
// Tedious does not use Readable stream, so we cannot support Any-DB fully :(.
var result = new EventEmitter();
result.text = query;
if (parameters && parameters instanceof Function && !callback) {
result.values = false;
result.callback = parameters;
}
else {
result.values = parameters;
result.callback = callback;
}
result._connection = null;
result._request = null;
result._resultSet = null;
result._isDone = false;
result._emittedError = false;
return result;
};
/**
* Extend Any-DB API by providing function that returns Tedious type for given generic type name.
*
* Following type names are supported (aside from "native" type names supported by Tedious):
*
* - integer
* - float
* - real
* - boolean
* - text
* - string
* - date
* - time
* - datetime
* - binary
*
* Unrecognized types will be handled as binary.
*
* @param {string} typeName
* @return {Tedious~Type} Tedious type definition object
*/
exports.getTypeByName = function(typeName) {
if (sql.TYPES.hasOwnProperty(typeName)) {
return sql.TYPES[typeName];
}
switch (typeName) {
case 'int':
case 'integer':
return sql.TYPES.Int;
case 'float':
case 'real':
return sql.TYPES.Real;
case 'bool':
case 'boolean':
case 'bit':
return sql.TYPES.Bit;
case 'text':
case 'string':
return sql.TYPES.NVarChar;
case 'date':
return sql.TYPES.Date;
case 'datetime':
return sql.TYPES.DateTimeOffset;
case 'time':
return sql.TYPES.Time;
case 'binary':
return sql.TYPES.VarBinary;
default:
return sql.TYPES.VarBinary;
}
};