Aliheym / typeorm-transactional

A Transactional Method Decorator for TypeORM that uses Async Local Storage or cls-hooked to handle and propagate transactions between different repositories and service methods.
MIT License
219 stars 30 forks source link
decorator nestjs transactions typeorm typescript

Typeorm Transactional

npm version

It's a fork of typeorm-transactional-cls-hooked for new versions of TypeORM.

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

See Changelog

## npm
npm install --save typeorm-transactional

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

Or

yarn add typeorm-transactional

## 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 transactional context before your application is started

import { initializeTransactionalContext, StorageDriver } from 'typeorm-transactional';

initializeTransactionalContext({ storageDriver: StorageDriver.AUTO });
...
app = express()
...

IMPORTANT NOTE

Calling initializeTransactionalContext must happen BEFORE any application context is initialized!

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.

To be able to use TypeORM entities in transactions, you must first add a DataSource using the addTransactionalDataSource function:

import { DataSource } from 'typeorm';
import { initializeTransactionalContext, addTransactionalDataSource, StorageDriver } from 'typeorm-transactional';
...
const dataSource = new DataSource({
      type: 'postgres',
    host: 'localhost',
    port: 5435,
    username: 'postgres',
    password: 'postgres'
});
...

initializeTransactionalContext({ storageDriver: StorageDriver.AUTO });
addTransactionalDataSource(dataSource);

...

Example for Nest.js:

// main.ts

import { NestFactory } from '@nestjs/core';
import { initializeTransactionalContext, StorageDriver } from 'typeorm-transactional';

import { AppModule } from './app';

const bootstrap = async () => {
  initializeTransactionalContext({ storageDriver: StorageDriver.AUTO });

  const app = await NestFactory.create(AppModule, {
    abortOnError: true,
  });

  await app.listen(3000);
};

bootstrap();
// app.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { addTransactionalDataSource } from 'typeorm-transactional';

@Module({
    imports: [
       TypeOrmModule.forRootAsync({
         useFactory() {
           return {
             type: 'postgres',
             host: 'localhost',
             port: 5435,
             username: 'postgres',
             password: 'postgres',
             synchronize: true,
             logging: false,
           };
         },
         async dataSourceFactory(options) {
           if (!options) {
             throw new Error('Invalid options passed');
           }

           return addTransactionalDataSource(new DataSource(options));
         },
       }),

       ...
     ],
     providers: [...],
     exports: [...],
})
class AppModule {}

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

You can also use this library with custom TypeORM repositories. You can read more about them here and here.

NOTE: You can add multiple DataSource if you need it

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

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

Data Sources

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:

addTransactionalDataSource({
    name: 'second-data-source',
    dataSource: new DataSource(...)
});

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

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

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 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', () => ({
  Transactional: () => () => ({}),
}));

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

API

Library Options

{
  storageDriver?: StorageDriver,
  maxHookHandlers?: number
}

Transaction Options

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

Storage Driver

Option that determines which underlying mechanism the library should use for handling and propagating transactions.

The possible variants:

⚠️ WARNING: Currently, we use CLS_HOOKED by default for backward compatibility. However, in the next major release, this default will be switched to AUTO.

import { StorageDriver } from 'typeorm-transactional'

initializeTransactionalContext({ storageDriver: StorageDriver.AUTO });

initializeTransactionalContext(options): void

Initialize transactional context.

initializeTransactionalContext(options?: TypeormTransactionalOptions);

Optionally, you can set some options.

addTransactionalDataSource(input): DataSource

Add TypeORM DataSource to transactional context.

addTransactionalDataSource(new DataSource(...));

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

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

...

runOnTransactionCommit(cb: Callback): void

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: Callback): void

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: Callback): void

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