Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to access current http context inside component method #173

Closed
darioxtx opened this issue Sep 29, 2017 · 43 comments
Closed

How to access current http context inside component method #173

darioxtx opened this issue Sep 29, 2017 · 43 comments

Comments

@darioxtx
Copy link

Hi,

How to access current http context inside component method?
As an example i would like to access current session user inside "typeorm" EventSubscriber class to automatically set user who inserted/update/deleted record.

Thanks for nice framework!

@kamilmysliwiec
Copy link
Member

Hi @darioxtx,
Can you say something more about what you wanna achieve? In most cases, you only have to pass the request object to the component method.

@darioxtx
Copy link
Author

darioxtx commented Oct 2, 2017

Hi @kamilmysliwiec,

Each request have user object inside and i want to access that data in component and use current user object to filter data from database based on logged in user ex.:

@Controller('users')
@UseGuards(RolesGuard)
export class UsersController {
    constructor(private readonly usersService: UsersService) { }

    @Get('/profile')
    @Roles(RolesEnum.Owner)
    profile() {
        return this.usersService.getCurrentUserProfile();
    }
}
@Component()
export class UsersService {

    constructor(@Inject('DbConnection') private dbConnection: Connection,
                       @Inject('CurrentContext') private context: CurrentContext){

    }

    async getCurrentUserProfile() {
        let repo = this.dbConnection.getRepository(Profile);
        let profile = await repo.find({
			userId: context.req.user.id
		});

        return profile;
    }  
}

An other place where I want to get user is EventSubscriber here i want to know witch user created or updated or deleted entity:

import {EventSubscriber, EntitySubscriberInterface} from "typeorm";

@EventSubscriber()
export class EverythingSubscriber implements EntitySubscriberInterface<any> {
   constructor(@Inject('CurrentContext') private context: CurrentContext){ }

    beforeInsert(event: InsertEvent<any>) {
        event.entity.createdBy = context.req.user.id || context.req.user.username;
    }
}

Thanks

@kamilmysliwiec
Copy link
Member

Hi @darioxtx,
To achieve some kind of 'request context' you can try to use this library https://www.npmjs.com/package/request-context
But referring to your issue, I'm afraid that it might be not such easy. The instances of the typeorm event subscribers are created in the core of this ORM. As far as I know, to set up the subscribers, you have to include classes (or directories where they are) inside the options object. It means that typeorm and nest would use different instances of this class. If there's a possibility to pass instances instead of types to typeorm, it'd much easier to make it work together.

@darioxtx
Copy link
Author

darioxtx commented Oct 2, 2017

Thank you for your answer. I will share my experience solving this issue.

@saskodh
Copy link

saskodh commented Oct 2, 2017

Hi @darioxtx,
you can also try using Zone.js as middleware and see how it goes.

@kamilmysliwiec, maybe Zone.js can be integrated in the framework? The zone will provide this request context if each request handler is run in a new zone. Then that can be injected in the components as RequestContext.

BTW, request-context is implemented on top of the Node.js Domain API which is deprecated for quite some time.

@darioxtx
Copy link
Author

darioxtx commented Oct 2, 2017

@saskodh thanks for sharing info. I will try Zone.js.

@cdiaz
Copy link

cdiaz commented Oct 4, 2017

@darioxtx did you find any solution?

@darioxtx
Copy link
Author

darioxtx commented Oct 4, 2017

@cdiaz yes my solution is to use Zone.js. it seems promising and is under angular team control.
But i faced issue using async/await with target ES2017. with target ES2016 works good.

I can share code later if anyone interested.

@cdiaz
Copy link

cdiaz commented Oct 4, 2017

Great, I'm interested to see your code to know how to implement CurrentUser context.
thanks

@darioxtx
Copy link
Author

darioxtx commented Oct 5, 2017

Hi,

Here is my Zone.js integration with nest.

first I import zone.js to server.ts like this

import 'zone.js';
import "zone.js/dist/zone-node.js";
import "zone.js/dist/long-stack-trace-zone.js";

