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 | ;
|
7 |
|
8 | const g = require('strong-globalize')();
|
9 | const debug = require('debug')('loopback:connector:transaction');
|
10 | const uuid = require('uuid');
|
11 | const utils = require('./utils');
|
12 | const jutil = require('./jutil');
|
13 | const ObserverMixin = require('./observer');
|
14 |
|
15 | const Transaction = require('loopback-connector').Transaction;
|
16 |
|
17 | module.exports = TransactionMixin;
|
18 |
|
19 | /**
|
20 | * TransactionMixin class. Use to add transaction APIs to a model class.
|
21 | *
|
22 | * @class TransactionMixin
|
23 | */
|
24 | function 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 | */
|
74 | TransactionMixin.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
|
117 | if (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 |
|
216 | TransactionMixin.Transaction = Transaction;
|