Reputation: 611
I am using NestJS as a backend framework in NodeJS +16
I am trying to implement: https://medium.com/@sascha.wolff/advanced-nestjs-how-to-have-access-to-the-current-user-in-every-service-without-request-scope-2586665741f
My idea is to have a @Injectable() service that will have, among other things, methods like:
hasUserSomeStuff(){
const user = UserStorage.get()
if(user) {
// do magic
}
and then pass this service around as it is usually done in NestJS
To avoid passing the request down the rabbit hole, or bubbling up the request scope so every dependency gets instantiated for each request, but also avoiding to use UserStorage everywhere where I need to get the user from the current request and do stuff
I've gone through the docs many times, it is my understanding that node would take care of instantiating a new storage for each async context (in my case each request), but what seems to happen to me is that when I first run my backend, it works just fine, I've got the user from the current request, but once the first async context / promise is completed, I retrieved data for the consumer, and in the next request UserStorage returns a undefined (as doc states it will if you are outside of the same async context, which I guess it is not what happens, as it should be a brand new async context)
However if I debug, what seems to happen is that this UserStorage is called and a new AsyncLocalStorage is instantiated at init, before the app is ready to be used, and then the very first request returns a undefined user.
I am failing to understand what is going on, can anyone help me on this, or any better approach to achieve my goal?
Thanks in advance
Upvotes: 3
Views: 1692
Reputation: 33
First we need to understand how AsyncLocalStorage works. You will be able to retrieve any information within the context that you explicitly execute from your AsyncLocalStorage instance.
for example:
//global scope
const context = new AsyncLocalStorage()
//with typescript
//const context = new AsyncLocalStorage<UserPayload>()
async function test() {
const userPayload = {
id: 1,
name: 'hiki9'
}
context.run(userPayload, () => whateverFunction())
}
async function whateverFunction() {
const unknownPayload = context.getStore();
//which is userPayload
}
Every function called after whateverFunction
will share the context.
Assuming you are using NestJS, here is a common usage.
//author-context.service.ts
export interface AuthorContext {
id: number
name: string
}
@Injectable()
export class AuthorUserContextService extends AsyncLocalStorage<AuthorContext>{}
//user.service.ts
@Injectable()
export class UserCreateService {
constructor(private readonly context: AuthorUserContextService){}
execute(dto: UserCreateDTO) {
const store: AuthorContext = this.context.getStore()
//const user = new User(store.whatever)
//await this.userRepository.save(user)
//inside userRepository, you can still `getStore`
}
}
Then you need to AsyncLocalStorage.run
at start of your execution thread. In the case you're doing some api with http, just bind it at begin of the Controller. In that case, using NestJS would be better configure a MiddlewareConsumer
.
//iam.module.ts
import { MiddlewareConsumer, Module } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Module({
providers: [
AuthorUserContextService,
UserCreateService,
]
})
export class IamModule{
constructor(
//whateverServiceThatYouNeed: WhateverServiceThatYouNeed
) {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req: Request, res: Response, next: NextFunction) => {
const userId = request.headers['user-internal-id'] //it will depends
const store = {
userId,
requestBody: request.body,
//...
}
this.context.run(store, () => next());
})
.forRoutes('*');
}
}
Upvotes: 1