Then I created NestMiddleware

import {Middleware, NestMiddleware} from '@nestjs/common';
import {RequestContext} from "../models/request-context";

@Middleware()
export class RequestContextMiddleware implements NestMiddleware {
    constructor() {}
    resolve() {
        return(req, res, next) => {
            var requestContext = new RequestContext(req, res);
            Zone.current.fork({
                name: RequestContext.name,
                properties: {
                    [RequestContext.name]: requestContext
                }
            }).fork(Zone['longStackTraceZoneSpec']).run(async () => {
                await next();
            } );
        }
    }
}

RequestContext looks like this

import {CurrentUser} from "../../modules/auth/models/current-user";

export class RequestContext {

    public readonly id: Number;
    public request: Request;
    public response: Response;

    constructor(request: Request, response: Response) {
        this.id = Math.random();
        this.request = request;
        this.response = response;
    }

    public static currentRequestContext(): RequestContext {
        return Zone.current.get(RequestContext.name);
    }

    public static currentRequest(): Request {
        let requestContext = RequestContext.currentRequestContext();

        if(requestContext) {
            return requestContext.request;
        }

        return null;
    }

    public static currentUser(): CurrentUser {
        let requestContext = RequestContext.currentRequestContext();

        if(requestContext) {
            return requestContext.request['user'];
        }

        return null;
    }
}

RequestContextMiddleware is used in main ApplicationModude

@Module({
    modules: [AuthModule, UsersModule, DatabaseModule]
})
export class ApplicationModule {
    configure(consumer: MiddlewaresConsumer) {
        consumer
            .apply(RequestContextMiddleware)
            .forRoutes({path: "*"})
    }
}

Current RequestContext can be accessed everywhere in your code

import {EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent} from "typeorm";
import {RequestContext} from "../../common/models/request-context";

@EventSubscriber()
export class EverythingSubscriber implements EntitySubscriberInterface<any> {

    beforeInsert(event: InsertEvent<any>) {
        let currentUser = RequestContext.currentUser();
        if(!currentUser) {
            throw new Error("Unknown user context");
        }

        event.entity.updatedBy = currentUser.username;
        event.entity.createdBy = currentUser.username;
        event.entity.createdOn = new Date();
        event.entity.updatedOn = new Date();

        console.log(`user: `, currentUser.username);
        console.log(`BEFORE ENTITY INSERTED: `, event.entity);
    }
}

I think that's it, also i like exception trace that provide Zone.js. It's really easy to find where your code crashed.

@saskodh thanks for right directions.

@jselesan
Copy link

jselesan commented Oct 5, 2017

Great work @darioxtx !

@JustGreg
Copy link

JustGreg commented Oct 7, 2017

Some weeks ago i tried zone.js in node to get the current user (and some other information). But zone.js often caused crashes when the app becomes complex.

Therefore i upgraded to node 8.4 and used cls-hooked.
The latest version of cls-hooked uses async_hooks API when run in Node >= 8.2.1

The implementation works in a very similar way.
There is also a type definition file for cls-hooked

@saskodh
Copy link

saskodh commented Oct 7, 2017

@JustGreg, this is excellent, thanks for sharing, I wasn't aware that this API was added in Node.js.
I've also used Zone.js experimentally in Node.js project and even though I didn't noticed any issues back then I still felt uncomfortable. Zone.js in order to preserve the context does monkey patching of all async APIs that are in the browser and in Node. That means something could very easily break in the next upgrade. I would definitely go with this API instead of Zone.js even though it's in experimental phase.

@kamilmysliwiec, this is excellent opportunity for adding new features that depend on the 'thread-local' storage like declarative transaction management. While it's in experimental phase Nest can leave the decision to the user whether he will enable this feature or not. What are your thoughts?

@kamilmysliwiec
Copy link
Member

Hi everyone,
A lot of great stuff, that's awesome 🎉

@saskodh since this API is still in experimental phase (stability: 1) I'm not gonna integrate it with nest in any way now. What I'm trying to do is to make sure that learning curve is not too big, so I see this feature as a 3rd-party module rather than the 'core thing'.

