Node-Boot Framework
  • 👋Welcome to Node-Boot
  • 💡Why Node-Boot?
    • No CLI
    • No Boilerplate
    • Standardisation at its core
    • Spring-Boot Features Parity
  • 🥇Mission
  • 🚀Getting Started
  • Letter to Developers
    • 📝Letter to Node.Js Developers
    • 📝Letter to Web3 Developers
    • 📝Letter to Spring-Boot Developers
    • 📝Letter to Node.Js Framework Developers
  • Fundamentals
    • 🧩Architecture
      • Overview
      • Bootstrap
      • Lifecycle
      • Application Context
    • 🪄Core Features
      • Dependency Injection
      • Bean and Configuration
      • Configuration Properties
        • Static Configuration
        • Reading Configuration
        • Writing Configuration
        • Defining Configuration
        • Configuration Properties
      • Controller
        • Available Decorators
        • Custom Decorator
        • Versioning
      • Service
      • Component
      • Logging
      • Error Handling
      • Transactions
        • Transaction Principles
        • Transactions vs Concurrency
        • 🙈Using Transactions
      • Middleware/Interceptors
      • Authorization
      • Marshalling vs Unmarshalling
    • 🗝️Auto-Configuration
    • 🔐Security
  • Servers
    • 🚂Servers Concept
    • 🚃Available Servers
      • Fastify
      • Express
      • Koa
    • 🔌Integrate Server?
  • Starters
    • 🏗️Starters Overview
    • 🎁Available Starters
      • Persistency
      • Validations
      • Actuator
      • Open API
      • Redis
Powered by GitBook
On this page
  • Using Transactional Decorator
  • Transaction Propagation
  • Isolation Levels
  • Transaction Hooks
  • Transactions API
  • Transactions Configuration
  • @Transactional Options
  • Storage Driver
  • Utility Functions
  1. Fundamentals
  2. Core Features
  3. Transactions

Using Transactions

PreviousTransactions vs ConcurrencyNextMiddleware/Interceptors

Last updated 1 year ago

Node-Boot supports transactions trough a @Transactional()decorator that benefits from the auto-configurations provided by the starter package.

Transactions propagation across the request chain is ensured by leveraging capabilities.

In order to use transactions, you have to have the persistence layer active and configured by installing the package and configuring it in order to use your preferred database:

After 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 propagation as argument to define the .

  • The decorator can take an optional isolationLevel as argument to define the (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:

@Service()
export class PostService {

  constructor(
    readonly repository: PostRepository, 
    readonly dataSource: DataSource
  ){}

  @Transactional() // Will open a transaction if one doesn't already exist
  async createAndGetPost(id, message): Promise<Post> {
    const post = this.repository.create({ id, message })

    await this.repository.save(post)

    return dataSource.createQueryBuilder(Post, 'p').where('id = :id', id).getOne();
  }
}

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 like REQUIRED else.

  • 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.

Note: If a transaction already exist and a method is decorated with @Transactional and propagation does not equal to REQUIRES_NEW, then the declared isolationLevel value will not be taken into account.

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

@Transactional()
async createPost(id, message): Promise<Post> {
    const post = this.repository.create({ id, message });
    const result = await this.repository.save(post);
    
    runOnTransactionCommit(() => this.events.emit('post created'));
    
    return result;
}
  • 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.

@Transactional()
async createPost(id, message): Promise<Post> {
    const post = this.repository.create({ id, message });
    const result = await this.repository.save(post);
    
    runOnTransactionRollback((e) => this.events.emit(e));
    
    return result;
}
  • 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.

 @Transactional()
 async createPost(id, message): Promise<Post> {
      const post = this.repository.create({ id, message });
      const result = await this.repository.save(post);

      runOnTransactionComplete((e) => this.events.emit(e ? e : 'post created'));

      return result;
 }

Transactions API

Transactions Configuration

{
  storageDriver?: StorageDriver,
  maxHookHandlers?: number
}
  • 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 to 0 or Infinity to indicate an unlimited number of listeners. By default, it's 10.

node-boot:
    persistence:
        ...
        transactions:
            maxHookHandlers: 10
            storageDriver: "AUTO";

@Transactional Options

{
  isolationLevel?: IsolationLevel;
  propagation?: Propagation;
}

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, using AsyncLocalStorage for Node.js versions 16 and above, and defaulting to cls-hooked for earlier versions.

  • CLS_HOOKED - Utilizes the cls-hooked package to provide context storage, supporting both legacy Node.js versions with AsyncWrap for versions below 8.2.1, and using async_hooks for later versions.

  • ASYNC_LOCAL_STORAGE - Uses the built-in AsyncLocalStorage feature, available from Node.js version 16 onwards,

If no storage driver config is provided, the library is configured with Auto mode.

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.

...

runInTransaction(() => {
	...

	const user = this.usersRepo.update({ id: 1000 }, { state: action });

	...
}, { propagation: Propagation.REQUIRES_NEW });

...

wrapInTransaction(fn: Callback, options?: Options): WrappedFunction

Wrap function in transactional context

...

const updateUser = wrapInTransaction(() => {
	...

	const user = this.usersRepo.update({ id: 1000 }, { state: action });

	...
}, { propagation: Propagation.NEVER });

...

await updateUser();

...

storageDriver - Determines which (like Async Local Storage or cls-hooked) the library should use for handling and propagating transactions. By default, it's StorageDriver.AUTO.

isolationLevel- isolation level for transactional context ()

propagation- propagation behaviour's for nest transactional contexts ()

🪄
🙈
@node-boot/starter-persistence
Node.js Async Hooks
@node-boot/starter-persistence
Persistency
propagation behaviour
isolation level
underlying mechanism
isolation levels
propagation behaviors