Conditionally instantiate service class as a provider in NestJS

I have a controller called User and two service classes: UserAdminService and UserSuperAdminService. When a user makes a request to any endpoint of the User controller, I want to check if the user making the request is an Admin or a Super Admin (based on the roles in the token) and instantiate the correct service (UserAdminService or UserSuperAdminService). Note that the two services implement the same UserService interface (just the internals of the methods that change a bit). How can I make this with NestJS?

What I tried:

user.module.ts

providers: [
    {
      provide: "UserService",
      inject: [REQUEST],
      useFactory: (request: Request) => UserServiceFactory(request)
    }
  ],

user-service.factory.ts

export function UserServiceFactory(request: Request) {

    const { realm_access } = JwtService.parseJwt(
        request.headers["authorization"].split(' ')[1]
    );
    if (realm_access["roles"].includes(RolesEnum.SuperAdmin))
        return UserSuperAdminService;
    else
        return UserAdminService;
}

user.controller.ts

  constructor(
      @Inject("UserService") private readonly userService: UserServiceInterface
  ) {}

One of the reasons my code is not working is because I am returning the classes and not the instantiated objects from the factory, but I want NestJS to resolve the services dependencies. Any ideas?

Upvotes: 2

Views: 2930

Answers (2)

Jay McDoniel
Jay McDoniel

Reputation: 70490

Rather than passing back the class to instantiate, which Nest doesn't handle, you could add the UserSuperAdminService and UserAdminService to the inject array, and pass back the instance that Nest then would create per request.

providers: [
    {
      provide: "UserService",
      inject: [REQUEST, UserSuperAdminService, UserAdminService],
      useFactory: (request: Request, superAdminService: UserSuperAdminService, adminService: UserAdminService) => UserServiceFactory(request, superAdminService, adminService)
    }
...
]

export function UserServiceFactory(request: Request, superAdminService: UserSuperAdminService, adminService: UserAdminService) {

    const { realm_access } = JwtService.parseJwt(
        request.headers["authorization"].split(' ')[1]
    );
    if (realm_access["roles"].includes(RolesEnum.SuperAdmin))
        return superAdminService;
    else
        return adminService;
}

Upvotes: 6

mh377
mh377

Reputation: 1856

Instead of trying to conditionally instantiate a service class you could create a global middleware to redirect the request to the appropriate controller e.g.

import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class AdminUserMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
  
    const { realm_access } = JwtService.parseJwt(
        req.headers["authorization"].split(' ')[1]
    );
    
    if (realm_access["roles"].includes(RolesEnum.SuperAdmin)) {
        req.url = req.url.replace(/^\/, '/super-admin/');
       }

    next();
  }
}

Then you can apply it to all routes in your app.module.ts

@Module({
  imports: [HttpModule],
  controllers: [UserAdminController, UserSuperAdminController]
  providers: [UserSuperAdminService, UserAdminService]
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AdminUserMiddleware)
      .forRoutes('/');
  }
}

and have the following controlers:

@Controller('/')
export class UserAdminController {
  private readonly logger: Logger = new Logger(UserAdminController.name);

  constructor(private readonly userAdminService: UserAdminService) {}

@Controller('/super-admin')
export class UserSuperAdminController {
  private readonly logger: Logger = new Logger(UserSuperAdminController.name);

  constructor(private readonly userSuperAdminService: UserSuperAdminService) {}
  
}

See the NestJS docs and this post for further details

Upvotes: 0

Related Questions