This repository shows how to implement unit of work pattern in NestJs Framework to realize transactional work with TypeORM using AsyncLocalStorage.
This implementation is an improvement of NestJs - Unit Of Work repository, because now the dependencies are singleton and do not cause overhead on each request.
@Controller('/v1/order')
export class OrderController {
constructor(
private readonly unitOfWork: UnitOfWorkService, // <-- necessary only to #first way
private readonly orderService: OrderService,
) {}
@Post('firstWay')
async createFirstWay(@Body() orderDto: CreateOrderRequestDto) {
return this.unitOfWork.doTransactional(async (): Promise<Order> => {
return this.orderService.createOrder(orderDto);
});
}
@Post('secondWay')
@UseInterceptors(TransactionStorageInterceptor) // <-- this intercept uses the UnitOfWorkService to wrap the transaction
async createSecondWay(@Body() orderDto: CreateOrderRequestDto) {
return this.orderService.createOrder(orderDto);
}
}
The database.sql
file has the database structure necessary to run this example.
The configuration of database connection is set in src/app.module.ts
.
Installing the dependencies:
npm install
Running the application:
npm run start:dev
Making a request using Javascript (run in your browser console and check the console application):
ATTENTION: to create a real case, the OrderRepository is configured to throw exception randomly during the Order creation, allowing us to check the consistency during successful or error operation.
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
var raw = JSON.stringify({
"date":"2021-11-03",
"description":"Testing transaction",
"items":[
{ "name": "T-Shirt", "quantity": 1 }
]
});
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
fetch("http://localhost:3000/v1/order/secondWay", requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
NestJs it's an amazing frameworks to NodeJs, with a powerful Dependency Injection Service, BUT when we need to work with TypeORM Repositories in transaction operation, the beautiful of repository injection fade away.
See below some issues about it:
- ISSUE: Better transaction management
- ISSUE: Transaction management in service layer
- ISSUE: support Distributed Transaction Service , like spring JTA
- ISSUE: Transactions in NestJs
- ISSUE: Transactions support
There are other repositories/libs that try to resolve the same problem:
My implementation uses a service class called UnitOfWorkService to share the same connection between the custom repositories using AsyncLocalStorage.
@Injectable()
export class UnitOfWorkService {
constructor(
@InjectConnection()
private readonly connection: Connection,
) {
this.asyncLocalStorage = new AsyncLocalStorage();
}
private readonly asyncLocalStorage: AsyncLocalStorage<any>;
getManager(): EntityManager {
const storage = this.asyncLocalStorage.getStore();
if (storage && storage.has('typeOrmEntityManager')) {
return this.asyncLocalStorage.getStore().get('typeOrmEntityManager');
}
return this.connection.createEntityManager();
}
// Used manually
async doTransactional<T>(fn): Promise<T> {
return await this.connection.transaction(async (manager) => {
let response: T;
await this.asyncLocalStorage.run(new Map<string, EntityManager>(), async () => {
this.asyncLocalStorage.getStore().set('typeOrmEntityManager', manager);
response = await fn(manager);
});
return response;
});
}
// Used by Interceptors
async doTransactionalCallHandler(fn: CallHandler): Promise<Observable<any>> {
return await this.connection.transaction(async (manager) => {
let response: Observable<any>;
await this.asyncLocalStorage.run(new Map<string, EntityManager>(), async () => {
this.asyncLocalStorage.getStore().set('typeOrmEntityManager', manager);
response = await fn.handle().toPromise();
});
return response;
});
}
}
My approach doesn't work with default TypeORM repositories provided by injection, you need to implement your own repository or generate it from TypeORM EntityManager shared.
import { Injectable } from '@nestjs/common';
import { Order } from '../models/order.model';
import { UnitOfWorkService } from '../../core/services/unit-of-work.service';
import { Item } from '../models/item.model';
@Injectable()
export class OrderRepository {
constructor(private readonly uow: UnitOfWorkService) {} // <-- receive the UnitOfWorkService with the manager
async getAll(): Promise<Order[]> {
return this.uow.getManager().find(Order, {
relations: ['items'],
});
}
async getById(idOrder: number): Promise<Order> {
return this.uow.getManager().findOneOrFail(Order, idOrder, {
relations: ['items'],
});
}
async saveOrder(order: Order): Promise<Order> {
return this.uow.getManager().save(order);
}
async saveOrderItem(item: Item): Promise<Item> {
return this.uow.getManager().save(item);
}
}
The entity service must only receive the custom repository by injection:
import { Injectable } from '@nestjs/common';
import { OrderRepository } from '../repositories/order.repository';
import { CreateOrderRequestDto } from '../dto/create-order-request.dto';
import { Order } from '../models/order.model';
import { Item } from '../models/item.model';
@Injectable()
export class OrderService {
constructor(private readonly orderRepository: OrderRepository) {} // <-- the custom repo created before
async getAll(): Promise<Order[]> {
return this.orderRepository.getAll();
}
async getById(id: number): Promise<Order> {
return this.orderRepository.getById(id);
}
async createOrder(orderDto: CreateOrderRequestDto): Promise<Order> {
const order = new Order();
order.date = orderDto.date;
order.description = orderDto.description;
await this.orderRepository.saveOrder(order);
for (const itemDto of orderDto.items) {
const item = new Item();
item.name = itemDto.name;
item.quantity = itemDto.quantity;
item.order = order;
await this.orderRepository.saveOrderItem(item);
}
return order;
}
}
The controller needs only receive the UnitOfWorkService by injection:
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { OrderService } from '../services/order.service';
import { Order } from '../models/order.model';
import { CreateOrderRequestDto } from '../dto/create-order-request.dto';
import { UnitOfWorkService } from '../../core/services/unit-of-work.service';
@Controller('/v1/order')
export class OrderController {
constructor(
private readonly unitOfWork: UnitOfWorkService,
private readonly orderService: OrderService,
) {}
@Get()
async all(): Promise<Order[]> {
return this.orderService.getAll(); // <-- use the service with the default (non-transactional) manager
}
@Get()
async getById(@Param('id') id: number): Promise<Order> {
return this.orderService.getById(id); // <-- use the service with the default (non-transactional) manager
}
@Post('firstWay')
async createFirstWay(@Body() orderDto: CreateOrderRequestDto) {
return this.unitOfWork.doTransactional(async (): Promise<Order> => {
return this.orderService.createOrder(orderDto);
});
}
@Post('secondWay')
@UseInterceptors(TransactionStorageInterceptor) // <-- this intercept uses the UnitOfWorkService to wrap the transaction
async createSecondWay(@Body() orderDto: CreateOrderRequestDto) {
return this.orderService.createOrder(orderDto);
}
}
Contributions are always welcome!
I will be glad to know if this approach help you or if you know a better way to resolve the same problem.