npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

nestjs-transaction

v0.1.8

Published

A library that extracts and provides only some of the functions of the 'typeorm-transactional' npm module that are needed to operate in the Nestjs + TypeORM environment

Downloads

550

Readme

Nestjs Transactional

npm npm GitHub issues GitHub Repo stars GitHub forks

It's a fork of typeorm-transactional for Nestjs customization

A Transactional Method Decorator for typeorm that uses ALS to handle and propagate transactions between different repositories and service methods.

To facilitate the use of typeorm-transactional in Nest.js, several features have been added, including the TransactionModule, and the @toss/nestjsaop library is being used to provide transaction capabilities that can be customized based on injectable providers in future services.

what's different

  • NESTED propagation properties have different behavior NESTED transaction behaves the same as REQUIRED if it is a root transaction, but does not propagate its own rollback to parents if it is a child transaction
  • Declarative Event Hook feature Declarative definition of handlers for transaction-related events such as onCommit, onRollback, etc., thus eliminating duplication of event handlers and designing flexible structures.
  • Includes testing and management of multiple data sources

Index

Installation

## npm
npm install --save nestjs-transaction

## Needed dependencies
npm install --save typeorm reflect-metadata

Or

yarn add nestjs-transaction

## Needed dependencies
yarn add typeorm reflect-metadata

Note: You will need to import reflect-metadata somewhere in the global place of your app - https://github.com/typeorm/typeorm#installation

1️⃣ Usage

New versions of TypeORM use DataSource instead of Connection, so most of the API has been changed and the old API has become deprecated.

Register TransactionModule with AppModule. Once the module is registered, it automatically finds all TypeORM DataSources that exist and adds them to the Transactional DataSource so that @Transactional can be used.

Example for Nest.js:

// app.module.ts
import { Module } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { TransactionModule } from 'nestjs-transactional;

@Module({
  imports: [
    TransactionModule.forRoot()
  ],
})
export class AppModule {}

Unlike typeorm-transactional-cls-hooked, you do not need to use BaseRepositoryor otherwise define repositories.

NOTE You can select specific DataSource if you need it

2️⃣ Using Transactional Decorator

  • Every service method that needs to be transactional, need to use the @Transactional() decorator
  • The decorator can take a connectionName as argument (by default it is default) to specify the data source to be user
  • The decorator can take an optional propagation as argument to define the propagation behaviour
  • The decorator can take an optional isolationLevel as argument to define the isolation level (by default it will use your database driver's default isolation level)
export class PostService {
  constructor(readonly repository: PostRepository);

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

You can also use DataSource/EntityManager objects together with repositories in transactions:

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();
  }
}

3️⃣ Data Sources

Multiple DataSources

In new versions of TypeORM the name property in Connection / DataSource is deprecated, so to work conveniently with multiple DataSource the function addTransactionalDataSource allows you to specify custom the name.

@Module({
  imports: [
    TransactionModule.forRoot(),

    // Postgres Database
    TypeOrmModule.forRootAsync({
      useFactory: () => ({
        type: 'postgres',
        host: '127.0.0.1',
        port: 5435,
        username: 'beobwoo',
        password: 'testtest',
        database: 'test_db',
      }),
    }),
    // Postgres Database 2
    TypeOrmModule.forRoot({
      name: 'second-data-source',
      type: 'postgres',
      host: '127.0.0.1',
      port: 5436,
      username: 'beobwoo',
      password: 'testtest',
      database: 'test_db_2',
    }),
  ],
  providers: [],
})
export class AppModule {}

If you don't specify a name, it defaults to default.

Now, you can use this name in API by passing the connectionName property as options to explicitly define which Data Source you want to use:

  @Transactional({ connectionName: 'second-data-source' })
  async fn() { ... }

OR

runInTransaction(
  () => {
    // ...
  },
  { connectionName: 'second-data-source' },
);

Note: If you use TypeORM.forRootAsync when Nest.js uses DataSourceName, you must also enter the name attribute in the body. - Nestjs Docs

Select the name of the data source to participate

