UNPKG

sequelize-dm

Version:

Sequelize is a promise-based Node.js ORM tool for Postgres, MySQL, MariaDB, SQLite, Microsoft SQL Server, Amazon Redshift and Snowflake’s Data Cloud. It features solid transaction support, relations, eager and lazy loading, read replication and more.

424 lines (378 loc) 12 kB
import assert from 'assert'; import type { Logging, Sequelize, Deferrable, PartlyRequired, Connection } from '..'; type AfterTransactionCommitCallback = (transaction: Transaction) => void | Promise<void>; /** * The transaction object is used to identify a running transaction. * It is created by calling `Sequelize.transaction()`. * To run a query under a transaction, you should pass the transaction in the options object. * * @class Transaction * @see {Sequelize.transaction} */ export class Transaction { sequelize: Sequelize; private readonly _afterCommitHooks: Set<AfterTransactionCommitCallback> = new Set(); private readonly savepoints: Transaction[] = []; private readonly options: PartlyRequired<TransactionOptions, 'type' | 'isolationLevel' | 'readOnly'>; private readonly parent: Transaction | null; private readonly id: string; private readonly name: string; private finished: 'commit' | undefined; private connection: Connection | undefined; /** * Creates a new transaction instance * * @param sequelize A configured sequelize Instance * @param options An object with options * @param [options.type] Sets the type of the transaction. Sqlite only * @param [options.isolationLevel] Sets the isolation level of the transaction. * @param [options.deferrable] Sets the constraints to be deferred or immediately checked. PostgreSQL only */ constructor(sequelize: Sequelize, options: TransactionOptions) { this.sequelize = sequelize; // get dialect specific transaction options // @ts-expect-error Typings for .queryGenerator are not available yet (this will error once that is resolved). const generateTransactionId = this.sequelize.dialect .queryGenerator .generateTransactionId; this.options = { type: sequelize.options.transactionType, isolationLevel: sequelize.options.isolationLevel, readOnly: false, ...options, }; this.parent = this.options.transaction ?? null; if (this.parent) { this.id = this.parent.id; this.parent.savepoints.push(this); this.name = `${this.id}-sp-${this.parent.savepoints.length}`; } else { const id = generateTransactionId(); this.id = id; this.name = id; } delete this.options.transaction; } /** * Commit the transaction. */ async commit(): Promise<void> { if (this.finished) { throw new Error(`Transaction cannot be committed because it has been finished with state: ${this.finished}`); } try { return await this.sequelize.getQueryInterface().commitTransaction(this, this.options); } finally { this.finished = 'commit'; await this.cleanup(); for (const hook of this._afterCommitHooks) { // eslint-disable-next-line no-await-in-loop -- sequentially call hooks await Reflect.apply(hook, this, [this]); } } } /** * Rollback (abort) the transaction */ async rollback(): Promise<void> { if (this.finished) { throw new Error(`Transaction cannot be rolled back because it has been finished with state: ${this.finished}`); } if (!this.connection) { throw new Error('Transaction cannot be rolled back because it never started'); } try { return await this .sequelize .getQueryInterface() .rollbackTransaction(this, this.options); } finally { await this.cleanup(); } } /** * Called to acquire a connection to use and set the correct options on the connection. * We should ensure all of the environment that's set up is cleaned up in `cleanup()` below. * * @param 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. */ async prepareEnvironment(useCLS?: boolean) { if (useCLS === undefined) { useCLS = true; } let connection; if (this.parent) { connection = this.parent.connection; } else { connection = await this.sequelize.connectionManager.getConnection({ type: this.options.readOnly ? 'read' : 'write', uuid: this.id, }); } assert(connection != null, 'Transaction failed to acquire Connection.'); connection.uuid = this.id; this.connection = connection; let result; try { await this.begin(); result = await this.setDeferrable(); } catch (error) { try { result = await this.rollback(); } finally { throw error; // eslint-disable-line no-unsafe-finally -- while this will mask the error thrown by `rollback`, the previous error is more important. } } if (useCLS && this.sequelize.Sequelize._cls) { this.sequelize.Sequelize._cls.set('transaction', this); } return result; } async setDeferrable(): Promise<void> { if (this.options.deferrable) { await this .sequelize .getQueryInterface() .deferConstraints(this, this.options); } } async begin() { const queryInterface = this.sequelize.getQueryInterface(); if (this.sequelize.dialect.supports.settingIsolationLevelDuringTransaction) { await queryInterface.startTransaction(this, this.options); return queryInterface.setIsolationLevel(this, this.options.isolationLevel, this.options); } await queryInterface.setIsolationLevel(this, this.options.isolationLevel, this.options); return queryInterface.startTransaction(this, this.options); } async cleanup(): Promise<void> { // Don't release the connection if there's a parent transaction or // if we've already cleaned up if (this.parent || this.connection?.uuid === undefined) { return; } this._clearCls(); const res = this.sequelize.connectionManager.releaseConnection(this.connection); this.connection.uuid = undefined; await res; } _clearCls() { const cls = this.sequelize.Sequelize._cls; if (cls && cls.get('transaction') === this) { cls.set('transaction', null); } } /** * Adds a hook that is run after a transaction is committed. * * @param fn A callback function that is called with the committed transaction * @name afterCommit * @memberof Sequelize.Transaction */ afterCommit(fn: AfterTransactionCommitCallback): this { if (typeof fn !== 'function') { throw new TypeError('"fn" must be a function'); } this._afterCommitHooks.add(fn); return this; } /** * Types can be set per-transaction by passing `options.type` to `sequelize.transaction`. * Default to `DEFERRED` but you can override the default type by passing `options.transactionType` in `new Sequelize`. * Sqlite only. * * Pass in the desired level as the first argument: * * @example * try { * await sequelize.transaction({ type: Sequelize.Transaction.TYPES.EXCLUSIVE }, transaction => { * // your transactions * }); * // transaction has been committed. Do something after the commit if required. * } catch(err) { * // do something with the err. * } * * @property DEFERRED * @property IMMEDIATE * @property EXCLUSIVE */ static get TYPES() { return TRANSACTION_TYPES; } /** * Isolation levels can be set per-transaction by passing `options.isolationLevel` to `sequelize.transaction`. * Sequelize uses the default isolation level of the database, you can override this by passing `options.isolationLevel` in Sequelize constructor options. * * Pass in the desired level as the first argument: * * @example * try { * const result = await sequelize.transaction({isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE}, transaction => { * // your transactions * }); * // transaction has been committed. Do something after the commit if required. * } catch(err) { * // do something with the err. * } * * @property READ_UNCOMMITTED * @property READ_COMMITTED * @property REPEATABLE_READ * @property SERIALIZABLE */ static get ISOLATION_LEVELS() { return ISOLATION_LEVELS; } /** * Possible options for row locking. Used in conjunction with `find` calls: * * @example * // t1 is a transaction * Model.findAll({ * where: ..., * transaction: t1, * lock: t1.LOCK... * }); * * @example <caption>Postgres also supports specific locks while eager loading by using OF:</caption> * UserModel.findAll({ * where: ..., * include: [TaskModel, ...], * transaction: t1, * lock: { * level: t1.LOCK..., * of: UserModel * } * }); * * # UserModel will be locked but TaskModel won't! * * @example <caption>You can also skip locked rows:</caption> * // t1 is a transaction * Model.findAll({ * where: ..., * transaction: t1, * lock: true, * skipLocked: true * }); * # The query will now return any rows that aren't locked by another transaction * * @returns possible options for row locking * @property UPDATE * @property SHARE * @property KEY_SHARE Postgres 9.3+ only * @property NO_KEY_UPDATE Postgres 9.3+ only */ static get LOCK() { return LOCK; } /** * Same as {@link Transaction.LOCK}, but can also be called on instances of * transactions to get possible options for row locking directly from the * instance. */ get LOCK() { return LOCK; } } /** * Isolations levels can be set per-transaction by passing `options.isolationLevel` to `sequelize.transaction`. * Default to `REPEATABLE_READ` but you can override the default isolation level by passing `options.isolationLevel` in `new Sequelize`. * * The possible isolations levels to use when starting a transaction: * * ```js * { * READ_UNCOMMITTED: "READ UNCOMMITTED", * READ_COMMITTED: "READ COMMITTED", * REPEATABLE_READ: "REPEATABLE READ", * SERIALIZABLE: "SERIALIZABLE" * } * ``` * * Pass in the desired level as the first argument: * * ```js * try { * await sequelize.transaction({isolationLevel: Sequelize.Transaction.SERIALIZABLE}, transaction => { * // your transactions * }); * // transaction has been committed. Do something after the commit if required. * } catch(err) { * // do something with the err. * } * ``` */ export enum ISOLATION_LEVELS { READ_UNCOMMITTED = 'READ UNCOMMITTED', READ_COMMITTED = 'READ COMMITTED', REPEATABLE_READ = 'REPEATABLE READ', SERIALIZABLE = 'SERIALIZABLE', } export enum TRANSACTION_TYPES { DEFERRED = 'DEFERRED', IMMEDIATE = 'IMMEDIATE', EXCLUSIVE = 'EXCLUSIVE', } /** * Possible options for row locking. Used in conjunction with `find` calls: * * ```js * t1 // is a transaction * t1.LOCK.UPDATE, * t1.LOCK.SHARE, * t1.LOCK.KEY_SHARE, // Postgres 9.3+ only * t1.LOCK.NO_KEY_UPDATE // Postgres 9.3+ only * ``` * * Usage: * ```js * t1 // is a transaction * Model.findAll({ * where: ..., * transaction: t1, * lock: t1.LOCK... * }); * ``` * * Postgres also supports specific locks while eager loading by using OF: * ```js * UserModel.findAll({ * where: ..., * include: [TaskModel, ...], * transaction: t1, * lock: { * level: t1.LOCK..., * of: UserModel * } * }); * ``` * UserModel will be locked but TaskModel won't! */ export enum LOCK { UPDATE = 'UPDATE', SHARE = 'SHARE', /** * Postgres 9.3+ only */ KEY_SHARE = 'KEY SHARE', /** * Postgres 9.3+ only */ NO_KEY_UPDATE = 'NO KEY UPDATE', } /** * Options provided when the transaction is created */ export interface TransactionOptions extends Logging { readOnly?: boolean; autocommit?: boolean; isolationLevel?: ISOLATION_LEVELS; type?: TRANSACTION_TYPES; deferrable?: string | Deferrable.Deferrable; /** * Parent transaction. */ transaction?: Transaction | null; }