loopback-connector-oracle
Version:
Loopback Oracle Connector
638 lines (579 loc) • 18.3 kB
JavaScript
// Copyright IBM Corp. 2013,2019. All Rights Reserved.
// Node module: loopback-connector-oracle
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
const g = require('strong-globalize')();
/*!
* Oracle connector for LoopBack
*/
const oracle = require('oracledb');
const SqlConnector = require('loopback-connector').SqlConnector;
const ParameterizedSQL = SqlConnector.ParameterizedSQL;
const debug = require('debug')('loopback:connector:oracle');
const stream = require('stream');
const async = require('async');
/*!
* @module loopback-connector-oracle
*
* Initialize the Oracle connector against the given data source
*
* @param {DataSource} dataSource The loopback-datasource-juggler dataSource
* @param {Function} [callback] The callback function
*/
exports.initialize = function initializeDataSource(dataSource, callback) {
if (!oracle) {
return;
}
const s = dataSource.settings || {};
const configProperties = {
autoCommit: true,
connectionClass: 'loopback-connector-oracle',
extendedMetaData: undefined,
externalAuth: undefined,
fetchAsString: undefined,
lobPrefetchSize: undefined,
maxRows: undefined,
outFormat: oracle.OBJECT,
poolIncrement: undefined,
poolMax: undefined,
poolMin: undefined,
poolPingInterval: undefined,
poolTimeout: undefined,
prefetchRows: undefined,
fetchArraySize: undefined,
Promise: undefined,
queueRequests: undefined,
queueTimeout: undefined,
stmtCacheSize: undefined,
_enableStats: undefined,
};
const oracleSettings = {
connectString: s.connectString || s.url || s.tns,
user: s.username || s.user,
password: s.password,
debug: s.debug || debug.enabled,
poolMin: s.poolMin || s.minConn || 1,
poolMax: s.poolMax || s.maxConn || 10,
poolIncrement: s.poolIncrement || s.incrConn || 1,
poolTimeout: s.poolTimeout || s.timeout || 60,
autoCommit: s.autoCommit || s.isAutoCommit,
outFormat: oracle.OBJECT,
maxRows: s.maxRows || 0,
stmtCacheSize: s.stmtCacheSize || 30,
_enableStats: s._enableStats || false,
connectionClass: 'loopback-connector-oracle',
};
if (oracleSettings.autoCommit === undefined) {
oracleSettings.autoCommit = true; // Default to true
}
if (!oracleSettings.connectString) {
const hostname = s.host || s.hostname || 'localhost';
const port = s.port || 1521;
const database = s.database || 'XE';
oracleSettings.connectString = '//' + hostname + ':' + port +
'/' + database;
}
for (const p in s) {
if (!(p in oracleSettings) && p in configProperties) {
oracleSettings[p] = s[p];
}
}
dataSource.connector = new Oracle(oracle, oracleSettings);
dataSource.connector.dataSource = dataSource;
if (callback) {
if (s.lazyConnect) {
process.nextTick(function() {
callback();
});
} else {
dataSource.connector.connect(callback);
}
}
};
exports.Oracle = Oracle;
/**
* Oracle connector constructor
*
*
* @param {object} driver Oracle node.js binding
* @options {Object} settings Options specifying data source settings; see below.
* @prop {String} hostname The host name or ip address of the Oracle DB server
* @prop {Number} port The port number of the Oracle DB Server
* @prop {String} user The user name
* @prop {String} password The password
* @prop {String} database The database name (TNS listener name)
* @prop {Boolean|Number} debug If true, print debug messages. If Number, ?
* @class
*/
function Oracle(oracle, settings) {
this.constructor.super_.call(this, 'oracle', settings);
this.driver = oracle;
this.pool = null;
this.parallelLimit = settings.maxConn || settings.poolMax || 16;
if (settings.debug || debug.enabled) {
debug('Settings: %j', settings);
}
oracle.fetchAsString = settings.fetchAsString || [oracle.CLOB];
oracle.fetchAsBuffer = settings.fetchAsBuffer || [oracle.BLOB];
this.executeOptions = {
autoCommit: settings.autoCommit,
fetchAsString: settings.fetchAsString || [oracle.CLOB],
fetchAsBuffer: settings.fetchAsBuffer || [oracle.BLOB],
lobPrefetchSize: settings.lobPrefetchSize,
maxRows: settings.maxRows | 0,
outFormat: oracle.OBJECT,
fetchArraySize: settings.fetchArraySize || settings.prefetchRows,
};
}
// Inherit from loopback-datasource-juggler BaseSQL
require('util').inherits(Oracle, SqlConnector);
Oracle.prototype.debug = function() {
if (this.settings.debug || debug.enabled) {
debug.apply(null, arguments);
}
};
/**
* Connect to Oracle
* @param {Function} [callback] The callback after the connection is established
*/
Oracle.prototype.connect = function(callback) {
const self = this;
if (this.pool) {
if (callback) {
process.nextTick(function() {
if (callback) callback(null, self.pool);
});
}
return;
}
if (this.settings.debug) {
this.debug('Connecting to ' +
(this.settings.hostname || this.settings.connectString));
}
this.driver.createPool(this.settings, function(err, pool) {
if (!err) {
self.pool = pool;
if (self.settings.debug) {
self.debug('Connected to ' +
(self.settings.hostname || self.settings.connectString));
self.debug('Connection pool ', pool);
}
}
if (callback) callback(err, pool);
});
};
/**
* Execute the SQL statement.
*
* @param {String} sql The SQL statement.
* @param {String[]} params The parameter values for the SQL statement.
* @param {Function} [callback] The callback after the SQL statement is executed.
*/
Oracle.prototype.executeSQL = function(sql, params, options, callback) {
const self = this;
if (self.settings.debug) {
if (params && params.length > 0) {
self.debug('SQL: %s \nParameters: %j', sql, params);
} else {
self.debug('SQL: %s', sql);
}
}
const executeOptions = {};
for (const i in this.executeOptions) {
executeOptions[i] = this.executeOptions[i];
}
const transaction = options.transaction;
if (transaction && transaction.connection &&
transaction.connector === this) {
debug('Execute SQL within a transaction');
executeOptions.autoCommit = false;
transaction.connection.execute(sql, params, executeOptions,
function(err, data) {
if (err && self.settings.debug) {
self.debug(err);
}
if (self.settings.debug && data) {
self.debug('Result: %j', data);
}
if (data && data.rows) {
data = data.rows;
}
callback(err, data);
});
return;
}
self.pool.getConnection(function(err, connection) {
if (err) {
if (callback) callback(err);
return;
}
if (self.settings.debug) {
self.debug('Connection acquired: ', self.pool);
}
connection.clientId = self.settings.clientId || 'LoopBack';
connection.module = self.settings.module || 'loopback-connector-oracle';
connection.action = self.settings.action || '';
executeOptions.autoCommit = true;
connection.execute(sql, params, executeOptions,
function(err, data) {
if (err && self.settings.debug) {
self.debug(err);
}
if (!err && data) {
if (data.rows) {
data = data.rows;
if (self.settings.debug && data) {
self.debug('Result: %j', data);
}
}
}
releaseConnectionAndCallback();
function releaseConnectionAndCallback() {
connection.release(function(err2) {
if (err2) {
self.debug(err2);
}
if (self.settings.debug) {
self.debug('Connection released: ', self.pool);
}
callback(err || err2, data);
});
}
});
});
};
/**
* 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
*/
Oracle.prototype.getPlaceholderForValue = function(key) {
return ':' + key;
};
Oracle.prototype.getCountForAffectedRows = function(model, info) {
return info && info.rowsAffected;
};
Oracle.prototype.getInsertedId = function(model, info) {
return info && info.outBinds && info.outBinds[0][0];
};
Oracle.prototype.buildInsertDefaultValues = function(model, data, options) {
// Oracle doesn't like empty column/value list
const idCol = this.idColumnEscaped(model);
return '(' + idCol + ') VALUES(DEFAULT)';
};
Oracle.prototype.buildInsertReturning = function(model, data, options) {
const modelDef = this.getModelDefinition(model);
const type = modelDef.properties[this.idName(model)].type;
let outParam = null;
if (type === Number) {
outParam = {type: oracle.NUMBER, dir: oracle.BIND_OUT};
} else if (type === Date) {
outParam = {type: oracle.DATE, dir: oracle.BIND_OUT};
} else {
outParam = {type: oracle.STRING, dir: oracle.BIND_OUT};
}
const params = [outParam];
const returningStmt = new ParameterizedSQL('RETURNING ' +
this.idColumnEscaped(model) + ' into ?', params);
return returningStmt;
};
/**
* Create the data model in Oracle
*
* @param {String} model The model name
* @param {Object} data The model instance data
* @param {Function} [callback] The callback function
*/
Oracle.prototype.create = function(model, data, options, callback) {
const self = this;
const stmt = this.buildInsert(model, data, options);
this.execute(stmt.sql, stmt.params, options, function(err, info) {
if (err) {
if (err.toString().indexOf('ORA-00001: unique constraint') >= 0) {
// Transform the error so that duplicate can be checked using regex
err = new Error(g.f('%s. Duplicate id detected.', err.toString()));
}
callback(err);
} else {
const insertedId = self.getInsertedId(model, info);
callback(err, insertedId);
}
});
};
function dateToOracle(val, dateOnly) {
function fz(v) {
return v < 10 ? '0' + v : v;
}
function ms(v) {
if (v < 10) {
return '00' + v;
} else if (v < 100) {
return '0' + v;
} else {
return '' + v;
}
}
let dateStr = [
val.getUTCFullYear(),
fz(val.getUTCMonth() + 1),
fz(val.getUTCDate()),
].join('-') + ' ' + [
fz(val.getUTCHours()),
fz(val.getUTCMinutes()),
fz(val.getUTCSeconds()),
].join(':');
if (!dateOnly) {
dateStr += '.' + ms(val.getMilliseconds());
}
if (dateOnly) {
return new ParameterizedSQL(
"to_date(?,'yyyy-mm-dd hh24:mi:ss')", [dateStr],
);
} else {
return new ParameterizedSQL(
"to_timestamp(?,'yyyy-mm-dd hh24:mi:ss.ff3')", [dateStr],
);
}
}
Oracle.prototype.toColumnValue = function(prop, val) {
if (val == null) {
// PostgreSQL complains with NULLs in not null columns
// If we have an autoincrement value, return DEFAULT instead
if (prop.autoIncrement || prop.id) {
return new ParameterizedSQL('DEFAULT');
} else {
return null;
}
}
if (prop.type === String) {
return String(val);
}
if (prop.type === Number) {
if (isNaN(val)) {
// Map NaN to NULL
return val;
}
return val;
}
if (prop.type === Date || prop.type.name === 'Timestamp') {
return dateToOracle(val, prop.type === Date);
}
// Oracle support char(1) Y/N
if (prop.type === Boolean) {
if (val) {
return 'Y';
} else {
return 'N';
}
}
return this.serializeObject(val);
};
Oracle.prototype.fromColumnValue = function(prop, val) {
if (val == null) {
return val;
}
const type = prop && prop.type;
if (type === Boolean) {
if (typeof val === 'boolean') {
return val;
} else {
return (val === 'Y' || val === 'y' || val === 'T' ||
val === 't' || val === '1');
}
}
return val;
};
/*!
* Convert to the Database name
* @param {String} name The name
* @returns {String} The converted name
*/
Oracle.prototype.dbName = function(name) {
if (!name) {
return name;
}
return name.toUpperCase();
};
/*!
* Escape the name for Oracle DB
* @param {String} name The name
* @returns {String} The escaped name
*/
Oracle.prototype.escapeName = function(name) {
if (!name) {
return name;
}
return '"' + name.replace(/\./g, '"."') + '"';
};
Oracle.prototype.tableEscaped = function(model) {
const schemaName = this.schema(model);
if (schemaName && schemaName !== this.settings.user) {
return this.escapeName(schemaName) + '.' +
this.escapeName(this.table(model));
} else {
return this.escapeName(this.table(model));
}
};
Oracle.prototype.buildExpression =
function(columnName, operator, columnValue, propertyDescriptor) {
let val = columnValue;
if (columnValue instanceof RegExp) {
val = columnValue.source;
operator = 'regexp';
}
switch (operator) {
case 'like':
return new ParameterizedSQL({
sql: columnName + " LIKE ? ESCAPE '\\'",
params: [val],
});
case 'nlike':
return new ParameterizedSQL({
sql: columnName + " NOT LIKE ? ESCAPE '\\'",
params: [val],
});
case 'regexp':
/**
* match_parameter is a text literal that lets you change the default
* matching behavior of the function. You can specify one or more of
* the following values for match_parameter:
* - 'i' specifies case-insensitive matching.
* - 'c' specifies case-sensitive matching.
* - 'n' allows the period (.), which is the match-any-character
* wildcard character, to match the newline character. If you omit this
* parameter, the period does not match the newline character.
* - 'm' treats the source string as multiple lines. Oracle interprets
* ^ and $ as the start and end, respectively, of any line anywhere in
* the source string, rather than only at the start or end of the entire
* source string. If you omit this parameter, Oracle treats the source
* string as a single line.
*
* If you specify multiple contradictory values, Oracle uses the last
* value. For example, if you specify 'ic', then Oracle uses
* case-sensitive matching. If you specify a character other than those
* shown above, then Oracle returns an error.
*
* If you omit match_parameter, then:
* - The default case sensitivity is determined by the value of the NLS_SORT parameter.
* - A period (.) does not match the newline character.
* - The source string is treated as a single line.
*/
let flag = '';
if (columnValue.ignoreCase) {
flag += 'i';
}
if (columnValue.multiline) {
flag += 'm';
}
if (columnValue.global) {
g.warn('{{Oracle}} regex syntax does not respect the {{`g`}} flag');
}
if (flag) {
return new ParameterizedSQL({
sql: 'REGEXP_LIKE(' + columnName + ', ?, ?)',
params: [val, flag],
});
} else {
return new ParameterizedSQL({
sql: 'REGEXP_LIKE(' + columnName + ', ?)',
params: [val],
});
}
default:
// Invoke the base implementation of `buildExpression`
const exp = this.invokeSuper('buildExpression',
columnName, operator, columnValue, propertyDescriptor);
return exp;
}
};
function buildLimit(limit, offset) {
if (isNaN(offset)) {
offset = 0;
}
let sql = 'OFFSET ' + offset + ' ROWS';
if (limit >= 0) {
sql += ' FETCH NEXT ' + limit + ' ROWS ONLY';
}
return sql;
}
Oracle.prototype.applyPagination =
function(model, stmt, filter) {
const offset = filter.offset || filter.skip || 0;
if (this.settings.supportsOffsetFetch) {
// Oracle 12.c or later
const limitClause =
buildLimit(filter.limit, filter.offset || filter.skip);
return stmt.merge(limitClause);
} else {
let paginatedSQL = 'SELECT * FROM (' + stmt.sql + ' ' +
')' + ' ' + ' WHERE R > ' + offset;
if (filter.limit !== -1) {
paginatedSQL += ' AND R <= ' + (offset + filter.limit);
}
stmt.sql = paginatedSQL + ' ';
return stmt;
}
};
Oracle.prototype.buildColumnNames = function(model, filter) {
let columnNames = this.invokeSuper('buildColumnNames', model, filter);
if (filter.limit || filter.offset || filter.skip) {
const orderBy = this.buildOrderBy(model, filter.order);
columnNames += ',ROW_NUMBER() OVER' + ' (' + orderBy + ') R';
}
return columnNames;
};
Oracle.prototype.buildSelect = function(model, filter, options) {
if (!filter.order) {
const idNames = this.idNames(model);
if (idNames && idNames.length) {
filter.order = idNames;
}
}
let selectStmt = new ParameterizedSQL('SELECT ' +
this.buildColumnNames(model, filter) +
' FROM ' + this.tableEscaped(model));
if (filter) {
if (filter.where) {
const whereStmt = this.buildWhere(model, filter.where);
selectStmt.merge(whereStmt);
}
if (filter.limit || filter.skip || filter.offset) {
selectStmt = this.applyPagination(
model, selectStmt, filter,
);
} else {
if (filter.order) {
selectStmt.merge(this.buildOrderBy(model, filter.order));
}
}
}
return this.parameterize(selectStmt);
};
/**
* Disconnect from Oracle
* @param {Function} [cb] The callback function
*/
Oracle.prototype.disconnect = function disconnect(cb) {
const err = null;
if (this.pool) {
if (this.settings.debug) {
this.debug('Disconnecting from ' +
(this.settings.hostname || this.settings.connectString));
}
const pool = this.pool;
this.pool = null;
return pool.terminate(cb);
}
if (cb) {
process.nextTick(function() {
cb(err);
});
}
};
Oracle.prototype.ping = function(cb) {
this.execute('select count(*) as result from user_tables', [], cb);
};
require('./migration')(Oracle, oracle);
require('./discovery')(Oracle, oracle);
require('./transaction')(Oracle, oracle);