Skip to content

Commit 4dcda62

Browse files
authored
feat(core): add saveCtx option in enhancer setup (#212)
* refactor(core): split constants file into internal and public * feat(core): add saveCtx option in enhancer setup This allows storing the ExecutionContext in the CLS (enabled by default) * docs: add docs on CLS_CTX
1 parent bf0f871 commit 4dcda62

File tree

12 files changed

+285
-72
lines changed

12 files changed

+285
-72
lines changed

docs/docs/03_features-and-use-cases/06_proxy-providers.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export class UserInterceptor implements NestInterceptor {
7171

7272
It is also possible to inject other providers into the Proxy Provider to avoid having to do this in a separate component.
7373

74-
For the convenience, the `CLS_REQ` and `CLS_RES` are also made into Proxy Providers and are exported from the `ClsModule`.
74+
For the convenience, the `CLS_REQ` and `CLS_RES` (if enabled) and `CLS_CTX` (when an enhancer is used) are also made into Proxy Providers and are exported from the `ClsModule`.
7575

7676
```ts title=user-with-rile.proxy.ts
7777
@InjectableProxy()
@@ -105,6 +105,8 @@ ClsModule.forFeatureAsync({
105105

106106
Using `@Inject(CLS_REQ)`, you can entirely replace `@Inject(REQUEST)` in REQUEST-SCOPED providers to turn them into CLS-enabled singletons without changing the implementation.
107107

108+
Also `@INJECT(CLS_CTX)` can be used to replace `@Inject(CONTEXT)`.
109+
108110
:::
109111

110112
## Factory Proxy Providers
@@ -305,6 +307,6 @@ In versions prior to `v4.0`, calling `typeof` on an instance of a Proxy provider
305307

306308
### Limited support for injecting Proxy Providers into each other
307309

308-
Apart from the built-in `CLS_REQ` and `CLS_RES` proxy providers, custom Proxy Providers cannot be _reliably_ injected into other Proxy Providers, because there is no system in place to resolve them in the correct order (as far as Nest is concerned, all of them have already been bootstrapped, so it can't help us here), so it may happen, that during the proxy provider resolution phase, a Proxy Provider that is injected into another Proxy Provider is not yet resolved and falls back to an empty object.
310+
Apart from the built-in `CLS_REQ` and `CLS_RES` and `CLS_CTX` proxy providers, custom Proxy Providers cannot be _reliably_ injected into other Proxy Providers, because there is no system in place to resolve them in the correct order (as far as Nest is concerned, all of them have already been bootstrapped, so it can't help us here), so it may happen, that during the proxy provider resolution phase, a Proxy Provider that is injected into another Proxy Provider is not yet resolved and falls back to an empty object.
309311

310312
There is an open [feature request](https://github.com/Papooch/nestjs-cls/issues/169) to address this shortcoming, but until then, refer to the manual [Selective resolution of Proxy Providers](#selective-resolution-of-proxy-providers) technique. You can also leverage the [strict](#strict-proxy-providers) mode to find out which Proxy Providers are not yet resolved.

docs/docs/04_api/02_module-options.md

+51-46
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@
44

55
The `ClsModule.forRoot()` method takes the following **`ClsModuleOptions`**:
66

7-
- **_`middleware?:`_**`ClsMiddlewareOptions`
8-
An object with additional options for the `ClsMiddleware`, see [below](#middleware--enhancer-options).
7+
- **_`middleware?:`_**`ClsMiddlewareOptions`
8+
An object with additional options for the `ClsMiddleware`, see [below](#middleware--enhancer-options).
99

10-
- **_`guard?:`_**`ClsGuardOptions`
11-
An object with additional options for the `ClsGuard`, see [below](#middleware--enhancer-options).
10+
- **_`guard?:`_**`ClsGuardOptions`
11+
An object with additional options for the `ClsGuard`, see [below](#middleware--enhancer-options).
1212

13-
- **_`interceptor?:`_**`ClsInterceptorOptions`
14-
An object with additional options for the `ClsInterceptor`, see [below](#middleware--enhancer-options).
13+
- **_`interceptor?:`_**`ClsInterceptorOptions`
14+
An object with additional options for the `ClsInterceptor`, see [below](#middleware--enhancer-options).
1515

16-
- **_`global?:`_**`boolean`\*\* (default _`false`_)
17-
Whether to make the module global, so you do not have to import `ClsModule.forFeature()` in other modules.
16+
- **_`global?:`_**`boolean`\*\* (default _`false`_)
17+
Whether to make the module global, so you do not have to import `ClsModule.forFeature()` in other modules.
1818

19-
- **_`proxyProviders?:`_**`Type[]`
20-
Array of [Proxy Providers](../03_features-and-use-cases/06_proxy-providers.md) that should be registered in the root module. Currently only accepts sync class Proxy providers, use `ClsModule.forFeatureAsync()` for more complex use-cases.
19+
- **_`proxyProviders?:`_**`Type[]`
20+
Array of [Proxy Providers](../03_features-and-use-cases/06_proxy-providers.md) that should be registered in the root module. Currently only accepts sync class Proxy providers, use `ClsModule.forFeatureAsync()` for more complex use-cases.
2121

2222
`ClsModule.forRootAsync()` is also available. You can supply the usual `imports`, `inject` and `useFactory` parameters as usual.
2323

@@ -33,65 +33,70 @@ The `ClsModule.forFeature()` method can be used to register a [Proxy Providers](
3333

3434
The `ClsModule.forFeatureAsync()` method accepts either `ClsModuleProxyClassProviderOptions` or `ClsModuleProxyFactoryProviderOptions` that both accept these options:
3535

36-
- **_`provide?:`_**`any`
37-
Custom injection token to use for the provider. In case of a class provider, this parameter is optional, as the class reference passed to `useClass` will be used by default.
36+
- **_`provide?:`_**`any`
37+
Custom injection token to use for the provider. In case of a class provider, this parameter is optional, as the class reference passed to `useClass` will be used by default.
3838

39-
- **_`imports?`_**`any[]`
40-
Optional list of imported modules that export the providers which are required for the provider.
39+
- **_`imports?`_**`any[]`
40+
Optional list of imported modules that export the providers which are required for the provider.
4141

42-
- **_`extraProviders?:`_**`Provider[]`
43-
Optional list of additional providers that should be available to the Proxy. Useful for passing configuration from a parent dynamic module.
42+
- **_`extraProviders?:`_**`Provider[]`
43+
Optional list of additional providers that should be available to the Proxy. Useful for passing configuration from a parent dynamic module.
4444

4545
The `ClsModuleProxyClassProviderOptions` interface further accepts:
4646

47-
- **_`useClass:`_**`Type`
48-
The target class that will be used by this Proxy Provider. Make sure it is decorated with `@InjectableProxy`.
47+
- **_`useClass:`_**`Type`
48+
The target class that will be used by this Proxy Provider. Make sure it is decorated with `@InjectableProxy`.
4949

5050
The `ClsModuleProxyFactoryProviderOptions` interface further accepts:
5151

52-
- **_`inject:`_**`any[]`
53-
An array of injection tokens for providers used in the `useFactory`.
52+
- **_`inject:`_**`any[]`
53+
An array of injection tokens for providers used in the `useFactory`.
5454

55-
- **_`useFactory:`_**`(...args: any[]) => any`
56-
Factory function that accepts an array of providers in the order of the according tokens in the `inject` array. Returns (or resolves with) an object (or a function) that will be used by this Proxy Provider.
55+
- **_`useFactory:`_**`(...args: any[]) => any`
56+
Factory function that accepts an array of providers in the order of the according tokens in the `inject` array. Returns (or resolves with) an object (or a function) that will be used by this Proxy Provider.
5757

58-
- **_`type?:`_**`'function' | 'object'`
59-
Whether the Proxy Provider should be a function or an object. Defaults to `'object'`. See [Caveats](../03_features-and-use-cases/06_proxy-providers.md#caveats) for more information.
58+
- **_`type?:`_**`'function' | 'object'`
59+
Whether the Proxy Provider should be a function or an object. Defaults to `'object'`. See [Caveats](../03_features-and-use-cases/06_proxy-providers.md#caveats) for more information.
6060

61-
- **_`strict?:`_**`boolean`
62-
Whether to register this Proxy Provider in [strict mode](../03_features-and-use-cases/06_proxy-providers.md#strict-proxy-providers). Defaults to `false`.
61+
- **_`strict?:`_**`boolean`
62+
Whether to register this Proxy Provider in [strict mode](../03_features-and-use-cases/06_proxy-providers.md#strict-proxy-providers). Defaults to `false`.
6363

6464
## Middleware & Enhancer options
6565

6666
All of the **`Cls{Middleware,Guard,Interceptor}Options`** take the following parameters (either in `ClsModuleOptions` or directly when instantiating them manually):
6767

68-
- **_`mount?:`_**`boolean` (default _`false`_)
69-
Whether to automatically mount the middleware/guard/interceptor to every route (not applicable when instantiating them manually)
68+
- **_`mount?:`_**`boolean` (default _`false`_)
69+
Whether to automatically mount the middleware/guard/interceptor to every route (not applicable when instantiating them manually)
7070

71-
- **_`generateId?:`_**`boolean` (default _`false`_)
72-
Whether to automatically generate a request ID. It will be available under the `CLS_ID` key.
71+
- **_`generateId?:`_**`boolean` (default _`false`_)
72+
Whether to automatically generate a request ID. It will be available under the `CLS_ID` key.
7373

74-
- **_`idGenerator?:`_**`(req: Request) => string | Promise<string>`
75-
**_`idGenerator?:`_**`(ctx: ExecutionContext) => string | Promise<string>`
76-
An optional function for generating the request ID. It takes the `Request` object (or the `ExecutionContext` in case of a Guard or Interceptor) as an argument and (synchronously or asynchronously) returns a string. The default implementation uses `Math.random()` to generate a string of 8 characters.
74+
- **_`idGenerator?:`_**`(req: Request) => string | Promise<string>`
75+
**_`idGenerator?:`_**`(ctx: ExecutionContext) => string | Promise<string>`
76+
An optional function for generating the request ID. It takes the `Request` object (or the `ExecutionContext` in case of a Guard or Interceptor) as an argument and (synchronously or asynchronously) returns a string. The default implementation uses `Math.random()` to generate a string of 8 characters.
7777

78-
- **_`setup?:`_**`(cls: ClsService, req: Request) => void | Promise<void>;`
79-
**_`setup?:`_**`(cls: ClsService, ctx: ExecutionContext) => void | Promise<void>;`
80-
Function that executes after the CLS context had been initialised. It can be used to put additional variables in the CLS context.
78+
- **_`setup?:`_**`(cls: ClsService, req: Request) => void | Promise<void>;`
79+
**_`setup?:`_**`(cls: ClsService, ctx: ExecutionContext) => void | Promise<void>;`
80+
Function that executes after the CLS context had been initialised. It can be used to put additional variables in the CLS context.
8181

82-
- **_`resolveProxyProviders?:`_**`boolean` (default _`true`_)
83-
Whether to automatically resolve Proxy Providers in the enhancer (if any are registered).
82+
- **_`resolveProxyProviders?:`_**`boolean` (default _`true`_)
83+
Whether to automatically resolve Proxy Providers in the enhancer (if any are registered).
8484

85-
- **_`initializePlugins?:`_**`boolean` (default _`true`_)
86-
Whether to run the `onClsInit` hook for plugins as a part of the CLS context registration (runs before `resolveProxyProviders` just after `setup`).
85+
- **_`initializePlugins?:`_**`boolean` (default _`true`_)
86+
Whether to run the `onClsInit` hook for plugins as a part of the CLS context registration (runs before `resolveProxyProviders` just after `setup`).
8787

8888
The `ClsMiddlewareOptions` additionally takes the following parameters:
8989

90-
- **_`saveReq?:`_**`boolean` (default _`true`_)
91-
Whether to store the _Request_ object to the context. It will be available under the `CLS_REQ` key.
90+
- **_`saveReq?:`_**`boolean` (default _`true`_)
91+
Whether to store the _Request_ object to the context. It will be available under the `CLS_REQ` key.
9292

93-
- **_`saveRes?:`_**`boolean` (default _`false`_)
94-
Whether to store the _Response_ object to the context. It will be available under the `CLS_RES` key
93+
- **_`saveRes?:`_**`boolean` (default _`false`_)
94+
Whether to store the _Response_ object to the context. It will be available under the `CLS_RES` key
9595

96-
- **_`useEnterWith?:`_**`boolean` (default _`false`_)
97-
Set to `true` to set up the context using a call to [`AsyncLocalStorage#enterWith`](https://nodejs.org/api/async_context.html#async_context_asynclocalstorage_enterwith_store) instead of wrapping the `next()` call with the safer [`AsyncLocalStorage#run`](https://nodejs.org/api/async_context.html#async_context_asynclocalstorage_run_store_callback_args). Most of the time this should not be necessary, but [some frameworks](../05_considerations/02_compatibility.md#graphql) have been known to lose the context with `run`.
96+
- **_`useEnterWith?:`_**`boolean` (default _`false`_)
97+
Set to `true` to set up the context using a call to [`AsyncLocalStorage#enterWith`](https://nodejs.org/api/async_context.html#async_context_asynclocalstorage_enterwith_store) instead of wrapping the `next()` call with the safer [`AsyncLocalStorage#run`](https://nodejs.org/api/async_context.html#async_context_asynclocalstorage_run_store_callback_args). Most of the time this should not be necessary, but [some frameworks](../05_considerations/02_compatibility.md#graphql) have been known to lose the context with `run`.
98+
99+
The `Cls{Guard,Interceptor}Options` additionally takes the following parameters:
100+
101+
- **_`saveCtx?:`_**`boolean` (default _`true`_) <small>Since `v5.1.0`</small>
102+
Whether to store the _ExecutionContext_ object to the context. It will be available under the `CLS_CTX` key.

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
Injectable,
66
} from '@nestjs/common';
77
import { ClsServiceManager } from '../cls-service-manager';
8-
import { CLS_GUARD_OPTIONS, CLS_ID } from '../cls.constants';
8+
import { CLS_CTX, CLS_ID } from '../cls.constants';
9+
import { CLS_GUARD_OPTIONS } from '../cls.internal-constants';
910
import { ClsGuardOptions } from '../cls.options';
1011
import { ContextClsStoreMap } from './utils/context-cls-store-map';
1112

@@ -33,6 +34,9 @@ export class ClsGuard implements CanActivate {
3334
const id = await this.options.idGenerator?.(context);
3435
cls.setIfUndefined<any>(CLS_ID, id);
3536
}
37+
if (this.options.saveCtx) {
38+
cls.set<ExecutionContext>(CLS_CTX, context);
39+
}
3640
if (this.options.setup) {
3741
await this.options.setup(cls, context);
3842
}

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
} from '@nestjs/common';
88
import { Observable } from 'rxjs';
99
import { ClsServiceManager } from '../cls-service-manager';
10-
import { CLS_ID, CLS_INTERCEPTOR_OPTIONS } from '../cls.constants';
10+
import { CLS_CTX, CLS_ID } from '../cls.constants';
11+
import { CLS_INTERCEPTOR_OPTIONS } from '../cls.internal-constants';
1112
import { ClsInterceptorOptions } from '../cls.options';
1213
import { ContextClsStoreMap } from './utils/context-cls-store-map';
1314

@@ -32,6 +33,9 @@ export class ClsInterceptor implements NestInterceptor {
3233
const id = await this.options.idGenerator?.(context);
3334
cls.setIfUndefined<any>(CLS_ID, id);
3435
}
36+
if (this.options.saveCtx) {
37+
cls.set<ExecutionContext>(CLS_CTX, context);
38+
}
3539
if (this.options.setup) {
3640
await this.options.setup(cls, context);
3741
}

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

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import { Inject, Injectable, NestMiddleware } from '@nestjs/common';
22
import { ClsServiceManager } from '../cls-service-manager';
3-
import {
4-
CLS_ID,
5-
CLS_MIDDLEWARE_OPTIONS,
6-
CLS_REQ,
7-
CLS_RES,
8-
} from '../cls.constants';
3+
import { CLS_ID, CLS_REQ, CLS_RES } from '../cls.constants';
94
import { ClsMiddlewareOptions } from '../cls.options';
105
import { ContextClsStoreMap } from './utils/context-cls-store-map';
6+
import { CLS_MIDDLEWARE_OPTIONS } from '../cls.internal-constants';
117

128
@Injectable()
139
export class ClsMiddleware implements NestMiddleware {

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

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Module, ValueProvider } from '@nestjs/common';
22
import { ClsServiceManager } from '../cls-service-manager';
3-
import { CLS_REQ, CLS_RES } from '../cls.constants';
3+
import { CLS_CTX, CLS_REQ, CLS_RES } from '../cls.constants';
44
import { ClsService } from '../cls.service';
55

66
import { ProxyProviderManager } from '../proxy-provider/proxy-provider-manager';
@@ -12,8 +12,15 @@ const clsServiceProvider: ValueProvider<ClsService> = {
1212

1313
const commonProviders = [
1414
clsServiceProvider,
15-
ProxyProviderManager.createProxyProviderFromExistingKey(CLS_REQ),
16-
ProxyProviderManager.createProxyProviderFromExistingKey(CLS_RES),
15+
ProxyProviderManager.createProxyProviderFromExistingKey(CLS_REQ, {
16+
strict: true,
17+
}),
18+
ProxyProviderManager.createProxyProviderFromExistingKey(CLS_RES, {
19+
strict: true,
20+
}),
21+
ProxyProviderManager.createProxyProviderFromExistingKey(CLS_CTX, {
22+
strict: true,
23+
}),
1724
];
1825

1926
/**

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
CLS_INTERCEPTOR_OPTIONS,
2525
CLS_MIDDLEWARE_OPTIONS,
2626
CLS_MODULE_OPTIONS,
27-
} from '../cls.constants';
27+
} from '../cls.internal-constants';
2828
import {
2929
ClsGuardOptions,
3030
ClsInterceptorOptions,

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

+28-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
1-
export const CLS_REQ = Symbol('CLS_REQUEST');
2-
export const CLS_RES = Symbol('CLS_RESPONSE');
1+
/**
2+
* Symbol for the Request object stored in the CLS context.
3+
*
4+
* Only available in the CLS if the `saveReq` option of `middleware` (`ClsMiddleware`) options
5+
* is set to `true` (default).
6+
*/
7+
export const CLS_REQ = Symbol('CLS_REQ');
8+
/**
9+
* Symbol for the Response object stored in the CLS context.
10+
*
11+
* Only available in the CLS if the `saveRes` option of `middleware` (`ClsMiddleware`) options
12+
* is set to `true` (default is `false`).
13+
*/
14+
export const CLS_RES = Symbol('CLS_RES');
15+
/**
16+
* Symbol for the CLS ExecutionContext object stored in the CLS context.
17+
*
18+
* Only available if the `saveCtx` options of either `interceptor` (ClsInterceptor) or
19+
* `guard` (ClsGuard) options is set to `true` (default).
20+
*/
21+
export const CLS_CTX = Symbol('CLS_CTX');
22+
/**
23+
* Symbol for the ID of the CLS context stored in the CLS context.
24+
*
25+
* Only available in the CLS if the `generateId` option is set to `true` (default is `false`)
26+
*
27+
* Also available via `cls.getId()`
28+
*/
329
export const CLS_ID = Symbol('CLS_ID');
4-
export const CLS_MODULE_OPTIONS = 'ClsModuleOptions';
5-
export const CLS_MIDDLEWARE_OPTIONS = 'ClsMiddlewareOptions';
6-
export const CLS_GUARD_OPTIONS = 'ClsGuardOptions';
7-
export const CLS_INTERCEPTOR_OPTIONS = 'ClsInterceptorOptions';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const CLS_MODULE_OPTIONS = 'ClsModuleOptions';
2+
export const CLS_MIDDLEWARE_OPTIONS = 'ClsMiddlewareOptions';
3+
export const CLS_GUARD_OPTIONS = 'ClsGuardOptions';
4+
export const CLS_INTERCEPTOR_OPTIONS = 'ClsInterceptorOptions';

0 commit comments

Comments
 (0)