1- import 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' ;
1+ import type { DynamicModule , Provider as NestProvider } from '@nestjs/common' ;
2+ import { Module , ExecutionContext , ConfigurableModuleBuilder } from '@nestjs/common' ;
93import type {
104 Client ,
115 Hook ,
@@ -22,69 +16,116 @@ import { APP_INTERCEPTOR } from '@nestjs/core';
2216import { EvaluationContextInterceptor } from './evaluation-context-interceptor' ;
2317import { ShutdownService } from './shutdown.service' ;
2418
19+ export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN , OPTIONS_TYPE , ASYNC_OPTIONS_TYPE } =
20+ new ConfigurableModuleBuilder < OpenFeatureModuleOptions > ( )
21+ . setClassMethodName ( 'forRoot' )
22+ . setExtras < OpenFeatureModuleExtras > (
23+ { isGlobal : true , useGlobalInterceptor : true , domains : [ ] } ,
24+ ( definition , extras ) => ( {
25+ ...definition ,
26+ global : extras . isGlobal ,
27+ } ) ,
28+ )
29+ . build ( ) ;
30+
2531/**
2632 * OpenFeatureModule is a NestJS wrapper for OpenFeature Server-SDK.
2733 */
2834@Module ( { } )
29- export class OpenFeatureModule {
30- static forRoot ( { useGlobalInterceptor = true , ...options } : OpenFeatureModuleOptions ) : DynamicModule {
31- OpenFeature . setTransactionContextPropagator ( new AsyncLocalStorageTransactionContextPropagator ( ) ) ;
35+ export class OpenFeatureModule extends ConfigurableModuleClass {
36+ static forRoot ( options : typeof OPTIONS_TYPE ) : DynamicModule {
37+ const baseDynamicModule = super . forRoot ( options ) ;
38+ return this . buildModule ( baseDynamicModule , options ) ;
39+ }
3240
33- if ( options . logger ) {
34- OpenFeature . setLogger ( options . logger ) ;
35- }
41+ static forRootAsync ( options : typeof ASYNC_OPTIONS_TYPE ) : DynamicModule {
42+ const baseDynamicModule = super . forRootAsync ( options ) ;
43+ return this . buildModule ( baseDynamicModule , options ) ;
44+ }
3645
37- if ( options . hooks ) {
38- OpenFeature . addHooks ( ...options . hooks ) ;
39- }
46+ private static buildModule ( baseDynamicModule : DynamicModule , extras : OpenFeatureModuleExtras = { } ) : DynamicModule {
47+ const { useGlobalInterceptor = true , domains = [ ] } = extras ;
4048
41- options . handlers ?. forEach ( ( [ event , handler ] ) => {
42- OpenFeature . addHandler ( event , handler ) ;
43- } ) ;
49+ const providers : NestProvider [ ] = [
50+ ...( baseDynamicModule . providers || [ ] ) ,
51+ ShutdownService ,
52+ // Initialize OpenFeature when options become available,
53+ {
54+ provide : 'OPEN_FEATURE_INIT' ,
55+ inject : [ MODULE_OPTIONS_TOKEN ] ,
56+ useFactory : async ( options : OpenFeatureModuleOptions ) => {
57+ OpenFeature . setTransactionContextPropagator ( new AsyncLocalStorageTransactionContextPropagator ( ) ) ;
4458
45- const clientValueProviders : NestFactoryProvider < Client > [ ] = [
59+ if ( options . logger ) {
60+ OpenFeature . setLogger ( options . logger ) ;
61+ }
62+
63+ if ( options . hooks ) {
64+ OpenFeature . addHooks ( ...options . hooks ) ;
65+ }
66+
67+ options . handlers ?. forEach ( ( [ event , handler ] ) => {
68+ OpenFeature . addHandler ( event , handler ) ;
69+ } ) ;
70+
71+ if ( options . defaultProvider ) {
72+ await OpenFeature . setProviderAndWait ( options . defaultProvider ) ;
73+ }
74+
75+ if ( options . providers ) {
76+ await Promise . all (
77+ Object . entries ( options . providers ) . map ( ( [ domain , provider ] ) =>
78+ OpenFeature . setProviderAndWait ( domain , provider ) ,
79+ ) ,
80+ ) ;
81+ }
82+
83+ return options ;
84+ } ,
85+ } ,
86+ // Default client
4687 {
4788 provide : getOpenFeatureClientToken ( ) ,
89+ inject : [ 'OPEN_FEATURE_INIT' ] ,
4890 useFactory : ( ) => OpenFeature . getClient ( ) ,
4991 } ,
92+ // Context factory
93+ {
94+ provide : ContextFactoryToken ,
95+ inject : [ 'OPEN_FEATURE_INIT' ] ,
96+ useFactory : ( options : OpenFeatureModuleOptions ) => options . contextFactory ,
97+ } ,
5098 ] ;
5199
52- if ( options ?. defaultProvider ) {
53- OpenFeature . setProvider ( options . defaultProvider ) ;
54- }
100+ // Domain-scoped clients from extras
101+ const domainClientTokens = domains . map ( ( domain : string ) => {
102+ const token = getOpenFeatureClientToken ( domain ) ;
55103
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- } ) ;
104+ providers . push ( {
105+ provide : token ,
106+ useFactory : ( ) => OpenFeature . getClient ( domain ) ,
107+ inject : [ 'OPEN_FEATURE_INIT' ] ,
63108 } ) ;
64- }
65109
66- const nestProviders : NestProvider [ ] = [ ShutdownService ] ;
67- nestProviders . push ( ...clientValueProviders ) ;
68-
69- const contextFactoryProvider : ValueProvider = {
70- provide : ContextFactoryToken ,
71- useValue : options ?. contextFactory ,
72- } ;
73- nestProviders . push ( contextFactoryProvider ) ;
110+ return token ;
111+ } ) ;
74112
75113 if ( useGlobalInterceptor ) {
76- const interceptorProvider : ClassProvider = {
114+ providers . push ( {
77115 provide : APP_INTERCEPTOR ,
78116 useClass : EvaluationContextInterceptor ,
79- } ;
80- nestProviders . push ( interceptorProvider ) ;
117+ } ) ;
81118 }
82119
83120 return {
84- global : true ,
85- module : OpenFeatureModule ,
86- providers : nestProviders ,
87- exports : [ ...clientValueProviders , ContextFactoryToken ] ,
121+ ...baseDynamicModule ,
122+ providers,
123+ exports : [
124+ ...( baseDynamicModule . exports || [ ] ) ,
125+ getOpenFeatureClientToken ( ) ,
126+ ContextFactoryToken ,
127+ ...domainClientTokens ,
128+ ] ,
88129 } ;
89130 }
90131}
@@ -132,6 +173,12 @@ export interface OpenFeatureModuleOptions {
132173 * @see {@link AsyncLocalStorageTransactionContextPropagator }
133174 */
134175 contextFactory ?: ContextFactory ;
176+ }
177+
178+ /**
179+ * Extra options available at module definition time
180+ */
181+ export interface OpenFeatureModuleExtras {
135182 /**
136183 * If set to false, the global {@link EvaluationContextInterceptor} is disabled.
137184 * This means that automatic propagation of the {@link EvaluationContext} created by the {@link this#contextFactory} is not working.
@@ -145,6 +192,16 @@ export interface OpenFeatureModuleOptions {
145192 * @default true
146193 */
147194 useGlobalInterceptor ?: boolean ;
195+ /**
196+ * Whether the module should be global.
197+ * @default true
198+ */
199+ isGlobal ?: boolean ;
200+ /**
201+ * Domains for which to create domain-scoped OpenFeature clients.
202+ * Each domain will get its own injectable client token via {@link getOpenFeatureClientToken}.
203+ */
204+ domains ?: string [ ] ;
148205}
149206
150207/**
0 commit comments