@JustGreg
Copy link

JustGreg commented Nov 5, 2017

Just for information:
There is also a node.js zone spec which is currently stage-0. Maybe it becomes a nativ node.js feature in the next release. http://node.green/#ESNEXT-strawman--stage-0--zones

Another npm package (based on typescript) which uses async_hookes is https://github.com/gms1/node-async-context

@donaldinho
Copy link

I'm trying to access a service from a typeorm EventSubscriber.
Is there a simpler way to access a service without using Zone.js. I don't need the Request Context. I just need to be able to send updates to a service. It's not immediately clear how I'd do this with zone.js

@Melse
Copy link

Melse commented Mar 3, 2018

@donaldinho
Maybe you can try for using the Execution Context?

Just export a app: INestApplication instance, then use const serviceInstance = app.select(SomeModule).get(SomeService) to get the service instance?

Is that what you need?

@v1d3rm3
Copy link

v1d3rm3 commented Jun 7, 2018

Can we just modify the target of tsconfig.json, to Zone.js work on Nest 5.x?

@basvdijk
Copy link

basvdijk commented Jul 7, 2018

@darioxtx I am trying to implement your solution (thanks for sharing!) However inside my controller let requestContext = RequestContext.currentRequest(); and let currentUser = RequestContext.currentUser(); stays null.

Inside the middleware I noticed that let requestContext = RequestContext.currentRequestContext(); gives null and:

 public static currentRequestContext(): RequestContext {
    console.log('zone name', RequestContext.name);
    console.log('zone get', Zone.current.get(RequestContext.name));
    return Zone.current.get(RequestContext.name);
  }

gives:

zone name RequestContext
zone get undefined

I don't have a server.ts only main.ts maybe this is the issue, since for what I understand server.ts belongs to microservices.

I am compiling using es2017

Any thoughts on what I might do wrong here?

@darioxtx
Copy link
Author

darioxtx commented Jul 8, 2018

Hi @basvdijk,
I don't use zone.js any more because it does not work with es2017 and all async/await features. So now I use cls-hooked library witch use node.js async hooks.

import { HttpException, HttpStatus } from "@nestjs/common";
import * as cls from 'cls-hooked';
import { IncomingMessage } from "http";
import { CurrentUser } from "../../modules/models/current-user.model";

export class RequestContext {

    public static nsid = 'some_random_guid';
    public readonly id: Number;
    public request: IncomingMessage;
    public response: Response;
    
    constructor(request: IncomingMessage, response: Response) {
        this.id = Math.random();
        this.request = request;
        this.response = response;
    }

    public static currentRequestContext(): RequestContext {
        const session = cls.getNamespace(RequestContext.nsid);
        if (session && session.active) {
            return session.get(RequestContext.name);
        }
        
        return null;
    }

    public static currentRequest(): IncomingMessage {
        let requestContext = RequestContext.currentRequestContext();

        if (requestContext) {
            return requestContext.request;
        }

        return null;
    }

    public static currentUser(throwError?: boolean): CurrentUser {
        let requestContext = RequestContext.currentRequestContext();

        if (requestContext) {
            const user: any = requestContext.request['user'];
            if (user) {
                return new CurrentUser(user);
            }
        }

        if (throwError) {
            throw new HttpException("Unauthorized", HttpStatus.UNAUTHORIZED);
        }

        return null;
    }
}

and Middleware looks like this

import { Injectable, NestMiddleware } from '@nestjs/common';
import * as cls from 'cls-hooked';
import { RequestContext } from "../models/request-context";


@Injectable()
export class RequestContextMiddleware implements NestMiddleware {

    resolve() {
        return (req, res, next) => {
            const requestContext = new RequestContext(req, res);
            const session = cls.getNamespace(RequestContext.nsid) || cls.createNamespace(RequestContext.nsid);

            session.run(async () => {
                session.set(RequestContext.name, requestContext);
                next();
            })
        }
    }
}

I hope this will help you :)

