Papooch / nestjs-cls

A continuation-local storage (async context) module compatible with NestJS's dependency injection.

Home Page:https://papooch.github.io/nestjs-cls/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to use in a standalone CLI app?

sam-artuso opened this issue · comments

I'm using nestjs-cls to create a database transaction object that is created for each request and propagate it through the app.

My app can also work in CLI only mode – there are no network listeners and no REQUEST. I followed this article.

What is the best way to create a transaction object in such a way that it works both in normal mode and in CLI mode?

I am working on a new plugin for transactions that should make it seamless: #96

In the meantime, all you really need to do (if you already have the mechanism for creating and storing the transaction object in the CLS) is to make sure that the CLS context is initialized. For HTTP based apps (where request exists), this is as easy as registering the root module with the mount option for an enhancer. When there is no request, you need to wrap the entry point yourself: https://papooch.github.io/nestjs-cls/features-and-use-cases/usage-outside-of-web-request

From an architecture "best practises" perspective:

  • Say that there is a CatsService that relies on CLS context to be initialized and is to be used in both the http-based app and a standalone CLI app.
  • For the HTTP based app, there is a CatsController that automatically uses the ClsMiddleware (or other enhancer configured by the root module).
  • For the CLI app, there should be a different entry point, say a CatsCommandController, whose methods can be decorated by the @UseCls decorator.
  • That way, the service itself doesn't really care where its called from as long as the caller makes sure to initialize the context.

A CLI App usually starts and bootstraps the entire application on each command invocation, therefore you can probably wrap the call just after the application bootsraps, like so:

async function bootstrap() {
  const app = await NestFactory.createApplicationContext(AppModule)
  // [...] some more setup

  const commandController = app.get(CommandController)

  await app.get(ClsService).run(
    () => commandController.runCommand()
  )

  await app.close()
}

I ended up doing something very similar:

import { CommandFactory } from 'nest-commander'
import { ClsService } from 'nestjs-cls'

import { AppModule } from './app.module'
import { getAuthTokenForCliMode } from './common/cli'

import type { DbTx } from './db/types'

async function bootstrap() {
  const app = await CommandFactory.createWithoutRunning(AppModule)

  const clsService = app.get(ClsService)
  const dbService = app.get('Db')

  await clsService.run(() =>
    dbService.tx(async (tx: DbTx) => {
      clsService.set('Tx', tx)
      clsService.set('AuthHeader', `Bearer ${getAuthTokenForCliMode()}`)

      await CommandFactory.runApplication(app)
      await dbService.disconnect()
    }),
  )
}

// eslint-disable-next-line no-void
void bootstrap()

Thank you for your help!