Skip to content

Commit

Permalink
Merge branch 'main' into fix/transaction-propagation
Browse files Browse the repository at this point in the history
  • Loading branch information
Papooch authored Jul 26, 2024
2 parents 897a662 + 8916670 commit 57b9e77
Show file tree
Hide file tree
Showing 25 changed files with 451 additions and 135 deletions.
126 changes: 93 additions & 33 deletions docs/docs/03_features-and-use-cases/06_proxy-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,41 +147,11 @@ class DogsService {
}
```

## Caveats

### No primitive values

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

### `function` Proxies must be explicitly enabled

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.

```ts
{
provide: SOME_FUNCTION,
useFactory: () => {
return () => {
// do something
};
},
// highlight-start
type: 'function',
// highlight-end
}
```

:::note

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)

:::

## Delayed resolution of Proxy Providers

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 .

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

```ts
ClsModule.forRoot({
Expand All @@ -191,15 +161,19 @@ ClsModule.forRoot({
// highlight-end
},
});

//... later

await this.cls.resolveProxyProviders();
```

### Outside web request

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`.
This might also be necessary [outside the context of web request](./04_usage-outside-of-web-request.md).

#### With cls.run()

If you set up the context with `cls.run()` to wrap any subsequent code thar relies on Proxy Providers.
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_.

```ts title=cron.controller.ts
@Injectable()
Expand Down Expand Up @@ -248,3 +222,89 @@ export class CronController {
}
}
```

### Selective resolution of Proxy Providers

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.

```ts
// resolves ProviderA and ProviderB only
await this.cls.resolveProxyProviders([ProviderA, ProviderB]);

// ... later

// resolves the rest of the providers that have not been resolved yet
await this.cls.resolveProxyProviders();
```

## Strict Proxy Providers

<small>since `v4.4.0`</small>

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.

For Class Proxy Providers, you can use the according option on the `@InjectableProxy()` decorator.

```ts title=user.proxy.ts
@InjectableProxy({
// highlight-start
strict: true,
// highlight-end
})
export class User {
id: number;
role: string;
}
```

In case of Factory Proxy Providers, use the option on the `ClsModule.forFeatureAsync()` registration.

```ts
ClsModule.forFeatureAsync({
provide: TENANT_CONNECTION,
import: [DatabaseConnectionModule],
inject: [CLS_REQ],
useFactory: async (req: Request) => {
// ... some implementation
},
// highlight-start
strict: true,
// highlight-end
});
```

## Caveats

### No primitive values

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

### `function` Proxies must be explicitly enabled

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.

```ts
{
provide: SOME_FUNCTION,
useFactory: () => {
return () => {
// do something
};
},
// highlight-start
type: 'function',
// highlight-end
}
```

:::note

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)

:::

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

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.
3 changes: 3 additions & 0 deletions docs/docs/04_api/02_module-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ The `ClsModuleProxyFactoryProviderOptions` interface further accepts:
- **_`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`.

## Middleware & Enhancer options

All of the **`Cls{Middleware,Guard,Interceptor}Options`** take the following parameters (either in `ClsModuleOptions` or directly when instantiating them manually):
Expand Down
11 changes: 11 additions & 0 deletions packages/core/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

<!-- MONODEPLOY:BELOW -->

## [4.4.0](https://github.com/Papooch/nestjs-cls/compare/nestjs-cls@4.3.0...nestjs-cls@4.4.0) "nestjs-cls" (2024-07-26)<a name="4.4.0"></a>

### Features

* add `strict` option to proxy providers ([3f3de78](https://github.com/Papooch/nestjs-cls/commits/3f3de78))
* enable setting proxy provider `strict` option via a decorator. ([3f3de78](https://github.com/Papooch/nestjs-cls/commits/3f3de78))
* enable `strict` mode for Proxy Providers (#171) ([3f3de78](https://github.com/Papooch/nestjs-cls/commits/3f3de78))




## [4.3.0](https://github.com/Papooch/nestjs-cls/compare/nestjs-cls@4.2.1...nestjs-cls@4.3.0) "nestjs-cls" (2024-03-22)<a name="4.3.0"></a>

### Features
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nestjs-cls",
"version": "4.3.0",
"version": "4.4.0",
"description": "A continuation-local storage module compatible with NestJS's dependency injection.",
"author": "papooch",
"license": "MIT",
Expand Down
17 changes: 10 additions & 7 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
export * from './lib/cls-service-manager';
export * from './lib/cls.constants';
export * from './lib/cls-initializers/cls.middleware';
export * from './lib/cls-initializers/cls.interceptor';
export * from './lib/cls-initializers/cls.guard';
export * from './lib/cls-initializers/cls.interceptor';
export * from './lib/cls-initializers/cls.middleware';
export * from './lib/cls-initializers/use-cls.decorator';
export * from './lib/cls-initializers/utils/context-cls-store-map';
export * from './lib/cls-service-manager';
export * from './lib/cls.constants';
export * from './lib/cls.module';
export * from './lib/cls.service';
export * from './lib/cls.decorators';
export * from './lib/cls.options';
export * from './lib/cls.service';
export * from './lib/inject-cls.decorator';
export * from './lib/plugin/cls-plugin.interface';
export * from './utils/copy-method-metadata';
export * from './lib/proxy-provider/injectable-proxy.decorator';
export * from './lib/proxy-provider/proxy-provider.exceptions';
export * from './lib/proxy-provider/proxy-provider.interfaces';
export { Terminal } from './types/terminal.type';
export * from './utils/copy-method-metadata';
18 changes: 0 additions & 18 deletions packages/core/src/lib/cls.decorators.ts

This file was deleted.

2 changes: 2 additions & 0 deletions packages/core/src/lib/cls.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export class ClsModule implements NestModule {
static forRoot(options?: ClsModuleOptions): DynamicModule {
options = { ...new ClsModuleOptions(), ...options };
const { providers, exports } = this.getProviders();
ProxyProviderManager.reset(); // ensure that the proxy manager's state is clean
const proxyProviders = this.createProxyClassProviders(
options.proxyProviders,
);
Expand Down Expand Up @@ -125,6 +126,7 @@ export class ClsModule implements NestModule {
*/
static forRootAsync(asyncOptions: ClsModuleAsyncOptions): DynamicModule {
const { providers, exports } = this.getProviders();
ProxyProviderManager.reset(); // ensure that the proxy manager's state is clean
const proxyProviders = this.createProxyClassProviders(
asyncOptions.proxyProviders,
);
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/lib/inject-cls.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Inject } from '@nestjs/common';
import { ClsService } from './cls.service';

/**
* Use to explicitly inject the ClsService
*/
export function InjectCls() {
return Inject(ClsService);
}
23 changes: 23 additions & 0 deletions packages/core/src/lib/proxy-provider/injectable-proxy.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Injectable, SetMetadata } from '@nestjs/common';
import { CLS_PROXY_METADATA_KEY } from './proxy-provider.constants';

export type InjectableProxyMetadata = {
/**
* If true, accessing any property on this provider while it is unresolved will throw an exception.
*
* Otherwise, the application behaves as if accessing a property on an empty object.
*
* Default: false
*
* Note - setting this option again in the forRootAsync method will override the value set in the decorator.
*/
strict?: boolean;
};

/**
* Mark a Proxy provider with this decorator to distinguish it from regular NestJS singleton providers
*/
export function InjectableProxy(options: InjectableProxyMetadata = {}) {
return (target: any) =>
Injectable()(SetMetadata(CLS_PROXY_METADATA_KEY, options)(target));
}
Loading

0 comments on commit 57b9e77

Please sign in to comment.