1+ import type { DynamicModule , Provider as NestProvider } from '@nestjs/common' ;
2+ import { Module , ExecutionContext , ConfigurableModuleBuilder } from '@nestjs/common' ;
13import type {
2- DynamicModule ,
3- FactoryProvider as NestFactoryProvider ,
4- ValueProvider ,
5- ClassProvider ,
6- Provider as NestProvider ,
7- } from '@nestjs/common' ;
8- import { Module , ExecutionContext } from '@nestjs/common' ;
9- import type {
10- Client ,
114 Hook ,
125 Provider ,
136 EvaluationContext ,
@@ -22,69 +15,121 @@ import { APP_INTERCEPTOR } from '@nestjs/core';
2215import { EvaluationContextInterceptor } from './evaluation-context-interceptor' ;
2316import { ShutdownService } from './shutdown.service' ;
2417
18+ export const OPEN_FEATURE_INIT_TOKEN = 'OPEN_FEATURE_INIT' ;
19+
20+ export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN , OPTIONS_TYPE , ASYNC_OPTIONS_TYPE } =
21+ new ConfigurableModuleBuilder < OpenFeatureModuleOptions > ( )
22+ . setClassMethodName ( 'forRoot' )
23+ . setExtras < OpenFeatureModuleExtras > (
24+ { isGlobal : true , useGlobalInterceptor : true , domains : [ ] } ,
25+ ( definition , extras ) => ( {
26+ ...definition ,
27+ global : extras . isGlobal ,
28+ } ) ,
29+ )
30+ . build ( ) ;
31+
2532/**
2633 * OpenFeatureModule is a NestJS wrapper for OpenFeature Server-SDK.
2734 */
2835@Module ( { } )
29- export class OpenFeatureModule {
30- static forRoot ( { useGlobalInterceptor = true , ...options } : OpenFeatureModuleOptions ) : DynamicModule {
31- OpenFeature . setTransactionContextPropagator ( new AsyncLocalStorageTransactionContextPropagator ( ) ) ;
36+ export class OpenFeatureModule extends ConfigurableModuleClass {
37+ static forRoot ( options : typeof OPTIONS_TYPE ) : DynamicModule {
38+ const baseDynamicModule = super . forRoot ( options ) ;
39+ return this . buildModule ( baseDynamicModule , options ) ;
40+ }
3241
33- if ( options . logger ) {
34- OpenFeature . setLogger ( options . logger ) ;
35- }
42+ static forRootAsync ( options : typeof ASYNC_OPTIONS_TYPE ) : DynamicModule {
43+ const baseDynamicModule = super . forRootAsync ( options ) ;
44+ return this . buildModule ( baseDynamicModule , options ) ;
45+ }
3646
37- if ( options . hooks ) {
38- OpenFeature . addHooks ( ...options . hooks ) ;
39- }
47+ private static buildModule (
48+ baseDynamicModule : DynamicModule ,
49+ options : typeof ASYNC_OPTIONS_TYPE | typeof OPTIONS_TYPE ,
50+ ) : DynamicModule {
51+ const { useGlobalInterceptor, domains } = options ;
4052
41- options . handlers ?. forEach ( ( [ event , handler ] ) => {
42- OpenFeature . addHandler ( event , handler ) ;
43- } ) ;
53+ const providers : NestProvider [ ] = [
54+ ...( baseDynamicModule . providers || [ ] ) ,
55+ ShutdownService ,
56+ // Initialize OpenFeature when options become available,
57+ {
58+ provide : OPEN_FEATURE_INIT_TOKEN ,
59+ inject : [ MODULE_OPTIONS_TOKEN ] ,
60+ useFactory : async ( options : OpenFeatureModuleOptions ) => {
61+ OpenFeature . setTransactionContextPropagator ( new AsyncLocalStorageTransactionContextPropagator ( ) ) ;
62+
63+ if ( options . logger ) {
64+ OpenFeature . setLogger ( options . logger ) ;
65+ }
66+
67+ if ( options . hooks ) {
68+ OpenFeature . addHooks ( ...options . hooks ) ;
69+ }
70+
71+ options . handlers ?. forEach ( ( [ event , handler ] ) => {
72+ OpenFeature . addHandler ( event , handler ) ;
73+ } ) ;
4474
45- const clientValueProviders : NestFactoryProvider < Client > [ ] = [
75+ if ( options . defaultProvider ) {
76+ await OpenFeature . setProviderAndWait ( options . defaultProvider ) ;
77+ }
78+
79+ if ( options . providers ) {
80+ await Promise . all (
81+ Object . entries ( options . providers ) . map ( ( [ domain , provider ] ) =>
82+ OpenFeature . setProviderAndWait ( domain , provider ) ,
83+ ) ,
84+ ) ;
85+ }
86+
87+ return options ;
88+ } ,
89+ } ,
90+ // Default client
4691 {
4792 provide : getOpenFeatureClientToken ( ) ,
93+ inject : [ OPEN_FEATURE_INIT_TOKEN ] ,
4894 useFactory : ( ) => OpenFeature . getClient ( ) ,
4995 } ,
96+ // Context factory
97+ {
98+ provide : ContextFactoryToken ,
99+ inject : [ OPEN_FEATURE_INIT_TOKEN ] ,
100+ useFactory : ( options : OpenFeatureModuleOptions ) => options . contextFactory ,
101+ } ,
50102 ] ;
51103
52- if ( options ?. defaultProvider ) {
53- OpenFeature . setProvider ( options . defaultProvider ) ;
54- }
104+ // Domain-scoped clients from extras
105+ const domainClientTokens = domains ?. map ( ( domain : string ) => {
106+ const token = getOpenFeatureClientToken ( domain ) ;
55107
56- if ( options ?. providers ) {
57- Object . entries ( options . providers ) . forEach ( ( [ domain , provider ] ) => {
58- OpenFeature . setProvider ( domain , provider ) ;
59- clientValueProviders . push ( {
60- provide : getOpenFeatureClientToken ( domain ) ,
61- useFactory : ( ) => OpenFeature . getClient ( domain ) ,
62- } ) ;
108+ providers . push ( {
109+ provide : token ,
110+ useFactory : ( ) => OpenFeature . getClient ( domain ) ,
111+ inject : [ OPEN_FEATURE_INIT_TOKEN ] ,
63112 } ) ;
64- }
65-
66- const nestProviders : NestProvider [ ] = [ ShutdownService ] ;
67- nestProviders . push ( ...clientValueProviders ) ;
68113
69- const contextFactoryProvider : ValueProvider = {
70- provide : ContextFactoryToken ,
71- useValue : options ?. contextFactory ,
72- } ;
73- nestProviders . push ( contextFactoryProvider ) ;
114+ return token ;
115+ } ) ;
74116
75117 if ( useGlobalInterceptor ) {
76- const interceptorProvider : ClassProvider = {
118+ providers . push ( {
77119 provide : APP_INTERCEPTOR ,
78120 useClass : EvaluationContextInterceptor ,
79- } ;
80- nestProviders . push ( interceptorProvider ) ;
121+ } ) ;
81122 }
82123
83124 return {
84- global : true ,
85- module : OpenFeatureModule ,
86- providers : nestProviders ,
87- exports : [ ...clientValueProviders , ContextFactoryToken ] ,
125+ ...baseDynamicModule ,
126+ providers,
127+ exports : [
128+ ...( baseDynamicModule . exports || [ ] ) ,
129+ getOpenFeatureClientToken ( ) ,
130+ ContextFactoryToken ,
131+ ...( domainClientTokens || [ ] ) ,
132+ ] ,
88133 } ;
89134 }
90135}
@@ -132,6 +177,12 @@ export interface OpenFeatureModuleOptions {
132177 * @see {@link AsyncLocalStorageTransactionContextPropagator }
133178 */
134179 contextFactory ?: ContextFactory ;
180+ }
181+
182+ /**
183+ * Extra options available at module definition time
184+ */
185+ export interface OpenFeatureModuleExtras {
135186 /**
136187 * If set to false, the global {@link EvaluationContextInterceptor} is disabled.
137188 * This means that automatic propagation of the {@link EvaluationContext} created by the {@link this#contextFactory} is not working.
@@ -145,6 +196,16 @@ export interface OpenFeatureModuleOptions {
145196 * @default true
146197 */
147198 useGlobalInterceptor ?: boolean ;
199+ /**
200+ * Whether the module should be global.
201+ * @default true
202+ */
203+ isGlobal ?: boolean ;
204+ /**
205+ * Domains for which to create domain-scoped OpenFeature clients.
206+ * Each domain will get its own injectable client token via {@link getOpenFeatureClientToken}.
207+ */
208+ domains ?: string [ ] ;
148209}
149210
150211/**
0 commit comments