TsRT: TypeORM Transactions
Convenient transactions support for great TypeORM.
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
- CLS Namespace should be initialized before transactions usage.
- Everything should be wrapped into created CLS Namespace.
-
Your repositories should (one of):
- extend BaseRepository from
@tsrt/typeorm-transactions
(annotated w/ TypeORM'sEntityRepository
). - extend TypeORM's Repository (annotated w/ TypeORM's
EntityRepository
) and before usagepatchTypeOrmRepository
should be called. - any class w/
manager
(TypeORM'sEntityManager
) property and before usagepatchTypeOrmRepository
should be called. - any class w/ TypeORM's
EntityManager
as first argument in constructor and before usagepatchTypeOrmRepository
should be called (or extend BaseRepository).
- extend BaseRepository from
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):
- bind namespace to express AFTER calling
express-session
middleware. - call
express-session
middleware and bindnext
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
BaseRepository
from @tsrt/typeorm-transactions
:
1. Extending // 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(/* ... */);
Repository
and patching it w/ patchTypeOrmRepository
:
2. Extending TypeORM's native // 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(/* ... */);
patchTypeOrmRepository
:
3. Creating any repository from scratch and call // 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(/* ... */);
EntityManager
as first constructor argument:
4. Creating any repository w/ 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
- Transaction - constructor to create new single Transaction.
-
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.
-
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
Before 0.8.0 there was ability to use Transactions without cls-hooked..
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 instantiates separate repositories for each transaction.
For more docs please refer to legacy docs.
License
This project is licensed under the terms of the MIT license.