If you register using the forRoot method, you need the ability to select from multiple DataSources. If used without additional options, register all DataSources automatically.

TransactionModule.forRoot({
  dataSourceNames: ['default'], // if you want regist Default DataSource only (no name TypeORM dataSource)
}),

4️⃣ Transaction Propagation

The following propagation options can be specified

REQUIRED(default)

Join if there is a transaction in progress, and if there is no transaction in progress, start a new transaction with yourself as the root transaction.

SUPPORTS

Join if there is a transaction in progress. This is exactly the same as what REQUIRED does. But if there is no transaction in progress, run a query without a transaction.

NESTED

If there is no transaction in progress, start a new transaction with yourself as the root transaction. This is exactly the same as what REQUIRED does. If there is a transaction in progress, start a nested transaction. Errors within overlapping transactions are not captured and rolled back by higher transactions. (If you want to spread errors within overlapping transactions to higher transactions, you should use 'REQUIRED.')

REQUIRES_NEW

Always bring a new connection and start a new transaction.

5️⃣ 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.

6️⃣ Commit, RollBack hooks

With a custom event listener, you can effectively design duplicate transaction event hooks.

The library provides commit, rollback hooks provided by existing typeorm-transactional as functions (hooks API), but additionally provides the ability to register the method to be executed after commit or rollback success in the form of a method decorator in the form of a listener.

import { Injectable } from '@nestjs/common';
import { TransactionEventListener } from 'nestjs-transactional';
import { UserService } from './user.service';

@Injectable()
export class CustomTransactionEventListener implements TransactionEventListener {
  constructor(private readonly userService: UserService) {}

  async onCommit(...param: unknown[]): Promise<void> {
    await this.userService.userRepository.find();
  }

  async onRollBack(e: Error, ...param: unknown[]): Promise<void> {
    await this.userService.userRepository.find();
  }
}

The arguments for both the onCommit and onRollback methods are the same as the arguments for the target method, but for onRollBack, the error object that triggered rollback is additionally handed over to the first argument.

// user.module.ts
@Module({
  ...,
  controllers: [UserController],
  providers: [
    CustomTransactionEventListener,
  ],
})
export class UserModule {}

Transaction commit, rollback defines the functionality that should be executed upon successful implementation of the TransactionEventListener in the form of a provider. It is the same as a typical Nestjs provider and can be implemented by injecting other services within the module.

@Transactional()
@TransactionalEventListeners(CustomTransactionEventListener)
async createUser(...param: unknown[]) {
  ...
}

7️⃣ Test Mocking

Unit Test

@Transactional can be mocked to prevent running any of the transactional code in unit tests.

This can be accomplished in Jest with:

jest.mock('nestjs-transactional', () => ({
  Transactional: () => () => ({}),
}));

Repositories, services, etc. can be mocked as usual.

Integration Test

Note: This feature was Deprecated from 0.1.5^.

import { getTestQueryRunnerToken, TestTransactionModule } from 'nestjs-transaction';

beforeAll(async () => {
  const module = await Test.createTestingModule({
    imports: [
      AppModule,
      TestTransactionModule.forRoot(), // <-- if regist TestTransactionModule, you can use testQueryRunner
    ],
  }).compile();

  app = module.createNestApplication();
  await app.init(); // NOTE : TransactionModule using lifecycle hooks
  dataSource = app.get<DataSource>(getDataSourceToken());

  // A QueryRunner used by @Transactional applied methods
  // while participating in a transaction to execute a query
  testQueryRunner = app.get<QueryRunner>(getTestQueryRunnerToken());
});

beforeEach(async () => {
  await testQueryRunner.startTransaction();
});

afterEach(async () => {
  await testQueryRunner.rollbackTransaction();
});

it('it should be return ResponseDTO Array in body', async () => {
  const response = await request(app.getHttpServer())
    .patch(`/v1/<<url>>`)
    .send(dto)
    .expect(HttpStatus.OK);

  expect(response.body).toStrictEqual([]);
});

