nestjsx / nest-access-control

Role and Attribute based Access Control for Nestjs 🔐
MIT License
1.13k stars 78 forks source link

inject dependencies forRootAsync #17

Open anitricks opened 5 years ago

anitricks commented 5 years ago

I am using the forRootAsync method to load roles dynamically from the database however I am getting an error. I am a newbie to Nestjs, so please let me know if I am doing something wrong

Error: Nest can't resolve dependencies of the __roles_builder__ (?). Please make sure that the argument at index [0] is available in the AccessControlModule context.

I am trying to to import the AccessControlModule in the root module and I have a RolesService to inject as a dependency in the forRootAsync method. The code is below

AccessControlModule.forRootAsync({
      inject: [{
        provide: ROLE_REPO,
        useClass: Role
      }],
      useFactory: (service: repo) => {
        return new RolesBuilder([
          ...adminGrants // testing local file grants
        ]);
      }
    })
bashleigh commented 5 years ago

I think your inject token is incorrect? Try this

AccessControlModule.forRootAsync({
      inject: [ROLE_REPO],
      useFactory: (service: Role) => {
        return new RolesBuilder([
          ...adminGrants // testing local file grants
        ]);
      },
    }),

Let me know how that goes. I think that should be right? Inject takes an array of provider tokens? Not Providers themselves? Think the required type (without looking at the documentation/type itself) it's string, Symbol, Function, Provider Where Provider is Function, String, Symbol?

anitricks commented 5 years ago

@bashleigh i tried putting the token as the string but its throwing me error that it wants it this way: { provide, useClass } I need to inject a TypeORM entity or a RoleService to fetch the roles from the DB

anitricks commented 5 years ago

can someone provide an example of injecting a database connection into forRootAsync method?

bashleigh commented 5 years ago

@anitricks Sorry been mega busy! Could you provide an example application?

thamera commented 5 years ago

How is this async? The useFactory doesn't accept an async promise of rolesbuilder so I don't see how this can be served from a database which will always need to wait for an async promise. in the forRootAsync, would it work to change it to be something like the following?

static forRootAsync(options: {
        inject?: Provider[];
        useFactory: (...args: any) => Promise<RolesBuilder> | RolesBuilder;
    }): DynamicModule;
bashleigh commented 5 years ago

@thamera oh yea, that was stupid of me. Wanna make a PR for it with test? Really busy sorry.

thamera commented 5 years ago

Wish I could. I'm still learning a lot here and am not seeming to grasp some core elements here. I tried making the simple change directly in my local and it does work to be async but I still can't get this to load roles/grants from my db. I ended up moving my roles data calls into their own module (just a standard CRUD module using mongoose. I tried importing that module into my access control module where this loads with an import of my roles module. Trying to inject the roles service into the forrootasync but keep getting errors about resolving dependencies. I'm thinking the accesscontrolmodule loads too early in this setup to access the other module/service? Below is some of where I'm at if anyone has a suggestion:

Roles Module

@Module({
  imports: [
    MongooseModule.forFeature([{name: 'role', schema: RoleSchema}]),
    RolesModule
  ],
  controllers: [RolesController],
  providers: [RolesService],
  exports: [
    RolesService,
    MongooseModule.forFeature([{name: 'role', schema: RoleSchema}])    
  ]
})
export class RolesModule {}

Roles Service:

@Injectable()
export class RolesService implements IRolesService {
    constructor(@InjectModel('role') private readonly roleModel: Model<IRole> ) {}

    async findAll(): Promise<IRole[]> {
        return await this.roleModel.find().exec();
    }
...

Access Control Module:

@Module({
  imports: [
    RolesModule,
    AccessControlModule.forRootAsync({
       inject: [RolesService],
       useFactory: async (service: RolesService) => {            //service: RolesService
           const roles = await service.findAll();
           let roleGrants = [];

           ... populate roleGrants array to roles/grants from db

           return new RolesBuilder([
               ...roleGrants
           ]);
       }
   })
  ],
  providers: [AccesscontrolService],
  exports: [
    //...AccesscontrolProviders
  ]
})
export class AccesscontrolModule {}
shajeeck commented 5 years ago

@thamera any updates over a solution or an exmaple?

creaux commented 4 years ago

I just needed something what works. I have realised that usually when you are using external module for db management it is worth of it to have also imports option.

What about something like this https://github.com/creaux/nest-access-control/blob/2974fe2d0cdef35d031f64023cf3f6b403b0f72c/src/access-control.module.ts#L54

bashleigh commented 3 years ago

Ok so! Firstly! Sorry it's taken me nearly 2 years to reply. I've had an absolutely mental year and finally taken some time off to come back to reality. I did a quick test #49 and discovered that for some reason I added an import property to the forRootAsync method which I thought was weird. But turns out, that's what fixes your issue!

Not entirely sure why a provider specified as a provider is not found but an imported module, I think (trying to remember some research I did nearly 2 years ago) nestjs creates the imported modules before creating the providers you've specified so a theAccessControlModule.forRootAsync static method would be called before your provider is created therefore, cannot find your specified provider! SO! I'd added an import property, which I guess was added to get around this issue! I added my module with the provider I wanted to the imports property and BOOM, it worked. This will solve your issue!

@Injectable()
    class TestProvider {
    }

    @Module({
      providers: [TestProvider],
      exports: [TestProvider],
    })
    class TestModule {

    }

  @Module({
      imports: [
        AccessControlModule.forRootAsync({
          imports: [TestModule],
          inject: [TestProvider],
          useFactory: (test: TestProvider): RolesBuilder => {
            return new RolesBuilder();
          },
        }),
      ],
    })
 export class ExampleModule {}

So! Given this example and your provided snippet, it should work like this

 @Module({
  imports: [
    AccessControlModule.forRootAsync({
       imports: [RolesModule], // <=============== notice change here! 
       inject: [RolesService],
       useFactory: async (service: RolesService) => {            //service: RolesService
           const roles = await service.findAll();
           let roleGrants = [];

           ... populate roleGrants array to roles/grants from db

           return new RolesBuilder([
               ...roleGrants
           ]);
       }
   })
  ],
  providers: [AccesscontrolService],
  exports: [
    //...AccesscontrolProviders
  ]
})
export class AccesscontrolModule {}

This then ensures your RoleModule is created and exports your injectable provider BEFORE your factory is called.

Now to travel back in time and give this info to you 2 years ago!

rxst commented 2 years ago

Hi, all! If I understand correctly at Nest.js we can't use providers in the forRootAsync function? Because when I try to import the provider that provides all my configs in forRootAsync. That provider can't be injected into the Module for usage. Maby somebody has a solution for this issue or any ideas on how to move around it?

cantoute commented 2 years ago

Hi I'm not sure... One thing to look at is what version of nestjs you have. Since v8 this package is no more needed. Documentation describes how to implement castle, perhaps you want to have a look at it.