Fred Johnson
Fred Johnson

Reputation: 2695

What is the right way to pass request data to services in nestjs?

I have many services that all need to know the tenant ID from the request (kept in JWT auth token). The request is either GRPC (jwt stored in MetaData) or Graphql (jwt stored in context.headers.authorization).

I would like to be able to force myself not to forget to pass this tenant id when using the services. Ideally I dont want to even have to constantly write the same code to get the info from the request and pass it through. However the only ways I've managed to do it was using:

@Inject(REQUEST) for grpc in the service constructor. This doesn't work for the graphql requests. The only other way I saw was to only return service methods AFTER providing the data, which looks ugly as hell:

class MyService {
   private _actions: {
      myMethod1() { ... }
   }
   withTenantDetails(details) { 
       this._details = details;
       return this._actions;
   }
}

If I can somehow get the execution context within MyService that would be a good option, and make this easy using:

const getTenantId = (context: ExecutionContext) => {
  if (context.getType() === 'rpc') {
    logger.debug('received rpc request');
    const request = context.switchToRpc().getContext();
    const token = request.context.get("x-authorization");

    return {
        token,
        id: parseTokenTenantInfo(token)
    };
}
else if (context.getType<GqlContextType>() === 'graphql') {
    logger.debug('received graphql request');
    const gqlContext = GqlExecutionContext.create(context);
    const request = gqlContext.getContext().request;
    const token = request.get('Authorization');

    return {
        token,
        id: parseTokenTenantInfo(token)
    };
}
else {
    throw new Error(`Unknown context type receiving in tenant param decorator`)
}
}

But I can't find any way to get the executioncontext across to the service without also having to remember to pass it every time.

Upvotes: 2

Views: 1680

Answers (1)

Thierry Falvo
Thierry Falvo

Reputation: 6300

It's possible to inject Request into injectable service. For that, the Service will be Scope.Request, and no more Singleton, so a new instance will be created for each request. It's an important consideration, to avoid creating too many resources for performance reason.

It's possible to explicit this scope with :

@Injectable({ scope: Scope.REQUEST })

app.service.ts :

@Injectable({ scope: Scope.REQUEST })
export class AppService {
  tenantId: string;

  constructor(@Inject(REQUEST) private request: Request) {
    // because of @Inject(REQUEST),
    // this service becomes REQUEST SCOPED
    // and no more SINGLETON
    // so this will be executed for each request
    this.tenantId = getTenantIdFromRequest(this.request);
  }

  getData(): Data {
    // some logic here
    return {
      tenantId: this.tenantId,
      //...
    };
  }
}

// this is for example...
const getTenantIdFromRequest = (request: Request): string => {
  return request?.header('tenant_id');
};

Note that, instead of decode a JWT token in order to retrieve TENANT_ID for each request, and maybe for other service (one per service), an other approach could be to decode JWT one single time, and then add it in Request object.

It could be done with a global Guard, same as authorization guard examples of official docs.

Here just a simple example : (could be merged with a Auth Guard)

@Injectable()
export class TenantIdGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    request['tenantId'] = getTenantIdFromRequest(request);

    return true; // or any other validation
  }
}

For GraphQL applications, we should inject CONTEXT in place of REQUEST :

constructor(@Inject(CONTEXT) private context) {}

You have to set either request inside context, or directly TENANT_ID inside context in order to retrieve it after inside service.

Upvotes: 2

Related Questions