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

Add support for request-scoped providers based on CLS #30

Closed Papooch closed 2 years ago

Papooch commented 2 years ago

This is still just an idea, but I think it's very much possible to use a Proxy in combination of CLS to create a truly request-scoped providers in the Nest sense of the word but without the need to recreate the whole provider chain.

The implementation would be is similar to the one of request-scoped beans in the Spring framework for Java.

This would enable the following usage:

@Injectable()
export class UserService {
  constructor(
    // inject a singleton provider, that is actually a Proxy instance with correctly set up accessor traps
    @InjectCls(RequestUser) user: RequestUser
  ) {}

  getCurrentUserName() {
    // when accessing the object, we the call is proxied to the current user object in the CLS
    return this.user.name
  }
}

or even

@Injectable()
export class UserService {
  constructor(
    // it could be possible to inject arbitrary provider - in this case a connection to the tenant database
    @InjectCls('TENANT_CONNECTION') db: Knex
  ) {}

  getCurrentUserFromDb() {
    // the call would be again proxied to the current TENANT_CONNENCTION object in the CLS
    return this.db.select('whatever').from('users') ...
  }
}

The registration of such providers could be done as follows:

imports: [
   ClsModule.forFeature(RequestUser)
]

and If we were to support the second use case - that is arbitrary factory providers, then even

imports: [
  ClsModule.forFeature({
    provide: 'TENANT_CONNENCTION',
    import: [DatabaseModule],
    inject: [ClsService, DatabaseService],
    useFactory: async (cls: ClsService, databaseService: DatabaseService) => {
      const tenantId = cls.get('tenantId');
      const tenantConnection: Knex = await databaseService.getTenantConnection(tenantId);
      return tenantConnection;
    }
  })
]

It would be needed to figure out where and when to populate the CLS store with the provider. It is too late to do it at the proxy access time, because the provider could be itself async so it would have to have been already constructed at the time of access.

Next issue is how to inject providers and such. All this would probably happen in the enhancer when the CLS context is being set up using moduleRef.

Also, what about situations where we're setting up the CLS store manually with cls.run()?. We'd need to construct the providers there somehow and since the operation must be async, it can't be done in cls.run() directly. We'll probably need an explicit call - something like await cls.boostrapProxyProviders().