Skip to content

Commit 9abedf8

Browse files
committed
feat(core): introduce hooks for the Plugin API
BREAKING CHANGE: The experimental Plugin API has been changed
1 parent 206a5cc commit 9abedf8

16 files changed

+747
-214
lines changed

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from './lib/cls.options';
1010
export * from './lib/cls.service';
1111
export * from './lib/inject-cls.decorator';
1212
export * from './lib/plugin/cls-plugin.interface';
13+
export * from './lib/plugin/cls-plugin-base';
1314
export * from './lib/proxy-provider/injectable-proxy.decorator';
1415
export * from './lib/proxy-provider/proxy-provider.exceptions';
1516
export * from './lib/proxy-provider/proxy-provider.interfaces';

packages/core/src/lib/cls-initializers/cls.guard.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { ClsServiceManager } from '../cls-service-manager';
88
import { CLS_CTX, CLS_ID } from '../cls.constants';
99
import { CLS_GUARD_OPTIONS } from '../cls.internal-constants';
1010
import { ClsGuardOptions } from '../cls.options';
11+
import { ClsEnhancerInitContext } from '../plugin/cls-plugin.interface';
12+
import { ClsPluginsHooksHost } from '../plugin/cls-plugin-hooks-host';
1113
import { ContextClsStoreMap } from './utils/context-cls-store-map';
1214