@basvdijk
Copy link

basvdijk commented Jul 9, 2018

@darioxtx Thanks I'll check it out. I was wondering if it smart to still use cls-hooked, since since the last commit was from 26 Jul 2017.

@basvdijk
Copy link

basvdijk commented Jul 9, 2018

@darioxtx Applied your solution to my project and works like a charm 👍

@heyplusyou
Copy link

This thread helped a lot, but after updating npm modules the solution with zone.js fell apart.

I found another solution working with pg, node > 10 and async/await:

node-request-context did the magic again with the following code:

Middleware function:

import {RequestContext} from "./request.context";
import { getNamespace, createNamespace } from 'node-request-context';
	
export function RequestContextMiddleware(req, res, next) {
   let rc = new RequestContext(req, res);
	
   const namespace = getNamespace('myapp.mynamespace') || createNamespace('myapp.mynamespace');
	
    namespace.run(() => { 
        namespace.set('tid', rc); 
      next(); 
    });
};

RequestContext

	import { AppUser } from "../../entities/appuser/appuser.entity";
	import { getNamespace } from 'node-request-context';
	
	export class RequestContext {
	
	    public readonly id: Number;
	    public request: Request;
	    public response: Response;
	
	    constructor(request: Request, response: Response) {
	        this.id = Math.random();
	        this.request = request;
	        this.response = response;
	    }
	
	    public static currentRequestContext(): RequestContext {
	        let namespace = getNamespace('myapp.mynamespace');
	        let rc = namespace.get('tid');
	        return rc;
	    }
	
	    public static currentRequest(): Request {
	        let requestContext = RequestContext.currentRequestContext();
	        return requestContext.request;
	    }
	
	    public static currentUser(): AppUser {
	        let requestContext = RequestContext.currentRequestContext();
	        return requestContext.request['user'];
	    }
	}

Middleware called globally

	import { NestFactory } from '@nestjs/core';
	import { ApplicationModule } from './app.module';
	import { RequestContextMiddleware } from './auth/zone/request.context.middleware';
	import { frontenddispatcher } from './frontend.middleware2';
	
	async function bootstrap() {
		const app = await NestFactory.create(ApplicationModule);
		app.use(RequestContextMiddleware, frontenddispatcher);
		await app.listen(3000);
	}
	bootstrap();

Usage in EventSubscriber

	import {EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent} from "typeorm";
	import { RequestContext } from "../auth/zone/request.context";
	
	@EventSubscriber()
	export class EverythingSubscriber implements EntitySubscriberInterface {
	
	    beforeInsert(event: InsertEvent<any>) {
	        let currentUser = RequestContext.currentUser();
	        console.log(JSON.stringify(currentUser))
	        if(!currentUser || !currentUser.email) {
	            throw new Error("Unknown user context");
	        }
	
	        event.entity.updatedBy = currentUser.email;
	        event.entity.createdBy = currentUser.email;
	        event.entity.createdOn = new Date();
	        event.entity.updatedOn = new Date();
	
	        console.log(`user: `, currentUser.email);
	        console.log(`BEFORE ENTITY INSERTED: `, event.entity);
	    }
	
	    beforeUpdate(event: UpdateEvent<any>) {
	        let currentUser = RequestContext.currentUser();
	        console.log(JSON.stringify(currentUser))
	        if(!currentUser || !currentUser.email) {
	            throw new Error("Unknown user context");
	        }
	
	        event.entity.updatedBy = currentUser.email;
	        event.entity.updatedOn = new Date();
	
	        console.log(`user: `, currentUser.email);
	        console.log(`BEFORE ENTITY UPDATED: `, event.entity);
	    }
	}

@zWaR
Copy link

zWaR commented Oct 16, 2018

@heyplusyou thanks for this example. Since node-request-context is based on async_hooks, which are at this moment in experimental phase, I was wondering if you are using this in production environment. How is the stability of this solution?

@heyplusyou
Copy link

@zWaR Not with a customer in production but internally within my company.

@yogevlahyani
Copy link

