Tsed: Get user in services

Created on 4 Dec 2020  路  20Comments  路  Source: tsedio/tsed

Hi, I'm using jwt to authenticate users. Everything is working fine and I can retrieve the user in controllers. Now I'd like to get user in services. I know I can pass in the user from controller, but it seems over complicated that I need to have an extra user parameter in each method. Can I inject user into service or store the user in some http context and then retrieve it in services? Is it something supported natively in tsed?

Thanks...

question released

All 20 comments

Might not be possible, if the service is application scoped and the controller is request scoped.

This is my way. What's your way ?

@Middleware()
export class LoginGuard implements IMiddleware {
  public async use(@Req() req: Req, @Context() ctx: Context) {
      const user = ...
      ctx.set('user', user);
  }
}

@Controller()
export class ApiController {
  @Get()
  @UseAuth(LoginGuard)
  self(@Context('user') user: User): User {
    return user;
  }
}

We use express-http-context

// import express-http-context;
this.app
  .use(httpContext.middleware)
  .use((req, _res, next) => {
    if (req.session) {
      httpContext.set('session', req.session);
    }
    next();

In service

// import express-http-context;
httpContext.get('session')

But it would be better to use solution like @bchoii

Hey @Romakita !
Have you looked into https://nodejs.org/api/async_hooks.html#async_hooks_class_asynclocalstorage ?
I guess it would be nice to have smth like ContextService, which we can call in any place of application to retrieve current request related data.
Especially, it will be useful for cases, when you want to isolate your request within DB-transaction in separate DB-connection.

The case looks like this:

  • your application get incoming request
  • connection to the database is taken from the connections pool and is associated with this incoming request
  • All your interactions with DB will be handled only in that connection which is associated with current incoming request. when you start DB-transaction, it will use that connection, which is associated with the current incoming request.

So any new incoming requests will be processed via a new DB-connection from the pool and won't affect any others currently handled incoming requests.

What do you think about this?

Hello @NachtRitter
The node.js AsyncHook API is interesting but I can't implement this without create a breaking change. Actually, Ts.ED works on Node.js v10 to v15 and this api is available since v12.

Currently, PlatformContext / Context is here for that, his only disadvantage is you have to forward the context by parameters. For me is not a disadvantage but I understand the point that is a pain point for you.

What is the cost of the performance? have you already experienced this API ?

See you
Romain

Hum after a quick reading, it seems to be relatively easily to implement this API without change the current code.

Example:

import {Injectable} from "@tsed/di";
import {OnRequest, OnResponse} from "@tsed/common";

const store: Map<string, any> = new Map();

const asyncHook = asyncHooks.createHook({
    init: (asyncId, _, triggerAsyncId) => {
        if (store.has(triggerAsyncId)) {
            store.set(asyncId, store.get(triggerAsyncId))
        }
    },
    destroy: (asyncId) => {
        if (store.has(asyncId)) {
            store.delete(asyncId);
        }
    }
});

asyncHook.enable();

@Injectable()
export class AsyncHookContext implement OnRequest, OnResponse { // OnRequest need latest version
   private store: Map<string, any> = new Map();

   protected $onRequest($ctx: PlatformRequest): void {
      // PlatformRequest contains all necessary stuff like requestId, DI container, request/response, etc...
      store.set(asyncHooks.executionAsyncId(), $ctx);
   }

  protected $onResponse($ctx: PlatformRequest): void {
     store.delete(asyncHooks.executionAsyncId());
  }

  get(): PlatformContext {
     return store.get(asyncHooks.executionAsyncId());
  }
}

Usage:

@Injectable()
class MyRepository {
   @Inject()
   protected asyncHookContext: AsyncHookContext;

   doSomething() {
       const $ctx = this.asyncHookContext.get()
       $ctx.logger({event: 'hello'});
   }
}

Can you try this code pls :)
Tell me if it's works, I'm interested by the result!

See you
Romain

@Romakita
Couple years ago I solved such case via domain API: https://nodejs.org/api/domain.html
However it worked totally fine only for callback-based codebase. With using of promises and async/await I faced sometimes with issue of missing domain during incoming request handling.

And now on another project I haven't implemented this yet. However, I made small investigation and found this new async_hooks and AsyncLocalStorage APIs.
Thanks for your spent time.
Maybe it will help someone to save time :)

@Romakita I have this same challenge / need and was lucky to find out this is already an open issue. I'm trying to implement it the way you mention but I'm confused where to put the first file or how to load it. I mean the file with AsyncHookContext

Just import the file in the server. Ts.ED will load the service automatically. I鈥檒l try to provide a new package with this code ASAP.

@Romakita some minimal issue with typings and iherited methods

Here, do you mean to use this.store?
Screen Shot 2021-02-16 at 8 38 11 AM

Screen Shot 2021-02-16 at 8 38 20 AM

Screen Shot 2021-02-16 at 8 37 27 AM

Ok the only problem is about the OnRequest Interface definition :). Thanks for your feedback.

We can create a new package @tsed/async-hook-context with this code (+ fix). It'll be a plugin in v6 and if the package works fine, we can integrate it into the common package!

@Romakita I still have a hard time figuring out when / how the $onRequest method of the AsyncHookContext class is fired.

AsyncContextProvider

import { Injectable } from '@tsed/di'
import { OnRequest, OnResponse, PlatformRequest, PlatformContext } from '@tsed/common'
import * as asyncHooks from 'async_hooks'

const store: Map<number, any> = new Map()

const asyncHook = asyncHooks.createHook({
  init: (asyncId, _, triggerAsyncId) => {
    if (store.has(triggerAsyncId)) {
      store.set(asyncId, store.get(triggerAsyncId))
    }
  },
  destroy: (asyncId) => {
    if (store.has(asyncId)) {
      store.delete(asyncId)
    }
  },
})

asyncHook.enable()

@Injectable()
export class AsyncHookContext implements OnRequest, OnResponse {
  /* eslint @typescript-eslint/ban-ts-comment: 0 */
  // @ts-ignore
  protected $onRequest($ctx: PlatformRequest): void {
    // PlatformRequest contains all necessary stuff like requestId, DI container, request/response, etc...
    store.set(asyncHooks.executionAsyncId(), $ctx)
  }

  public $onResponse(): void {
    store.delete(asyncHooks.executionAsyncId())
  }

  get(): PlatformContext {
    return store.get(asyncHooks.executionAsyncId())
  }
}

I imported in Server.ts like this:

import './providers/AsyncContextProvider'

However nothing seems to be happening, where I injected it, $ctx is undefined

I finally got it to work,

``` Server.ts
export class Server extends AsyncHookContext {

}
```

Log output of $ctx in the console after injecting in a custom service. Thank you guys

Screen Shot 2021-02-16 at 9 45 25 AM

Ok I understand now why it doesn't works XD.

@Romakita Another weird discovery, only works if Content-Type is set to application/json

@paschaldev You can review the PR here: https://github.com/TypedProject/tsed/pull/1246

It add a new package @tsed/async-hook-context :)

See you
Romain

Thanks @Romakita

:tada: This issue has been resolved in version 6.26.0 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

@paschaldev you can test the new package ;)

@Romakita Just tested it, works fine and much better than the manual setup I did earlier. We're good to go

Great :D

Was this page helpful?
0 / 5 - 0 ratings