Skip to content

Commit

Permalink
feat: add imperative API to get/set Proxy providers (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
Papooch authored Feb 16, 2024
1 parent a75b793 commit fbb27dc
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 19 deletions.
10 changes: 10 additions & 0 deletions docs/docs/04_api/01_service-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,19 @@ The `S` type parameter is used as the type of custom `ClsStore`.
- **_`isActive`_**`(): boolean`
Whether the current code runs within an active CLS context.

The following methods only apply to the [Proxy](../03_features-and-use-cases/06_proxy-providers.md) feature:

- **_`getProxy`_**`(proxyToken: any): any`
Retrieve a Proxy provider from the CLS context based on its injection token.

- **_`setProxy`_**`(proxyToken: any, value? any): any`
Replace an instance of a Proxy provider in the CLS context based on its injection token.

- **_`resolveProxyProviders`_**`(): Promise<void>`
Manually trigger resolution of Proxy Providers.

The following methods involve the [plugin lifecycle](../06_plugins/02_plugin-api.md):

- **_`initializePlugins`_**`(): Promise<void>`
Manually trigger `onClsInit` hooks of registered plugins.

Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/lib/cls.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { getValueFromPath, setValueFromPath } from '../utils/value-from-path';
import { CLS_ID } from './cls.constants';
import { ClsContextOptions, ClsStore } from './cls.options';
import { getProxyProviderSymbol } from './proxy-provider/get-proxy-provider-symbol';

export class ClsService<S extends ClsStore = ClsStore> {
constructor(private readonly als: AsyncLocalStorage<any>) {
Expand Down Expand Up @@ -196,6 +197,26 @@ export class ClsService<S extends ClsStore = ClsStore> {
return !!this.als.getStore();
}

/**
* Retrieve a Proxy provider from the CLS context
* based on its injection token.
*/
getProxy<T = any>(proxyToken: string | symbol): T;
getProxy<T>(proxyToken: new (...args: any) => T): T;
getProxy(proxyToken: any) {
return this.get(getProxyProviderSymbol(proxyToken));
}

/**
* Replace an instance of a Proxy provider in the CLS context
* based on its injection token.
*/
setProxy<T = any>(proxyToken: string | symbol, value: T): void;
setProxy<T>(proxyToken: new (...args: any) => T, value: T): void;
setProxy(proxyToken: any, value: any) {
return this.set(getProxyProviderSymbol(proxyToken), value);
}

/**
* Use to manually trigger resolution of Proxy Providers
* in case `resolveProxyProviders` is not enabled in the enhancer.
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/lib/proxy-provider/get-proxy-provider-symbol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Returns a symbol under which the given proxy provider is stored in the CLS.
*/
export function getProxyProviderSymbol(
proxyToken: symbol | string | { name: string },
) {
if (typeof proxyToken === 'symbol') return proxyToken;
if (typeof proxyToken === 'string') return Symbol.for(proxyToken);
if (proxyToken.name) return Symbol.for(proxyToken.name);
return Symbol.for(proxyToken.toString());
}
21 changes: 4 additions & 17 deletions packages/core/src/lib/proxy-provider/proxy-provider-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { FactoryProvider, Type, ValueProvider } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { UnknownDependenciesException } from '@nestjs/core/errors/exceptions/unknown-dependencies.exception';
import { globalClsService } from '../cls-service.globals';
import { getProxyProviderSymbol } from './get-proxy-provider-symbol';
import { CLS_PROXY_METADATA_KEY } from './proxy-provider.constants';
import {
ProxyProviderNotDecoratedException,
Expand All @@ -26,13 +27,14 @@ export class ProxyProviderManager {
private static proxyProviderMap = new Map<symbol, ProxyProvider>();

static createProxyProvider(options: ClsModuleProxyProviderOptions) {
const providerSymbol = this.getProxyProviderSymbol(options);
const providerToken = this.getProxyProviderToken(options);
const providerSymbol = getProxyProviderSymbol(providerToken);
const proxy = this.createProxy(
providerSymbol,
(options as ClsModuleProxyFactoryProviderOptions).type ?? 'object',
);
const proxyProvider: FactoryProvider = {
provide: this.getProxyProviderToken(options),
provide: providerToken,
inject: [
ModuleRef,
...((options as ClsModuleProxyFactoryProviderOptions).inject ??
Expand Down Expand Up @@ -70,21 +72,6 @@ export class ProxyProviderManager {
return proxyProvider;
}

private static getProxyProviderSymbol(
options: ClsModuleProxyProviderOptions,
) {
const maybeExistingSymbol =
typeof options.provide == 'symbol' ? options.provide : undefined;
return (
maybeExistingSymbol ??
Symbol.for(
options.provide?.toString() ??
(options as ClsModuleProxyClassProviderOptions).useClass
.name,
)
);
}

private static getProxyProviderToken(
options: ClsModuleProxyProviderOptions,
) {
Expand Down
49 changes: 47 additions & 2 deletions packages/core/test/proxy-providers/proxy-providers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ClsModule,
ClsModuleOptions,
ClsService,
CLS_ID,
InjectableProxy,
} from '../../src';
import {
Expand Down Expand Up @@ -44,12 +45,16 @@ class RequestScopedClass {
}

const FACTORY_PROVIDER = 'FACTORY_PROVIDER';

function requestScopedFactory(injected: InjectedClass, cls: ClsService) {
return {
id: cls.getId(),
getInjected: () => injected.property,
};
}
type RequestScopedFactoryResult = Awaited<
ReturnType<typeof requestScopedFactory>
>;

function randomString() {
return Math.random().toString(36).slice(-10);
Expand All @@ -60,7 +65,7 @@ class TestController {
constructor(
private readonly rsc: RequestScopedClass,
@Inject(FACTORY_PROVIDER)
private readonly rsf: Awaited<ReturnType<typeof requestScopedFactory>>,
private readonly rsf: RequestScopedFactoryResult,
) {}

@Get('/hello')
Expand Down Expand Up @@ -120,7 +125,7 @@ async function getTestApp(forRoorOptions: ClsModuleOptions) {
return app;
}

describe('Proxy providers', () => {
describe('Injecting Proxy providers', () => {
let app: INestApplication;

it('works with middleware', async () => {
Expand All @@ -145,6 +150,46 @@ describe('Proxy providers', () => {
});
});

describe('Proxy providers from CLS', () => {
let app: INestApplication;

it('allows getting a Class Proxy provider from CLS', async () => {
app = await getTestApp({});
const cls = app.get(ClsService);
const id = randomString();
await cls.runWith({ [CLS_ID]: id }, async () => {
await cls.resolveProxyProviders();
const rsc = cls.getProxy(RequestScopedClass);
expect(rsc.id).toEqual(id);
});
});

it('allows getting a factory Proxy provider from CLS', async () => {
app = await getTestApp({});
const cls = app.get(ClsService);
const id = randomString();
await cls.runWith({ [CLS_ID]: id }, async () => {
await cls.resolveProxyProviders();
const rsc =
cls.getProxy<RequestScopedFactoryResult>(FACTORY_PROVIDER);
expect(rsc.id).toEqual(id);
});
});

it('allows setting a Class Proxy provider in CLS', async () => {
app = await getTestApp({});
const cls = app.get(ClsService);
const id = randomString();
await cls.runWith({ [CLS_ID]: id }, async () => {
await cls.resolveProxyProviders();
const rsc = new RequestScopedClass(cls, new InjectedClass());
cls.setProxy(RequestScopedClass, rsc);
const rscFromCls = cls.getProxy(RequestScopedClass);
expect(rscFromCls).toEqual(rsc);
});
});
});

describe('Edge cases', () => {
it('proxy should allow setting falsy value', async () => {
const clsService = globalClsService;
Expand Down

0 comments on commit fbb27dc

Please sign in to comment.