Is there any solution that's not based on async_hooks?

@kamilmysliwiec
Copy link
Member

Soon in the incoming Nest 6 release #1486

@yogevlahyani
Copy link

Thanks @kamilmysliwiec

@v1d3rm3

This comment has been minimized.

@nelsonec87
Copy link

This thread helped a lot, but after updating npm modules the solution with zone.js fell apart.

I found another solution working with pg, node > 10 and async/await:

node-request-context did the magic again with the following code:

Middleware function:

import {RequestContext} from "./request.context";
import { getNamespace, createNamespace } from 'node-request-context';
	
export function RequestContextMiddleware(req, res, next) {
   let rc = new RequestContext(req, res);
	
   const namespace = getNamespace('myapp.mynamespace') || createNamespace('myapp.mynamespace');
	
    namespace.run(() => { 
        namespace.set('tid', rc); 
      next(); 
    });
};

RequestContext

	import { AppUser } from "../../entities/appuser/appuser.entity";
	import { getNamespace } from 'node-request-context';
	
	export class RequestContext {
	
	    public readonly id: Number;
	    public request: Request;
	    public response: Response;
	
	    constructor(request: Request, response: Response) {
	        this.id = Math.random();
	        this.request = request;
	        this.response = response;
	    }
	
	    public static currentRequestContext(): RequestContext {
	        let namespace = getNamespace('myapp.mynamespace');
	        let rc = namespace.get('tid');
	        return rc;
	    }
	
	    public static currentRequest(): Request {
	        let requestContext = RequestContext.currentRequestContext();
	        return requestContext.request;
	    }
	
	    public static currentUser(): AppUser {
	        let requestContext = RequestContext.currentRequestContext();
	        return requestContext.request['user'];
	    }
	}

Middleware called globally

	import { NestFactory } from '@nestjs/core';
	import { ApplicationModule } from './app.module';
	import { RequestContextMiddleware } from './auth/zone/request.context.middleware';
	import { frontenddispatcher } from './frontend.middleware2';
	
	async function bootstrap() {
		const app = await NestFactory.create(ApplicationModule);
		app.use(RequestContextMiddleware, frontenddispatcher);
		await app.listen(3000);
	}
	bootstrap();

Usage in EventSubscriber

	import {EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent} from "typeorm";
	import { RequestContext } from "../auth/zone/request.context";
	
	@EventSubscriber()
	export class EverythingSubscriber implements EntitySubscriberInterface {
	
	    beforeInsert(event: InsertEvent<any>) {
	        let currentUser = RequestContext.currentUser();
	        console.log(JSON.stringify(currentUser))
	        if(!currentUser || !currentUser.email) {
	            throw new Error("Unknown user context");
	        }
	
	        event.entity.updatedBy = currentUser.email;
	        event.entity.createdBy = currentUser.email;
	        event.entity.createdOn = new Date();
	        event.entity.updatedOn = new Date();
	
	        console.log(`user: `, currentUser.email);
	        console.log(`BEFORE ENTITY INSERTED: `, event.entity);
	    }
	
	    beforeUpdate(event: UpdateEvent<any>) {
	        let currentUser = RequestContext.currentUser();
	        console.log(JSON.stringify(currentUser))
	        if(!currentUser || !currentUser.email) {
	            throw new Error("Unknown user context");
	        }
	
	        event.entity.updatedBy = currentUser.email;
	        event.entity.updatedOn = new Date();
	
	        console.log(`user: `, currentUser.email);
	        console.log(`BEFORE ENTITY UPDATED: `, event.entity);
	    }
	}

How would I do this in Nest 6 without async_hooks?

@adrien2p
Copy link

You should be able to achieve that by injecting the request as @Inject(REQUEST)

@yogevlahyani
Copy link

@adrien2p Can you provide a small example please?
I want to use Typeorm's Listener's to update createdBy when inserting a new entity and get the user from the request (req.user)

@adrien2p
Copy link

