Skip to content

Commit

Permalink
Feature/registerAsync (Papooch#7)
Browse files Browse the repository at this point in the history
* refactor: prepare providers for registerAsync

* feat: add registerAsync

* test: registerAsync

* docs: update readme

* docs: updated docstrings
  • Loading branch information
Papooch authored Oct 25, 2021
1 parent cde9f6b commit 7f25d60
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 50 deletions.
35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,18 +240,20 @@ The injectable `ClsService` provides the following API to manipulate the cls con

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

- **_`namespaceName`_: `string`**
The name of the cls namespace. This is the namespace that will be used by the ClsService and ClsMiddleware (most of the time you will not need to touch this setting)
- **_`global:`_ `boolean`** (default _`false`_)
Whether to make the module global, so you do not have to import `ClsModule.forFeature()` in other modules.
- **_`middleware:`_ `ClsMiddlewareOptions`**
An object with additional options for the ClsMiddleware, see below
An object with additional options for the `ClsMiddleware`, see below
- **_`guard:`_ `ClsGuardOptions`**
An object with additional options for the ClsGuard, see below
An object with additional options for the `ClsGuard`, see below
- **_`interceptor:`_ `ClsInterceptorOptions`**
An object with additional options for the ClsInterceptor, see below
An object with additional options for the `ClsInterceptor`, see below
- **_`global:`_ `boolean`** (default _`false`_)
Whether to make the module global, so you do not have to import `ClsModule.forFeature()` in other modules.
- **_`namespaceName`_: `string`** (default _unset_)
The namespace that will be set up. When used, `ClsService` must be injected using the `@InjectCls('name')` decorator. (most of the time you will not need to touch this setting)

> Important: the `middleware`, `guard` and `interceptor` options are _mutually exclusive_ - do not use more than one of them, otherwise the context will get overridden with the one that runs after.
> **Please note**: the `middleware`, `guard` and `interceptor` options are _mutually exclusive_ - do not use more than one of them, otherwise the context will get overridden with the one that runs after.
`ClsModule.registerAsync()` is also available. You can supply the usual `imports`, `inject` and `useFactory` parameters.

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

Expand Down Expand Up @@ -408,9 +410,7 @@ Use the `ClsGuard` or `ClsInterceptor` to set up context with any other platform

> Warning: Namespace support is currently experimental and has no tests. While the API is mostly stable now, it can still change any time.
The default CLS namespace that the `ClsService` provides should be enough for most application, but should you need it, this package provides a way to use multiple CLS namespaces in order to be fully compatible with `cls-hooked`.

> **Note**: Since cls-hooked was ditched in version 1.2, it is no longer necessary to strive for compatibility with it. Still, the namespace support was there and there's no reason to remove it.
The default CLS namespace that the `ClsService` provides should be enough for most application, but should you need it, this package provides a way to use multiple CLS namespaces simultaneously.

To use custom namespace provider, use `ClsModule.forFeature('my-namespace')`.

Expand All @@ -426,6 +426,8 @@ export class HelloModule {}
This creates a namespaces `ClsService` provider that you can inject using `@InjectCls`

```ts
// hello.service.ts

@Injectable()
class HelloService {
constructor(
Expand All @@ -434,14 +436,11 @@ class HelloService {
) {}

sayHello() {
return this.myCls.get('hi');
return this.myCls.run('hi');
}
}
```

> **Note**: `@InjectCls('x')` is equivalent to `@Inject(getNamespaceToken('x'))`. If you don't pass an argument to `@InjectCls()`, the default ClsService will be injected and is equivalent to omitting the decorator altogether.

```ts
// hello.controller.ts
@Injectable()
export class HelloController {
constructor(
Expand All @@ -452,11 +451,13 @@ export class HelloController {

@Get('/hello')
hello2() {
// seting up cls context manually
// setting up cls context manually
return this.myCls.run(() => {
this.myCls.set('hi', 'Hello');
return this.helloService.sayHello();
});
}
}
```

> **Note**: `@InjectCls('x')` is equivalent to `@Inject(getNamespaceToken('x'))`. If you don't pass an argument to `@InjectCls()`, the default ClsService will be injected and is equivalent to omitting the decorator altogether.
8 changes: 6 additions & 2 deletions src/lib/cls-service-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ export const getClsServiceToken = (namespace: string) =>
`ClsService-${namespace}`;

export class ClsServiceManager {
private static namespaces: Record<string, AsyncLocalStorage<any>> = {};
private static namespaces: Record<
string,
AsyncLocalStorage<any> & { name?: string }
> = {};

private static clsServices: Map<string | typeof ClsService, ClsService> =
new Map([
Expand All @@ -20,11 +23,12 @@ export class ClsServiceManager {
private static resolveNamespace(name: string) {
if (!this.namespaces[name]) {
this.namespaces[name] = new AsyncLocalStorage();
this.namespaces[name].name = name;
}
return this.namespaces[name];
}

static addClsService(name: string) {
static addClsService(name: string = CLS_DEFAULT_NAMESPACE) {
const service = new ClsService(this.resolveNamespace(name));
this.clsServices.set(
getClsServiceToken(name),
Expand Down
1 change: 1 addition & 0 deletions src/lib/cls.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const CLS_REQ = 'CLS_REQUEST';
export const CLS_RES = 'CLS_RESPONSE';
export const CLS_ID = 'CLS_ID';
export const CLS_DEFAULT_NAMESPACE = 'CLS_DEFAULT_NAMESPACE';
export const CLS_MODULE_OPTIONS = 'ClsModuleOptions';
export const CLS_MIDDLEWARE_OPTIONS = 'ClsMiddlewareOptions';
export const CLS_GUARD_OPTIONS = 'ClsGuardOptions';
export const CLS_INTERCEPTOR_OPTIONS = 'ClsInterceptorOptions';
46 changes: 34 additions & 12 deletions src/lib/cls.interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,57 @@
import { ExecutionContext } from '@nestjs/common';
import { ExecutionContext, ModuleMetadata } from '@nestjs/common';
import { CLS_DEFAULT_NAMESPACE } from './cls.constants';
import { ClsService } from './cls.service';

export class ClsModuleOptions {
/**
* The name of the cls namespace. This is the namespace
* that will be used by the ClsService and ClsMiddleware/Interc
* (most of the time you will not need to touch this setting)
*/
namespaceName? = CLS_DEFAULT_NAMESPACE;

/**
* whether to make the module global, so you don't need
* to import `ClsModule` in other modules
* to import ClsModule.forFeature()` in other modules
*/
global? = false;

/**
* Cls middleware options
* An object with additional options for the `ClsMiddleware`
*/
middleware?: ClsMiddlewareOptions = null;

/**
* Cls guard options
* An object with additional options for the `ClsGuard`
*/
guard?: ClsGuardOptions = null;

/**
* Cls interceptor options
* An object with additional options for the `ClsInterceptor`
*/
interceptor?: ClsInterceptorOptions = null;

/**
* The namespace that will be set up. When used, `ClsService`
* must be injected using the `@InjectCls('name')` decorator.
* (most of the time you will not need to touch this setting)
*/
namespaceName? = CLS_DEFAULT_NAMESPACE;
}

export type ClsModuleFactoryOptions = Omit<
ClsModuleOptions,
'global' | 'namespaceName'
>;
export interface ClsModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
inject?: any[];
useFactory?: (
...args: any[]
) => Promise<ClsModuleFactoryOptions> | ClsModuleFactoryOptions;
/**
* whether to make the module global, so you don't need
* to import `ClsModule.forFeature()` in other modules
*/
global?: boolean;
/**
* The namespace that will be set up. When used, `ClsService`
* must be injected using the `@InjectCls('name')` decorator.
* (most of the time you will not need to touch this setting)
*/
namespaceName?: string;
}

export class ClsMiddlewareOptions {
Expand Down
120 changes: 101 additions & 19 deletions src/lib/cls.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
CanActivate,
DynamicModule,
Logger,
MiddlewareConsumer,
Module,
NestInterceptor,
NestModule,
Provider,
} from '@nestjs/common';
Expand All @@ -12,6 +14,7 @@ import {
HttpAdapterHost,
ModuleRef,
} from '@nestjs/core';
import { CLS_MODULE_OPTIONS } from '..';
import { ClsServiceManager, getClsServiceToken } from './cls-service-manager';
import {
CLS_GUARD_OPTIONS,
Expand All @@ -24,6 +27,7 @@ import {
ClsGuardOptions,
ClsInterceptorOptions,
ClsMiddlewareOptions,
ClsModuleAsyncOptions,
ClsModuleOptions,
} from './cls.interfaces';

Expand All @@ -49,7 +53,7 @@ export class ClsModule implements NestModule {
// we are running configure, so we mount the middleware
options = this.moduleRef.get(CLS_MIDDLEWARE_OPTIONS);
} catch (e) {
// we are running static import, so do not mount it
// we are running forFeature import, so do not mount it
return;
}

Expand Down Expand Up @@ -78,58 +82,136 @@ export class ClsModule implements NestModule {
};
}

static register(options?: ClsModuleOptions): DynamicModule {
options = { ...new ClsModuleOptions(), ...options };
ClsServiceManager.addClsService(options.namespaceName);
private static clsMiddlewareOptionsFactory(
options: ClsModuleOptions,
): ClsMiddlewareOptions {
const clsMiddlewareOptions = {
...new ClsMiddlewareOptions(),
...options.middleware,
namespaceName: options.namespaceName,
};
return clsMiddlewareOptions;
}

private static clsGuardOptionsFactory(
options: ClsModuleOptions,
): ClsGuardOptions {
const clsGuardOptions = {
...new ClsGuardOptions(),
...options.guard,
namespaceName: options.namespaceName,
};
return clsGuardOptions;
}

private static clsInterceptorOptionsFactory(
options: ClsModuleOptions,
): ClsInterceptorOptions {
const clsInterceptorOptions = {
...new ClsInterceptorOptions(),
...options.interceptor,
namespaceName: options.namespaceName,
};
return clsInterceptorOptions;
}

private static clsGuardFactory(options: ClsGuardOptions): CanActivate {
if (options.mount) {
ClsModule.logger.debug('ClsGuard will be automatically mounted');
return new ClsGuard(options);
}
return {
canActivate: () => true,
};
}

private static clsInterceptorFactory(
options: ClsInterceptorOptions,
): NestInterceptor {
if (options.mount) {
ClsModule.logger.debug(
'ClsInterceptor will be automatically mounted',
);
return new ClsInterceptor(options);
}
return {
intercept: (_, next) => next.handle(),
};
}

private static getProviders(options: { namespaceName?: string }) {
ClsServiceManager.addClsService(options.namespaceName);
const providers: Provider[] = [
...ClsServiceManager.getClsServicesAsProviders(),
{
provide: CLS_MIDDLEWARE_OPTIONS,
useValue: clsMiddlewareOptions,
inject: [CLS_MODULE_OPTIONS],
useFactory: this.clsMiddlewareOptionsFactory,
},
{
provide: CLS_GUARD_OPTIONS,
useValue: clsGuardOptions,
inject: [CLS_MODULE_OPTIONS],
useFactory: this.clsGuardOptionsFactory,
},
{
provide: CLS_INTERCEPTOR_OPTIONS,
useValue: clsInterceptorOptions,
inject: [CLS_MODULE_OPTIONS],
useFactory: this.clsInterceptorOptionsFactory,
},
];
const enhancerArr = [];
if (clsGuardOptions.mount) {
enhancerArr.push({
const enhancerArr: Provider[] = [
{
provide: APP_GUARD,
useClass: ClsGuard,
});
}
if (clsInterceptorOptions.mount) {
enhancerArr.push({
inject: [CLS_GUARD_OPTIONS],
useFactory: this.clsGuardFactory,
},
{
provide: APP_INTERCEPTOR,
useClass: ClsInterceptor,
});
}
inject: [CLS_INTERCEPTOR_OPTIONS],
useFactory: this.clsInterceptorFactory,
},
];

return {
module: ClsModule,
providers: providers.concat(...enhancerArr),
exports: providers,
};
}

static register(options?: ClsModuleOptions): DynamicModule {
options = { ...new ClsModuleOptions(), ...options };
const { providers, exports } = this.getProviders(options);

return {
module: ClsModule,
providers: [
{
provide: CLS_MODULE_OPTIONS,
useValue: options,
},
...providers,
],
exports,
global: options.global,
};
}

static registerAsync(asyncOptions: ClsModuleAsyncOptions): DynamicModule {
const { providers, exports } = this.getProviders(asyncOptions);

return {
module: ClsModule,
imports: asyncOptions.imports,
providers: [
{
provide: CLS_MODULE_OPTIONS,
inject: asyncOptions.inject,
useFactory: asyncOptions.useFactory,
},
...providers,
],
exports,
global: asyncOptions.global,
};
}
}
5 changes: 5 additions & 0 deletions test/rest/main-express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { TestHttpController, TestHttpService } from './http.app';

@Module({
imports: [
// ClsModule.registerAsync({
// useFactory: () => ({
// middleware: { mount: true, generateId: true },
// }),
// }),
ClsModule.register({
middleware: { mount: true, generateId: true },
}),
Expand Down
Loading

0 comments on commit 7f25d60

Please sign in to comment.