From aff1cb97bc5d268ac1f161583ec08e5923f576bf Mon Sep 17 00:00:00 2001 From: Philipp Busse Date: Mon, 7 Dec 2020 11:04:40 +0100 Subject: [PATCH] feat: add forRootAsync() --- e2e/src/app.module.ts | 15 ++- .../unleash-client.interfaces.ts | 18 +++ src/unleash-client/unleash-client.module.ts | 40 ++++++- .../unleash-strategies.interface.ts | 24 ++++ .../unleash-strategies.module.ts | 90 ++++++++++++--- .../unleash-strategies.service.ts | 18 ++- src/unleash/unleash.constants.ts | 1 + src/unleash/unleash.interfaces.ts | 12 +- src/unleash/unleash.module.ts | 106 ++++++++++++++---- 9 files changed, 280 insertions(+), 44 deletions(-) diff --git a/e2e/src/app.module.ts b/e2e/src/app.module.ts index dab55719..688b07a3 100644 --- a/e2e/src/app.module.ts +++ b/e2e/src/app.module.ts @@ -11,13 +11,24 @@ import { UsersService } from './users.service' // url: 'http://127.0.0.1:3000/unleash', url: 'https://unleash.herokuapp.com/api/client', appName: 'my-app-name', - instanceId: process.pid.toString(), + instanceId: 'my-unique-instance', //process.pid.toString(), refreshInterval: 20000, // metricsInterval: 3000, strategies: [MyCustomStrategy], }), + // UnleashModule.forRootAsync({ + // useFactory: () => ({ + // // url: 'http://127.0.0.1:3000/unleash', + // url: 'https://unleash.herokuapp.com/api/client', + // appName: 'my-app-name', + // instanceId: 'my-unique-instance', //process.pid.toString(), + // refreshInterval: 20000, + // // metricsInterval: 3000, + // strategies: [MyCustomStrategy], + // }), + // }), ], - providers: [UsersService], + providers: [UsersService, MyCustomStrategy], controllers: [AppController, UnleashMockController], }) export class ApplicationModule {} diff --git a/src/unleash-client/unleash-client.interfaces.ts b/src/unleash-client/unleash-client.interfaces.ts index ee1f0378..3f0bd09d 100644 --- a/src/unleash-client/unleash-client.interfaces.ts +++ b/src/unleash-client/unleash-client.interfaces.ts @@ -1,6 +1,24 @@ +import { ModuleMetadata, Type } from '@nestjs/common' + export interface UnleashClientModuleOptions { baseURL: string instanceId: string appName: string timeout: number } + +export interface UnleashClientModuleOptionsFactory { + createStrategiesOptions(): + | Promise + | UnleashClientModuleOptions +} + +export interface UnleashClientModuleAsyncOptions + extends Pick { + inject?: any[] + useExisting?: Type + useClass?: Type + useFactory?: ( + ...args: any[] + ) => Promise | UnleashClientModuleOptions +} diff --git a/src/unleash-client/unleash-client.module.ts b/src/unleash-client/unleash-client.module.ts index 7665dc09..85f8ad84 100644 --- a/src/unleash-client/unleash-client.module.ts +++ b/src/unleash-client/unleash-client.module.ts @@ -1,4 +1,4 @@ -import { DynamicModule, HttpModule, Module } from '@nestjs/common' +import { DynamicModule, HttpModule, Module, Provider } from '@nestjs/common' import { UnleashFeaturesClient, UnleashMetricsClient, @@ -7,7 +7,10 @@ import { import { UnleashStrategiesModule } from '..' import { UnleashClient } from './unleash-client' import { UNLEASH_CLIENT_OPTIONS } from './unleash-client.constants' -import { UnleashClientModuleOptions } from './unleash-client.interfaces' +import { + UnleashClientModuleAsyncOptions, + UnleashClientModuleOptions, +} from './unleash-client.interfaces' @Module({}) export class UnleashClientModule { @@ -25,14 +28,45 @@ export class UnleashClientModule { }), UnleashStrategiesModule, ], + providers: [{ provide: UNLEASH_CLIENT_OPTIONS, useValue: options }], + } + } + + public static registerAsync( + options: UnleashClientModuleAsyncOptions, + ): DynamicModule { + const provider: Provider = { + provide: UNLEASH_CLIENT_OPTIONS, + useFactory: options.useFactory!, + inject: options.inject, + } + return { + module: UnleashStrategiesModule, + imports: [ + ...(options.imports ?? []), + HttpModule.registerAsync({ + useFactory: (options: UnleashClientModuleOptions) => ({ + baseURL: options.baseURL, + headers: { + 'UNLEASH-INSTANCEID': options.instanceId, + 'UNLEASH-APPNAME': options.appName, + }, + timeout: options.timeout, + }), + inject: [UNLEASH_CLIENT_OPTIONS], + }), + ], providers: [ - { provide: UNLEASH_CLIENT_OPTIONS, useValue: options }, + provider, + UnleashStrategiesModule, UnleashClient, UnleashFeaturesClient, UnleashMetricsClient, UnleashRegisterClient, ], exports: [ + provider, + UnleashClient, UnleashFeaturesClient, UnleashMetricsClient, UnleashRegisterClient, diff --git a/src/unleash-strategies/unleash-strategies.interface.ts b/src/unleash-strategies/unleash-strategies.interface.ts index 79f0d199..f382edce 100644 --- a/src/unleash-strategies/unleash-strategies.interface.ts +++ b/src/unleash-strategies/unleash-strategies.interface.ts @@ -1,3 +1,6 @@ +import { ModuleMetadata, Provider, Type } from '@nestjs/common' +import { UnleashStrategy } from './strategy' + // https://github.com/SerayaEryn/fastify-session/blob/master/types/types.d.ts export interface FastifySession { sessionId: string @@ -27,3 +30,24 @@ export interface Context { appName?: string properties?: Properties } + +export interface UnleashStrategiesOptionsFactory { + createStrategiesOptions(): + | Promise + | UnleashStrategiesModuleOptions +} + +export interface UnleashStrategiesModuleOptions { + strategies: Type[] +} + +export interface UnleashStrategiesModuleAsyncOptions + extends Pick { + extraProviders?: Provider[] + inject?: any[] + useExisting?: Type + useClass?: Type + useFactory?: ( + ...args: any[] + ) => Promise | UnleashStrategiesModuleOptions +} diff --git a/src/unleash-strategies/unleash-strategies.module.ts b/src/unleash-strategies/unleash-strategies.module.ts index 62c3eaff..510b5f8d 100644 --- a/src/unleash-strategies/unleash-strategies.module.ts +++ b/src/unleash-strategies/unleash-strategies.module.ts @@ -1,11 +1,15 @@ -import { DynamicModule, Module, Type } from '@nestjs/common' +import { DynamicModule, Module, Provider } from '@nestjs/common' +import { + UnleashStrategiesModuleAsyncOptions, + UnleashStrategiesModuleOptions, + UnleashStrategiesOptionsFactory, +} from '.' import { ApplicationHostnameStrategy, DefaultStrategy, FlexibleRolloutStrategy, GradualRolloutSessionIdStrategy, RemoteAddressStrategy, - UnleashStrategy, UserWithIdStrategy, } from './strategy' import { GradualRolloutRandomStrategy } from './strategy/gradual-rollout-random' @@ -13,25 +17,83 @@ import { GradualRolloutUserIdStrategy } from './strategy/gradual-rollout-user-id import { CUSTOM_STRATEGIES } from './unleash-strategies.constants' import { UnleashStrategiesService } from './unleash-strategies.service' -@Module({}) +@Module({ + providers: [ + DefaultStrategy, + ApplicationHostnameStrategy, + FlexibleRolloutStrategy, + RemoteAddressStrategy, + UserWithIdStrategy, + GradualRolloutRandomStrategy, + GradualRolloutSessionIdStrategy, + GradualRolloutUserIdStrategy, + ], +}) export class UnleashStrategiesModule { - static register(strategies: Type[] = []): DynamicModule { + static register({ + strategies = [], + }: UnleashStrategiesModuleOptions): DynamicModule { return { module: UnleashStrategiesModule, providers: [ UnleashStrategiesService, - DefaultStrategy, - ApplicationHostnameStrategy, - FlexibleRolloutStrategy, - RemoteAddressStrategy, - UserWithIdStrategy, - GradualRolloutRandomStrategy, - GradualRolloutSessionIdStrategy, - GradualRolloutUserIdStrategy, - ...strategies, { provide: CUSTOM_STRATEGIES, useValue: strategies }, ], - exports: [UnleashStrategiesService], + exports: [ + UnleashStrategiesService, + { provide: CUSTOM_STRATEGIES, useValue: strategies }, + ], + } + } + + public static registerAsync( + options: UnleashStrategiesModuleAsyncOptions, + ): DynamicModule { + const providers = this.createStrategiesProviders(options) + return { + module: UnleashStrategiesModule, + imports: options.imports, + providers: [ + ...(options.extraProviders ?? []), + ...providers, + UnleashStrategiesService, + ], + exports: [...providers, UnleashStrategiesService], + } + } + + static createStrategiesProviders( + options: UnleashStrategiesModuleAsyncOptions, + ): Provider[] { + if (options.useExisting || options.useFactory) { + return [this.createStrategiesOptionsProvider(options)] + } + + return [ + this.createStrategiesOptionsProvider(options), + { + provide: options.useClass!, + useClass: options.useClass!, + }, + ] + } + + private static createStrategiesOptionsProvider( + options: UnleashStrategiesModuleAsyncOptions, + ): Provider { + if (options.useFactory) { + return { + provide: CUSTOM_STRATEGIES, + useFactory: options.useFactory, + inject: options.inject, + } + } + + return { + provide: CUSTOM_STRATEGIES, + useFactory: async (optionsFactory: UnleashStrategiesOptionsFactory) => + await optionsFactory.createStrategiesOptions(), + inject: [options.useExisting || options.useClass!], } } } diff --git a/src/unleash-strategies/unleash-strategies.service.ts b/src/unleash-strategies/unleash-strategies.service.ts index d1d1bf3d..93e06526 100644 --- a/src/unleash-strategies/unleash-strategies.service.ts +++ b/src/unleash-strategies/unleash-strategies.service.ts @@ -1,5 +1,6 @@ -import { Inject, Injectable, Type } from '@nestjs/common' +import { Inject, Injectable, OnModuleInit } from '@nestjs/common' import { ModuleRef } from '@nestjs/core' +import { UnleashStrategiesModuleOptions } from '.' import { ApplicationHostnameStrategy, DefaultStrategy, @@ -14,7 +15,7 @@ import { GradualRolloutUserIdStrategy } from './strategy/gradual-rollout-user-id import { CUSTOM_STRATEGIES } from './unleash-strategies.constants' @Injectable() -export class UnleashStrategiesService { +export class UnleashStrategiesService implements OnModuleInit { private strategies: UnleashStrategy[] constructor( @@ -26,8 +27,9 @@ export class UnleashStrategiesService { private readonly gradualRolloutRandom: GradualRolloutRandomStrategy, private readonly gradualRolloutUserId: GradualRolloutUserIdStrategy, private readonly gradualRolloutSessionId: GradualRolloutSessionIdStrategy, - @Inject(CUSTOM_STRATEGIES) strategies: Type[] = [], - module: ModuleRef, + @Inject(CUSTOM_STRATEGIES) + private readonly options: UnleashStrategiesModuleOptions, + private readonly moduleRef: ModuleRef, ) { this.strategies = [ userWithId, @@ -38,10 +40,16 @@ export class UnleashStrategiesService { gradualRolloutRandom, gradualRolloutUserId, gradualRolloutSessionId, - ...strategies.map((strategy) => module.get(strategy)), + // ...strategies.map((strategy) => module.get(strategy)), ] } + async onModuleInit(): Promise { + for (const customStrategy of this.options.strategies) { + this.strategies.push(await this.moduleRef.create(customStrategy)) + } + } + findAll(): UnleashStrategy[] { return this.strategies } diff --git a/src/unleash/unleash.constants.ts b/src/unleash/unleash.constants.ts index cb280c8d..f70151d1 100644 --- a/src/unleash/unleash.constants.ts +++ b/src/unleash/unleash.constants.ts @@ -1,3 +1,4 @@ export const REFRESH_INTERVAL = 'REFRESH_INTERVAL' export const METRICS_INTERVAL = 'METRICS_INTERVAL' export const APP_NAME = 'APP_NAME' +export const UNLEASH_MODULE_OPTIONS = 'UNLEASH_MODULE_OPTIONS' diff --git a/src/unleash/unleash.interfaces.ts b/src/unleash/unleash.interfaces.ts index c99e3257..b5ddf34c 100644 --- a/src/unleash/unleash.interfaces.ts +++ b/src/unleash/unleash.interfaces.ts @@ -1,4 +1,4 @@ -import { Type } from '@nestjs/common' +import { ModuleMetadata, Provider, Type } from '@nestjs/common' import { UnleashStrategy } from '../unleash-strategies' export interface UnleashModuleOptions { @@ -45,3 +45,13 @@ export interface UnleashModuleOptions { */ strategies?: Type[] } + +export interface UnleashModuleAsyncOptions + extends Pick { + global?: boolean + extraProviders?: Provider[] + inject?: any[] + useFactory?: ( + ...args: any[] + ) => Promise | UnleashModuleOptions +} diff --git a/src/unleash/unleash.module.ts b/src/unleash/unleash.module.ts index 2bf7087a..2253f804 100644 --- a/src/unleash/unleash.module.ts +++ b/src/unleash/unleash.module.ts @@ -5,21 +5,27 @@ import { Module, OnModuleInit, } from '@nestjs/common' +import { ModuleRef } from '@nestjs/core' import { ScheduleModule } from '@nestjs/schedule' import { - METRICS_INTERVAL, - REFRESH_INTERVAL, UnleashContext, + UnleashModuleAsyncOptions, UnleashModuleOptions, } from '.' import { UnleashClientModule, UnleashRegisterClient } from '../unleash-client' import { UnleashStrategiesModule, + UnleashStrategiesModuleOptions, UnleashStrategiesService, } from '../unleash-strategies' import { MetricsService } from './metrics.service' import { MetricsRepository } from './repository/metrics-repository' import { ToggleRepository } from './repository/toggle-repository' +import { + METRICS_INTERVAL, + REFRESH_INTERVAL, + UNLEASH_MODULE_OPTIONS, +} from './unleash.constants' import { UnleashService } from './unleash.service' import { MetricsUpdaterService } from './updaters/metrics-updater.service' import { TogglesUpdaterService } from './updaters/toggles-updater.service' @@ -51,34 +57,49 @@ export class UnleashModule implements OnModuleInit { private readonly strategies: UnleashStrategiesService, ) {} - onModuleInit(): void { - void this.togglesUpdater.start() + async onModuleInit(): Promise { + await this.togglesUpdater.start() - void this.registerClient - .register( + try { + await this.registerClient.register( this.metricsInterval, this.strategies.findAll().map((strategy) => strategy.name), ) - .then(() => this.metricsUpdater.start()) - .catch((error) => { - this.logger.error(error) - }) + await this.metricsUpdater.start() + } catch (error) { + this.logger.error(error) + } } + // eslint-disable-next-line complexity static forRoot(options: UnleashModuleOptions): DynamicModule { + const strategiesModule = UnleashStrategiesModule.registerAsync({ + useFactory: ( + options: UnleashModuleOptions, + ): UnleashStrategiesModuleOptions => ({ + strategies: options.strategies ?? [], + }), + inject: [UNLEASH_MODULE_OPTIONS], + }) + const clientModule = UnleashClientModule.registerAsync({ + useFactory: (options: UnleashModuleOptions) => ({ + baseURL: options.url, + appName: options.appName, + instanceId: options.instanceId, + timeout: options.timeout || DEFAULT_TIMEOUT, + }), + inject: [UNLEASH_MODULE_OPTIONS], + }) return { global: options?.global ?? true, module: UnleashModule, - imports: [ - UnleashClientModule.register({ - baseURL: options.url, - appName: options.appName, - instanceId: options.instanceId, - timeout: options.timeout || DEFAULT_TIMEOUT, - }), - UnleashStrategiesModule.register(options.strategies), - ], + imports: [strategiesModule, clientModule], + exports: [clientModule, strategiesModule, UNLEASH_MODULE_OPTIONS], providers: [ + { + provide: UNLEASH_MODULE_OPTIONS, + useValue: options, + }, { provide: REFRESH_INTERVAL, useValue: options.refreshInterval ?? DEFAULT_INTERVAL, @@ -90,4 +111,51 @@ export class UnleashModule implements OnModuleInit { ], } } + + // eslint-disable-next-line complexity + static forRootAsync(options: UnleashModuleAsyncOptions): DynamicModule { + const strategiesModule = UnleashStrategiesModule.registerAsync({ + // extraProviders: options.strategies, + useFactory: ( + options: UnleashModuleOptions, + ): UnleashStrategiesModuleOptions => ({ + strategies: options.strategies ?? [], + }), + inject: [UNLEASH_MODULE_OPTIONS, ModuleRef], + }) + const clientModule = UnleashClientModule.registerAsync({ + useFactory: (options: UnleashModuleOptions) => ({ + baseURL: options.url, + appName: options.appName, + instanceId: options.instanceId, + timeout: options.timeout || DEFAULT_TIMEOUT, + }), + inject: [UNLEASH_MODULE_OPTIONS], + }) + return { + global: options?.global ?? true, + module: UnleashModule, + imports: [strategiesModule, clientModule], + exports: [clientModule, strategiesModule, UNLEASH_MODULE_OPTIONS], + providers: [ + { + provide: UNLEASH_MODULE_OPTIONS, + useFactory: options.useFactory!, + inject: options.inject, + }, + { + provide: REFRESH_INTERVAL, + useFactory: (options: UnleashModuleOptions) => + options.refreshInterval ?? DEFAULT_INTERVAL, + inject: [UNLEASH_MODULE_OPTIONS], + }, + { + provide: METRICS_INTERVAL, + useFactory: (options: UnleashModuleOptions) => + options.metricsInterval ?? DEFAULT_INTERVAL, + inject: [UNLEASH_MODULE_OPTIONS], + }, + ], + } + } }