【问题标题】:Create scoped module with providers for module feature使用模块功能的提供者创建范围模块
【发布时间】:2019-07-03 18:16:50
【问题描述】:

当我多次使用导入 PolicyModule.forFeature 时,PolicyModule 的下一次导入会覆盖 PolicyStorage 中的门。
当我通过调用PolicyProvider 尝试在CandidateModuleCandidateEducationService 中使用PolicyProvider

await this.policy.denyAccessUnlessGranted('canDelete', education);

我得到了异常Gate by entity 'CandidateEducationEntity' not found

我在CandidateEducationService 中输出PolicyStorage 并得到JobPolicy 的数组门

PolicyStorage {
  gates:
    [ { policy: [Function: JobPolicy], entity: [Function: JobEntity] } ]
}

但我期待

PolicyStorage {
  gates:
    [ { policy: [Function: CandidateEducationPolicy], entity: [Function: CandidateEducationEntity] } ]
}

我创建了一个动态模块PolicyModule

@Module({})
export class PolicyModule {
    public static forFeature(gates: PolicyGate[]): DynamicModule {
        const providers: Provider[] = [
            ...gates.map(gate => gate.policy),
            {
                provide: PolicyStorage,
                useValue: new PolicyStorage(gates),
            },
            PolicyProvider,
        ];

        return {
            module: PolicyModule,
            imports: [
                CommonModule,
            ],
            providers,
            exports: providers,
        };
    }
}

PolicyStorage

@Injectable()
export class PolicyStorage {
    constructor(private gates: PolicyGate[]) {
        console.log(this.gates);
    }

    public find(name: string): PolicyGate | null {
        return this.gates.find(policy => policy.entity.name === name);
    }
}

PolicyProvider

@Injectable()
export class PolicyProvider<E, P> {
    constructor(
        private readonly moduleRef: ModuleRef,
        private readonly gateStorage: PolicyStorage,
        private readonly appContext: AppContextService,
    ) {
    }

    public async denyAccessUnlessGranted(methodNames: MethodKeys<P>, entity: E, customData?: any) {
        if (await this.denies(methodNames, entity, customData)) {
            throw new ForbiddenException();
        }
    }

    public async allowAccessIfGranted(methodNames: MethodKeys<P>, entity: E, customData?: any) {
        const allowed = await this.allows(methodNames, entity, customData);
        if (!allowed) {
            throw new ForbiddenException();
        }
    }

    private async allows(methodNames: MethodKeys<P>, entity: E, customData?: any): Promise<boolean> {
        const results = await this.getPolicyResults(methodNames, entity, customData);

        return results.every(res => res === true);
    }

    private async denies(methodNames: MethodKeys<P>, entity: E, customData?: any): Promise<boolean> {
        const results = await this.getPolicyResults(methodNames, entity, customData);

        return results.every(res => res === false);
    }

    private async getPolicyResults(methodNames: MethodKeys<P>, entity: E, customData?: any): Promise<boolean[]> {
        const methodNamesArray = Array.isArray(methodNames) ? methodNames : [methodNames];
        const gate = this.findByClassName(entity.constructor.name);
        const user = this.appContext.get('user');
        const policy = await this.moduleRef.get<P>(gate.policy, {strict: false});
        const results = [];

        for (const methodName of methodNamesArray) {
            results.push(!!await policy[methodName as string](entity, user, customData));
        }

        return results;
    }

    private findByClassName(name: string) {
        const gate = this.gateStorage.find(name);

        if (!gate) {
            throw new RuntimeException(`Gate by entity '${name}' not found`);
        }

        return gate;
    }
}

在其他模块中使用模块。示例:
JobsModule

@Module({
    imports: [
        TypeOrmModule.forFeature(
            [
                JobEntity,
            ],
        ),
        PolicyModule.forFeature([
            {
                policy: JobPolicy,
                entity: JobEntity,
            },
        ]),
    ],
    controllers: [
        ManagerJobsController,
    ],
    providers: [
        ManagerJobsService,
    ],
})
export class JobsModule {
}

CandidateModule

@Module({
    imports: [
        TypeOrmModule.forFeature(
            [
                CandidateEducationEntity,
            ],
        ),
        PolicyModule.forFeature([
            {
                policy: CandidateEducationPolicy,
                entity: CandidateEducationEntity,
            },
        ]),
    ],
    controllers: [
        CandidateEducationController,
    ],
    providers: [
        CandidateEducationService,
    ],
})
export class CandidateModule {
}

【问题讨论】:

标签: javascript node.js typescript dependency-injection nestjs


【解决方案1】:

更新:

Nest v6 引入了请求范围的提供程序,请参阅 this answer


所有模块及其提供者都是单例的。如果您在同一个模块中使用同一个令牌两次注册一个提供程序,它将被覆盖。

如果您查看 TypeOrmModule,您可以在每个实体的唯一 custom token 下看到 registers its repository providers

export function getRepositoryToken(entity: Function) {
  if (
    entity.prototype instanceof Repository ||
    entity.prototype instanceof AbstractRepository
  ) {
    return getCustomRepositoryToken(entity);
  }
  return `${entity.name}Repository`;
}

因此,在您的情况下,您可以拥有 getPolicyProviderTokengetPolicyStorageToken 函数,并在每个导入模块唯一的这些令牌下注册和注入您的提供程序。

【讨论】:

  • 我认为所有服务都是单例它的反模式。糟糕,非常糟糕。
  • 是的,你需要使用@Inject 装饰器(或者创建你自己的)。 Nestjs 版本 6 将具有瞬态/请求绑定提供程序,因此您将有其他选择,而不仅仅是单例。
  • 所有提供者都是单例的,原因是nestjs在启动应用程序之前创建了DI图。我认为通过单例反模式制作所有提供者,但它对性能很有用。
  • 我创建了动态名称令牌门${entityName}Gates 并通过实体名称const gates = this.moduleRef.get&lt;PolicyGateInterface[]&gt;(entityGatesToken(name));PolicyProvider 中获取门。不需要使用装饰器Inject,但我对使用这种方式时的性能感兴趣。
  • 我预计它不会对您的性能产​​生明显影响,但当然您只有在测试时才会知道。不过我不会担心的
猜你喜欢
  • 1970-01-01
  • 2019-02-15
  • 2021-10-29
  • 2019-06-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多