Papooch / nestjs-cls

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

Allow me to get request inside a singleton service #64

Closed justinpenguin45 closed 1 year ago

justinpenguin45 commented 1 year ago

How does this allow me to get access to the request object in a singleton service - or better yet, how does it allow a singleton service to have dependencies or access to request scoped services?

Papooch commented 1 year ago

I encourage you to revisit the documentation and to watch this video on AsyncLocalStorage.

If you have any further specific questions after going through these, I'll try to answer them as best as I can.

The key idea is that you do not use reqest-scoped providers from Nest, but replace them with AsyncLocalStorage-enabled singletons.

justinpenguin45 commented 1 year ago

Thank you for the quick response. I guess I initially misunderstood thinking that this would be somewhat of a drop-in and I would not have to modify much. Sounds like though, that I am going to have to change quite a bit. Looking at the documentation I am still not really sure how to do this. If I have a singleton (application scoped) provider that wants to use a request scoped provider - how do I replace the request scoped provider? The request scoped provider has a dependency (constructor injected current request object) - does that provider stay unchanged? The docs dont seem to handle these questions. Thank you

Papooch commented 1 year ago

With the help of Proxy Providers (see docs), you shouldn't need to change much.

If you could provide specific issues, I can suggest concrete solutions.

justinpenguin45 commented 1 year ago

Do I have to put the @InjectableProxy attribute on every dependency? It seems to be a domino effect, once I put that attribute on a class, then any dependencies of that class also require the attribute. Is this the intended way?

Papooch commented 1 year ago

I don't know how your project topology looks like, but the injectable proxy only goes on the class that should be instantiated for every request. If you just want to inject the request as a proxy, it's already available as it.

However, if you keep request-scoped state in your classes, then yes, they need the decorator (I would not recommend doing that though)

justinpenguin45 commented 1 year ago

Lets say I have class ServiceA, its a singleton (@Injectable). ServiceA has a dependency on ServiceC (in its constructor). ServiceC has a dependency on ServiceB and ServiceD. ServiceB is request scoped and has a dependency on the current request (in its constructor @Inject(REQUEST) private readonly request: Request) and a dependency on ServiceE. ServiceD is a singleton. ServiceE is from an external module, I cannot add the @InjectableProxy attribute to it. I tried to use the ClsModule.forFeatureAsync for ServiceE but I get an error "Cannot create a Proxy provider for ServiceE. The class must be explicitly decorated with the @InjectableProxy()". 1) How do I get around the fact that I cannot add the @InjectableProxy attribute to an external module (npm package)? 2) Where in this hierarchy would the @InjectableProxy attribute(s) go? If I put one on ServiceC then it seems that every dependency of ServiceC , and on down the dependency chain, has to have the @InjectableProxy attribute on it

Papooch commented 1 year ago

I think you misunderstood a bit. Maybe I need to clarify the readme, but you only put @InjectableProxy on your classes that you want to be request-scoped (i.e. get a new proxied-instance of them in each request, usually in order to store state in them). The whole point of them is that the scope does NOT bubble up as is the case of actual request-scoped providers. They can also inject other singleton providers no problem.

The only reason that the example in the docs:

@InjectableProxy()
export class AutoBootstrappingUser {
    id: number;
    role: string;

    constructor(@Inject(CLS_REQ) request: Request) {
        this.id = request.user.id;
        this.role = 'admin';
    }
}

uses @InjectableProxy, is because we store state inside, which needs to be different for each request, but the @Inject(CLS_REQ) request: Request is actually another Proxied singleton.

With your setup, you probably don't need @InjectableProxy() anywhere. Since the only thing that you need is the Request in your ServiceB, you can probably just replace @Inject(REQUEST) with @Inject(CLS_REQ) and be good. Unless, of course, you store some state in ServiceB itself that needs to be different per request, then @InjectableProxy() over ServiceB solves that. Again, anything that injects ServiceB will get a singleton proxy instead, so it will stay singleton, too.


Important: Remember that if you wanted to provide your proxied version of a ServiceB, you'd need to register as

ClsModule.forFeatureAsync({
  useClass: ServiceB,
  import: [WhateverModuleThatProvidesServiceE]
})

and not providers: [ServiceB], since that would not trigger the "proxyfication" of it with ClsModule.

justinpenguin45 commented 1 year ago

Do I have to do "ClsModule.forFeature(RequestHelper)" in every module that uses my proxied class (RequestHelper)? It appears that I do, because when I dont then I get errors

Papooch commented 1 year ago

Yes, that is correct. The same applies to most other NestJS modules that have the forFeature functionality.

If you want to register it globally, you have two options - either create a global wrapper module in which you import (and export) ClsModule.forFeature, or alternatively, use the proxyProviders option in ClsModule.forRoot registration.