Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add saveCtx option in enhancer setup #212

Merged
merged 3 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/docs/03_features-and-use-cases/06_proxy-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class UserInterceptor implements NestInterceptor {

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

For the convenience, the `CLS_REQ` and `CLS_RES` are also made into Proxy Providers and are exported from the `ClsModule`.
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`.

```ts title=user-with-rile.proxy.ts
@InjectableProxy()
Expand Down Expand Up @@ -105,6 +105,8 @@ ClsModule.forFeatureAsync({

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.

Also `@INJECT(CLS_CTX)` can be used to replace `@Inject(CONTEXT)`.

:::

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

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

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.
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.

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.
97 changes: 51 additions & 46 deletions docs/docs/04_api/02_module-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@

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

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

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

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

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

- **_`proxyProviders?:`_**`Type[]`
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.
- **_`proxyProviders?:`_**`Type[]`
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.

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

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

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

- **_`provide?:`_**`any`
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.
- **_`provide?:`_**`any`
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.

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

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

The `ClsModuleProxyClassProviderOptions` interface further accepts:

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

The `ClsModuleProxyFactoryProviderOptions` interface further accepts:

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

- **_`useFactory:`_**`(...args: any[]) => any`
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.
- **_`useFactory:`_**`(...args: any[]) => any`
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.

- **_`type?:`_**`'function' | 'object'`
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.
- **_`type?:`_**`'function' | 'object'`
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.

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

## Middleware & Enhancer options

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

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

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

- **_`idGenerator?:`_**`(req: Request) => string | Promise<string>`
**_`idGenerator?:`_**`(ctx: ExecutionContext) => string | Promise<string>`
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.
- **_`idGenerator?:`_**`(req: Request) => string | Promise<string>`
**_`idGenerator?:`_**`(ctx: ExecutionContext) => string | Promise<string>`
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.

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

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

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

The `ClsMiddlewareOptions` additionally takes the following parameters:

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

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

- **_`useEnterWith?:`_**`boolean` (default _`false`_)
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`.
- **_`useEnterWith?:`_**`boolean` (default _`false`_)
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`.

The `Cls{Guard,Interceptor}Options` additionally takes the following parameters:

- **_`saveCtx?:`_**`boolean` (default _`true`_) <small>Since `v5.1.0`</small>
Whether to store the _ExecutionContext_ object to the context. It will be available under the `CLS_CTX` key.
6 changes: 5 additions & 1 deletion packages/core/src/lib/cls-initializers/cls.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
Injectable,
} from '@nestjs/common';
import { ClsServiceManager } from '../cls-service-manager';
import { CLS_GUARD_OPTIONS, CLS_ID } from '../cls.constants';
import { CLS_CTX, CLS_ID } from '../cls.constants';
import { CLS_GUARD_OPTIONS } from '../cls.internal-constants';
import { ClsGuardOptions } from '../cls.options';
import { ContextClsStoreMap } from './utils/context-cls-store-map';

Expand Down Expand Up @@ -33,6 +34,9 @@ export class ClsGuard implements CanActivate {
const id = await this.options.idGenerator?.(context);
cls.setIfUndefined<any>(CLS_ID, id);
}
if (this.options.saveCtx) {
cls.set<ExecutionContext>(CLS_CTX, context);
}
if (this.options.setup) {
await this.options.setup(cls, context);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/lib/cls-initializers/cls.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { ClsServiceManager } from '../cls-service-manager';
import { CLS_ID, CLS_INTERCEPTOR_OPTIONS } from '../cls.constants';
import { CLS_CTX, CLS_ID } from '../cls.constants';
import { CLS_INTERCEPTOR_OPTIONS } from '../cls.internal-constants';
import { ClsInterceptorOptions } from '../cls.options';
import { ContextClsStoreMap } from './utils/context-cls-store-map';

Expand All @@ -32,6 +33,9 @@ export class ClsInterceptor implements NestInterceptor {
const id = await this.options.idGenerator?.(context);
cls.setIfUndefined<any>(CLS_ID, id);
}
if (this.options.saveCtx) {
cls.set<ExecutionContext>(CLS_CTX, context);
}
if (this.options.setup) {
await this.options.setup(cls, context);
}
Expand Down
8 changes: 2 additions & 6 deletions packages/core/src/lib/cls-initializers/cls.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { Inject, Injectable, NestMiddleware } from '@nestjs/common';
import { ClsServiceManager } from '../cls-service-manager';
import {
CLS_ID,
CLS_MIDDLEWARE_OPTIONS,
CLS_REQ,
CLS_RES,
} from '../cls.constants';
import { CLS_ID, CLS_REQ, CLS_RES } from '../cls.constants';
import { ClsMiddlewareOptions } from '../cls.options';
import { ContextClsStoreMap } from './utils/context-cls-store-map';
import { CLS_MIDDLEWARE_OPTIONS } from '../cls.internal-constants';

@Injectable()
export class ClsMiddleware implements NestMiddleware {
Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/lib/cls-module/cls-common.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Module, ValueProvider } from '@nestjs/common';
import { ClsServiceManager } from '../cls-service-manager';
import { CLS_REQ, CLS_RES } from '../cls.constants';
import { CLS_CTX, CLS_REQ, CLS_RES } from '../cls.constants';
import { ClsService } from '../cls.service';

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

const commonProviders = [
clsServiceProvider,
ProxyProviderManager.createProxyProviderFromExistingKey(CLS_REQ),
ProxyProviderManager.createProxyProviderFromExistingKey(CLS_RES),
ProxyProviderManager.createProxyProviderFromExistingKey(CLS_REQ, {
strict: true,
}),
ProxyProviderManager.createProxyProviderFromExistingKey(CLS_RES, {
strict: true,
}),
ProxyProviderManager.createProxyProviderFromExistingKey(CLS_CTX, {
strict: true,
}),
];

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib/cls-module/cls-root.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
CLS_INTERCEPTOR_OPTIONS,
CLS_MIDDLEWARE_OPTIONS,
CLS_MODULE_OPTIONS,
} from '../cls.constants';
} from '../cls.internal-constants';
import {
ClsGuardOptions,
ClsInterceptorOptions,
Expand Down
34 changes: 28 additions & 6 deletions packages/core/src/lib/cls.constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
export const CLS_REQ = Symbol('CLS_REQUEST');
export const CLS_RES = Symbol('CLS_RESPONSE');
/**
* Symbol for the Request object stored in the CLS context.
*
* Only available in the CLS if the `saveReq` option of `middleware` (`ClsMiddleware`) options
* is set to `true` (default).
*/
export const CLS_REQ = Symbol('CLS_REQ');
/**
* Symbol for the Response object stored in the CLS context.
*
* Only available in the CLS if the `saveRes` option of `middleware` (`ClsMiddleware`) options
* is set to `true` (default is `false`).
*/
export const CLS_RES = Symbol('CLS_RES');
/**
* Symbol for the CLS ExecutionContext object stored in the CLS context.
*
* Only available if the `saveCtx` options of either `interceptor` (ClsInterceptor) or
* `guard` (ClsGuard) options is set to `true` (default).
*/
export const CLS_CTX = Symbol('CLS_CTX');
/**
* Symbol for the ID of the CLS context stored in the CLS context.
*
* Only available in the CLS if the `generateId` option is set to `true` (default is `false`)
*
* Also available via `cls.getId()`
*/
export const CLS_ID = Symbol('CLS_ID');
export const CLS_MODULE_OPTIONS = 'ClsModuleOptions';
export const CLS_MIDDLEWARE_OPTIONS = 'ClsMiddlewareOptions';
export const CLS_GUARD_OPTIONS = 'ClsGuardOptions';
export const CLS_INTERCEPTOR_OPTIONS = 'ClsInterceptorOptions';
4 changes: 4 additions & 0 deletions packages/core/src/lib/cls.internal-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const CLS_MODULE_OPTIONS = 'ClsModuleOptions';
export const CLS_MIDDLEWARE_OPTIONS = 'ClsMiddlewareOptions';
export const CLS_GUARD_OPTIONS = 'ClsGuardOptions';
export const CLS_INTERCEPTOR_OPTIONS = 'ClsInterceptorOptions';
Loading