Database
The @zeltjs/db package provides ORM-agnostic database abstraction with automatic transaction propagation using AsyncLocalStorage.
Installation
pnpm add @zeltjs/db
Overview
Zelt's database abstraction solves a common problem: propagating transactions through your service layer without passing transaction objects explicitly. Using Node.js AsyncLocalStorage, transactions automatically flow through async call chains.
Creating a Database Service
Extend DatabaseService to integrate your ORM:
import { DatabaseService } from '@zeltjs/db';
import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
export class DrizzleService extends DatabaseService<PostgresJsDatabase> {
private sql!: postgres.Sql;
async setup(): Promise<PostgresJsDatabase> {
this.sql = postgres(process.env.DATABASE_URL!);
const db = drizzle(this.sql);
this.onShutdown(async () => {
await this.sql.end();
});
return db;
}
transaction<T>(
client: PostgresJsDatabase,
fn: (tx: PostgresJsDatabase) => Promise<T>,
): Promise<T> {
return client.transaction(fn);
}
}
Key points:
setup()— Initialize and return your database clienttransaction()— Execute a function within a transactiononShutdown()— Register cleanup handlers for graceful shutdown
Using the Database Service
Direct Usage
Inject the service and access the client:
@Injectable()
export class UserRepository {
constructor(private db = inject(DrizzleService)) {}
async findAll() {
return this.db.client.select().from(users);
}
async create(name: string, email: string) {
return this.db.client.insert(users).values({ name, email });
}
}
The client property automatically returns:
- The transaction client if inside a transaction
- The original client otherwise
Transaction Decorator
Create a decorator for your database service:
import { createTransactionDecorator } from '@zeltjs/db';
export const Transaction = createTransactionDecorator(DrizzleService);
Apply it to methods that should run in a transaction:
import { Injectable, inject } from '@zeltjs/core';
@Injectable()
export class OrderService {
constructor(
private orderRepo = inject(OrderRepository),
private inventoryRepo = inject(InventoryRepository),
) {}
@Transaction()
async placeOrder(userId: string, items: OrderItem[]) {
const order = await this.orderRepo.create(userId, items);
for (const item of items) {
await this.inventoryRepo.decrement(item.productId, item.quantity);
}
return order;
}
}
All repository calls within placeOrder automatically use the same transaction. If any operation fails, the entire transaction rolls back.
Transaction Middleware
For request-scoped transactions, use middleware:
import { createTransactionMiddleware } from '@zeltjs/db';
export const TransactionMiddleware = createTransactionMiddleware(DrizzleService);
Apply to controllers:
import { Controller, Post, body, UseMiddleware } from '@zeltjs/core';
@Controller('/orders')
@UseMiddleware(TransactionMiddleware)
export class OrderController {
constructor(private orderService = inject(OrderService)) {}
@Post('/')
async create(data = body<CreateOrderDto>()) {
return this.orderService.placeOrder(data.userId, data.items);
}
}
Every request to this controller runs in a transaction.
Transaction Propagation
Transactions propagate through async call chains automatically:
@Injectable()
export class PaymentService {
@Transaction()
async processPayment(orderId: string, amount: number) {
await this.ledgerRepo.debit(orderId, amount);
await this.notificationService.sendReceipt(orderId);
}
}
@Injectable()
export class OrderService {
@Transaction()
async completeOrder(orderId: string) {
await this.orderRepo.markComplete(orderId);
await this.paymentService.processPayment(orderId, 100);
}
}
When completeOrder calls processPayment, both run in the same transaction — the inner @Transaction() joins the existing transaction rather than starting a new one.
Lifecycle Integration
DatabaseService integrates with Zelt's lifecycle system:
import { createApp, http } from '@zeltjs/core';
const app = createApp([http({
controllers: [OrderController],
})], { configs: [DrizzleService] });
The service:
- Calls
setup()during app startup - Calls
shutdown()handlers during app shutdown
API Reference
DatabaseService
| Property/Method | Description |
|---|---|
client | Current database client (transaction-aware) |
setup() | Abstract: Initialize database connection |
transaction(client, fn) | Abstract: Execute function in transaction |
withTransaction(fn) | Run function in a new or existing transaction |
onShutdown(fn) | Register shutdown handler |
Factory Functions
| Function | Description |
|---|---|
createTransactionDecorator(Service) | Create @Transaction() decorator |
createTransactionMiddleware(Service) | Create transaction middleware class |