UNPKG

@tsrt/typeorm-transactions

Version:
258 lines (186 loc) 9.57 kB
# TsRT: TypeORM Transactions [![npm version](https://img.shields.io/npm/v/@tsrt/typeorm-transactions.svg)](https://www.npmjs.com/package/@tsrt/typeorm-transactions) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/tsReusableTools/tsrt/blob/master/LICENSE) [![Size](https://img.shields.io/bundlephobia/minzip/@tsrt/typeorm-transactions.svg)](https://www.npmjs.com/package/@tsrt/typeorm-transactions) [![Downloads](https://img.shields.io/npm/dm/@tsrt/typeorm-transactions.svg)](https://www.npmjs.com/package/@tsrt/typeorm-transactions) Convenient transactions support for great [TypeORM](https://github.com/typeorm/typeorm). ### The most basic example: [Refer to full example for more info](#full-example) ```ts 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](#init-cls-namespace). 2. [Everything should be wrapped into created CLS Namespace](#wrap-into-cls-namespace). 3. [Your repositories should (one of)](#repositories): - 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 ```ts // 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 ```ts 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](https://www.npmjs.com/package/express-session) package can lead to [context loss](https://github.com/othiym23/node-continuation-local-storage/issues/29). 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: ```ts 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`: ```ts // 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`: ```ts // 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`: ```ts // 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: ```ts 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 ```ts 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 Before __0.8.0__ there was ability to use Transactions without [cls-hooked](https://www.npmjs.com/package/cls-hooked).. Still old API is available for usage under `@tsrt/typeorm-transactions/dist/legacy`. Most basic usage is same as in [this example](#the-most-basic-example), w/ only one difference - it is not necessary to initialise and use clsNamespace. Legacy API instantiates separate repositories for each transaction. For more docs please refer to [legacy docs](https://www.npmjs.com/package/@tsrt/typeorm-transactions/v/0.7.11#usage). ## License This project is licensed under the terms of the [MIT license](https://github.com/tsReusableTools/tsrt/blob/master/LICENSE).