odavid / typeorm-transactional-cls-hooked

A Transactional Method Decorator for typeorm that uses cls-hooked to handle and propagate transactions between different repositories and service methods. Inpired by Spring Trasnactional Annotation and Sequelize CLS
MIT License
524 stars 86 forks source link
decorator nestjs transaction typeorm typescript

typeorm-transactional-cls-hooked

npm version

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

Inspired by Spring Transactional Annotation and Sequelize CLS

See Changelog

Installation

npm install --save typeorm-transactional-cls-hooked
## Needed dependencies
npm install --save typeorm reflect-metadata

Or

yarn add typeorm-transactional-cls-hooked
## 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

Initialization

In order to use it, you will first need to initialize the cls-hooked namespace before your application is started

import { initializeTransactionalContext } from 'typeorm-transactional-cls-hooked';

initializeTransactionalContext() // Initialize cls-hooked
...
app = express()
...

BaseRepository

Since this is an external library, all your typeorm repositories will need to be a custom repository extending either the BaseRepository (when using TypeORM's Entity) or the BaseTreeRepository class (when using TypeORM's TreeEntity).

// Post.entity.ts
@Entity()
export class Post{
  @PrimaryGeneratedColumn()
  id: number

  @Column
  message: string
  ...
}

// Post.repository.ts
import { EntityRepository } from 'typeorm';
import { BaseRepository } from 'typeorm-transactional-cls-hooked';

@EntityRepository(Post)
export class PostRepository extends BaseRepository<Post> {}

The only purpose of the BaseRepository class is to make sure the manager property of the repository will always be the right one. In cases where inheritance is not possible, you can always Patch the Repository/TreeRepository to enable the same functionality as the BaseRepository

Patching TypeORM Repository

Sometimes there is a need to keep using the TypeORM Repository instead of using the BaseRepository. For this cases, you will need to "mixin/patch" the original Repository with the BaseRepository. By doing so, you will be able to use the original Repository and not change the code or use BaseRepository.

This method was taken from https://gist.github.com/Diluka/87efbd9169cae96a012a43d1e5695667 (Thanks @Diluka)

In order to do that, the following should be done during initialization:

import { initializeTransactionalContext, patchTypeORMRepositoryWithBaseRepository } from 'typeorm-transactional-cls-hooked';

initializeTransactionalContext() // Initialize cls-hooked
patchTypeORMRepositoryWithBaseRepository() // patch Repository with BaseRepository.

If there is a need to keep using the TypeORM TreeRepository instead of using BaseTreeRepository, use patchTypeORMTreeRepositoryWithBaseTreeRepository.


IMPORTANT NOTE

Calling initializeTransactionalContext and patchTypeORMRepositoryWithBaseRepository must happen BEFORE any application context is initialized!


Using Transactional Decorator

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

Transaction Propagation

The following propagation options can be specified:

Isolation Levels

The following isolation level options can be specified:

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.

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

export class PostService {
    constructor(readonly repository: PostRepository, readonly events: EventService) {}

    @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
    }
}

Unit Test Mocking

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

This can be accomplished in Jest with:

jest.mock('typeorm-transactional-cls-hooked', () => ({
  Transactional: () => () => ({}),
  BaseRepository: class {},
}));

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

Logging / Debug

The Transactional uses the Typeorm Connection logger to emit log messages.

In order to enable logs, you should set logging: ["log"] or logging: ["all"] to your typeorm logging configuration.

The Transactional log message structure looks as follows:

Transactional@UNIQ_ID|CONNECTION_NAME|METHOD_NAME|ISOLATION|PROPAGATION - MESSAGE

During initialization and patching repositories, the Typeorm Connection logger is not available yet. For this reason, the console.log() is being used, but only if TRANSACTIONAL_CONSOLE_DEBUG environment variable is defined.