Papooch / nestjs-cls

A continuation-local storage (async context) module compatible with NestJS's dependency injection.
https://papooch.github.io/nestjs-cls/
MIT License
434 stars 28 forks source link

New plugin `@nestjs-cls/transactional` #96

Closed Papooch closed 8 months ago

Papooch commented 8 months ago

Should be a generic plugin that would enable seamless propagation of database transaction using a Proxy provider. Should be generic enough to support writing custom adapters for ORMs and libraries that support callback-type transactions (interactive transactions).

Example (with prisma):

Registration

@Module({
  imports: [
    ClsModule.forRoot({
      middleware: { mount: true },
      plugins: [
        new ClsPluginTransactional({
          // option to choose a built-in adapter or a custom one
          adapter: new PrismaAdapter({
            // custom adapter options.
            // This one accepts the injection token for the Prisma client
            prismaClientToken: Prisma
          })
        })
      ]
    })
  ]
  providers: [CallingService, CalledService]
})
export class AppModule {}

Usage

@Injectable()
class CallingService {
  constructor(
    private readonly calledService: CalledService,
    private readonly txHost: TransactionHost<Prisma>,
  ) {}

  // this wraps the method with `prisma.$transaction(( txClient ) => { ... })`
  // and stores the `txClient` in CLS
  @Transactional()
  async startTransaction() {
    // due to magic of AsyncLocalStorage, both of these calls will receive the same txClient
    await this.calledService.doWork()
    await this.calledService.doOtherWork()
  }

  // Optionally, we're able to pass in adapter-specific transaction options
  @Transactional<PrismaAdapter>({
    isolationLevel: Prisma.TransactionIsolationLevel.Serializable
  })
  async otherTransactionalWork() {
    await this.calledService.doWork()
    await this.calledService.doOtherWork()
  }

  async imperativeTransaction() {
    // alternatively, we can inect the `TransactionHost` and use
    // the imperative API to start a CLS-enabled transaction
    await this.txHost.runWithTransaction(async () => {
      await this.calledService.doWork()
      await this.calledService.doOtherWork()      
    })
  }
}
@Injectable()
class CalledService {
  constructor(
    // This inject a CLS-powered Proxy provider that refers to the
    // tx client that is currently in the CLS.
    // If no client is in CLS, it uses the non-transactional one (or throws, this might be configurable)
    private readonly txHost: TransactionHost<Prisma>
  ) {}

  async doWork() {
     this.txHost.tx.someTable.create({ something: ... })
  }

  async doOtherWork() {
    this.txHost.tx.someOtherTable.create({ somethingElse: ... })
  }
}
sam-artuso commented 8 months ago

This is an exciting feature! ⭐

Papooch commented 8 months ago

Released with v4 :rocket: