loopback-connector-db2ibmi
Version:
LoopBack Connector for DB2 on IBM i
380 lines (336 loc) • 10.2 kB
JavaScript
var nv = process.version.match(/\d+/)[0];
var driverPath = nv === 4
? '/QOpenSys/QIBM/ProdData/OPS/Node4/os400/db2i/lib/db2a'
: '/QOpenSys/QIBM/ProdData/OPS/Node6/os400/db2i/lib/db2a';
var db = require(driverPath);
var SqlConnector = require('loopback-connector').SqlConnector;
var util = require('util');
var debug = require('debug')('loopback:connector:db2ibmi');
/**
* Initialize the connector against the given data source
*
* @param {DataSource} dataSource The loopback-datasource-juggler dataSource
* @param {Function} [callback] The callback function
*/
exports.initialize = function initializeDataSource(ds, cb) {
ds.connector = new DB2IBMi(ds.settings);
ds.connector.dataSource = ds;
ds.connector.connect(function(err, conn) {
ds.client = conn;
cb && cb(err, conn);
});
cb();
};
function DB2IBMi(settings) {
SqlConnector.call(this, 'db2ibmi', settings);
this.setConnectionProperties(settings);
}
util.inherits(DB2IBMi, SqlConnector);
DB2IBMi.prototype.connect = function(cb) {
var self = this;
self.dataSource.connecting = true;
var dbconn = new db.dbconn();
dbconn.setConnAttr(db.SQL_ATTR_DBC_DEFAULT_LIB, self.schema);
dbconn.conn(self.hostname);
// Cannot figure out how to check if connection errored...
self.dataSource.connected = true;
self.dataSource.connecting = false;
self.dataSource.connection = new db.dbstmt(dbconn);
self.dataSource.emit('connected');
self.client = dbconn;
cb && cb(null, self.dataSource.connection);
};
DB2IBMi.prototype.disconnect = function(cb) {
this.client.disconn();
};
DB2IBMi.prototype.ping = function(cb) {
debug('DB2IBMi.prototype.ping');
var self = this;
var sql = 'SELECT COUNT(*) AS COUNT FROM SYSIBM.SYSDUMMY1';
if (self.dataSource.connection) {
ping(self.dataSource.connection, cb);
} else {
self.connect(function(err, conn) {
if (err) {
return cb(err);
}
ping(conn, function(err, res) {
conn.close(function(cerr) {
if (err || cerr) {
return cb(err || cerr);
}
return cb(null, res);
});
});
});
}
function ping(conn, cb) {
conn.exec(sql, function(results) {
cb && cb(null, results.length > 0 && results[0]['COUNT'] > 0);
});
}
}
/**
* Escape an identifier such as the column name
* DB2IBMi requires double quotes for case-sensitivity
*
* @param {string} name A database identifier
* @returns {string} The escaped database identifier
*/
DB2IBMi.prototype.escapeName = function(name) {
debug('DB2IBMi.prototype.escapeName name=%j', name);
if (!name) return name;
name.replace(/["]/g, '""');
return '"' + name + '"';
};
/**
* Create the data model in DB2
*
* @param {string} model The model name
* @param {Object} data The model instance data
* @param {Object} options Options object
* @param {Function} [callback] The callback function
*/
DB2IBMi.prototype.create = function(model, data, options, callback) {
console.log('hello');
var self = this;
var stmt = self.buildInsert(model, data, options);
var id = self.idColumn(model);
var sql = 'SELECT \"' + id + '\" FROM FINAL TABLE (' + stmt.sql + ')';
if (!options.transaction) {
sql += ' WITH NC';
}
self.executeSQL(sql, stmt.params, options, function(err, info) {
if (err) {
callback(err);
} else {
callback(err, info[0][id]);
}
});
};
/**
* Execute the sql statement
*
*/
DB2IBMi.prototype.executeSQL = function(sql, params, options, callback) {
debug('DB2IBMi.prototype.executeSQL (enter)',
sql, params, options);
var self = this;
function executeStatement(conn, cb) {
var limit = 0;
var offset = 0;
var stmt = {};
stmt.noResults = options && options.noResultSet ?
options.noResultSet : false;
// This is standard DB2 syntax. LIMIT and OFFSET
// are configured off by default. Enable these to
// leverage LIMIT and OFFSET.
if (!self.useLimitOffset) {
var res = sql.match(self.limitRE);
if (res) {
limit = parseInt(res[1], 10);
sql = sql.replace(self.limitRE, '');
}
res = sql.match(self.offsetRE);
if (res) {
offset = parseInt(res[1], 10);
sql = sql.replace(self.offsetRE, '');
}
}
// Build the stmt object that will be passed into the query call.
// This is done because the query call can take an object or a set
// of parameters. Depending on the SQL being passed in the call with
// parameters may fail due to improper handling in the ibm_db module.
stmt.sql = sql;
stmt.params = params;
if (params.length > 0) {
// Find a better way. This is insecure.
var placeholderIndices = [];
for (var i = 0; i < sql.length; i++) {
if (sql[i] === "?") placeholderIndices.push(i);
}
var idx = 0;
var increase = 0;
var tempParam = '';
for (var j = 0; j < placeholderIndices.length; j++) {
// this only works with a number (id)
tempParam = isNaN(params[j]) ? "'" + params[j] + "'" : params[j];
sql = sql.substr(0, placeholderIndices[j] + increase) +
tempParam +
sql.substr(placeholderIndices[j] + increase + 1);
increase += tempParam.length - 1;
}
conn.exec(sql, function(results) {
cb && cb(null, results);
});
} else {
conn.exec(sql, function(results) {
cb && cb(null, results);
});
}
};
if (options.transaction) {
var conn = options.transaction.connection;
executeStatement(conn, function(err, data) { callback(err, data); });
} else {
this.connect(function(err, conn) {
if (err) return callback(err);
executeStatement(conn, function(err, data) {
db.close();
callback(err, data);
});
});
}
};
DB2IBMi.prototype.getCountForAffectedRows = function(model, info) {
// Not sure how to implement this. As long as it is defined, CRUD works.
console.log(model);
console.log(info);
};
/**
* Convert the data from database column to model property
*
* @param {object} Model property descriptor
* @param {*} val Column value
* @returns {*} Model property value
*/
DB2IBMi.prototype.fromColumnValue = function(prop, val) {
debug('DB2IBMi.prototype.fromColumnValue %j %j', prop, val);
if (val === null || !prop) {
return val;
}
switch (prop.type.name) {
case 'Number':
return Number(val);
case 'String':
return String(val);
case 'Date':
val = this.parseDate(val);
return new Date(Date.parse(val));
case 'Boolean':
return Boolean(val);
case 'GeoPoint':
case 'Point':
case 'List':
case 'Array':
case 'Object':
case 'JSON':
return JSON.parse(val);
default:
return val;
}
};
/**
* Convert property name/value to an escaped DB column value
*
* @param {Object} prop Property descriptor
* @param {*} val Property value
* @returns {*} The escaped value of DB column
*/
DB2IBMi.prototype.toColumnValue = function(prop, val) {
debug('DB2IBMi.prototype.toColumnValue prop=%j val=%j', prop, val);
if (val === null) {
// if (prop.autoIncrement || prop.id) {
// return new ParameterizedSQL('DEFAULT');
// }
return null;
}
if (!prop) {
return val;
}
switch (prop.type.name) {
default:
case 'Array':
case 'Number':
case 'String':
return val;
case 'Boolean':
return Number(val);
case 'GeoPoint':
case 'Point':
case 'List':
case 'Object':
case 'ModelConstructor':
return JSON.stringify(val);
case 'JSON':
return String(val);
case 'Date':
return dateToIBMDB(val);
}
};
/**
* Get the place holder in SQL for values, such as :1 or ?
*
* @param {string} key Optional key, such as 1 or id
* @returns {string} The place holder
*/
DB2IBMi.prototype.getPlaceholderForValue = function(key) {
debug('IBMDB.prototype.getPlaceholderForValue key=%j', key);
return '?';
};
function buildLimit(limit, offset) {
if (isNaN(limit)) { limit = 0; }
if (isNaN(offset)) { offset = 0; }
if (!limit && !offset) {
return '';
}
if (limit && !offset) {
return 'FETCH FIRST ' + limit + ' ROWS ONLY';
}
if (offset && !limit) {
return 'OFFSET ' + offset;
}
return 'LIMIT ' + limit + ' OFFSET ' + offset;
}
DB2IBMi.prototype.applyPagination = function(model, stmt, filter) {
debug('IBMDB.prototype.applyPagination');
var limitClause = buildLimit(filter.limit, filter.offset || filter.skip);
return stmt.merge(limitClause);
};
function dateToIBMDB(val) {
var dateStr = val.getFullYear() + '-'
+ fillZeros(val.getMonth() + 1) + '-'
+ fillZeros(val.getDate()) + '-'
+ fillZeros(val.getHours()) + '.'
+ fillZeros(val.getMinutes()) + '.'
+ fillZeros(val.getSeconds()) + '.';
var ms = val.getMilliseconds();
if (ms < 10) {
ms = '00000' + ms;
} else if (ms < 100) {
ms = '0000' + ms;
} else {
ms = '000' + ms;
}
return dateStr + ms;
function fillZeros(v) {
return v < 10 ? '0' + v : v;
}
};
DB2IBMi.prototype.parseDate = function(date) {
// oh... my... goat...
// there has got to be a better way!
var thirdDashIndex = date
.split('-', 3)
.join('-')
.length;
var dateObj = new Date();
var offset = -dateObj.getTimezoneOffset();
var prefix = offset >= 0 ? "+" : "-";
offset = (prefix) + parseInt(offset / 60) + ":" + offset % 60;
date = date.substr(0, thirdDashIndex) +
'T' + date.substr(thirdDashIndex + 1);
// Timezone needs to not be hardcoded
var parts = date.split('.');
date = parts.slice(0,-1).join(':') + '.' + parts.slice(-1);
date = date.substring(0, date.length - 3) + '+01:00';
return date;
};
DB2IBMi.prototype.setConnectionProperties = function(settings) {
var self = this;
self.hostname = settings.hostname || '*LOCAL';
self.dbname = settings.database || settings.db || 'testdb';
self.username = settings.username || settings.user;
self.password = settings.password;
self.schema = settings.schema;
};