1315
@Injectable()
@@ -16,20 +18,28 @@ export class ClsGuard implements CanActivate {
1618

1719
constructor(
1820
@Inject(CLS_GUARD_OPTIONS)
19-
options: Omit<ClsGuardOptions, 'mount'>,
21+
options: Omit<ClsGuardOptions, 'mount'> | undefined,
2022
) {
2123
this.options = { ...new ClsGuardOptions(), ...options };
2224
}
2325

2426
async canActivate(context: ExecutionContext): Promise<boolean> {
2527
const cls = ClsServiceManager.getClsService();
2628
const existingStore = ContextClsStoreMap.get(context);
29+
const pluginHooks = ClsPluginsHooksHost.getInstance();
2730
if (existingStore) {
2831
cls.enter({ ifNested: 'reuse' });
2932
} else {
3033
cls.enterWith({});
3134
ContextClsStoreMap.set(context, cls.get());
3235
}
36+
const pluginCtx: ClsEnhancerInitContext = {
37+
kind: 'guard',
38+
ctx: context,
39+
};
40+
if (this.options.initializePlugins) {
41+
await pluginHooks.beforeSetup(pluginCtx);
42+
}
3343
if (this.options.generateId) {
3444
const id = await this.options.idGenerator?.(context);
3545
cls.setIfUndefined<any>(CLS_ID, id);
@@ -41,7 +51,7 @@ export class ClsGuard implements CanActivate {
4151
await this.options.setup(cls, context);
4252
}
4353
if (this.options.initializePlugins) {
44-
await cls.initializePlugins();
54+
await pluginHooks.afterSetup(pluginCtx);
4555
}
4656
if (this.options.resolveProxyProviders) {
4757
await cls.resolveProxyProviders();

packages/core/src/lib/cls-initializers/cls.interceptor.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { ClsServiceManager } from '../cls-service-manager';
1010
import { CLS_CTX, CLS_ID } from '../cls.constants';
1111
import { CLS_INTERCEPTOR_OPTIONS } from '../cls.internal-constants';
1212
import { ClsInterceptorOptions } from '../cls.options';
13+
import { ClsEnhancerInitContext } from '../plugin/cls-plugin.interface';
14+
import { ClsPluginsHooksHost } from '../plugin/cls-plugin-hooks-host';
1315
import { ContextClsStoreMap } from './utils/context-cls-store-map';
1416

1517
@Injectable()
@@ -18,17 +20,25 @@ export class ClsInterceptor implements NestInterceptor {
1820

1921
constructor(
2022
@Inject(CLS_INTERCEPTOR_OPTIONS)
21-
options?: Omit<ClsInterceptorOptions, 'mount'>,
23+
options?: Omit<ClsInterceptorOptions, 'mount'> | undefined,
2224
) {
2325
this.options = { ...new ClsInterceptorOptions(), ...options };
2426
}
2527

2628
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
2729
const cls = ClsServiceManager.getClsService<any>();
2830
const clsStore = ContextClsStoreMap.get(context) ?? {};
31+
const pluginHooks = ClsPluginsHooksHost.getInstance();
2932
ContextClsStoreMap.set(context, clsStore);
3033
return new Observable((subscriber) => {
3134
cls.runWith(clsStore, async () => {
35+
const pluginCtx: ClsEnhancerInitContext = {
36+
kind: 'interceptor',
37+
ctx: context,
38+
};
39+
if (this.options.initializePlugins) {
40+
await pluginHooks.beforeSetup(pluginCtx);
41+
}
3242
if (this.options.generateId) {
3343
const id = await this.options.idGenerator?.(context);
3444
cls.setIfUndefined<any>(CLS_ID, id);
@@ -40,7 +50,7 @@ export class ClsInterceptor implements NestInterceptor {
4050
await this.options.setup(cls, context);
4151
}
4252
if (this.options.initializePlugins) {
43-
await cls.initializePlugins();
53+
await pluginHooks.afterSetup(pluginCtx);
4454
}
4555
if (this.options.resolveProxyProviders) {
4656
await cls.resolveProxyProviders();

packages/core/src/lib/cls-initializers/cls.middleware.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,37 @@
11
import { Inject, Injectable, NestMiddleware } from '@nestjs/common';
22
import { ClsServiceManager } from '../cls-service-manager';
33
import { CLS_ID, CLS_REQ, CLS_RES } from '../cls.constants';
4+
import { CLS_MIDDLEWARE_OPTIONS } from '../cls.internal-constants';
45
import { ClsMiddlewareOptions } from '../cls.options';
6+
import { ClsMiddlewareInitContext } from '../plugin/cls-plugin.interface';
7+
import { ClsPluginsHooksHost } from '../plugin/cls-plugin-hooks-host';
58
import { ContextClsStoreMap } from './utils/context-cls-store-map';
6-
import { CLS_MIDDLEWARE_OPTIONS } from '../cls.internal-constants';
79

810
@Injectable()
911
export class ClsMiddleware implements NestMiddleware {
1012
private readonly options: Omit<ClsMiddlewareOptions, 'mount'>;
1113

1214
constructor(
1315
@Inject(CLS_MIDDLEWARE_OPTIONS)
14-
options?: Omit<ClsMiddlewareOptions, 'mount'>,
16+
options: Omit<ClsMiddlewareOptions, 'mount'> | undefined,
1517
) {
1618
this.options = { ...new ClsMiddlewareOptions(), ...options };
1719
}
1820
use = async (req: any, res: any, next: (err?: any) => any) => {
1921
const cls = ClsServiceManager.getClsService();
22+
const pluginHooks = ClsPluginsHooksHost.getInstance();
2023
const callback = async () => {
2124
try {
25+
const pluginCtx: ClsMiddlewareInitContext = {
26+
kind: 'middleware',
27+
req,
28+
res,
29+
};
2230
this.options.useEnterWith && cls.enter();
2331
ContextClsStoreMap.setByRaw(req, cls.get());
32+
if (this.options.initializePlugins) {
33+
await pluginHooks.beforeSetup(pluginCtx);
34+
}
2435
if (this.options.generateId) {
2536
const id = await this.options.idGenerator?.(req);
2637
cls.setIfUndefined<any>(CLS_ID, id);
@@ -31,7 +42,7 @@ export class ClsMiddleware implements NestMiddleware {
3142
await this.options.setup(cls, req, res);
3243
}
3344
if (this.options.initializePlugins) {
34-
await cls.initializePlugins();
45+
await pluginHooks.afterSetup(pluginCtx);
3546
}
3647
if (this.options.resolveProxyProviders) {
3748
await cls.resolveProxyProviders();

packages/core/src/lib/cls-initializers/use-cls.decorator.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { copyMethodMetadata } from '../../utils/copy-method-metadata';
33
import { ClsServiceManager } from '../cls-service-manager';
44
import { CLS_ID } from '../cls.constants';
55
import { ClsDecoratorOptions } from '../cls.options';
6+
import { ClsDecoratorInitContext } from '../plugin/cls-plugin.interface';
7+
import { ClsPluginsHooksHost } from '../plugin/cls-plugin-hooks-host';
68

79
/**
810
* Wraps the decorated method in a CLS context.
@@ -42,24 +44,37 @@ export function UseCls<TArgs extends any[]>(
4244
`The @UseCls decorator can be only used on functions, but ${propertyKey.toString()} is not a function.`,
4345
);
4446
}
45-
descriptor.value = function (...args: TArgs) {
46-
return cls.run(options.runOptions ?? {}, async () => {
47-
if (options.generateId) {
48-
const id = await options.idGenerator?.apply(this, args);
49-
cls.set<string>(CLS_ID, id);
50-
}
51-
if (options.setup) {
52-
await options.setup.apply(this, [cls, ...args]);
53-
}
54-
if (options.initializePlugins) {
55-
await cls.initializePlugins();
56-
}
57-
if (options.resolveProxyProviders) {
58-
await cls.resolveProxyProviders();
59-
}
60-
return original.apply(this, args);
61-
});
62-
};
47+
descriptor.value = new Proxy(original, {
48+
apply: function (_, outerThis, args: TArgs[]) {
49+
const pluginHooks = ClsPluginsHooksHost.getInstance();
50+
return cls.run(options.runOptions ?? {}, async () => {
51+
const pluginCtx: ClsDecoratorInitContext = {
52+
kind: 'decorator',
53+
args,
54+
};
55+
if (options.initializePlugins) {
56+
await pluginHooks.beforeSetup(pluginCtx);
57+
}
58+
if (options.generateId) {
59+
const id = await options.idGenerator?.apply(
60+
outerThis,
61+
args,
62+
);
63+
cls.set<string>(CLS_ID, id);
64+
}
65+
if (options.setup) {
66+
await options.setup.apply(outerThis, [cls, ...args]);
67+
}
68+
if (options.initializePlugins) {
69+
await pluginHooks.afterSetup(pluginCtx);
70+
}
71+
if (options.resolveProxyProviders) {
72+
await cls.resolveProxyProviders();
73+
}
74+
return original.apply(outerThis, args);
75+
});
76+
},
77+
});
6378
copyMethodMetadata(original, descriptor.value);
6479
};
6580
}

packages/core/src/lib/cls-module/cls-root.module.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
HttpAdapterHost,
1818
ModuleRef,
1919
} from '@nestjs/core';
20+
import { isNonNullable } from '../../utils/is-non-nullable';
2021
import { ClsGuard } from '../cls-initializers/cls.guard';
2122
import { ClsInterceptor } from '../cls-initializers/cls.interceptor';
2223
import { ClsMiddleware } from '../cls-initializers/cls.middleware';
@@ -33,7 +34,7 @@ import {
3334
ClsModuleAsyncOptions,
3435
ClsModuleOptions,
3536
} from '../cls.options';
36-
import { ClsPluginManager } from '../plugin/cls-plugin-manager';
37+
import { ClsPluginsModule } from '../plugin/cls-plugins.module';
3738
import { ProxyProviderManager } from '../proxy-provider/proxy-provider-manager';
3839
import { ClsCommonModule } from './cls-common.module';
3940
import { getMiddlewareMountPoint } from './middleware.utils';
@@ -43,7 +44,8 @@ import { getMiddlewareMountPoint } from './middleware.utils';
4344
*/
4445
@Global()
4546
@Module({
46-
imports: [ClsCommonModule],
47+
imports: [ClsCommonModule, ClsPluginsModule.registerPluginHooks()],
48+
exports: [ClsPluginsModule],
4749
})
4850
export class ClsRootModule implements NestModule, OnModuleInit {
4951
private static logger = new Logger('ClsModule');
@@ -85,7 +87,7 @@ export class ClsRootModule implements NestModule, OnModuleInit {
8587

8688
return {
8789
module: ClsRootModule,
88-
imports: ClsPluginManager.registerPlugins(options.plugins),
90+
imports: [],
8991
providers: [
9092
{
9193
provide: CLS_MODULE_OPTIONS,
@@ -94,7 +96,10 @@ export class ClsRootModule implements NestModule, OnModuleInit {
9496
...providers,
9597
...proxyProviders,
9698
],
97-
exports: [...exports, ...proxyProviders.map((p) => p.provide)],
99+
exports: [
100+
...exports,
101+
...proxyProviders.map((p) => p.provide),
102+
].filter(isNonNullable),
98103
global: false,
99104
};
100105
}
@@ -112,10 +117,7 @@ export class ClsRootModule implements NestModule, OnModuleInit {
112117

113118
return {
114119
module: ClsRootModule,
115-
imports: [
116-
...(asyncOptions.imports ?? []),
117-
...ClsPluginManager.registerPlugins(asyncOptions.plugins),
118-
],
120+
imports: asyncOptions.imports ?? [],
119121
providers: [
120122
{
121123
provide: CLS_MODULE_OPTIONS,

packages/core/src/lib/cls-module/cls.module.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { DynamicModule, Module, Type } from '@nestjs/common';
22
import { ClsModuleAsyncOptions, ClsModuleOptions } from '../cls.options';
3-
import { ClsPluginManager } from '../plugin/cls-plugin-manager';
43

54
import { ClsPlugin } from '../plugin/cls-plugin.interface';
5+
import { ClsPluginsModule } from '../plugin/cls-plugins.module';
66
import { ProxyProviderManager } from '../proxy-provider/proxy-provider-manager';
77
import { ClsModuleProxyProviderOptions } from '../proxy-provider/proxy-provider.interfaces';
88
import { ClsCommonModule } from './cls-common.module';
@@ -24,7 +24,10 @@ export class ClsModule {
2424
static forRoot(options?: ClsModuleOptions): DynamicModule {
2525
return {
2626
module: ClsModule,
27-
imports: [ClsRootModule.forRoot(options)],
27+
imports: [
28+
ClsRootModule.forRoot(options),
29+
...ClsPluginsModule.createPluginModules(options?.plugins),
30+
],
2831
global: options?.global,
2932
};
3033
}
@@ -37,7 +40,10 @@ export class ClsModule {
3740
static forRootAsync(asyncOptions: ClsModuleAsyncOptions): DynamicModule {
3841
return {
3942
module: ClsModule,
40-
imports: [ClsRootModule.forRootAsync(asyncOptions)],
43+
imports: [
44+
ClsRootModule.forRootAsync(asyncOptions),
45+
...ClsPluginsModule.createPluginModules(asyncOptions.plugins),
46+
],
4147
global: asyncOptions?.global,
4248
};
4349
}
@@ -87,7 +93,7 @@ export class ClsModule {
8793
static registerPlugins(plugins: ClsPlugin[]): DynamicModule {
8894
return {
8995
module: ClsModule,
90-
imports: ClsPluginManager.registerPlugins(plugins),
96+
imports: ClsPluginsModule.createPluginModules(plugins),
9197
};
9298
}
9399
}

packages/core/src/lib/cls.service.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -235,18 +235,4 @@ export class ClsService<S extends ClsStore = ClsStore> {
235235
: [];
236236
await ProxyProviderManager.resolveProxyProviders(proxySymbols);
237237
}
238-
239-
/**
240-
* @deprecated This method will be removed in a future release and replaced
241-
* with a different mechanism for plugin initialization.
242-
*
243-
* Since the plugin API is still experimental, this method will become a np-op
244-
* and will be eventually removed, possibly in a minor release.
245-
*/
246-
async initializePlugins() {
247-
const { ClsPluginManager } = await import(
248-
'./plugin/cls-plugin-manager'
249-
);
250-
await ClsPluginManager.onClsInit();
251-
}
252238
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Provider, InjectionToken } from '@nestjs/common';
2+
import { ClsPlugin, ClsPluginHooks } from './cls-plugin.interface';
3+
4+
/**
5+
* Extend this class to create a new ClsPlugin
6+
*
7+
* It contains the basic structure for a plugin, including
8+
* some helper methods for common operations.
9+
*/
10+
export abstract class ClsPluginBase implements ClsPlugin {
11+
imports: any[] = [];
12+
providers: Provider[] = [];
13+
exports: any[] = [];
14+
15+
/**
16+
* @param name {@link ClsPlugin.name}
17+
*/
18+
constructor(public readonly name: string) {}
19+
20+
protected get hooksProviderToken() {
21+
return getPluginHooksToken(this.name);
22+
}
23+
24+
/**
25+
* Register the plugin hooks provider
26+
*
27+
* This is a shorthand for manually registering a provider
28+
* that returns the plugin hooks object provided under
29+
* the `hooksProviderToken` token.
30+
*
31+
* @example
32+
* ```
33+
* this.registerHooks({
34+
* inject: [OPTIONS],
35+
* useFactory: (options: PluginOptions<any>): ClsPluginHooks => ({
36+
* afterSetup: (cls: ClsService) => {
37+
* cls.set('some-key', options.pluginData);
38+
* }
39+
* })
40+
* })
41+
* ```
42+
*/
43+
protected registerHooks(opts: {
44+
inject: InjectionToken<any>[];
45+
useFactory: (...args: any[]) => ClsPluginHooks;
46+
}) {
47+
this.providers.push({
48+
provide: this.hooksProviderToken,
49+
inject: opts.inject,
50+
useFactory: opts.useFactory,
51+
});
52+
this.exports.push(this.hooksProviderToken);
53+
}
54+
}
55+
56+
export function getPluginHooksToken(name: string) {
57+
return `CLS_PLUGIN_HOOKS_${name}`;
58+
}
59+
60+
export function isPluginHooksToken(token: string | symbol) {
61+
return typeof token === 'string' && token.startsWith('CLS_PLUGIN_HOOKS_');
62+
}

0 commit comments

Comments
 (0)