UNPKG

6.85 kBJavaScriptView Raw
1// Copyright IBM Corp. 2015,2019. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6'use strict';
7
8const g = require('strong-globalize')();
9const debug = require('debug')('loopback:connector:transaction');
10const uuid = require('uuid');
11const utils = require('./utils');
12const jutil = require('./jutil');
13const ObserverMixin = require('./observer');
14
15const Transaction = require('loopback-connector').Transaction;
16
17module.exports = TransactionMixin;
18
19/**
20 * TransactionMixin class. Use to add transaction APIs to a model class.
21 *
22 * @class TransactionMixin
23 */
24function TransactionMixin() {
25}
26
27/**
28 * Begin a new transaction.
29 *
30 * A transaction can be committed or rolled back. If timeout happens, the
31 * transaction will be rolled back. Please note a transaction is typically
32 * associated with a pooled connection. Committing or rolling back a transaction
33 * will release the connection back to the pool.
34 *
35 * Once the transaction is committed or rolled back, the connection property
36 * will be set to null to mark the transaction to be inactive. Trying to commit
37 * or rollback an inactive transaction will receive an error from the callback.
38 *
39 * Please also note that the transaction is only honored with the same data
40 * source/connector instance. CRUD methods will not join the current transaction
41 * if its model is not attached the same data source.
42 *
43 * Example:
44 *
45 * To pass the transaction context to one of the CRUD methods, use the `options`
46 * argument with `transaction` property, for example,
47 *
48 * ```js
49 * MyModel.beginTransaction('READ COMMITTED', function(err, tx) {
50 * MyModel.create({x: 1, y: 'a'}, {transaction: tx}, function(err, inst) {
51 * MyModel.find({x: 1}, {transaction: tx}, function(err, results) {
52 * // ...
53 * tx.commit(function(err) {...});
54 * });
55 * });
56 * });
57 * ```
58 *
59 * @param {Object|String} options Options to be passed upon transaction.
60 *
61 * Can be one of the forms:
62 * - Object: {isolationLevel: '...', timeout: 1000}
63 * - String: isolationLevel
64 *
65 * Valid values of `isolationLevel` are:
66 *
67 * - Transaction.READ_COMMITTED = 'READ COMMITTED'; // default
68 * - Transaction.READ_UNCOMMITTED = 'READ UNCOMMITTED';
69 * - Transaction.SERIALIZABLE = 'SERIALIZABLE';
70 * - Transaction.REPEATABLE_READ = 'REPEATABLE READ';
71 * @callback {Function} cb Callback function.
72 * @returns {Promise|undefined} Returns a callback promise.
73 */
74TransactionMixin.beginTransaction = function(options, cb) {
75 cb = cb || utils.createPromiseCallback();
76 if (Transaction) {
77 const connector = this.getConnector();
78 Transaction.begin(connector, options, function(err, transaction) {
79 if (err) return cb(err);
80 // NOTE(lehni) As part of the process of moving the handling of
81 // transaction id and timeout from TransactionMixin.beginTransaction() to
82 // Transaction.begin() in loopback-connector, switch to only setting id
83 // and timeout if it wasn't taken care of already by Transaction.begin().
84 // Eventually, we can remove the following two if-blocks altogether.
85 if (!transaction.id) {
86 // Set an informational transaction id
87 transaction.id = uuid.v1();
88 }
89 if (options.timeout && !transaction.timeout) {
90 transaction.timeout = setTimeout(function() {
91 const context = {
92 transaction: transaction,
93 operation: 'timeout',
94 };
95 transaction.notifyObserversOf('timeout', context, function(err) {
96 if (!err) {
97 transaction.rollback(function() {
98 debug('Transaction %s is rolled back due to timeout',
99 transaction.id);
100 });
101 }
102 });
103 }, options.timeout);
104 }
105 cb(err, transaction);
106 });
107 } else {
108 process.nextTick(function() {
109 const err = new Error(g.f('{{Transaction}} is not supported'));
110 cb(err);
111 });
112 }
113 return cb.promise;
114};
115
116// Promisify the transaction apis
117if (Transaction) {
118 jutil.mixin(Transaction.prototype, ObserverMixin);
119 /**
120 * Commit a transaction and release it back to the pool.
121 *
122 * Example:
123 *
124 * ```js
125 * MyModel.beginTransaction('READ COMMITTED', function(err, tx) {
126 * // some crud operation of your choice
127 * tx.commit(function(err) {
128 * // release the connection pool upon committing
129 * tx.close(err);
130 * });
131 * });
132 * ```
133 *
134 * @callback {Function} cb Callback function.
135 * @returns {Promise|undefined} Returns a callback promise.
136 */
137 Transaction.prototype.commit = function(cb) {
138 cb = cb || utils.createPromiseCallback();
139 if (this.ensureActive(cb)) {
140 const context = {
141 transaction: this,
142 operation: 'commit',
143 };
144 this.notifyObserversAround('commit', context,
145 done => {
146 this.connector.commit(this.connection, done);
147 },
148 err => {
149 // Deference the connection to mark the transaction is not active
150 // The connection should have been released back the pool
151 this.connection = null;
152 cb(err);
153 });
154 }
155 return cb.promise;
156 };
157
158 /**
159 * Rollback a transaction and release it back to the pool.
160 *
161 * Example:
162 *
163 * ```js
164 * MyModel.beginTransaction('READ COMMITTED', function(err, tx) {
165 * // some crud operation of your choice
166 * tx.rollback(function(err) {
167 * // release the connection pool upon committing
168 * tx.close(err);
169 * });
170 * });
171 * ```
172 *
173 * @callback {Function} cb Callback function.
174 * @returns {Promise|undefined} Returns a callback promise.
175 */
176 Transaction.prototype.rollback = function(cb) {
177 cb = cb || utils.createPromiseCallback();
178 if (this.ensureActive(cb)) {
179 const context = {
180 transaction: this,
181 operation: 'rollback',
182 };
183 this.notifyObserversAround('rollback', context,
184 done => {
185 this.connector.rollback(this.connection, done);
186 },
187 err => {
188 // Deference the connection to mark the transaction is not active
189 // The connection should have been released back the pool
190 this.connection = null;
191 cb(err);
192 });
193 }
194 return cb.promise;
195 };
196
197 Transaction.prototype.ensureActive = function(cb) {
198 // Report an error if the transaction is not active
199 if (!this.connection) {
200 process.nextTick(() => {
201 cb(new Error(g.f('The {{transaction}} is not active: %s', this.id)));
202 });
203 }
204 return !!this.connection;
205 };
206
207 Transaction.prototype.toJSON = function() {
208 return this.id;
209 };
210
211 Transaction.prototype.toString = function() {
212 return this.id;
213 };
214}
215
216TransactionMixin.Transaction = Transaction;