UNPKG

8.97 kBJavaScriptView Raw
1'use strict';
2
3/**
4 * The transaction object is used to identify a running transaction.
5 * It is created by calling `Sequelize.transaction()`.
6 * To run a query under a transaction, you should pass the transaction in the options object.
7 *
8 * @class Transaction
9 * @see {@link Sequelize.transaction}
10 */
11class Transaction {
12 /**
13 * Creates a new transaction instance
14 *
15 * @param {Sequelize} sequelize A configured sequelize Instance
16 * @param {object} options An object with options
17 * @param {string} [options.type] Sets the type of the transaction. Sqlite only
18 * @param {string} [options.isolationLevel] Sets the isolation level of the transaction.
19 * @param {string} [options.deferrable] Sets the constraints to be deferred or immediately checked. PostgreSQL only
20 */
21 constructor(sequelize, options) {
22 this.sequelize = sequelize;
23 this.savepoints = [];
24 this._afterCommitHooks = [];
25
26 // get dialect specific transaction options
27 const generateTransactionId = this.sequelize.dialect.queryGenerator.generateTransactionId;
28
29 this.options = {
30 type: sequelize.options.transactionType,
31 isolationLevel: sequelize.options.isolationLevel,
32 readOnly: false,
33 ...options
34 };
35
36 this.parent = this.options.transaction;
37
38 if (this.parent) {
39 this.id = this.parent.id;
40 this.parent.savepoints.push(this);
41 this.name = `${this.id}-sp-${this.parent.savepoints.length}`;
42 } else {
43 this.id = this.name = generateTransactionId();
44 }
45
46 delete this.options.transaction;
47 }
48
49 /**
50 * Commit the transaction
51 *
52 * @returns {Promise}
53 */
54 async commit() {
55 if (this.finished) {
56 throw new Error(`Transaction cannot be committed because it has been finished with state: ${this.finished}`);
57 }
58
59 try {
60 return await this.sequelize.getQueryInterface().commitTransaction(this, this.options);
61 } finally {
62 this.finished = 'commit';
63 this.cleanup();
64 for (const hook of this._afterCommitHooks) {
65 await hook.apply(this, [this]);
66 }
67 }
68 }
69
70 /**
71 * Rollback (abort) the transaction
72 *
73 * @returns {Promise}
74 */
75 async rollback() {
76 if (this.finished) {
77 throw new Error(`Transaction cannot be rolled back because it has been finished with state: ${this.finished}`);
78 }
79
80 if (!this.connection) {
81 throw new Error('Transaction cannot be rolled back because it never started');
82 }
83
84 try {
85 return await this
86 .sequelize
87 .getQueryInterface()
88 .rollbackTransaction(this, this.options);
89 } finally {
90 this.cleanup();
91 }
92 }
93
94 /**
95 * Called to acquire a connection to use and set the correct options on the connection.
96 * We should ensure all of the environment that's set up is cleaned up in `cleanup()` below.
97 *
98 * @param {boolean} useCLS Defaults to true: Use CLS (Continuation Local Storage) with Sequelize. With CLS, all queries within the transaction callback will automatically receive the transaction object.
99 * @returns {Promise}
100 */
101 async prepareEnvironment(useCLS) {
102 let connectionPromise;
103
104 if (useCLS === undefined) {
105 useCLS = true;
106 }
107
108 if (this.parent) {
109 connectionPromise = Promise.resolve(this.parent.connection);
110 } else {
111 const acquireOptions = { uuid: this.id };
112 if (this.options.readOnly) {
113 acquireOptions.type = 'SELECT';
114 }
115 connectionPromise = this.sequelize.connectionManager.getConnection(acquireOptions);
116 }
117
118 let result;
119 const connection = await connectionPromise;
120 this.connection = connection;
121 this.connection.uuid = this.id;
122
123 try {
124 await this.begin();
125 result = await this.setDeferrable();
126 } catch (setupErr) {
127 try {
128 result = await this.rollback();
129 } finally {
130 throw setupErr; // eslint-disable-line no-unsafe-finally
131 }
132 }
133
134 if (useCLS && this.sequelize.constructor._cls) {
135 this.sequelize.constructor._cls.set('transaction', this);
136 }
137
138 return result;
139 }
140
141 async setDeferrable() {
142 if (this.options.deferrable) {
143 return await this
144 .sequelize
145 .getQueryInterface()
146 .deferConstraints(this, this.options);
147 }
148 }
149
150 async begin() {
151 const queryInterface = this.sequelize.getQueryInterface();
152
153 if ( this.sequelize.dialect.supports.settingIsolationLevelDuringTransaction ) {
154 await queryInterface.startTransaction(this, this.options);
155 return queryInterface.setIsolationLevel(this, this.options.isolationLevel, this.options);
156 }
157
158 await queryInterface.setIsolationLevel(this, this.options.isolationLevel, this.options);
159
160 return queryInterface.startTransaction(this, this.options);
161 }
162
163 cleanup() {
164 // Don't release the connection if there's a parent transaction or
165 // if we've already cleaned up
166 if (this.parent || this.connection.uuid === undefined) return;
167
168 this._clearCls();
169 const res = this.sequelize.connectionManager.releaseConnection(this.connection);
170 this.connection.uuid = undefined;
171 return res;
172 }
173
174 _clearCls() {
175 const cls = this.sequelize.constructor._cls;
176
177 if (cls) {
178 if (cls.get('transaction') === this) {
179 cls.set('transaction', null);
180 }
181 }
182 }
183
184 /**
185 * A hook that is run after a transaction is committed
186 *
187 * @param {Function} fn A callback function that is called with the committed transaction
188 * @name afterCommit
189 * @memberof Sequelize.Transaction
190 */
191 afterCommit(fn) {
192 if (!fn || typeof fn !== 'function') {
193 throw new Error('"fn" must be a function');
194 }
195 this._afterCommitHooks.push(fn);
196 }
197
198 /**
199 * Types can be set per-transaction by passing `options.type` to `sequelize.transaction`.
200 * Default to `DEFERRED` but you can override the default type by passing `options.transactionType` in `new Sequelize`.
201 * Sqlite only.
202 *
203 * Pass in the desired level as the first argument:
204 *
205 * @example
206 * try {
207 * await sequelize.transaction({ type: Sequelize.Transaction.TYPES.EXCLUSIVE }, transaction => {
208 * // your transactions
209 * });
210 * // transaction has been committed. Do something after the commit if required.
211 * } catch(err) {
212 * // do something with the err.
213 * }
214 *
215 * @property DEFERRED
216 * @property IMMEDIATE
217 * @property EXCLUSIVE
218 */
219 static get TYPES() {
220 return {
221 DEFERRED: 'DEFERRED',
222 IMMEDIATE: 'IMMEDIATE',
223 EXCLUSIVE: 'EXCLUSIVE'
224 };
225 }
226
227 /**
228 * Isolation levels can be set per-transaction by passing `options.isolationLevel` to `sequelize.transaction`.
229 * Sequelize uses the default isolation level of the database, you can override this by passing `options.isolationLevel` in Sequelize constructor options.
230 *
231 * Pass in the desired level as the first argument:
232 *
233 * @example
234 * try {
235 * const result = await sequelize.transaction({isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE}, transaction => {
236 * // your transactions
237 * });
238 * // transaction has been committed. Do something after the commit if required.
239 * } catch(err) {
240 * // do something with the err.
241 * }
242 *
243 * @property READ_UNCOMMITTED
244 * @property READ_COMMITTED
245 * @property REPEATABLE_READ
246 * @property SERIALIZABLE
247 */
248 static get ISOLATION_LEVELS() {
249 return {
250 READ_UNCOMMITTED: 'READ UNCOMMITTED',
251 READ_COMMITTED: 'READ COMMITTED',
252 REPEATABLE_READ: 'REPEATABLE READ',
253 SERIALIZABLE: 'SERIALIZABLE'
254 };
255 }
256
257
258 /**
259 * Possible options for row locking. Used in conjunction with `find` calls:
260 *
261 * @example
262 * // t1 is a transaction
263 * Model.findAll({
264 * where: ...,
265 * transaction: t1,
266 * lock: t1.LOCK...
267 * });
268 *
269 * @example <caption>Postgres also supports specific locks while eager loading by using OF:</caption>
270 * UserModel.findAll({
271 * where: ...,
272 * include: [TaskModel, ...],
273 * transaction: t1,
274 * lock: {
275 * level: t1.LOCK...,
276 * of: UserModel
277 * }
278 * });
279 *
280 * # UserModel will be locked but TaskModel won't!
281 *
282 * @example <caption>You can also skip locked rows:</caption>
283 * // t1 is a transaction
284 * Model.findAll({
285 * where: ...,
286 * transaction: t1,
287 * lock: true,
288 * skipLocked: true
289 * });
290 * # The query will now return any rows that aren't locked by another transaction
291 *
292 * @returns {object}
293 * @property UPDATE
294 * @property SHARE
295 * @property KEY_SHARE Postgres 9.3+ only
296 * @property NO_KEY_UPDATE Postgres 9.3+ only
297 */
298 static get LOCK() {
299 return {
300 UPDATE: 'UPDATE',
301 SHARE: 'SHARE',
302 KEY_SHARE: 'KEY SHARE',
303 NO_KEY_UPDATE: 'NO KEY UPDATE'
304 };
305 }
306
307 /**
308 * Please see {@link Transaction.LOCK}
309 */
310 get LOCK() {
311 return Transaction.LOCK;
312 }
313}
314
315module.exports = Transaction;
316module.exports.Transaction = Transaction;
317module.exports.default = Transaction;