UNPKG

8.46 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 this._clearCls();
60
61 try {
62 return await this.sequelize.getQueryInterface().commitTransaction(this, this.options);
63 } finally {
64 this.finished = 'commit';
65 if (!this.parent) {
66 this.cleanup();
67 }
68 for (const hook of this._afterCommitHooks) {
69 await hook.apply(this, [this]);
70 }
71 }
72 }
73
74 /**
75 * Rollback (abort) the transaction
76 *
77 * @returns {Promise}
78 */
79 async rollback() {
80 if (this.finished) {
81 throw new Error(`Transaction cannot be rolled back because it has been finished with state: ${this.finished}`);
82 }
83
84 if (!this.connection) {
85 throw new Error('Transaction cannot be rolled back because it never started');
86 }
87
88 this._clearCls();
89
90 try {
91 return await this
92 .sequelize
93 .getQueryInterface()
94 .rollbackTransaction(this, this.options);
95 } finally {
96 if (!this.parent) {
97 this.cleanup();
98 }
99 }
100 }
101
102 async prepareEnvironment(useCLS) {
103 let connectionPromise;
104
105 if (useCLS === undefined) {
106 useCLS = true;
107 }
108
109 if (this.parent) {
110 connectionPromise = Promise.resolve(this.parent.connection);
111 } else {
112 const acquireOptions = { uuid: this.id };
113 if (this.options.readOnly) {
114 acquireOptions.type = 'SELECT';
115 }
116 connectionPromise = this.sequelize.connectionManager.getConnection(acquireOptions);
117 }
118
119 let result;
120 const connection = await connectionPromise;
121 this.connection = connection;
122 this.connection.uuid = this.id;
123
124 try {
125 await this.begin();
126 result = await this.setDeferrable();
127 } catch (setupErr) {
128 try {
129 result = await this.rollback();
130 } finally {
131 throw setupErr; // eslint-disable-line no-unsafe-finally
132 }
133 }
134
135 if (useCLS && this.sequelize.constructor._cls) {
136 this.sequelize.constructor._cls.set('transaction', this);
137 }
138
139 return result;
140 }
141
142 async setDeferrable() {
143 if (this.options.deferrable) {
144 return await this
145 .sequelize
146 .getQueryInterface()
147 .deferConstraints(this, this.options);
148 }
149 }
150
151 async begin() {
152 const queryInterface = this.sequelize.getQueryInterface();
153
154 if ( this.sequelize.dialect.supports.settingIsolationLevelDuringTransaction ) {
155 await queryInterface.startTransaction(this, this.options);
156 return queryInterface.setIsolationLevel(this, this.options.isolationLevel, this.options);
157 }
158
159 await queryInterface.setIsolationLevel(this, this.options.isolationLevel, this.options);
160
161 return queryInterface.startTransaction(this, this.options);
162 }
163
164 cleanup() {
165 const res = this.sequelize.connectionManager.releaseConnection(this.connection);
166 this.connection.uuid = undefined;
167 return res;
168 }
169
170 _clearCls() {
171 const cls = this.sequelize.constructor._cls;
172
173 if (cls) {
174 if (cls.get('transaction') === this) {
175 cls.set('transaction', null);
176 }
177 }
178 }
179
180 /**
181 * A hook that is run after a transaction is committed
182 *
183 * @param {Function} fn A callback function that is called with the committed transaction
184 * @name afterCommit
185 * @memberof Sequelize.Transaction
186 */
187 afterCommit(fn) {
188 if (!fn || typeof fn !== 'function') {
189 throw new Error('"fn" must be a function');
190 }
191 this._afterCommitHooks.push(fn);
192 }
193
194 /**
195 * Types can be set per-transaction by passing `options.type` to `sequelize.transaction`.
196 * Default to `DEFERRED` but you can override the default type by passing `options.transactionType` in `new Sequelize`.
197 * Sqlite only.
198 *
199 * Pass in the desired level as the first argument:
200 *
201 * @example
202 * try {
203 * await sequelize.transaction({ type: Sequelize.Transaction.TYPES.EXCLUSIVE }, transaction => {
204 * // your transactions
205 * });
206 * // transaction has been committed. Do something after the commit if required.
207 * } catch(err) {
208 * // do something with the err.
209 * }
210 *
211 * @property DEFERRED
212 * @property IMMEDIATE
213 * @property EXCLUSIVE
214 */
215 static get TYPES() {
216 return {
217 DEFERRED: 'DEFERRED',
218 IMMEDIATE: 'IMMEDIATE',
219 EXCLUSIVE: 'EXCLUSIVE'
220 };
221 }
222
223 /**
224 * Isolation levels can be set per-transaction by passing `options.isolationLevel` to `sequelize.transaction`.
225 * Sequelize uses the default isolation level of the database, you can override this by passing `options.isolationLevel` in Sequelize constructor options.
226 *
227 * Pass in the desired level as the first argument:
228 *
229 * @example
230 * try {
231 * const result = await sequelize.transaction({isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE}, transaction => {
232 * // your transactions
233 * });
234 * // transaction has been committed. Do something after the commit if required.
235 * } catch(err) {
236 * // do something with the err.
237 * }
238 *
239 * @property READ_UNCOMMITTED
240 * @property READ_COMMITTED
241 * @property REPEATABLE_READ
242 * @property SERIALIZABLE
243 */
244 static get ISOLATION_LEVELS() {
245 return {
246 READ_UNCOMMITTED: 'READ UNCOMMITTED',
247 READ_COMMITTED: 'READ COMMITTED',
248 REPEATABLE_READ: 'REPEATABLE READ',
249 SERIALIZABLE: 'SERIALIZABLE'
250 };
251 }
252
253
254 /**
255 * Possible options for row locking. Used in conjunction with `find` calls:
256 *
257 * @example
258 * // t1 is a transaction
259 * Model.findAll({
260 * where: ...,
261 * transaction: t1,
262 * lock: t1.LOCK...
263 * });
264 *
265 * @example <caption>Postgres also supports specific locks while eager loading by using OF:</caption>
266 * UserModel.findAll({
267 * where: ...,
268 * include: [TaskModel, ...],
269 * transaction: t1,
270 * lock: {
271 * level: t1.LOCK...,
272 * of: UserModel
273 * }
274 * });
275 *
276 * # UserModel will be locked but TaskModel won't!
277 *
278 * @example <caption>You can also skip locked rows:</caption>
279 * // t1 is a transaction
280 * Model.findAll({
281 * where: ...,
282 * transaction: t1,
283 * lock: true,
284 * skipLocked: true
285 * });
286 * # The query will now return any rows that aren't locked by another transaction
287 *
288 * @returns {object}
289 * @property UPDATE
290 * @property SHARE
291 * @property KEY_SHARE Postgres 9.3+ only
292 * @property NO_KEY_UPDATE Postgres 9.3+ only
293 */
294 static get LOCK() {
295 return {
296 UPDATE: 'UPDATE',
297 SHARE: 'SHARE',
298 KEY_SHARE: 'KEY SHARE',
299 NO_KEY_UPDATE: 'NO KEY UPDATE'
300 };
301 }
302
303 /**
304 * Please see {@link Transaction.LOCK}
305 */
306 get LOCK() {
307 return Transaction.LOCK;
308 }
309}
310
311module.exports = Transaction;
312module.exports.Transaction = Transaction;
313module.exports.default = Transaction;