Narnia's Psychedelic Mushrooms

    @tsrt/typeorm-transactions
    TypeScript icon, indicating that this package has built-in type declarations

    0.10.0 • Public • Published

    TsRT: TypeORM Transactions

    npm version GitHub license Size Downloads

    Convenient transactions support for great TypeORM.

    Important

    Until version 1.0.0 Api should be considered as unstable and may be changed. So prefer using exact version instead of version with ~ or ^.

    Modern API

    Since v0.8.0 - this package depends on cls-hooked.

    New API heavily inspired by this package but with more flexible API.

    The main purpose of using new API - ability to abstract from implementation, share transactions easily and have different propagation levels.

    The most basic example:

    Refer to full example for more info

    import { Transaction } from  '@tsrt/typeorm-transactions';
    
    async function makeStuffInTransaction(): Promise<any> {
      const t =  new Transaction({ ... });
      await t.begin();
    
      try {
        // await transaction.manager.createQueryBuilder()...
        // Or someRepository.create(...)
        await t.commit();
      } catch (err) {
        await t.rollback(err);
      }
    }

    Preconditions

    1. CLS Namespace should be initialized before transactions usage.
    2. Everything should be wrapped into created CLS Namespace.
    3. Your repositories should (one of):
      • extend BaseRepository from @tsrt/typeorm-transactions (annotated w/ TypeORM's EntityRepository).
      • extend TypeORM's Repository (annotated w/ TypeORM's EntityRepository) and before usage patchTypeOrmRepository should be called.
      • any class w/ manager (TypeORM's EntityManager) property and before usage patchTypeOrmRepository should be called.
      • any class w/ TypeORM's EntityManager as first argument in constructor and before usage patchTypeOrmRepository should be called (or extend BaseRepository).

    Init CLS Namespace

    // index.ts
    import { createTransactionsNamespace, patchTypeOrmRepository } from '@tsrt/typeorm-transactions';
    
    createTransactionsNamespace(); // This one should be called before any transactions usage
    patchTypeOrmRepository(); // This one is only necessary if repositories extends native TypeORM's Repository.

    Wrap into CLS Namespace

    import { bindTransactionsNamespace, execInTransactionsNamespace } from '@tsrt/typeorm-transactions';
    
    // Wrap only function:
    execInTransactionsNamespace(/* Any async code which uses transactions via CLS from this package */)
    
    // Example for Express:
    const app = express();
    app.use(bindTransactionsNamespace);
    
    // If using only `Transactional` decorator - no need to init namespace first.
    class Service {
      @Transactional()
      public async doStuff(): Promise<any> {
        // do stuff inside transaction
      }
    }

    !!! Note, that popular express-session package can lead to context loss. To prevent that try (one of):

    1. bind namespace to express AFTER calling express-session middleware.
    2. call express-session middleware and bind next function to namespace.

    Example:

    import express from 'express';
    import expressSession from 'express-session';
    import { createTransactionsNamespace, bindTransactionsNamespace } from '@tsrt/typeorm-transactions';;
    
    const ns = createTransactionsNamespace();
    const app = express();
    
    // 1. bind namespace to express _AFTER_ calling `express-session` middleware.
    app.use(expressSession({ ... }));
    app.use(bindTransactionsNamespace);
    
    
    // 2. call `express-session` middleware and bind `next` function to namespace.
    app.use((req, res, next) => expressSession({ ... })(req, res, ns.bind(next)));

    Repositories

    1. Extending BaseRepository from @tsrt/typeorm-transactions:
    // repository.ts
    import { BaseRepository } from '@tsrt/typeorm-transactions';
    import { EntityRepository } from 'typeorm';
    
    EntityRepository(SomeEntity)
    export class SomeBaseRepository extends BaseRepository { /* ... */ }
    
    
    // somewhereElse.ts
    import { getConnection } from 'typeorm';
    import { SomeBaseRepository } from 'path/to/repository.ts';
    
    getConnection(/* ... */).getCustomRepoistory(SomeBaseRepository).create(/* ... */);
    2. Extending TypeORM's native Repository and patching it w/ patchTypeOrmRepository:
    // index.ts
    import { patchTypeOrmRepository } from '@tsrt/typeorm-transactions';
    
    patchTypeOrmRepository(); // This on shoud be called somewhere just after `createTransactionsNamespace()`.
    
    
    // repository.ts
    import { Repository, EntityRepository } from 'typeorm';
    
    EntityRepository(SecondEntity)
    export class SomeRepository extends Repository { /* ... */ }
    
    
    // somewhereElse.ts
    import { getConnection } from 'typeorm';
    import { SomeRepository } from 'path/to/repository.ts';
    
    getConnection(/* ... */).getCustomRepoistory(SomeRepository).create(/* ... */);
    3. Creating any repository from scratch and call patchTypeOrmRepository:
    // repository.ts
    import { patchTypeOrmRepository } from '@tsrt/typeorm-transactions';
    import { Repository, EntityManager } from 'typeorm';
    
    export class SomeRepository {
      public readonly manager: EntityManager;
      /* ... */
    }
    patchTypeOrmRepository(SomeRepository, { /* ... connection options ... */ });
    
    // somewhereElse.ts
    import { SomeRepository } from 'path/to/repository.ts';
    
    new SomeRepository().create(/* ... */);
    4. Creating any repository w/ EntityManager as first constructor argument:
    import { patchTypeOrmRepository } from '@tsrt/typeorm-transactions';
    import { Repository, EntityManager, getManager() } from 'typeorm';
    
    export class SomeRepository {
      constructor(public readonly manager: EntityManager;) {}
      /* ... */
    }
    
    // Then
    // 1. patchTypeOrmRepository(SomeRepository, { /* ... connection options ... */ });
    // 2. Or extend BaseRepository
    // 3. Also could be annotated w/ TypeORM's `EntityRepository`.
    
    
    // somewhereElse.ts
    import { SomeRepository } from 'path/to/repository.ts';
    
    new SomeRepository(getManager()).create(/* ... */);

    Propagation

    • REQUIRED (default) - supports existing transaction if it is or creates new if there is no.
    • SEPARATE - creates new transaction even if there is already an existing transaction.
    • SUPPORT - supports only existing transaction and does not create new.

    APIs

    1. Transaction - constructor to create new single Transaction.
    2. TransactionManager - constructor to create TransactionManager for specific connection w/ default options. Has 3 methods:
      • createTransaction - creates new single Transaction w/ TransactionManager default options and connection.
      • transaction - creates and starts new single Transaction. Could be provided w/ callback to execute inside transaction which will rollback automatically if no manual commit is called. If no callback provided, commit and rollback should be done manually.
      • autoTransaction - same as transaction method (with callback case), but will commit automatically if no error thrown.
    3. Transactional - method decorator (more convenient/declarative, but less flexible comparing to TransactionManager). Has 2 variants:
      • createTransactional - factory function for creating Transactional decorator for specific connection w/ default options. (uses TransactionManager under the hood).
      • Transactional - decorator itself w/ ability to provide any connection and other options.

    Full example

    import { PrimaryGeneratedColumn, Column, Entity, Repository, getConnection, EntityRepository } from 'typeorm';
    import { createTransactionsNamespace, bindTransactionsNamespace, patchTypeOrmRepository, TransactionManager } from '@tsrt/typeorm-transactions';
    
    createTransactionsNamespace();
    patchTypeOrmRepository();
    
    const tm = new TransactionManager();
    
    const app = express();
    app.use(bindTransactionsNamespace);
    
    @Entity('Users')
    class User {
      @PrimaryGeneratedColumn()
      public id: number;
    
      @Column()
      public name: string;
    }
    
    @EntityRepository(User)
    class UsersRepository extends Repository {
      public async createUser(body: IUserPayload): Promise<User> {
        const user = this.manager.create(body);
        return this.manager.save(User, user);
      }
    }
    
    class UsersService {
      public async createUsers(users: IUserPayload[]): Promise<User[]> {
        return tm.autoTransaction(async () => {
          const repository = getConnection().getCustomRepository(UsersRepository);
          const promises = users.map((item) => repository.createUser(item));
          return Promise.all(promises);
        });
      }
    }
    
    app.use('/test', async (req, res) => {
      const users = await new UsersService().createUsers([
        { name: 'First User' },
        { name: 'Second User' },
      ]);
      res.status(200).send(users);
    });
    
    app.listen(3333);

    Legacy API

    From version 0.8.0 it is recommended to use new Modern API. Still old API is available for usage under @tsrt/typeorm-transactions/dist/legacy.

    Most basic usage is same as in this example, w/ only one difference - it is not necessary to initialise and use clsNamespace.

    Legacy API used separate repositories for each transaction.

    For more docs please refer to legacy docs.

    TODO

    License

    This project is licensed under the terms of the MIT license.

    Install

    npm i @tsrt/typeorm-transactions

    DownloadsWeekly Downloads

    488

    Version

    0.10.0

    License

    MIT

    Unpacked Size

    83.3 kB

    Total Files

    30

    Last publish

    Collaborators

    • mopc