πUsing Transactions
Node-Boot supports transactions trough a @Transactional()decorator that benefits from the auto-configurations provided by the @node-boot/starter-persistence starter package.
Transactions propagation across the request chain is ensured by leveraging Node.js Async Hooks capabilities.
In order to use transactions, you have to have the persistence layer active and configured by installing the @node-boot/starter-persistence package and configuring it in order to use your preferred database:
PersistencyAfter persistence layer is configured and working as expected, you can start decorating controller or service functions as transactional.
Using Transactional Decorator
Every service method that needs to be transactional, need to use the
@Transactional()decorator.The decorator can take an optional
propagationas argument to define the propagation behaviour.The decorator can take an optional
isolationLevelas argument to define theisolation level(by default it will use your database driver's default isolation level)
...
import {Transactional} from "@node-boot/starter-persistence";
@Service()
export class UserService {
constructor(
private readonly logger: Logger,
private readonly userRepository: UserRepository,
) {
}
@Transactional() // Will open a transaction if one doesn't already exist
public async createUser(userData: CreateUserDto): Promise<User> {
this.logger.info(`Creating user ${userData.name}`);
const existingUser = await this.userRepository.findOneBy({
email: userData.email,
});
return optionalOf(existingUser)
.ifPresentThrow(() => new HttpError(409, `This email ${userData.email} already exists`))
.elseAsync(() => this.userRepository.save(userData));
}
}You can also use DataSource and EntityManager objects together with repositories in transactions:
Transaction Propagation
The following propagation options can be specified:
MANDATORY- Support a current transaction, throw an exception if none exists.NESTED- Execute within a nested transaction if a current transaction exists, behave likeREQUIREDelse.NEVER- Execute non-transactionally, throw an exception if a transaction exists.NOT_SUPPORTED- Execute non-transactionally, suspend the current transaction if one exists.REQUIRED(default behaviour) - Support a current transaction, create a new one if none exists.REQUIRES_NEW- Create a new transaction, and suspend the current transaction if one exists.SUPPORTS- Support a current transaction, execute non-transactionally if none exists.
Isolation Levels
The following isolation level options can be specified:
READ_UNCOMMITTED- A constant indicating that dirty reads, non-repeatable reads and phantom reads can occur.READ_COMMITTED- A constant indicating that dirty reads are prevented; non-repeatable reads and phantom reads can occur.REPEATABLE_READ- A constant indicating that dirty reads and non-repeatable reads are prevented; phantom reads can occur.SERIALIZABLE= A constant indicating that dirty reads, non-repeatable reads and phantom reads are prevented.
Transaction Hooks
Because you hand over control of the transaction creation to this library, there is no way for you to know whether or not the current transaction was successfully persisted to the database.
To circumvent that, we expose three helper methods that allow you to hook into the transaction lifecycle and take appropriate action after a commit/rollback.
runOnTransactionCommit(cb)takes a callback to be executed after the current transaction was successfully committed
runOnTransactionRollback(cb)takes a callback to be executed after the current transaction rolls back. The callback gets the error that initiated the rollback as a parameter.
runOnTransactionComplete(cb)takes a callback to be executed at the completion of the current transactional context. If there was an error, it gets passed as an argument.
Transactions API
Transactions Configuration
storageDriver- Determines which underlying mechanism (like Async Local Storage or cls-hooked) the library should use for handling and propagating transactions. By default, it'sStorageDriver.AUTO.maxHookHandlers- Controls how many hooks (commit,rollback,complete) can be used simultaneously. If you exceed the number of hooks of same type, you get a warning. This is a useful to find possible memory leaks. You can set this options to0orInfinityto indicate an unlimited number of listeners. By default, it's10.
@Transactional Options
isolationLevel- isolation level for transactional context (isolation levels)propagation- propagation behaviour's for nest transactional contexts (propagation behaviors)
Storage Driver
Option that determines which underlying mechanism the library should use for handling and propagating transactions.
The possible variants:
AUTO- Automatically selects the appropriate storage mechanism based on the Node.js version, usingAsyncLocalStoragefor Node.js versions 16 and above, and defaulting tocls-hookedfor earlier versions.CLS_HOOKED- Utilizes thecls-hookedpackage to provide context storage, supporting both legacy Node.js versions with AsyncWrap for versions below 8.2.1, and usingasync_hooksfor later versions.ASYNC_LOCAL_STORAGE- Uses the built-inAsyncLocalStoragefeature, available from Node.js version 16 onwards,
Utility Functions
Just in case you are not a fun of decorators, you can make use of the following utility functions to handle transactions:
runInTransaction(fn: Callback, options?: Options): Promise<...>
Run code in transactional context.
wrapInTransaction(fn: Callback, options?: Options): WrappedFunction
Wrap function in transactional context
Last updated