2016-04-01 22:25:16 +00:00
|
|
|
// Copyright IBM Corp. 2015,2016. All Rights Reserved.
|
|
|
|
// Node module: loopback-datasource-juggler
|
|
|
|
// This file is licensed under the MIT License.
|
|
|
|
// License text available at https://opensource.org/licenses/MIT
|
2016-08-22 19:55:22 +00:00
|
|
|
'use strict';
|
2016-04-01 22:25:16 +00:00
|
|
|
|
2018-12-07 16:13:48 +00:00
|
|
|
const g = require('strong-globalize')();
|
|
|
|
const debug = require('debug')('loopback:connector:transaction');
|
|
|
|
const uuid = require('uuid');
|
|
|
|
const utils = require('./utils');
|
|
|
|
const jutil = require('./jutil');
|
|
|
|
const ObserverMixin = require('./observer');
|
2015-05-15 17:26:49 +00:00
|
|
|
|
2018-12-07 16:13:48 +00:00
|
|
|
const Transaction = require('loopback-connector').Transaction;
|
2015-05-13 16:33:49 +00:00
|
|
|
|
|
|
|
module.exports = TransactionMixin;
|
|
|
|
|
2015-05-18 16:00:49 +00:00
|
|
|
/**
|
|
|
|
* TransactionMixin class. Use to add transaction APIs to a model class.
|
|
|
|
*
|
|
|
|
* @class TransactionMixin
|
|
|
|
*/
|
2015-05-13 16:33:49 +00:00
|
|
|
function TransactionMixin() {
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-06-01 15:00:44 +00:00
|
|
|
* Begin a new transaction.
|
2015-05-13 16:33:49 +00:00
|
|
|
*
|
2017-06-01 15:00:44 +00:00
|
|
|
* A transaction can be committed or rolled back. If timeout happens, the
|
|
|
|
* transaction will be rolled back. Please note a transaction is typically
|
|
|
|
* associated with a pooled connection. Committing or rolling back a transaction
|
|
|
|
* will release the connection back to the pool.
|
2015-05-13 16:33:49 +00:00
|
|
|
*
|
2017-06-01 15:00:44 +00:00
|
|
|
* Once the transaction is committed or rolled back, the connection property
|
|
|
|
* will be set to null to mark the transaction to be inactive. Trying to commit
|
|
|
|
* or rollback an inactive transaction will receive an error from the callback.
|
|
|
|
*
|
|
|
|
* Please also note that the transaction is only honored with the same data
|
|
|
|
* source/connector instance. CRUD methods will not join the current transaction
|
|
|
|
* if its model is not attached the same data source.
|
|
|
|
*
|
|
|
|
* Example:
|
2015-05-13 16:33:49 +00:00
|
|
|
*
|
|
|
|
* To pass the transaction context to one of the CRUD methods, use the `options`
|
|
|
|
* argument with `transaction` property, for example,
|
|
|
|
*
|
|
|
|
* ```js
|
|
|
|
* MyModel.beginTransaction('READ COMMITTED', function(err, tx) {
|
|
|
|
* MyModel.create({x: 1, y: 'a'}, {transaction: tx}, function(err, inst) {
|
|
|
|
* MyModel.find({x: 1}, {transaction: tx}, function(err, results) {
|
|
|
|
* // ...
|
|
|
|
* tx.commit(function(err) {...});
|
|
|
|
* });
|
|
|
|
* });
|
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*
|
2017-06-01 15:00:44 +00:00
|
|
|
* @param {Object|String} options Options to be passed upon transaction.
|
2015-05-13 16:33:49 +00:00
|
|
|
*
|
2017-06-01 15:00:44 +00:00
|
|
|
* Can be one of the forms:
|
|
|
|
* - Object: {isolationLevel: '...', timeout: 1000}
|
|
|
|
* - String: isolationLevel
|
2015-05-15 17:26:49 +00:00
|
|
|
*
|
2017-06-01 15:00:44 +00:00
|
|
|
* Valid values of `isolationLevel` are:
|
2015-05-15 17:26:49 +00:00
|
|
|
*
|
2017-06-01 15:00:44 +00:00
|
|
|
* - Transaction.READ_COMMITTED = 'READ COMMITTED'; // default
|
|
|
|
* - Transaction.READ_UNCOMMITTED = 'READ UNCOMMITTED';
|
|
|
|
* - Transaction.SERIALIZABLE = 'SERIALIZABLE';
|
|
|
|
* - Transaction.REPEATABLE_READ = 'REPEATABLE READ';
|
|
|
|
* @callback {Function} cb Callback function.
|
|
|
|
* @returns {Promise|undefined} Returns a callback promise.
|
2015-05-13 16:33:49 +00:00
|
|
|
*/
|
|
|
|
TransactionMixin.beginTransaction = function(options, cb) {
|
2015-05-15 17:26:49 +00:00
|
|
|
cb = cb || utils.createPromiseCallback();
|
2015-05-13 16:33:49 +00:00
|
|
|
if (Transaction) {
|
2018-12-07 16:13:48 +00:00
|
|
|
const connector = this.getConnector();
|
2015-05-15 17:26:49 +00:00
|
|
|
Transaction.begin(connector, options, function(err, transaction) {
|
|
|
|
if (err) return cb(err);
|
2017-08-31 15:10:17 +00:00
|
|
|
// NOTE(lehni) As part of the process of moving the handling of
|
|
|
|
// transaction id and timeout from TransactionMixin.beginTransaction() to
|
|
|
|
// Transaction.begin() in loopback-connector, switch to only setting id
|
|
|
|
// and timeout if it wasn't taken care of already by Transaction.begin().
|
|
|
|
// Eventually, we can remove the following two if-blocks altogether.
|
|
|
|
if (!transaction.id) {
|
2015-05-15 17:26:49 +00:00
|
|
|
// Set an informational transaction id
|
|
|
|
transaction.id = uuid.v1();
|
|
|
|
}
|
2017-08-31 15:10:17 +00:00
|
|
|
if (options.timeout && !transaction.timeout) {
|
|
|
|
transaction.timeout = setTimeout(function() {
|
2018-12-07 16:13:48 +00:00
|
|
|
const context = {
|
2015-05-15 17:26:49 +00:00
|
|
|
transaction: transaction,
|
2016-04-01 11:48:17 +00:00
|
|
|
operation: 'timeout',
|
2015-05-15 17:26:49 +00:00
|
|
|
};
|
|
|
|
transaction.notifyObserversOf('timeout', context, function(err) {
|
|
|
|
if (!err) {
|
|
|
|
transaction.rollback(function() {
|
|
|
|
debug('Transaction %s is rolled back due to timeout',
|
|
|
|
transaction.id);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}, options.timeout);
|
|
|
|
}
|
|
|
|
cb(err, transaction);
|
|
|
|
});
|
2015-05-13 16:33:49 +00:00
|
|
|
} else {
|
|
|
|
process.nextTick(function() {
|
2018-12-07 16:13:48 +00:00
|
|
|
const err = new Error(g.f('{{Transaction}} is not supported'));
|
2015-05-13 16:33:49 +00:00
|
|
|
cb(err);
|
|
|
|
});
|
|
|
|
}
|
2015-05-15 17:26:49 +00:00
|
|
|
return cb.promise;
|
2015-05-13 16:33:49 +00:00
|
|
|
};
|
|
|
|
|
2015-05-15 17:26:49 +00:00
|
|
|
// Promisify the transaction apis
|
|
|
|
if (Transaction) {
|
|
|
|
jutil.mixin(Transaction.prototype, ObserverMixin);
|
|
|
|
/**
|
2017-06-01 15:00:44 +00:00
|
|
|
* Commit a transaction and release it back to the pool.
|
|
|
|
*
|
|
|
|
* Example:
|
|
|
|
*
|
|
|
|
* ```js
|
|
|
|
* MyModel.beginTransaction('READ COMMITTED', function(err, tx) {
|
|
|
|
* // some crud operation of your choice
|
|
|
|
* tx.commit(function(err) {
|
|
|
|
* // release the connection pool upon committing
|
|
|
|
* tx.close(err);
|
|
|
|
* });
|
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @callback {Function} cb Callback function.
|
|
|
|
* @returns {Promise|undefined} Returns a callback promise.
|
2015-05-15 17:26:49 +00:00
|
|
|
*/
|
|
|
|
Transaction.prototype.commit = function(cb) {
|
|
|
|
cb = cb || utils.createPromiseCallback();
|
2017-09-06 05:10:57 +00:00
|
|
|
if (this.ensureActive(cb)) {
|
2018-12-07 16:13:48 +00:00
|
|
|
const context = {
|
2017-09-06 05:10:57 +00:00
|
|
|
transaction: this,
|
|
|
|
operation: 'commit',
|
|
|
|
};
|
|
|
|
this.notifyObserversAround('commit', context,
|
|
|
|
done => {
|
|
|
|
this.connector.commit(this.connection, done);
|
|
|
|
},
|
|
|
|
err => {
|
|
|
|
// Deference the connection to mark the transaction is not active
|
|
|
|
// The connection should have been released back the pool
|
|
|
|
this.connection = null;
|
|
|
|
cb(err);
|
2018-07-16 06:46:25 +00:00
|
|
|
});
|
2015-05-15 17:26:49 +00:00
|
|
|
}
|
|
|
|
return cb.promise;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2017-06-01 15:00:44 +00:00
|
|
|
* Rollback a transaction and release it back to the pool.
|
|
|
|
*
|
|
|
|
* Example:
|
|
|
|
*
|
|
|
|
* ```js
|
|
|
|
* MyModel.beginTransaction('READ COMMITTED', function(err, tx) {
|
|
|
|
* // some crud operation of your choice
|
|
|
|
* tx.rollback(function(err) {
|
|
|
|
* // release the connection pool upon committing
|
|
|
|
* tx.close(err);
|
|
|
|
* });
|
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @callback {Function} cb Callback function.
|
|
|
|
* @returns {Promise|undefined} Returns a callback promise.
|
2015-05-15 17:26:49 +00:00
|
|
|
*/
|
|
|
|
Transaction.prototype.rollback = function(cb) {
|
|
|
|
cb = cb || utils.createPromiseCallback();
|
2017-09-06 05:10:57 +00:00
|
|
|
if (this.ensureActive(cb)) {
|
2018-12-07 16:13:48 +00:00
|
|
|
const context = {
|
2017-09-06 05:10:57 +00:00
|
|
|
transaction: this,
|
|
|
|
operation: 'rollback',
|
|
|
|
};
|
|
|
|
this.notifyObserversAround('rollback', context,
|
|
|
|
done => {
|
|
|
|
this.connector.rollback(this.connection, done);
|
|
|
|
},
|
|
|
|
err => {
|
|
|
|
// Deference the connection to mark the transaction is not active
|
|
|
|
// The connection should have been released back the pool
|
|
|
|
this.connection = null;
|
|
|
|
cb(err);
|
2018-07-16 06:46:25 +00:00
|
|
|
});
|
2015-05-15 17:26:49 +00:00
|
|
|
}
|
2017-09-06 05:10:57 +00:00
|
|
|
return cb.promise;
|
|
|
|
};
|
2015-05-20 22:02:44 +00:00
|
|
|
|
2017-09-06 05:10:57 +00:00
|
|
|
Transaction.prototype.ensureActive = function(cb) {
|
|
|
|
// Report an error if the transaction is not active
|
|
|
|
if (!this.connection) {
|
|
|
|
process.nextTick(() => {
|
|
|
|
cb(new Error(g.f('The {{transaction}} is not active: %s', this.id)));
|
|
|
|
});
|
2015-05-20 22:02:44 +00:00
|
|
|
}
|
2017-09-06 05:10:57 +00:00
|
|
|
return !!this.connection;
|
2015-05-15 17:26:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Transaction.prototype.toJSON = function() {
|
|
|
|
return this.id;
|
|
|
|
};
|
|
|
|
|
|
|
|
Transaction.prototype.toString = function() {
|
|
|
|
return this.id;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2015-05-18 16:00:49 +00:00
|
|
|
TransactionMixin.Transaction = Transaction;
|