npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nestjs-transactional/typeorm

v1.0.0-alpha.1

Published

TypeORM adapter for @nestjs-transactional/core — EntityManager propagation, savepoints, multi-datasource

Readme

@nestjs-transactional/typeorm

npm version License: MIT

TypeORM adapter for @nestjs-transactional/core.

Overview

  • TypeOrmTransactionAdapter — implements the core TransactionAdapter SPI over TypeORM's DataSource. Handles BEGIN / COMMIT / ROLLBACK via DataSource.transaction(...) and issues raw SAVEPOINT / ROLLBACK TO SAVEPOINT / RELEASE SAVEPOINT SQL for nested transactions.
  • Transparent transactional repositories@InjectRepository(Entity) instances, @InjectEntityManager() em.getRepository(E), @InjectDataSource() ds.manager.save(...), and ds.getRepository(E).save(...) automatically dispatch through the active @Transactional() scope's EntityManager. No getCurrentEntityManager() boilerplate. Custom repositories via Repository.extend(...) and TreeRepository work transparently. See Transparent transactional behaviour below.
  • getCurrentEntityManager(dataSource?, fallback?) — escape-hatch helper that returns the transaction-aware EntityManager from the current async context (or falls back to dataSource.manager outside a transaction). Mostly needed for the documented limitations below; standard injection paths cover everything else.
  • isInTransaction(dataSource?) — predicate for the current context.
  • TypeOrmTransactionalModule.forRoot({ dataSource?, isDefault? }) — NestJS dynamic module that activates the transparent patches and registers an adapter with the core AdapterRegistry. The DataSource itself resolves from DI under getDataSourceToken(dataSource) — the same convention @nestjs/typeorm uses for @InjectRepository(E, dataSource).
  • TypeOrmTransactionalModule.forRootAsync({ useFactory, inject?, imports? }) — async variant for ConfigService-driven setups. Registers via OnModuleInit to defer DataSource resolution past @nestjs/typeorm's async DataSource provider settling (Convention #22).
  • Multi-DataSource: call forRoot once per dataSource. Mirrors OutboxModule (ADR-019) and TransactionalModule.

Installation

pnpm add @nestjs-transactional/typeorm @nestjs-transactional/core typeorm @nestjs/typeorm reflect-metadata

Compatibility

| Peer | Supported range | | ----------------------------------- | -------------------------- | | Node.js | >=22.13.0 | | typeorm | ^0.3.0 \|\| ^1.0.0 | | @nestjs/typeorm | ^10.0.0 \|\| ^11.0.0 | | @nestjs/common / @nestjs/core | ^10.0.0 \|\| ^11.0.0 | | reflect-metadata | ^0.1.13 \|\| ^0.2.0 | | rxjs | ^7.0.0 |

The TypeORM range covers both stable 0.3.x and stable 1.x releases. CI runs the full unit and integration matrix (testcontainers Postgres) against both TypeORM majors, so the adapter is exercised end-to-end on every supported peer. TypeORM nightly / beta builds are not in the declared range; install them explicitly via pnpm.overrides if you need to pin to one.

Quick start

Minimal single-DataSource setup:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TransactionalModule } from '@nestjs-transactional/core';
import { TypeOrmTransactionalModule } from '@nestjs-transactional/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      url: process.env.DATABASE_URL,
      entities: [User],
      synchronize: false,
    }),
    TypeOrmModule.forFeature([User]),

    TransactionalModule.forRoot({ isGlobal: true }),
    TypeOrmTransactionalModule.forRoot(),
  ],
})
export class AppModule {}

Import order mattersTransactionalModule.forRoot({ isGlobal: true }) must be present (with isGlobal) so the AdapterRegistry is visible inside TypeOrmTransactionalModule's DI scope. The actual DataSource is resolved via @nestjs/typeorm's getDataSourceToken(name)TypeOrmModule.forRoot(...) registers it globally, so TypeOrmTransactionalModule.forRoot finds it automatically.

Transparent transactional behaviour

Once the module is imported, every Repository reachable through the standard @nestjs/typeorm injection paths automatically dispatches through the active @Transactional() scope. No getCurrentEntityManager() calls in user code:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Transactional } from '@nestjs-transactional/core';
import { Repository } from 'typeorm';
import { Order } from './order.entity';

@Injectable()
export class OrderService {
  constructor(
    @InjectRepository(Order)
    private readonly orderRepo: Repository<Order>,
  ) {}

  @Transactional()
  async placeOrder(dto: PlaceOrderDto): Promise<Order> {
    // `orderRepo.save(...)` automatically uses the transactional
    // EntityManager. If the method throws, the save rolls back.
    // Outside a @Transactional scope, the same call autocommits.
    return this.orderRepo.save(dto);
  }
}

Supported transparent patterns:

  • @InjectRepository(Entity) repo — the headline case.
  • @InjectEntityManager() em.getRepository(E).save(...).
  • @InjectDataSource() ds.getRepository(E).save(...).
  • @InjectDataSource() ds.manager.save(Entity, ...) (the patched DataSource manager getter routes through the active EM).
  • Custom repositories via Repository.extend(...).
  • TreeRepository and MongoRepository (inherit from Repository).

The mechanism is prototype-level patching modelled on the typeorm-transactional library; patches install at module-load time so they cover Repositories constructed by any DI factory, regardless of resolution order.

Documented limitations

Two patterns are NOT covered by the patches and require an escape hatch:

  1. @InjectEntityManager() em.save(Entity, ...) direct call is NOT transactional. The patches cover em.getRepository(E).save(...) (the typical pattern) but not direct method calls on the injected EntityManager. Use the Repository pattern instead, or call getCurrentEntityManager():

    @Transactional()
    async createUser(name: string) {
      // Option A — Repository pattern (recommended).
      return this.em.getRepository(User).save({ name });
    
      // Option B — escape hatch.
      // const em = getCurrentEntityManager();
      // return em.save(User, { name });
    }
  2. BaseEntity static methods (User.save(...) etc.) are NOT supported. The BaseEntity.useDataSource(...) API stores a captured DataSource reference that bypasses the patches. Use the Repository pattern.

