UNPKG

simple-oracledb

Version:

Extend capabilities of oracledb with simplified API for quicker development.

513 lines (465 loc) 19.8 kB
'use strict'; const debug = require('debuglog')('simple-oracledb'); const asyncLib = require('async'); const Connection = require('./connection'); const extensions = require('./extensions'); const emitter = require('./emitter'); const promiseHelper = require('./promise-helper'); const constants = require('./constants'); let poolIDCounter = 0; /** * Holds query invocation definitions. * * @typedef {Object} QuerySpec * @param {String} sql - The SQL to execute * @param {Object} [bindParams] - Optional bind parameters * @param {Object} [options] - Optional query options */ /** * This events is triggered when a connection is created via pool. * * @event Pool#connection-created * @param {Connection} connection - The connection instance */ /** * This events is triggered when a connection is released successfully. * * @event Pool#connection-released * @param {Connection} connection - The connection instance */ /** * This events is triggered after the pool is released successfully. * * @event Pool#release */ /*jslint debug: true */ /*istanbul ignore next*/ /** * This class holds all the extended capabilities added the oracledb pool. * * @author Sagie Gur-Ari * @class Pool * @public * @fires event:connection-created * @fires event:connection-released * @fires event:release */ function Pool() { //should not be called } /*jslint debug: false */ /** * Marker property. * * @member {Boolean} * @alias Pool.simplified * @memberof! Pool * @public */ Pool.prototype.simplified = true; /** * Sets up events based on connection events. * * @function * @memberof! Pool * @private * @param {Connection} connection - The connection object */ Pool.prototype.setupEvents = function (connection) { const self = this; if (connection) { connection.once('release', function onRelease() { self.emit('connection-released', connection); }); self.emit('connection-created', connection); } }; /*eslint-disable valid-jsdoc*/ //jscs:disable jsDoc /** * Wraps the original oracledb getConnection in order to provide an extended connection object.<br> * In addition, this function will attempt to fetch a connection from the pool and in case of any error will reattempt for a configurable amount of times.<br> * It will also ensure the provided connection is valid by running a test SQL and if validation fails, it will fetch another connection (continue to reattempt).<br> * See [getConnection](https://github.com/oracle/node-oracledb/blob/master/doc/api.md#getconnectionpool) for official API details.<br> * See [createPool](https://github.com/sagiegurari/simple-oracledb/blob/master/docs/api.md#SimpleOracleDB.oracle.createPool) for extended createPool API details. * * @function * @memberof! Pool * @public * @param {AsyncCallback} [callback] - Invoked with an error or an extended connection object * @returns {Promise} In case of no callback provided in input, this function will return a promise * @fires event:connection-created * @example * ```js * oracledb.createPool({ * retryCount: 5, //The max amount of retries to get a connection from the pool in case of any error (default to 10 if not provided) * retryInterval: 500, //The interval in millies between get connection retry attempts (defaults to 250 millies if not provided) * runValidationSQL: true, //True to ensure the connection returned is valid by running a test validation SQL (defaults to true) * usePingValidation: true, //If runValidationSQL, this flag will define if validation should first attempt to use connection.ping instead of running a SQL * validationSQL: 'SELECT 1 FROM DUAL', //The test SQL to invoke before returning a connection to validate the connection is open (defaults to 'SELECT 1 FROM DUAL') * //any other oracledb pool attributes * }, function onPoolCreated(error, pool) { * pool.getConnection(function onConnection(poolError, connection) { * //continue flow (connection, if provided, has been tested to ensure it is valid) * }); * }); * * //another example but with promise support * oracledb.createPool({ * retryCount: 5, //The max amount of retries to get a connection from the pool in case of any error (default to 10 if not provided) * retryInterval: 500, //The interval in millies between get connection retry attempts (defaults to 250 millies if not provided) * runValidationSQL: true, //True to ensure the connection returned is valid by running a test validation SQL (defaults to true) * usePingValidation: true, //If runValidationSQL, this flag will define if validation should first attempt to use connection.ping instead of running a SQL * validationSQL: 'SELECT 1 FROM DUAL', //The test SQL to invoke before returning a connection to validate the connection is open (defaults to 'SELECT 1 FROM DUAL') * //any other oracledb pool attributes * }).then(function onPoolCreated(pool) { * pool.getConnection(function onConnection(poolError, connection) { * //continue flow (connection, if provided, has been tested to ensure it is valid) * }); * }); * ``` */ Pool.prototype.getConnection = function (callback) { const self = this; const onWrapperConnectionCreated = function (error, connection) { /*istanbul ignore else*/ if ((!error) && connection) { self.setupEvents(connection); } callback(error, connection); }; asyncLib.retry({ times: self.poolAttributes.retryCount, interval: self.poolAttributes.retryInterval }, function attemptGetConnection(asyncCallback) { self.baseGetConnection(function onConnection(error, connection) { if (error) { debug('Unable to get pooled connection, ', error.stack); asyncCallback(error); } else if (self.poolAttributes.runValidationSQL && self.poolAttributes.validationSQL) { const onPing = function (testError) { if (testError) { debug('Pooled connection validation failed, ', testError.stack); connection.release(function onConnectionRelease(releaseError) { if (releaseError) { debug('Unable to release connection, ', releaseError.stack); } asyncCallback(testError); }); } else { asyncCallback(error, connection); } }; if (self.poolAttributes.usePingValidation && connection.ping && typeof connection.ping === 'function') { connection.ping(onPing); } else { connection.execute(self.poolAttributes.validationSQL, onPing); } } else { asyncCallback(error, connection); } }); }, Connection.wrapOnConnection(onWrapperConnectionCreated)); }; //jscs:enable jsDoc /*eslint-enable valid-jsdoc*/ //add promise support Pool.prototype.getConnection = promiseHelper.promisify(Pool.prototype.getConnection); /*eslint-disable valid-jsdoc*/ //jscs:disable jsDoc /** * This function invokes the provided action (function) with a valid connection object and a callback.<br> * The action can use the provided connection to run any connection operation/s (execute/query/transaction/...) and after finishing it * must call the callback with an error (if any) and result.<br> * For promise support, the action can simply return a promise instead of calling the provided callback.<br> * The pool will ensure the connection is released properly and only afterwards will call the provided callback with the action error/result.<br> * This function basically will remove the need of caller code to get and release a connection and focus on the actual database operation logic.<br> * For extended promise support, the action provided can return a promise instead of calling the provided callback (see examples). * * @function * @memberof! Pool * @public * @param {ConnectionAction} action - An action requested by the pool to be invoked. * @param {Object} [options] - Optional runtime options * @param {Boolean} [options.ignoreReleaseErrors=false] - If true, errors during connection.release() invoked by the pool will be ignored * @param {Object} [options.releaseOptions={force: false}] - The connection.release options (see connection.release for more info) * @param {Boolean} [options.releaseOptions.force=false] - If force=true the connection.break will be called before trying to release to ensure all running activities are aborted * @param {AsyncCallback} [callback] - Invoked with an error or the result of the action after the connection was released by the pool * @returns {Promise} In case of no callback provided in input, this function will return a promise * @example * ```js * pool.run(function (connection, callback) { * //run some query and the output will be available in the 'run' callback * connection.query('SELECT department_id, department_name FROM departments WHERE manager_id < :id', [110], callback); * }, function onActionDone(error, result) { * //do something with the result/error * }); * * pool.run(function (connection, callback) { * //run some database operations in a transaction * connection.transaction([ * function firstAction(callback) { * connection.insert(...., callback); * }, * function secondAction(callback) { * connection.update(...., callback); * } * ], { * sequence: true * }, callback); //at end of transaction, call the pool provided callback * }, { * ignoreReleaseErrors: false //enable/disable ignoring any release error (default not to ignore) * }, function onActionDone(error, result) { * //do something with the result/error * }); * * //another example but with promise support * pool.run(function (connection, callback) { * //run some query and the output will be available in the 'run' promise 'then' * connection.query('SELECT department_id, department_name FROM departments WHERE manager_id < :id', [110], callback); * }).then(function onActionDone(result) { * //do something with the result * }); * * //extended promise support (action is returning a promise instead of using the callback) * pool.run(function (connection) { * //run some query and the output will be available in the 'run' promise 'then' * return connection.query('SELECT department_id, department_name FROM departments WHERE manager_id < :id', [110]); //no need for a callback, instead return a promise * }).then(function onActionDone(result) { * //do something with the result * }); * ``` */ Pool.prototype.run = function (action, options, callback) { const self = this; if ((!callback) && (typeof options === 'function')) { callback = options; options = null; } if (action && (typeof action === 'function')) { options = options || {}; const releaseOptions = options.releaseOptions || {}; const simpleOracleDB = require('./simple-oracledb'); const actionRunner = simpleOracleDB.createOnConnectionCallback(action, options, releaseOptions, callback); self.getConnection(actionRunner); } else { callback(new Error('Illegal input provided.')); } }; //jscs:enable jsDoc /*eslint-enable valid-jsdoc*/ //add promise support Pool.prototype.run = promiseHelper.promisify(Pool.prototype.run, { callbackMinIndex: 1 }); /*eslint-disable valid-jsdoc*/ //jscs:disable jsDoc /** * This function invokes the requested queries in parallel (limiting it based on the amount of node.js thread pool size).<br> * In order for the queries to run in parallel, multiple connections will be used so use this with caution. * * @function * @memberof! Pool * @public * @param {QuerySpec[]} querySpec - Array of query spec objects * @param {Object} [options] - Optional runtime options * @param {Number} [options.limit] - The max connections to be used in parallel (if not provided, it will be calcaulated based on the current node.js thread pool size) * @param {AsyncCallback} [callback] - Invoked with an error or an array of query results * @returns {Promise} In case of no callback provided in input, this function will return a promise * @example * ```js * pool.parallelQuery([ * { * sql: 'SELECT department_id, department_name FROM departments WHERE manager_id = :id', * bindParams: [100], * options: { * //any options here * } * }, * { * sql: 'SELECT * FROM employees WHERE manager_id = :id', * bindParams: { * id: 100 * } * } * ], function onQueriesDone(error, results) { * //do something with the result/error * const query1Results = results[0]; * const query2Results = results[1]; * }); * * //another example but with promise support * pool.parallelQuery([ * { * sql: 'SELECT department_id, department_name FROM departments WHERE manager_id = :id', * bindParams: [100], * options: { * //any options here * } * }, * { * sql: 'SELECT * FROM employees WHERE manager_id = :id', * bindParams: { * id: 100 * } * } * ]).then(function onQueriesDone(results) { * //do something with the result * const query1Results = results[0]; * const query2Results = results[1]; * }); * ``` */ Pool.prototype.parallelQuery = function (querySpec, options, callback) { const self = this; if ((!callback) && (typeof options === 'function')) { callback = options; options = null; } if ((!querySpec) || (!querySpec.length)) { callback(new Error('Query spec not provided.')); } else { //get limit let limit = constants.parallelLimit; if (options && options.limit) { limit = options.limit; } const createQuery = function (spec) { return function run(asyncCallback) { self.run(function query(connection, queryCallback) { connection.query(spec.sql, spec.bindParams, spec.options, queryCallback); }, asyncCallback); }; }; const functions = []; for (let index = 0; index < querySpec.length; index++) { functions.push(createQuery(querySpec[index])); } asyncLib.parallelLimit(functions, limit, callback); } }; //jscs:enable jsDoc /*eslint-enable valid-jsdoc*/ //add promise support Pool.prototype.parallelQuery = promiseHelper.promisify(Pool.prototype.parallelQuery); /*eslint-disable valid-jsdoc*/ //jscs:disable jsDoc /** * This function modifies the existing pool.terminate function by enabling the input * callback to be an optional parameter.<br> * Since there is no real way to release the pool that fails to be terminated, all that you can do in the callback * is just log the error and continue.<br> * Therefore this function allows you to ignore the need to pass a callback and makes it as an optional parameter.<br> * The pool.terminate also has an alias pool.close for consistent close function naming to all relevant objects. * * @function * @memberof! Pool * @public * @param {function} [callback] - An optional terminate callback function (see oracledb docs) * @returns {Promise} In case of no callback provided in input and promise is supported, this function will return a promise * @example * ```js * pool.terminate(); //no callback needed * * //still possible to call with a terminate callback function * pool.terminate(function onTerminate(error) { * if (error) { * //now what? * } * }); * * //can also use close * pool.close(); * ``` */ Pool.prototype.terminate = function (callback) { const self = this; self.baseTerminate(function onPoolTerminate(error) { self.emit('release'); callback(error); }); }; //jscs:enable jsDoc /*eslint-enable valid-jsdoc*/ //add promise support Pool.prototype.terminate = promiseHelper.promisify(Pool.prototype.terminate, { defaultCallback: true }); /** * Alias for pool.terminate, see pool.terminate for more info. * * @function * @memberof! Pool * @public * @param {function} [callback] - An optional terminate callback function (see oracledb docs) * @returns {Promise} In case of no callback provided in input and promise is supported, this function will return a promise */ Pool.prototype.close = Pool.prototype.terminate; /** * Sets the pool attribute defaults. * * @function * @memberof! Pool * @private * @param {Object} [poolAttributes] - The connection pool attributes object * @returns {Object} The modified pool attributes to use for the newly created pool * @example * ```js * //set defaults * pool.poolAttributes = setupPoolAttributes(poolAttributes); * ``` */ function setupPoolAttributes(poolAttributes) { poolAttributes = poolAttributes || {}; //set defaults poolAttributes.retryCount = Math.max(poolAttributes.retryCount || 10, 1); poolAttributes.retryInterval = poolAttributes.retryInterval || 250; if (poolAttributes.runValidationSQL === undefined) { poolAttributes.runValidationSQL = true; } if (poolAttributes.usePingValidation === undefined) { poolAttributes.usePingValidation = true; } poolAttributes.validationSQL = poolAttributes.validationSQL || 'SELECT 1 FROM DUAL'; return poolAttributes; } module.exports = { /** * Extends the provided oracledb pool instance. * * @function * @memberof! Pool * @public * @param {Object} pool - The oracledb pool instance * @param {Object} [poolAttributes] - The connection pool attributes object * @param {Number} [poolAttributes.retryCount=10] - The max amount of retries to get a connection from the pool in case of any error * @param {Number} [poolAttributes.retryInterval=250] - The interval in millies between get connection retry attempts * @param {Boolean} [poolAttributes.runValidationSQL=true] - True to ensure the connection returned is valid by running a test ping or validation SQL * @param {Boolean} [poolAttributes.usePingValidation=true] - If runValidationSQL, this flag will define if validation should first attempt to use connection.ping instead of running a SQL * @param {String} [poolAttributes.validationSQL=SELECT 1 FROM DUAL] - The test SQL to invoke before returning a connection to validate the connection is open */ extend: function extend(pool, poolAttributes) { if (pool && (!pool.simplified)) { //set defaults pool.poolAttributes = setupPoolAttributes(poolAttributes); let properties = Object.keys(Pool.prototype); properties.forEach(function addProperty(property) { if (typeof pool[property] === 'function') { pool['base' + property.charAt(0).toUpperCase() + property.slice(1)] = pool[property]; } pool[property] = Pool.prototype[property]; }); const extendedCapabilities = extensions.get('pool'); properties = Object.keys(extendedCapabilities); properties.forEach(function addProperty(property) { if (!pool[property]) { pool[property] = extendedCapabilities[property]; } }); emitter(pool); pool.diagnosticInfo = pool.diagnosticInfo || {}; pool.diagnosticInfo.id = poolIDCounter; poolIDCounter++; } } };