loopback-connector/lib/transaction.js

144 lines
4.5 KiB
JavaScript

// Copyright IBM Corp. 2015,2020. All Rights Reserved.
// Node module: loopback-connector
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
const assert = require('assert');
const util = require('util');
const EventEmitter = require('events').EventEmitter;
const debug = require('debug')('loopback:connector:transaction');
const uuid = require('uuid');
const {createPromiseCallback} = require('./utils');
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',
TIMEOUT: 'timeout',
};
/**
* Commit a transaction and release it back to the pool
* @param cb
* @returns {*}
*/
Transaction.prototype.commit = function(cb) {
cb = cb || createPromiseCallback();
if (cb.promise) {
this.connector.commit(this.connection, cb);
return cb.promise;
} else {
return this.connector.commit(this.connection, cb);
}
};
/**
* Rollback a transaction and release it back to the pool
* @param cb
* @returns {*|boolean}
*/
Transaction.prototype.rollback = function(cb) {
cb = cb || createPromiseCallback();
if (cb.promise) {
this.connector.rollback(this.connection, cb);
return cb.promise;
} else {
return this.connector.rollback(this.connection, cb);
}
};
/**
* 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) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
cb = cb || createPromiseCallback();
if (typeof options === 'string') {
options = {isolationLevel: options};
}
const isolationLevel = options.isolationLevel || Transaction.READ_COMMITTED;
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);
}
let tx = connection;
// 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) {
tx = new Transaction(connector, connection);
}
// 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() {
const context = {
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);
}
cb(err, tx);
});
if (cb.promise) return cb.promise;
};
/**
* Check whether a transaction has an active connection.
*/
Transaction.prototype.isActive = function() {
return !!this.connection;
};