The escape hatch:

import { getCurrentEntityManager } from '@nestjs-transactional/typeorm';

@Injectable()
export class RawSqlService {
  constructor(@InjectDataSource() private readonly ds: DataSource) {}

  @Transactional()
  async runRawSql() {
    // Pass `ds` as fallback so the helper returns ds.manager when
    // no transaction is active (autocommit). Inside a tx, returns
    // the transactional EM.
    const em = getCurrentEntityManager('default', this.ds);
    await em.query(
      'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
      [100, 1],
    );
  }
}

Multi-DataSource

@Module({
  imports: [
    TypeOrmModule.forRoot({ name: 'default', /* ... */ }),
    TypeOrmModule.forRoot({ name: 'billing',  /* ... */ }),

    TransactionalModule.forRoot({ isGlobal: true }),
    TypeOrmTransactionalModule.forRoot({ isDefault: true }),       // 'default'
    TypeOrmTransactionalModule.forRoot({ dataSource: 'billing' }), // 'billing'
  ],
})
export class AppModule {}

Target a specific dataSource in a transactional method:

import { Transactional } from '@nestjs-transactional/core';

@Injectable()
export class BillingService {
  constructor(
    @InjectRepository(Invoice, 'billing')
    private readonly invoiceRepo: Repository<Invoice>,
  ) {}

  @Transactional({ dataSource: 'billing' })
  async chargeCard(/* ... */) {
    // Repository is bound to 'billing' DS — saves go to billing.
    return this.invoiceRepo.save(/* ... */);
  }
}

Cross-DS isolation (DD-023): a Repository bound to dataSource A inside a @Transactional({ dataSource: 'B' }) method autocommits — its patched manager getter looks up the active transaction for dataSource A, finds none, and falls back to its captured original manager. Distributed transactions across dataSources are explicitly NOT supported; cross-DS atomicity goes through the outbox.

Each forRoot call registers its adapter under typeorm:${dataSource} in the core AdapterRegistry. TransactionManager routes based on options.dataSource.

Async configuration

@Module({
  imports: [
    ConfigModule,
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (cfg: ConfigService) => ({
        type: 'postgres',
        url: cfg.get('DATABASE_URL'),
        entities: [User],
      }),
    }),

    TransactionalModule.forRoot({ isGlobal: true }),
    TypeOrmTransactionalModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (cfg: ConfigService) => ({
        dataSource: cfg.get('DATA_SOURCE_NAME', 'default'),
        isDefault: true,
      }),
    }),
  ],
})
export class AppModule {}

forRootAsync defers resolution of the dataSource name until the factory runs. Per-DS DI tokens (getTransactionalAdapterToken(ds)) are NOT registered in the async path because NestJS provider tokens must be declared statically — if you need direct adapter injection by per-DS token, use sync forRoot({ dataSource }) instead.

The async path uses an OnModuleInit-driven registration class so the DataSource resolves correctly even when paired with TypeOrmModule.forRootAsync (whose own DataSource provider is async). Pinned by packages/typeorm/test/integration/forrootasync.integration.spec.ts.

Testing

Unit tests — in-memory SQLite

For fast unit tests that don't need a real database, use TypeORM's sqljs driver:

import { DataSource } from 'typeorm';
import { TypeOrmTransactionAdapter } from '@nestjs-transactional/typeorm';

const ds = new DataSource({
  type: 'sqljs',
  synchronize: true,
  entities: [YourEntity],
});
await ds.initialize();

const adapter = new TypeOrmTransactionAdapter(ds, 'default');

Integration tests — testcontainers-node + real Postgres

Bundled helper for real Postgres integration:

import {
  startPostgresContainer,
  stopPostgresContainer,
  createAdditionalDatabase,
} from '@nestjs-transactional/typeorm/test/setup-testcontainers';

let ctx;
beforeAll(async () => {
  ctx = await startPostgresContainer({ entities: [User], synchronize: true });
});
afterAll(async () => {
  await stopPostgresContainer(ctx);
});

// Multi-DS: a second database inside the same container.
const secondary = await createAdditionalDatabase(ctx, 'billing_test', {
  entities: [User],
  synchronize: true,
});

Run integration tests:

pnpm --filter @nestjs-transactional/typeorm test:integration

The bundled docker-compose.yml is for manual local use (psql against a persistent instance). Testcontainers manages its own containers and does not require compose.

Savepoints and NESTED propagation

When a method uses PropagationMode.NESTED from inside an existing TypeORM transaction, the adapter issues a SAVEPOINT sp_<uuid-30> statement. Rollback rolls back to the savepoint; the outer transaction continues. Savepoint names are at most 33 characters long — valid on Postgres, MySQL, MariaDB, SQLite, and Oracle's identifier limit.

Worked examples

  • basic-transactional@Transactional() on @InjectRepository, single DataSource. Transparent repository showcase.
  • multi-datasource-basic — two DataSources with @Transactional({ dataSource }), no outbox.
  • read-write-separation — master + replica, only the master gets TypeOrmTransactionalModule.
  • async-config-from-environmentTypeOrmTransactionalModule.forRootAsync end-to-end with ConfigService + Joi profiles.
  • e-commerce-orders — three-DataSource flagship combining transparent repositories + per-DS outbox + CQRS + REST + Kafka externalization.

Full catalogue: examples/README.md.

Status

Alpha. Public API may change between 0.x releases.