Skip to content

Commit 3f3de78

Browse files
authored
feat: enable strict mode for Proxy Providers (#171)
* test: clean ProxyProviderManager state in forRoot to support tests * feat: add `strict` option to proxy providers * feat: enable setting proxy provider `strict` option via a decorator. * docs: clarify section on Proxy Providers * docs: add docs on strict proxy providers
1 parent 2fac34a commit 3f3de78

14 files changed

+401
-96
lines changed

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

Lines changed: 93 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -147,41 +147,11 @@ class DogsService {
147147
}
148148
```
149149

150-
## Caveats
151-
152-
### No primitive values
153-
154-
Proxy Factory providers _cannot_ return a _primitive value_. This is because the provider itself is the Proxy and it only delegates access once a property or a method is called on it (or if it itself is called in case the factory returns a function).
155-
156-
### `function` Proxies must be explicitly enabled
157-
158-
In order to support injecting proxies of _functions_, the underlying proxy _target_ must be a function, too, in order to be able to implement the "apply" trap. However, this information cannot be extracted from the factory function itself, so if your factory returns a function, you must explicitly set the `type` property to `function` in the provider definition.
159-
160-
```ts
161-
{
162-
provide: SOME_FUNCTION,
163-
useFactory: () => {
164-
return () => {
165-
// do something
166-
};
167-
},
168-
// highlight-start
169-
type: 'function',
170-
// highlight-end
171-
}
172-
```
173-
174-
:::note
175-
176-
In versions prior to `v4.0`, calling `typeof` on an instance of a Proxy provider always returned `function`, regardless of the value it holds. This is no longer the case. Please see [Issue #82](https://github.com/Papooch/nestjs-cls/issues/82)
177-
178-
:::
179-
180150
## Delayed resolution of Proxy Providers
181151

182152
By default, proxy providers are resolved as soon as the `setup` function in an enhancer (middleware/guard/interceptor) finishes. For some use cases, it might be required that the resolution is delayed until some later point in the request lifecycle once more information is present in the CLS .
183153

184-
To achieve that, set `resolveProxyProviders` to `false` in the enhancer options and call `ClsService#resolveProxyProviders()` manually at any time.
154+
To achieve that, set `resolveProxyProviders` to `false` in the enhancer options and call (and await) `ClsService#resolveProxyProviders()` manually at any time.
185155

186156
```ts
187157
ClsModule.forRoot({
@@ -191,15 +161,19 @@ ClsModule.forRoot({
191161
// highlight-end
192162
},
193163
});
164+
165+
//... later
166+
167+
await this.cls.resolveProxyProviders();
194168
```
195169

196170
### Outside web request
197171

198-
This is also necessary [outside the context of web request](./04_usage-outside-of-web-request.md), otherwise all access to an injected Proxy Provider will return `undefined`.
172+
This might also be necessary [outside the context of web request](./04_usage-outside-of-web-request.md).
199173

200174
#### With cls.run()
201175

202-
If you set up the context with `cls.run()` to wrap any subsequent code thar relies on Proxy Providers.
176+
If you set up the context with `cls.run()` to wrap any subsequent code thar relies on Proxy Providers, you _must_ call `ClsService#resolveProxyProviders()` before accessing them, otherwise access to any property of the injected Proxy Provider will return `undefined`, that is because an unresolved Proxy Provider falls back to an _empty object_.
203177

204178
```ts title=cron.controller.ts
205179
@Injectable()
@@ -248,3 +222,89 @@ export class CronController {
248222
}
249223
}
250224
```
225+
226+
### Selective resolution of Proxy Providers
227+
228+
You can also selectively resolve a subset of Proxy Providers, by passing a list of their injection tokens to `ClsService#resolveProxyProviders(tokens)`. This is useful if the providers need to be resolved in a specific order or when some part of the application does not need all of them.
229+
230+
```ts
231+
// resolves ProviderA and ProviderB only
232+
await this.cls.resolveProxyProviders([ProviderA, ProviderB]);
233+
234+
// ... later
235+
236+
// resolves the rest of the providers that have not been resolved yet
237+
await this.cls.resolveProxyProviders();
238+
```
239+
240+
## Strict Proxy Providers
241+
242+
<small>since `v4.4.0`</small>
243+
244+
By default, accessing an unresolved Proxy Provider behaves as if it was an _empty object_. In order to prevent silent failures, you can set the `strict` option to `true` in the proxy provider registration. In this case, any attempt to access a property or a method on an unresolved Proxy Provider will throw an error.
245+
246+
For Class Proxy Providers, you can use the according option on the `@InjectableProxy()` decorator.
247+
248+
```ts title=user.proxy.ts
249+
@InjectableProxy({
250+
// highlight-start
251+
strict: true,
252+
// highlight-end
253+
})
254+
export class User {
255+
id: number;
256+
role: string;
257+
}
258+
```
259+
260+
In case of Factory Proxy Providers, use the option on the `ClsModule.forFeatureAsync()` registration.
261+
262+
```ts
263+
ClsModule.forFeatureAsync({
264+
provide: TENANT_CONNECTION,
265+
import: [DatabaseConnectionModule],
266+
inject: [CLS_REQ],
267+
useFactory: async (req: Request) => {
268+
// ... some implementation
269+
},
270+
// highlight-start
271+
strict: true,
272+
// highlight-end
273+
});
274+
```
275+
276+
## Caveats
277+
278+
### No primitive values
279+
280+
Proxy Factory providers _cannot_ return a _primitive value_. This is because the provider itself is the Proxy and it only delegates access once a property or a method is called on it (or if it itself is called in case the factory returns a function).
281+
282+
### `function` Proxies must be explicitly enabled
283+
284+
In order to support injecting proxies of _functions_, the underlying proxy _target_ must be a function, too, in order to be able to implement the "apply" trap. However, this information cannot be extracted from the factory function itself, so if your factory returns a function, you must explicitly set the `type` property to `function` in the provider definition.
285+
286+
```ts
287+
{
288+
provide: SOME_FUNCTION,
289+
useFactory: () => {
290+
return () => {
291+
// do something
292+
};
293+
},
294+
// highlight-start
295+
type: 'function',
296+
// highlight-end
297+
}
298+
```
299+
300+
:::note
301+
302+
In versions prior to `v4.0`, calling `typeof` on an instance of a Proxy provider always returned `function`, regardless of the value it holds. This is no longer the case. Please see [Issue #82](https://github.com/Papooch/nestjs-cls/issues/82)
303+
304+
:::
305+
306+
### Limited support for injecting Proxy Providers into each other
307+
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.
309+
310+
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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ The `ClsModuleProxyFactoryProviderOptions` interface further accepts:
5858
- **_`type?:`_**`'function' | 'object'`
5959
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`.
63+
6164
## Middleware & Enhancer options
6265

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

packages/core/src/index.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
export * from './lib/cls-service-manager';
2-
export * from './lib/cls.constants';
3-
export * from './lib/cls-initializers/cls.middleware';
4-
export * from './lib/cls-initializers/cls.interceptor';
51
export * from './lib/cls-initializers/cls.guard';
2+
export * from './lib/cls-initializers/cls.interceptor';
3+
export * from './lib/cls-initializers/cls.middleware';
64
export * from './lib/cls-initializers/use-cls.decorator';
75
export * from './lib/cls-initializers/utils/context-cls-store-map';
6+
export * from './lib/cls-service-manager';
7+
export * from './lib/cls.constants';
88
export * from './lib/cls.module';
9-
export * from './lib/cls.service';
10-
export * from './lib/cls.decorators';
119
export * from './lib/cls.options';
10+
export * from './lib/cls.service';
11+
export * from './lib/inject-cls.decorator';
1212
export * from './lib/plugin/cls-plugin.interface';
13-
export * from './utils/copy-method-metadata';
13+
export * from './lib/proxy-provider/injectable-proxy.decorator';
14+
export * from './lib/proxy-provider/proxy-provider.exceptions';
15+
export * from './lib/proxy-provider/proxy-provider.interfaces';
1416
export { Terminal } from './types/terminal.type';
17+
export * from './utils/copy-method-metadata';

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

Lines changed: 0 additions & 18 deletions
This file was deleted.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export class ClsModule implements NestModule {
9898
static forRoot(options?: ClsModuleOptions): DynamicModule {
9999
options = { ...new ClsModuleOptions(), ...options };
100100
const { providers, exports } = this.getProviders();
101+
ProxyProviderManager.reset(); // ensure that the proxy manager's state is clean
101102
const proxyProviders = this.createProxyClassProviders(
102103
options.proxyProviders,
103104
);
@@ -125,6 +126,7 @@ export class ClsModule implements NestModule {
125126
*/
126127
static forRootAsync(asyncOptions: ClsModuleAsyncOptions): DynamicModule {
127128
const { providers, exports } = this.getProviders();
129+
ProxyProviderManager.reset(); // ensure that the proxy manager's state is clean
128130
const proxyProviders = this.createProxyClassProviders(
129131
asyncOptions.proxyProviders,
130132
);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Inject } from '@nestjs/common';
2+
import { ClsService } from './cls.service';
3+
4+
/**
5+
* Use to explicitly inject the ClsService
6+
*/
7+
export function InjectCls() {
8+
return Inject(ClsService);
9+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Injectable, SetMetadata } from '@nestjs/common';
2+
import { CLS_PROXY_METADATA_KEY } from './proxy-provider.constants';
3+
4+
export type InjectableProxyMetadata = {
5+
/**
6+
* If true, accessing any property on this provider while it is unresolved will throw an exception.
7+
*
8+
* Otherwise, the application behaves as if accessing a property on an empty object.
9+
*
10+
* Default: false
11+
*
12+
* Note - setting this option again in the forRootAsync method will override the value set in the decorator.
13+
*/
14+
strict?: boolean;
15+
};
16+
17+
/**
18+
* Mark a Proxy provider with this decorator to distinguish it from regular NestJS singleton providers
19+
*/
20+
export function InjectableProxy(options: InjectableProxyMetadata = {}) {
21+
return (target: any) =>
22+
Injectable()(SetMetadata(CLS_PROXY_METADATA_KEY, options)(target));
23+
}

0 commit comments

Comments
 (0)