Nest: Pipes, interceptors (etc) priority

Created on 4 Apr 2018  路  3Comments  路  Source: nestjs/nest

I'm submitting a...


[ ] Regression 
[ ] Bug report
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior


Pipes are running in sequence 'globalMetadata', 'classMetadata', 'methodMetadata'.

Expected behavior


Pipes are running in reverse sequence from concrete to common - 'methodMetadata', 'classMetadata', 'globalMetadata'.

Minimal reproduction of the problem with instructions

What is the motivation / use case for changing the behavior?


For example, you have some global pipe, which transform request params to database entities

@Get(':id')
async getOneAction(@RequestEntity() bot: Bot): Promise<Bot> {
    return bot;
}

RequestEntity decorator returns some data for global pipe. Global pipe with injected database service returns entity. But now for certain actions you might have to set additional parameters to database query through another pipe:

@Get(':id')
async getOneAction(@RequestEntity({}, new OnlyActiveEntityPipe()) bot: Bot): Promise<Bot> {
    return bot;
}

but right now pipe OnlyActiveEntityPipe can be executed only after global pipe

Environment


Nest version: X.Y.Z


For Tooling issues:
- Node version: XX  
- Platform:  

Others:

Looks like right now there is no way to change priority for pipes (interceptors): they are running in sequence 'globalMetadata', 'classMetadata', 'methodMetatada'.
I think, feature for setting priority for certain pipes might be useful.

Also, the logic when pipes are running from concrete to common appears more straightforward (and more useful)

question 馃檶

Most helpful comment

@kamilmysliwiec I have an interceptor applied at the controller level that is doing a db lookup and attaching an entity to request.team if an id parameter is provided. I also have a custom decorator to extract the value in the controller method.

Now i am trying to implement a guard, at the method level, that would deny access if request.team.ownerId !== request.user.id but the guard is always executed before the interceptor. So request.team is always undefined.

Do you have any suggestions on this use case?

Edit: Looks like the passport module is attaching request.user using a guard. So would the suggestion be to use a guard and not an interceptor then use a custom deocrator for say IsOwner(true)
https://github.com/nestjs/passport/blob/master/lib/auth.guard.ts#L48

Edit: my edit above works so i am rolling with that. Still would like to know if that is the best practice. I am no longer using an interceptor and my guard looks like this

@Injectable()
export class TeamGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private readonly teamsService: TeamsService,
  ) {}

  async canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    if (request.params.id) {
      request.team = await this.teamsService.findOne({ id: request.params.id });
    }

    const owner = this.reflector.get<string[]>('owner', context.getHandler());
    if (owner && request.team.ownerId !== request.user.id) {
      return false;
    }
    return true;
  }
}

then i am using guard on the controller and i have a custom decorater for Owner(true).

All 3 comments

Hi @anyx,
Thanks for sharing your thoughts.

Also, the logic when pipes are running from concrete to common appears more straightforward (and more useful)

All features, including pipes, interceptors, guards, and exception filters are running from common to concrete due to keeping a consistency between them, making them cohesive, and intuitive when you think about the incoming request as a pipeline. Think about interceptors, they follow a simple request processing pipeline, being executed from global to concrete once the request wants to hit an end-handler, and then (in response pipeline), they are executed from concrete to global (if you attach some asynchronous/mapping logic inside them and so on). It looks the same with guards. You could have a global authorization guard, then (tied at class-level) roles guard, and afterward (tied at method-level) permission guard that checks a set of user permissions to validate whether the request should be handled or not. Let's think about pipes - same here we should start with simple DTO validation (using, let's say built-in ValidationPipe), then, at class-level, we could have UserByIdPipe that selects user by ID passed in DTO.

RequestEntity decorator returns some data for global pipe. Global pipe with injected database service returns entity. But now for certain actions you might have to set additional parameters to database query through another pipe:

In your case, you should rather use interceptor that makes use of Reflector. Through @ReflectMetadata() decorator, you should define a metadata to the particular handler which then you could reflect inside the interceptor, and based on picked value - run an appropriate database query. Afterward, attach the result to the request object and pull out this value using custom decorators feature. 馃檪

@kamilmysliwiec I have an interceptor applied at the controller level that is doing a db lookup and attaching an entity to request.team if an id parameter is provided. I also have a custom decorator to extract the value in the controller method.

Now i am trying to implement a guard, at the method level, that would deny access if request.team.ownerId !== request.user.id but the guard is always executed before the interceptor. So request.team is always undefined.

Do you have any suggestions on this use case?

Edit: Looks like the passport module is attaching request.user using a guard. So would the suggestion be to use a guard and not an interceptor then use a custom deocrator for say IsOwner(true)
https://github.com/nestjs/passport/blob/master/lib/auth.guard.ts#L48

Edit: my edit above works so i am rolling with that. Still would like to know if that is the best practice. I am no longer using an interceptor and my guard looks like this

@Injectable()
export class TeamGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private readonly teamsService: TeamsService,
  ) {}

  async canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    if (request.params.id) {
      request.team = await this.teamsService.findOne({ id: request.params.id });
    }

    const owner = this.reflector.get<string[]>('owner', context.getHandler());
    if (owner && request.team.ownerId !== request.user.id) {
      return false;
    }
    return true;
  }
}

then i am using guard on the controller and i have a custom decorater for Owner(true).

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mishelashala picture mishelashala  路  3Comments

2233322 picture 2233322  路  3Comments

JulianBiermann picture JulianBiermann  路  3Comments

tronginc picture tronginc  路  3Comments

VRspace4 picture VRspace4  路  3Comments