You just have to use the inject decorator to inject the request, for that you need to make your provider request scoped. from the injected request you only need to do this.request.user to access the current user on the request. But don’t forget to attach the user through a guard. I am out of the office and on the phone, hard to write an example ^^ for the request scoped i invite you to have a look on this link https://docs.nestjs.com/fundamentals/injection-scopes

@mambax
Copy link

mambax commented Jul 17, 2019

I am still stuck on this topic, can you help regarding this issue?
I "sold" NestJS to our company to use it but this stops us from boosting it like "This is the way we do!"
https://stackoverflow.com/questions/57070997/request-scoped-logger

Instead of using it for an ORM I need the current request everytime someone calls "logger.log"...

@kamilmysliwiec
Copy link
Member

@mambax you have to set your logger scope to Scope.Request in @Injectable() decorator. Otherwise, we won't be able to actually inject REQUEST because the instance will be created once (during the application bootstrap)

@ants88
Copy link

ants88 commented Jul 30, 2019

Hello! If I add @Injectable({ scope: Scope.REQUEST }) decorator to my EntitySubscriberInterface, the beforeInsert event is not fired.

@Injectable({ scope: Scope.REQUEST })
@EventSubscriber()
export class CustomEntitySubscriber implements EntitySubscriberInterface {

  constructor(
    @Inject(REQUEST) private readonly request: Request,
    @InjectConnection() readonly connection: Connection,
  ) {
    connection.subscribers.push(this);
  }

  beforeInsert(event: InsertEvent<any>) {
    // called before insert
    const a = this;
    if (event.entity.firstname) {
        event.entity.firstname = `*****${event.entity.firstname}`;
    }
    const req = this.request;
  }
}

Instead with this structure the event is fired, but I need request

@Injectable()
@EventSubscriber()
export class CustomEntitySubscriber implements EntitySubscriberInterface {

  constructor(
    @InjectConnection() readonly connection: Connection,
  ) {
    connection.subscribers.push(this);
  }

  beforeInsert(event: InsertEvent<any>) {
    // called before insert
    const a = this;
    if (event.entity.firstname) {
        event.entity.firstname = `*****${event.entity.firstname}`;
    }
  }
}

Any suggestion? Thank you

@davidpodhola
Copy link

I know this issue is closed, but I got here by Google Search and also the last comment is pretty recent (16 days ago).

@ants88 I do not have a suggestion, but what I am experiencing right now is: if service in the Subscriber (using the NestJS way, not the TypeORM way) dependency tree (so not only services directly used in the Subscriber's constructor, but also anywhere deeper) is marked with @Injectable({ scope: Scope.REQUEST }), the Subscriber's constructor is not called. There is no error or warning reported.

@kamilmysliwiec if I understand what you mention in your comment it almost looks like a bug to me. Should I open a new issue and add a test case for this?

Since I need this badly too, I will try the other options suggested in this thread and update you with my findings.

@megazoll
Copy link

megazoll commented Sep 4, 2019

@kamilmysliwiec what do you think about introducing RequestHolder service, which will return actual Request object? In this case we can use Request in SINGLETON-scoped services.
Symfony framework works in such way: https://symfony.com/blog/new-in-symfony-2-4-the-request-stack

@iliuyt
Copy link

iliuyt commented Oct 8, 2019

so........How to access current http context inside "typeorm" EventSubscriber class??????????

@davidpodhola
Copy link

Just FYI right now I am using https://github.com/jeff-lewis/cls-hooked to pass the context I need (like @CurrentUser() user or EntityManager) like in async find(user: AppUser) : Promise<Array<T>> { return run( user, getManager(), async () => await getManager().getRepository(this.getCtor()).find() ); }.

Would be interested to know how this can be done better.

@devendrainfotech
Copy link

Soon in the incoming Nest 6 release #1486

Is this feature released in nest 6 release. If yes can we have a example how to do get request context in typeorm event subscribe class

@joelcoronah
Copy link

Is it already integrated?

@kamilmysliwiec
Copy link
Member

@nestjs nestjs locked as resolved and limited conversation to collaborators Feb 10, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests