2019-04-18 21:32:38 +00:00
|
|
|
// Copyright IBM Corp. 2015,2019. All Rights Reserved.
|
2016-05-06 04:50:59 +00:00
|
|
|
// Node module: loopback-connector
|
|
|
|
// This file is licensed under the MIT License.
|
|
|
|
// License text available at https://opensource.org/licenses/MIT
|
|
|
|
|
2017-03-06 23:40:47 +00:00
|
|
|
'use strict';
|
2019-08-02 13:50:51 +00:00
|
|
|
const assert = require('assert');
|
|
|
|
const util = require('util');
|
|
|
|
const EventEmitter = require('events').EventEmitter;
|
|
|
|
const debug = require('debug')('loopback:connector:transaction');
|
|
|
|
const uuid = require('uuid');
|
2019-06-26 11:39:15 +00:00
|
|
|
const {createPromiseCallback} = require('./utils');
|
2015-05-15 17:27:08 +00:00
|
|
|
|
|
|
|
module.exports = Transaction;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new Transaction object
|
|
|
|
* @param {Connector} connector The connector instance
|
|
|
|
* @param {*} connection A connection to the DB
|
|
|
|
* @constructor
|
|
|
|
*/
|
|
|
|
function Transaction(connector, connection) {
|
|
|
|
this.connector = connector;
|
|
|
|
this.connection = connection;
|
|
|
|
EventEmitter.call(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
util.inherits(Transaction, EventEmitter);
|
|
|
|
|
|
|
|
// Isolation levels
|
|
|
|
Transaction.SERIALIZABLE = 'SERIALIZABLE';
|
|
|
|
Transaction.REPEATABLE_READ = 'REPEATABLE READ';
|
|
|
|
Transaction.READ_COMMITTED = 'READ COMMITTED';
|
|
|
|
Transaction.READ_UNCOMMITTED = 'READ UNCOMMITTED';
|
|
|
|
|
|
|
|
Transaction.hookTypes = {
|
|
|
|
BEFORE_COMMIT: 'before commit',
|
|
|
|
AFTER_COMMIT: 'after commit',
|
|
|
|
BEFORE_ROLLBACK: 'before rollback',
|
|
|
|
AFTER_ROLLBACK: 'after rollback',
|
2016-04-09 18:35:52 +00:00
|
|
|
TIMEOUT: 'timeout',
|
2015-05-15 17:27:08 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Commit a transaction and release it back to the pool
|
|
|
|
* @param cb
|
|
|
|
* @returns {*}
|
|
|
|
*/
|
|
|
|
Transaction.prototype.commit = function(cb) {
|
2019-06-26 11:39:15 +00:00
|
|
|
cb = cb || createPromiseCallback();
|
|
|
|
if (cb.promise) {
|
|
|
|
this.connector.commit(this.connection, cb);
|
|
|
|
return cb.promise;
|
|
|
|
} else {
|
|
|
|
return this.connector.commit(this.connection, cb);
|
|
|
|
}
|
2015-05-15 17:27:08 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Rollback a transaction and release it back to the pool
|
|
|
|
* @param cb
|
|
|
|
* @returns {*|boolean}
|
|
|
|
*/
|
|
|
|
Transaction.prototype.rollback = function(cb) {
|
2019-06-26 11:39:15 +00:00
|
|
|
cb = cb || createPromiseCallback();
|
|
|
|
if (cb.promise) {
|
|
|
|
this.connector.rollback(this.connection, cb);
|
|
|
|
return cb.promise;
|
|
|
|
} else {
|
|
|
|
return this.connector.rollback(this.connection, cb);
|
|
|
|
}
|
2015-05-15 17:27:08 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Begin a new transaction
|
|
|
|
* @param {Connector} connector The connector instance
|
|
|
|
* @param {Object} [options] Options {isolationLevel: '...', timeout: 1000}
|
|
|
|
* @param cb
|
|
|
|
*/
|
|
|
|
Transaction.begin = function(connector, options, cb) {
|
2015-10-13 10:59:17 +00:00
|
|
|
if (typeof options === 'function' && cb === undefined) {
|
2015-05-15 17:27:08 +00:00
|
|
|
cb = options;
|
|
|
|
options = {};
|
|
|
|
}
|
2019-06-26 11:39:15 +00:00
|
|
|
cb = cb || createPromiseCallback();
|
2015-05-15 17:27:08 +00:00
|
|
|
if (typeof options === 'string') {
|
2017-03-06 23:40:47 +00:00
|
|
|
options = {isolationLevel: options};
|
2015-05-15 17:27:08 +00:00
|
|
|
}
|
2019-08-02 13:50:51 +00:00
|
|
|
const isolationLevel = options.isolationLevel || Transaction.READ_COMMITTED;
|
2015-05-15 17:27:08 +00:00
|
|
|
assert(isolationLevel === Transaction.SERIALIZABLE ||
|
|
|
|
isolationLevel === Transaction.REPEATABLE_READ ||
|
|
|
|
isolationLevel === Transaction.READ_COMMITTED ||
|
|
|
|
isolationLevel === Transaction.READ_UNCOMMITTED, 'Invalid isolationLevel');
|
|
|
|
|
|
|
|
debug('Starting a transaction with options: %j', options);
|
|
|
|
assert(typeof connector.beginTransaction === 'function',
|
|
|
|
'beginTransaction must be function implemented by the connector');
|
|
|
|
connector.beginTransaction(isolationLevel, function(err, connection) {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
2019-08-02 13:50:51 +00:00
|
|
|
let tx = connection;
|
2017-07-14 02:35:01 +00:00
|
|
|
|
|
|
|
// When the connector and juggler node module have different version of this module as a dependency,
|
|
|
|
// the transaction is not an instanceof Transaction.
|
|
|
|
// i.e. (connection instanceof Transaction) == false
|
|
|
|
// Check for existence of required functions and properties, instead of prototype inheritance.
|
|
|
|
if (connection.connector == undefined || connection.connection == undefined ||
|
|
|
|
connection.commit == undefined || connection.rollback == undefined) {
|
2015-05-15 17:27:08 +00:00
|
|
|
tx = new Transaction(connector, connection);
|
|
|
|
}
|
2017-08-31 14:43:51 +00:00
|
|
|
// Set an informational transaction id
|
|
|
|
tx.id = uuid.v1();
|
|
|
|
// NOTE(lehni) Handling of transaction timeouts here only works with recent
|
|
|
|
// versions of `loopback-datasource-juggler` which make its own handling of
|
|
|
|
// timeouts conditional based on the absence of an already set `tx.timeout`,
|
|
|
|
// see: https://github.com/strongloop/loopback-datasource-juggler/pull/1484
|
|
|
|
if (options.timeout) {
|
|
|
|
tx.timeout = setTimeout(function() {
|
2019-08-02 13:50:51 +00:00
|
|
|
const context = {
|
2017-08-31 14:43:51 +00:00
|
|
|
transaction: tx,
|
|
|
|
operation: 'timeout',
|
|
|
|
};
|
|
|
|
tx.notifyObserversOf('timeout', context, function(err) {
|
|
|
|
if (!err) {
|
|
|
|
tx.rollback(function() {
|
|
|
|
debug('Transaction %s is rolled back due to timeout', tx.id);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}, options.timeout);
|
|
|
|
}
|
2015-05-15 17:27:08 +00:00
|
|
|
cb(err, tx);
|
|
|
|
});
|
2019-06-26 11:39:15 +00:00
|
|
|
if (cb.promise) return cb.promise;
|
2015-05-15 17:27:08 +00:00
|
|
|
};
|
2020-01-16 19:57:53 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Check whether a transaction has an active connection.
|
|
|
|
*/
|
|
|
|
Transaction.prototype.isActive = function() {
|
|
|
|
return !!this.connection;
|
|
|
|
}
|