When using the above method in integrated tests using jest, each test result is automatically rolled back, eliminating the need to clean the table data after each test. This requires additional recall of TestTransactionModule, as opposed to registering only AppModule in general.

TestQueryRunner is not available unless you have registered a TestTransactionModule. It is of course possible to manually clean the test data without registering the TestTransactionModule.

8️⃣ API

Transaction Options

{
  connectionName?: string;
  isolationLevel?: IsolationLevel;
  propagation?: Propagation;
}
  • connectionName: The name of the DataSource to use for this transactional context. It allows you to specify a specific DataSource if you have multiple DataSources configured in your application. (the data sources)

  • isolationLevel: The isolation level for the transactional context. Isolation levels define the degree to which one transaction must be isolated from the effects of other concurrent transactions. Common isolation levels include READ_COMMITTED, REPEATABLE_READ, and SERIALIZABLE. (isolation levels)

  • propagation: The propagation behavior for nest transactional contexts. Propagation determines how transactions should be propagated from one method to another. Common propagation behaviors include REQUIRED, REQUIRES_NEW, and NESTED. (propagation behaviors)

runOnTransactionCommit

The runOnTransactionCommit function is part of the nestjs-transactional package. It is used within the context of a transactional operation in TypeORM. This function allows you to specify a callback function that will be executed when the transaction is successfully committed.

When you perform a transactional operation, such as inserting, updating, or deleting data in a database, you want to ensure that the changes are applied atomically. This means that either all the changes are committed successfully, or none of them are applied at all. The runOnTransactionCommit function provides a way to execute additional logic or actions after the transaction is successfully committed.

To use runOnTransactionCommit, you need to pass a callback function as an argument. This callback function will be invoked only if the transaction is committed successfully. You can use this callback function to perform any additional tasks or actions that should be executed after the transaction is completed.

Here's an example of how you can use runOnTransactionCommit:

import { runOnTransactionCommit } from 'nestjs-transactional';

@Injectable()
export class UserService {
    ...
    @Transactional()
    async createUser(id?: string) {
      const user = User.create({ id });

      await this.dataSource.manager.save(user);

      runOnTransactionCommit(async () => {
        await this.logService.saveSuccessLog(...);
        console.log('User Save Success.');
      });
    }
}

runOnTransactionRollBack

The runOnTransactionRollback function is part of the typeorm package. It is used within the context of a transactional operation in TypeORM. This function allows you to specify a callback function that will be executed when the transaction is rolled back.

A transaction is rolled back when an error occurs during the transactional operation. This means that all changes made during the transaction are undone, and the state of the database is reverted to what it was before the transaction started. The runOnTransactionRollback function provides a way to execute additional logic or actions after the transaction is rolled back.

To use runOnTransactionRollback, you need to pass a callback function as an argument. This callback function will be invoked only if the transaction is rolled back. You can use this callback function to perform any additional tasks or actions that should be executed after the transaction is rolled back.

Here's an example of how you can use runOnTransactionRollback:

import { runOnTransactionRollback } from 'nestjs-transactional';

@Injectable()
export class UserService {
    ...
    @Transactional()
    async createUser(id?: string) {
      const user = User.create({ id });

      await this.dataSource.manager.save(user);

      runOnTransactionRollback(async (e: unknown) => {
        console.log('User Save Failed.', e);
        await this.logService.saveFailLog(...);
      });
    }
}

addTransactionalDataSource(input): DataSource

Add TypeORM DataSource to transactional context.

addTransactionalDataSource(new DataSource(...));
addTransactionalDataSource({ name: 'default', : new DataSource(...) });

⛔️ Bug Report

GitHub issues

@Transactonal does not work when using an injected entity manager by @InjectEntityManager

@Injectable()
export class UserService {
  constructor(@InjectEntityManager() readonly entityManager: EntityManager) {}

  @Transactional()
  updateUser(userId: string, data: Partial<User>) {
    return this.entityManager.update(User, userId, data);
  }
}
  • Error reported in nested transactions

👍 Stay in touch

Author/Developer - KIMBEOBWOO

📜 License