nestjs-cls
Advanced tools
Comparing version
@@ -0,0 +0,0 @@ # Contributing to `nestjs-cls` |
@@ -1,12 +0,7 @@ | ||
import { ClassProvider, ValueProvider } from '@nestjs/common'; | ||
import { ClsStore } from './cls.interfaces'; | ||
import { ClsService } from './cls.service'; | ||
export declare function getClsServiceToken(): string; | ||
export declare function getClsServiceToken(namespace: string): string; | ||
export declare class ClsServiceManager { | ||
private static namespaces; | ||
private static clsServices; | ||
private static resolveNamespace; | ||
static addClsService(name?: string): ClsService<import("./cls.interfaces").ClsStore>; | ||
static getClsService(name?: string): ClsService<import("./cls.interfaces").ClsStore>; | ||
static getClsServicesAsProviders(): Array<ClassProvider<ClsService> | ValueProvider<ClsService>>; | ||
private static clsService; | ||
static getClsService<T extends ClsStore = ClsStore>(): ClsService<T>; | ||
static resolveProxyProviders(): Promise<void[]>; | ||
} |
"use strict"; | ||
var _a; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ClsServiceManager = exports.getClsServiceToken = void 0; | ||
const cls_constants_1 = require("./cls.constants"); | ||
const cls_service_1 = require("./cls.service"); | ||
const async_hooks_1 = require("async_hooks"); | ||
function getClsServiceToken(namespace = cls_constants_1.CLS_DEFAULT_NAMESPACE) { | ||
return `ClsService-${namespace}`; | ||
} | ||
exports.getClsServiceToken = getClsServiceToken; | ||
exports.ClsServiceManager = void 0; | ||
const cls_service_globals_1 = require("./cls-service.globals"); | ||
const proxy_provider_manager_1 = require("./proxy-provider/proxy-provider-manager"); | ||
class ClsServiceManager { | ||
static resolveNamespace(name) { | ||
if (!this.namespaces[name]) { | ||
this.namespaces[name] = new async_hooks_1.AsyncLocalStorage(); | ||
this.namespaces[name].name = name; | ||
} | ||
return this.namespaces[name]; | ||
} | ||
static addClsService(name = cls_constants_1.CLS_DEFAULT_NAMESPACE) { | ||
const service = new cls_service_1.ClsService(this.resolveNamespace(name)); | ||
this.clsServices.set(getClsServiceToken(name), new cls_service_1.ClsService(this.resolveNamespace(name))); | ||
return service; | ||
} | ||
static getClsService(name) { | ||
const cls = this.clsServices.get(name ? getClsServiceToken(name) : cls_service_1.ClsService); | ||
if (!cls) | ||
throw new Error(`ClsService with namespace ${name} does not exist`); | ||
static getClsService() { | ||
const cls = this.clsService; | ||
return cls; | ||
} | ||
static getClsServicesAsProviders() { | ||
return Array.from(this.clsServices.entries()).map(([provide, service]) => ({ | ||
provide, | ||
useValue: service, | ||
})); | ||
static async resolveProxyProviders() { | ||
return await proxy_provider_manager_1.ProxyProviderManager.resolveProxyProviders(); | ||
} | ||
} | ||
exports.ClsServiceManager = ClsServiceManager; | ||
_a = ClsServiceManager; | ||
ClsServiceManager.namespaces = {}; | ||
ClsServiceManager.clsServices = new Map([ | ||
[ | ||
cls_service_1.ClsService, | ||
new cls_service_1.ClsService(_a.resolveNamespace(cls_constants_1.CLS_DEFAULT_NAMESPACE)), | ||
], | ||
]); | ||
ClsServiceManager.clsService = cls_service_globals_1.globalClsSevice; | ||
//# sourceMappingURL=cls-service-manager.js.map |
export declare const CLS_REQ: unique symbol; | ||
export declare const CLS_RES: unique symbol; | ||
export declare const CLS_ID: unique symbol; | ||
export declare const CLS_DEFAULT_NAMESPACE = "CLS_DEFAULT_NAMESPACE"; | ||
export declare const CLS_MODULE_OPTIONS = "ClsModuleOptions"; | ||
@@ -6,0 +5,0 @@ export declare const CLS_MIDDLEWARE_OPTIONS = "ClsMiddlewareOptions"; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.CLS_INTERCEPTOR_OPTIONS = exports.CLS_GUARD_OPTIONS = exports.CLS_MIDDLEWARE_OPTIONS = exports.CLS_MODULE_OPTIONS = exports.CLS_DEFAULT_NAMESPACE = exports.CLS_ID = exports.CLS_RES = exports.CLS_REQ = void 0; | ||
exports.CLS_INTERCEPTOR_OPTIONS = exports.CLS_GUARD_OPTIONS = exports.CLS_MIDDLEWARE_OPTIONS = exports.CLS_MODULE_OPTIONS = exports.CLS_ID = exports.CLS_RES = exports.CLS_REQ = void 0; | ||
exports.CLS_REQ = Symbol('CLS_REQUEST'); | ||
exports.CLS_RES = Symbol('CLS_RESPONSE'); | ||
exports.CLS_ID = Symbol('CLS_ID'); | ||
exports.CLS_DEFAULT_NAMESPACE = 'CLS_DEFAULT_NAMESPACE'; | ||
exports.CLS_MODULE_OPTIONS = 'ClsModuleOptions'; | ||
@@ -9,0 +8,0 @@ exports.CLS_MIDDLEWARE_OPTIONS = 'ClsMiddlewareOptions'; |
@@ -1,2 +0,2 @@ | ||
export declare function InjectCls(): (target: any, key: string | symbol, index?: number) => void; | ||
export declare function InjectCls(namespace: string): (target: any, key: string | symbol, index?: number) => void; | ||
export declare function InjectCls(): (target: object, key: string | symbol, index?: number) => void; | ||
export declare function InjectableProxy(): (target: any) => any; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.InjectCls = void 0; | ||
exports.InjectableProxy = exports.InjectCls = void 0; | ||
const common_1 = require("@nestjs/common"); | ||
const cls_service_manager_1 = require("./cls-service-manager"); | ||
const cls_constants_1 = require("./cls.constants"); | ||
function InjectCls(namespace = cls_constants_1.CLS_DEFAULT_NAMESPACE) { | ||
return (0, common_1.Inject)((0, cls_service_manager_1.getClsServiceToken)(namespace)); | ||
const cls_service_1 = require("./cls.service"); | ||
const proxy_provider_1 = require("./proxy-provider"); | ||
function InjectCls() { | ||
return (0, common_1.Inject)(cls_service_1.ClsService); | ||
} | ||
exports.InjectCls = InjectCls; | ||
function InjectableProxy() { | ||
return (target) => (0, common_1.Injectable)()((0, common_1.SetMetadata)(proxy_provider_1.CLS_PROXY_METADATA_KEY, true)(target)); | ||
} | ||
exports.InjectableProxy = InjectableProxy; | ||
//# sourceMappingURL=cls.decorators.js.map |
@@ -26,3 +26,3 @@ "use strict"; | ||
async canActivate(context) { | ||
const cls = cls_service_manager_1.ClsServiceManager.getClsService(this.options.namespaceName); | ||
const cls = cls_service_manager_1.ClsServiceManager.getClsService(); | ||
return cls.exit(async () => { | ||
@@ -37,2 +37,3 @@ cls.enter(); | ||
} | ||
await cls_service_manager_1.ClsServiceManager.resolveProxyProviders(); | ||
return true; | ||
@@ -39,0 +40,0 @@ }); |
@@ -27,3 +27,3 @@ "use strict"; | ||
intercept(context, next) { | ||
const cls = cls_service_manager_1.ClsServiceManager.getClsService(this.options.namespaceName); | ||
const cls = cls_service_manager_1.ClsServiceManager.getClsService(); | ||
return new rxjs_1.Observable((subscriber) => { | ||
@@ -38,9 +38,15 @@ cls.run(async () => { | ||
} | ||
next.handle() | ||
.pipe() | ||
.subscribe({ | ||
next: (res) => subscriber.next(res), | ||
error: (err) => subscriber.error(err), | ||
complete: () => subscriber.complete(), | ||
}); | ||
try { | ||
await cls_service_manager_1.ClsServiceManager.resolveProxyProviders(); | ||
next.handle() | ||
.pipe() | ||
.subscribe({ | ||
next: (res) => subscriber.next(res), | ||
error: (err) => subscriber.error(err), | ||
complete: () => subscriber.complete(), | ||
}); | ||
} | ||
catch (e) { | ||
subscriber.error(e); | ||
} | ||
}); | ||
@@ -47,0 +53,0 @@ }); |
@@ -1,2 +0,2 @@ | ||
import { ExecutionContext, ModuleMetadata } from '@nestjs/common'; | ||
import { ExecutionContext, ModuleMetadata, Type } from '@nestjs/common'; | ||
import { ClsService } from './cls.service'; | ||
@@ -8,5 +8,5 @@ export declare class ClsModuleOptions { | ||
interceptor?: ClsInterceptorOptions; | ||
namespaceName?: string; | ||
proxyProviders?: Type[]; | ||
} | ||
export declare type ClsModuleFactoryOptions = Omit<ClsModuleOptions, 'global' | 'namespaceName'>; | ||
export declare type ClsModuleFactoryOptions = Omit<ClsModuleOptions, 'global' | 'providers'>; | ||
export interface ClsModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> { | ||
@@ -16,3 +16,3 @@ inject?: any[]; | ||
global?: boolean; | ||
namespaceName?: string; | ||
providers?: Type[]; | ||
} | ||
@@ -27,3 +27,2 @@ export declare class ClsMiddlewareOptions { | ||
useEnterWith?: boolean; | ||
readonly namespaceName?: string; | ||
} | ||
@@ -35,3 +34,2 @@ export declare class ClsGuardOptions { | ||
setup?: (cls: ClsService, context: ExecutionContext) => void | Promise<void>; | ||
readonly namespaceName?: string; | ||
} | ||
@@ -43,3 +41,2 @@ export declare class ClsInterceptorOptions { | ||
setup?: (cls: ClsService, context: ExecutionContext) => void | Promise<void>; | ||
readonly namespaceName?: string; | ||
} | ||
@@ -46,0 +43,0 @@ export interface ClsStore { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ClsInterceptorOptions = exports.ClsGuardOptions = exports.ClsMiddlewareOptions = exports.ClsModuleOptions = void 0; | ||
const cls_constants_1 = require("./cls.constants"); | ||
class ClsModuleOptions { | ||
@@ -11,3 +10,2 @@ constructor() { | ||
this.interceptor = null; | ||
this.namespaceName = cls_constants_1.CLS_DEFAULT_NAMESPACE; | ||
} | ||
@@ -14,0 +12,0 @@ } |
@@ -7,3 +7,3 @@ import { NestMiddleware } from '@nestjs/common'; | ||
constructor(options?: Omit<ClsMiddlewareOptions, 'mount'>); | ||
use: (req: any, res: any, next: () => any) => Promise<void>; | ||
use: (req: any, res: any, next: (err?: any) => any) => Promise<void>; | ||
} |
@@ -19,3 +19,2 @@ "use strict"; | ||
const cls_constants_1 = require("./cls.constants"); | ||
const cls_constants_2 = require("./cls.constants"); | ||
const cls_interfaces_1 = require("./cls.interfaces"); | ||
@@ -26,3 +25,3 @@ let ClsMiddleware = class ClsMiddleware { | ||
this.use = async (req, res, next) => { | ||
const cls = cls_service_manager_1.ClsServiceManager.getClsService(this.options.namespaceName); | ||
const cls = cls_service_manager_1.ClsServiceManager.getClsService(); | ||
const callback = async () => { | ||
@@ -35,9 +34,15 @@ this.options.useEnterWith && cls.enter(); | ||
if (this.options.saveReq) | ||
cls.set(cls_constants_2.CLS_REQ, req); | ||
cls.set(cls_constants_1.CLS_REQ, req); | ||
if (this.options.saveRes) | ||
cls.set(cls_constants_2.CLS_RES, res); | ||
cls.set(cls_constants_1.CLS_RES, res); | ||
if (this.options.setup) { | ||
await this.options.setup(cls, req); | ||
} | ||
next(); | ||
try { | ||
await cls_service_manager_1.ClsServiceManager.resolveProxyProviders(); | ||
next(); | ||
} | ||
catch (e) { | ||
next(e); | ||
} | ||
}; | ||
@@ -44,0 +49,0 @@ const runner = this.options.useEnterWith |
@@ -1,4 +0,5 @@ | ||
import { DynamicModule, MiddlewareConsumer, NestModule } from '@nestjs/common'; | ||
import { DynamicModule, MiddlewareConsumer, NestModule, Type } from '@nestjs/common'; | ||
import { HttpAdapterHost, ModuleRef } from '@nestjs/core'; | ||
import { ClsModuleAsyncOptions, ClsModuleOptions } from './cls.interfaces'; | ||
import { ClsModuleProxyProviderOptions } from './proxy-provider/proxy-provider.interfaces'; | ||
export declare class ClsModule implements NestModule { | ||
@@ -10,4 +11,8 @@ private readonly adapterHost; | ||
configure(consumer: MiddlewareConsumer): void; | ||
static forRoot(options?: ClsModuleOptions): DynamicModule; | ||
static forRootAsync(asyncOptions: ClsModuleAsyncOptions): DynamicModule; | ||
static forFeature(): DynamicModule; | ||
static forFeature(namespaceName: string): DynamicModule; | ||
static forFeature(...requestScopedProviders: Array<Type>): DynamicModule; | ||
static forFeatureAsync(options: ClsModuleProxyProviderOptions): DynamicModule; | ||
private static getProviders; | ||
private static clsMiddlewareOptionsFactory; | ||
@@ -18,5 +23,2 @@ private static clsGuardOptionsFactory; | ||
private static clsInterceptorFactory; | ||
private static getProviders; | ||
static register(options?: ClsModuleOptions): DynamicModule; | ||
static registerAsync(asyncOptions: ClsModuleAsyncOptions): DynamicModule; | ||
} |
@@ -23,2 +23,12 @@ "use strict"; | ||
const cls_service_1 = require("./cls.service"); | ||
const proxy_provider_manager_1 = require("./proxy-provider/proxy-provider-manager"); | ||
const clsServiceProvider = { | ||
provide: cls_service_1.ClsService, | ||
useValue: cls_service_manager_1.ClsServiceManager.getClsService(), | ||
}; | ||
const commonProviders = [ | ||
clsServiceProvider, | ||
proxy_provider_manager_1.ProxyProviderManager.createProxyProviderFromExistingKey(cls_constants_1.CLS_REQ), | ||
proxy_provider_manager_1.ProxyProviderManager.createProxyProviderFromExistingKey(cls_constants_1.CLS_RES), | ||
]; | ||
let ClsModule = ClsModule_1 = class ClsModule { | ||
@@ -47,45 +57,69 @@ constructor(adapterHost, moduleRef) { | ||
} | ||
static forFeature(namespaceName) { | ||
const providers = cls_service_manager_1.ClsServiceManager.getClsServicesAsProviders().filter((p) => p.provide === (0, cls_service_manager_1.getClsServiceToken)(namespaceName) || | ||
p.provide === cls_service_1.ClsService); | ||
static forRoot(options) { | ||
var _a, _b; | ||
options = Object.assign(Object.assign({}, new cls_interfaces_1.ClsModuleOptions()), options); | ||
const { providers, exports } = this.getProviders(); | ||
const proxyProviders = (_b = (_a = options.proxyProviders) === null || _a === void 0 ? void 0 : _a.map((providerClass) => proxy_provider_manager_1.ProxyProviderManager.createProxyProvider({ | ||
useClass: providerClass, | ||
}))) !== null && _b !== void 0 ? _b : []; | ||
return { | ||
module: ClsModule_1, | ||
providers, | ||
exports: providers, | ||
providers: [ | ||
{ | ||
provide: cls_constants_1.CLS_MODULE_OPTIONS, | ||
useValue: options, | ||
}, | ||
...providers, | ||
...proxyProviders, | ||
], | ||
exports: [...exports, ...proxyProviders.map((p) => p.provide)], | ||
global: options.global, | ||
}; | ||
} | ||
static clsMiddlewareOptionsFactory(options) { | ||
const clsMiddlewareOptions = Object.assign(Object.assign(Object.assign({}, new cls_interfaces_1.ClsMiddlewareOptions()), options.middleware), { namespaceName: options.namespaceName }); | ||
return clsMiddlewareOptions; | ||
static forRootAsync(asyncOptions) { | ||
const { providers, exports } = this.getProviders(); | ||
return { | ||
module: ClsModule_1, | ||
imports: asyncOptions.imports, | ||
providers: [ | ||
{ | ||
provide: cls_constants_1.CLS_MODULE_OPTIONS, | ||
inject: asyncOptions.inject, | ||
useFactory: asyncOptions.useFactory, | ||
}, | ||
...providers, | ||
], | ||
exports, | ||
global: asyncOptions.global, | ||
}; | ||
} | ||
static clsGuardOptionsFactory(options) { | ||
const clsGuardOptions = Object.assign(Object.assign(Object.assign({}, new cls_interfaces_1.ClsGuardOptions()), options.guard), { namespaceName: options.namespaceName }); | ||
return clsGuardOptions; | ||
} | ||
static clsInterceptorOptionsFactory(options) { | ||
const clsInterceptorOptions = Object.assign(Object.assign(Object.assign({}, new cls_interfaces_1.ClsInterceptorOptions()), options.interceptor), { namespaceName: options.namespaceName }); | ||
return clsInterceptorOptions; | ||
} | ||
static clsGuardFactory(options) { | ||
if (options.mount) { | ||
ClsModule_1.logger.debug('ClsGuard will be automatically mounted'); | ||
return new cls_guard_1.ClsGuard(options); | ||
} | ||
static forFeature(...requestScopedProviders) { | ||
var _a; | ||
const proxyProviders = (_a = requestScopedProviders.map((providerClass) => proxy_provider_manager_1.ProxyProviderManager.createProxyProvider({ | ||
useClass: providerClass, | ||
}))) !== null && _a !== void 0 ? _a : []; | ||
const providers = [...commonProviders]; | ||
return { | ||
canActivate: () => true, | ||
module: ClsModule_1, | ||
providers: [...providers, ...proxyProviders], | ||
exports: [...providers, ...proxyProviders.map((p) => p.provide)], | ||
}; | ||
} | ||
static clsInterceptorFactory(options) { | ||
if (options.mount) { | ||
ClsModule_1.logger.debug('ClsInterceptor will be automatically mounted'); | ||
return new cls_interceptor_1.ClsInterceptor(options); | ||
} | ||
static forFeatureAsync(options) { | ||
var _a, _b; | ||
const proxyProvider = proxy_provider_manager_1.ProxyProviderManager.createProxyProvider(options); | ||
const providers = [ | ||
...commonProviders, | ||
...((_a = options.extraProviders) !== null && _a !== void 0 ? _a : []), | ||
]; | ||
return { | ||
intercept: (_, next) => next.handle(), | ||
module: ClsModule_1, | ||
imports: (_b = options.imports) !== null && _b !== void 0 ? _b : [], | ||
providers: [...providers, proxyProvider], | ||
exports: [...commonProviders, proxyProvider.provide], | ||
}; | ||
} | ||
static getProviders(options) { | ||
cls_service_manager_1.ClsServiceManager.addClsService(options.namespaceName); | ||
static getProviders() { | ||
const providers = [ | ||
...cls_service_manager_1.ClsServiceManager.getClsServicesAsProviders(), | ||
...commonProviders, | ||
{ | ||
@@ -124,33 +158,30 @@ provide: cls_constants_1.CLS_MIDDLEWARE_OPTIONS, | ||
} | ||
static register(options) { | ||
options = Object.assign(Object.assign({}, new cls_interfaces_1.ClsModuleOptions()), options); | ||
const { providers, exports } = this.getProviders(options); | ||
static clsMiddlewareOptionsFactory(options) { | ||
const clsMiddlewareOptions = Object.assign(Object.assign({}, new cls_interfaces_1.ClsMiddlewareOptions()), options.middleware); | ||
return clsMiddlewareOptions; | ||
} | ||
static clsGuardOptionsFactory(options) { | ||
const clsGuardOptions = Object.assign(Object.assign({}, new cls_interfaces_1.ClsGuardOptions()), options.guard); | ||
return clsGuardOptions; | ||
} | ||
static clsInterceptorOptionsFactory(options) { | ||
const clsInterceptorOptions = Object.assign(Object.assign({}, new cls_interfaces_1.ClsInterceptorOptions()), options.interceptor); | ||
return clsInterceptorOptions; | ||
} | ||
static clsGuardFactory(options) { | ||
if (options.mount) { | ||
ClsModule_1.logger.debug('ClsGuard will be automatically mounted'); | ||
return new cls_guard_1.ClsGuard(options); | ||
} | ||
return { | ||
module: ClsModule_1, | ||
providers: [ | ||
{ | ||
provide: cls_constants_1.CLS_MODULE_OPTIONS, | ||
useValue: options, | ||
}, | ||
...providers, | ||
], | ||
exports, | ||
global: options.global, | ||
canActivate: () => true, | ||
}; | ||
} | ||
static registerAsync(asyncOptions) { | ||
const { providers, exports } = this.getProviders(asyncOptions); | ||
static clsInterceptorFactory(options) { | ||
if (options.mount) { | ||
ClsModule_1.logger.debug('ClsInterceptor will be automatically mounted'); | ||
return new cls_interceptor_1.ClsInterceptor(options); | ||
} | ||
return { | ||
module: ClsModule_1, | ||
imports: asyncOptions.imports, | ||
providers: [ | ||
{ | ||
provide: cls_constants_1.CLS_MODULE_OPTIONS, | ||
inject: asyncOptions.inject, | ||
useFactory: asyncOptions.useFactory, | ||
}, | ||
...providers, | ||
], | ||
exports, | ||
global: asyncOptions.global, | ||
intercept: (_, next) => next.handle(), | ||
}; | ||
@@ -162,4 +193,4 @@ } | ||
(0, common_1.Module)({ | ||
providers: [...cls_service_manager_1.ClsServiceManager.getClsServicesAsProviders()], | ||
exports: [...cls_service_manager_1.ClsServiceManager.getClsServicesAsProviders()], | ||
providers: [...commonProviders], | ||
exports: [...commonProviders], | ||
}), | ||
@@ -166,0 +197,0 @@ __metadata("design:paramtypes", [core_1.HttpAdapterHost, |
@@ -7,4 +7,4 @@ /// <reference types="node" /> | ||
export declare class ClsService<S extends ClsStore = ClsStore> { | ||
private readonly namespace; | ||
constructor(namespace: AsyncLocalStorage<any>); | ||
private readonly als; | ||
constructor(als: AsyncLocalStorage<any>); | ||
set<R = undefined, T extends RecursiveKeyOf<S> = any, P extends DeepPropertyType<S, T> = any>(key: StringIfNever<T> | keyof ClsStore, value: AnyIfNever<P>): void; | ||
@@ -11,0 +11,0 @@ get(): AnyIfNever<S>; |
@@ -7,9 +7,9 @@ "use strict"; | ||
class ClsService { | ||
constructor(namespace) { | ||
this.namespace = namespace; | ||
constructor(als) { | ||
this.als = als; | ||
} | ||
set(key, value) { | ||
const store = this.namespace.getStore(); | ||
const store = this.als.getStore(); | ||
if (!store) { | ||
throw new Error(`Cannot se the key "${String(key)}". No cls context available in namespace "${this.namespace['name']}", please make sure that a ClsMiddleware/Guard/Interceptor has set up the context, or wrap any calls that depend on cls with "ClsService#run"`); | ||
throw new Error(`Cannot se the key "${String(key)}". No CLS context available, please make sure that a ClsMiddleware/Guard/Interceptor has set up the context, or wrap any calls that depend on CLS with "ClsService#run"`); | ||
} | ||
@@ -24,3 +24,3 @@ if (typeof key === 'symbol') { | ||
get(key) { | ||
const store = this.namespace.getStore(); | ||
const store = this.als.getStore(); | ||
if (!key) | ||
@@ -34,3 +34,3 @@ return store; | ||
has(key) { | ||
const store = this.namespace.getStore(); | ||
const store = this.als.getStore(); | ||
if (typeof key === 'symbol') { | ||
@@ -42,22 +42,22 @@ return !!store[key]; | ||
getId() { | ||
const store = this.namespace.getStore(); | ||
const store = this.als.getStore(); | ||
return store === null || store === void 0 ? void 0 : store[cls_constants_1.CLS_ID]; | ||
} | ||
run(callback) { | ||
return this.namespace.run({}, callback); | ||
return this.als.run({}, callback); | ||
} | ||
runWith(store, callback) { | ||
return this.namespace.run(store !== null && store !== void 0 ? store : {}, callback); | ||
return this.als.run(store !== null && store !== void 0 ? store : {}, callback); | ||
} | ||
enter() { | ||
return this.namespace.enterWith({}); | ||
return this.als.enterWith({}); | ||
} | ||
enterWith(store) { | ||
return this.namespace.enterWith(store !== null && store !== void 0 ? store : {}); | ||
return this.als.enterWith(store !== null && store !== void 0 ? store : {}); | ||
} | ||
exit(callback) { | ||
return this.namespace.exit(callback); | ||
return this.als.exit(callback); | ||
} | ||
isActive() { | ||
return !!this.namespace.getStore(); | ||
return !!this.als.getStore(); | ||
} | ||
@@ -64,0 +64,0 @@ } |
@@ -15,3 +15,3 @@ "use strict"; | ||
provide: cls_service_1.ClsService, | ||
useValue: cls_service_manager_1.ClsServiceManager.addClsService(cls_constants_1.CLS_DEFAULT_NAMESPACE), | ||
useValue: cls_service_manager_1.ClsServiceManager.getClsService(), | ||
}, | ||
@@ -18,0 +18,0 @@ ], |
{ | ||
"name": "nestjs-cls", | ||
"version": "2.2.1", | ||
"version": "3.0.0", | ||
"description": "A continuation-local storage module compatible with NestJS's dependency injection.", | ||
@@ -24,3 +24,4 @@ "author": "papooch", | ||
"async_hooks", | ||
"request context" | ||
"request context", | ||
"async context" | ||
], | ||
@@ -45,12 +46,12 @@ "main": "dist/src/index.js", | ||
"devDependencies": { | ||
"@nestjs/apollo": "^10.0.17", | ||
"@nestjs/cli": "^9.0.0", | ||
"@nestjs/common": "^9.0.4", | ||
"@nestjs/core": "^9.0.4", | ||
"@nestjs/graphql": "^10.0.18", | ||
"@nestjs/mercurius": "^10.0.17", | ||
"@nestjs/platform-express": "^9.0.4", | ||
"@nestjs/platform-fastify": "^9.0.4", | ||
"@nestjs/schematics": "^9.0.1", | ||
"@nestjs/testing": "^9.0.4", | ||
"@nestjs/apollo": "^10.1.3", | ||
"@nestjs/cli": "^9.1.4", | ||
"@nestjs/common": "^9.1.2", | ||
"@nestjs/core": "^9.1.2", | ||
"@nestjs/graphql": "^10.1.3", | ||
"@nestjs/mercurius": "^10.1.3", | ||
"@nestjs/platform-express": "^9.1.2", | ||
"@nestjs/platform-fastify": "^9.1.2", | ||
"@nestjs/schematics": "^9.0.3", | ||
"@nestjs/testing": "^9.1.2", | ||
"@types/express": "^4.17.13", | ||
@@ -92,3 +93,3 @@ "@types/jest": "^28.1.2", | ||
"collectCoverageFrom": [ | ||
"**/*.(t|j)s" | ||
"src/**/*.(t|j)s" | ||
], | ||
@@ -95,0 +96,0 @@ "coverageDirectory": "../coverage", |
303
README.md
# NestJS CLS | ||
> **New**: Version `2.0` brings advanced [type safety and type inference](#type-safety-and-type-inference). However, it requires features from `typescript >= 4.4` - Namely allowing `symbol` members in interfaces. If you can't upgrade but still want to use this library, install version `1.6.2`, which lacks the typing features. | ||
A continuous-local\* storage module compatible with [NestJS](https://nestjs.com/)' dependency injection based on [AsyncLocalStorage](https://nodejs.org/api/async_context.html#async_context_class_asynclocalstorage). | ||
A continuation-local storage module compatible with [NestJS](https://nestjs.com/)'s dependency injection. | ||
> **New**: Version `3.0` introduces [_Proxy Providers_](#proxy-providers) as an alternative to the imperative API. (Minor breaking changes were introduced, see [Migration guide](#migration-guide)). | ||
> Version `2.0` brings advanced [type safety and type inference](#type-safety-and-type-inference). However, it requires features from `typescript >= 4.4` - Namely allowing `symbol` members in interfaces. If you can't upgrade but still want to use this library, install version `1.6.2`, which lacks the typing features. | ||
_Continuous-local storage allows to store state and propagate it throughout callbacks and promise chains. It allows storing data throughout the lifetime of a web request or any other asynchronous duration. It is similar to thread-local storage in other languages._ | ||
Some common use cases for CLS include: | ||
Some common use cases that this library enables include: | ||
- Tracing the Request ID and other metadata for logging purposes | ||
- Making the Tenant ID available everywhere in multi-tenant apps | ||
- Globally setting an authentication level for the request | ||
- Tracking the Request ID and other metadata for logging purposes | ||
- Keeping track of the user throughout the whole request | ||
- Making the dynamic Tenant database connection available everywhere in multi-tenant apps | ||
- Propagating the authentication level or role to restrict access to resources | ||
- Seamlessly propagating the `transaction` object of your favourite ORM across services without breaking encapsulation and isolation by explicitly passing it around. | ||
- Using "request" context in cases where actual REQUEST-scoped providers are not supported (passport strategies, cron controllers, websocket gateways, ...) | ||
Most of these are to some extent solvable using _request-scoped_ providers or passing the context as a parameter, but these solutions are often clunky and come with a whole lot of other issues. | ||
Most of these are to some extent solvable using _REQUEST-scoped_ providers or passing the context as a parameter, but these solutions are often clunky and come with a whole lot of other issues. | ||
> **Note**: This package uses [AsyncLocalStorage](https://nodejs.org/api/async_context.html#async_context_class_asynclocalstorage) from Node's `async_hooks` API. Most parts of it are marked as _stable_ now, see [Security considerations](#security-considerations) for more details. | ||
## The author's take: | ||
_NestJS is an amazing framework, but in the plethora of awesome built-in features, I still missed one_. | ||
_I created this library to solve a specific use case, which was limiting access to only to records which had the same TenantId as the request's user in a central manner. The repository code automatically added a `WHERE` clause to each query, which made sure that other developers couldn't accidentally mix tenant data (all tenants' data were held in the same database) without extra effort._ | ||
_`AsyncLocalStorage` is still fairly new and not many people know of its existence and benefits. I've invested a great deal of my personal time in making the use of it as pleasant as possible._ | ||
_While the use of `async_hooks` is sometimes criticized for [making Node run slower](https://gist.github.com/Aschen/5cc1f3f3b58f1e284b670b83bb53da7d), in my experience, the introduced overhead is negligible compared to any IO operation (like a DB or external API call). If you want fast, use a compiled language._ | ||
_Also if you use some tracing library, chances are it already makes use of `async_hooks` under the hood, so you might as well use it to your advantage._ | ||
> (\*) The name comes from the original implementation based on `cls-hooked`, which was since replaced by the native `AsyncLocalStorage`. | ||
# Outline | ||
@@ -28,3 +45,3 @@ | ||
- [Using an Interceptor](#using-an-interceptor) | ||
- [Other features](#other-features) | ||
- [Features and use cases](#features-and-use-cases) | ||
- [Request ID](#request-id) | ||
@@ -35,2 +52,5 @@ - [Additional CLS Setup](#additional-cls-setup) | ||
- [Type safety and type inference](#type-safety-and-type-inference) | ||
- [Proxy Providers](#proxy-providers) | ||
- [Classes](#class-proxy-providers) | ||
- [Factories](#factory-proxy-providers) | ||
- [API](#api) | ||
@@ -44,6 +64,5 @@ - [Service Interface](#service-interface) | ||
- [Others](#others) | ||
- [~~Namespaces~~](#namespaces-deprecated) (deprecated) | ||
- [Contributing](#contributing) | ||
- [Migration guide](#migration-guide) | ||
> **Notice**: I have deprecated [Namespaces](#namespaces-deprecated) since version `2.1.1` and will be removing them in `3.0` to make room for new features ([#31](https://github.com/Papooch/nestjs-cls/issues/31)). Namespace support was experimental from the begining, and I havent seen any justifiable use case to keep it around. | ||
# Install | ||
@@ -70,3 +89,3 @@ | ||
// Register the ClsModule and automatically mount the ClsMiddleware | ||
ClsModule.register({ | ||
ClsModule.forRoot({ | ||
global: true, | ||
@@ -149,3 +168,3 @@ middleware: { mount: true }, | ||
All you have to do is mount it to routes in which you want to use CLS, or pass `middleware: { mount: true }` to the `ClsModule.register` options which automatically mounts it to all routes. | ||
All you have to do is mount it to routes in which you want to use CLS, or pass `middleware: { mount: true }` to the `ClsModule.forRoot()` options which automatically mounts it to all routes. | ||
@@ -181,3 +200,3 @@ Once that is set up, the `ClsService` will have access to a common storage in all _Guards, Interceptors, Pipes, Controllers, Services and Exception Filters_ that are called within that route. | ||
> **Please note**: If you bind the middleware using `app.use()`, it will not respect middleware settings passed to `ClsModule.register()`, so you will have to provide them yourself in the constructor. | ||
> **Please note**: If you bind the middleware using `app.use()`, it will not respect middleware settings passed to `ClsModule.forRoot()`, so you will have to provide them yourself in the constructor. | ||
@@ -190,6 +209,6 @@ --- | ||
To use it, pass its configuration to the `guard` property to the `ClsModule.register` options: | ||
To use it, pass its configuration to the `guard` property to the `ClsModule.rotRoot()` options: | ||
```ts | ||
ClsModule.register({ | ||
ClsModule.forRoot({ | ||
guard: { generateId: true, mount: true } | ||
@@ -228,6 +247,6 @@ }), | ||
To use it, pass its configuration to the `interceptor` property to the `ClsModule.register` options: | ||
To use it, pass its configuration to the `interceptor` property to the `ClsModule.forRoot()` options: | ||
```ts | ||
ClsModule.register({ | ||
ClsModule.forRoot({ | ||
interceptor: { generateId: true, mount: true } | ||
@@ -245,3 +264,3 @@ }), | ||
# Other features | ||
# Features and use cases | ||
@@ -259,3 +278,3 @@ In addition to the basic functionality described in the [Quick start](#quick-start) chapter, this module provides several other features. | ||
```ts | ||
ClsModule.register({ | ||
ClsModule.forRoot({ | ||
middleware: { | ||
@@ -306,3 +325,3 @@ mount: true, | ||
```ts | ||
ClsModule.register({ | ||
ClsModule.forRoot({ | ||
middleware: { | ||
@@ -323,3 +342,3 @@ mount: true, | ||
Sometimes, a part of the app that relies on the CLS storage might need to be called outside of the context of a web request - for example, in a Cron job or during the application bootstrap. In such cases, there are no enhancers that can be bound to the handler to set up the context. | ||
Sometimes, a part of the app that relies on the CLS storage might need to be called outside of the context of a web request - for example, in a Cron job, while consuming a Queue or during the application bootstrap. In such cases, there are no enhancers that can be bound to the handler to set up the context. | ||
@@ -387,4 +406,6 @@ Therefore, you as the the developer are responsible for wrapping the execution with `ClsService#run` and set up the appropriate context variables. | ||
Therefore, it is possible to specify a custom interface for the `ClsService` and get proper typing and automatic type inference when retrieving or setting values. This works even for _nested objects_ using a dot notation. | ||
### Type-safe ClsService | ||
It is possible to specify a custom interface for the `ClsService` and get proper typing and automatic type inference when retrieving or setting values. This works even for _nested objects_ using a dot notation. | ||
To create a typed CLS Store, start by creating an interface that extends `ClsStore`. | ||
@@ -406,3 +427,3 @@ | ||
export class MyService { | ||
constructor(private readonly cls: ClsService<ClsStore>) {} | ||
constructor(private readonly cls: ClsService<MyClsStore>) {} | ||
@@ -413,3 +434,3 @@ doTheThing() { | ||
// tenantId will be inferred as a stirng | ||
// tenantId will be inferred as a string | ||
const tenantId = this.cls.get('tenantId'); | ||
@@ -451,5 +472,23 @@ | ||
It can happen, that the object you want to store in the context is too complex, or contains cyclic references. In that case, typescript might complain that _type instantiation is too deep, possibly infinite_. That is due to the fact that it tries to generate all possible paths inside the store. If that's the case, you can use the `Terminal` type to stop generating the paths for a certain subtree: | ||
For even more transparent approach without augmenting the declaration, you can create a typed `ClsService` by extending it and creating a custom provider out of it: | ||
```ts | ||
export class MyClsService extends ClsService<MyClsStore> | ||
@Module({ | ||
imports: [ClsModule.forFeature()] | ||
providers: [{ | ||
provide: MyClsService, | ||
useExisting: ClsService | ||
}], | ||
exports: [MyClsService] | ||
}) | ||
class MyClsModule | ||
``` | ||
### Terminal Type | ||
It can happen, that the object you want to store in the context is too complex, or contains cyclic references. In that case, typescript might complain that _type instantiation is too deep, possibly infinite_. That is due to the fact that it tries to generate all possible paths inside the ClsStore. If that's the case, you can use the `Terminal` type to stop generating the paths for a certain subtree: | ||
```ts | ||
interface ClsStore { | ||
@@ -466,2 +505,130 @@ tenantId: string; | ||
--- | ||
## Proxy Providers | ||
> Since `v3.0` | ||
This feature was inspired by how REQUEST-scoped providers (_"beans"_) work in the Spring framework for Java/Kotlin. | ||
Using this technique, NestJS does not need to re-create a whole DI-subtree on each request (which has [certain implications which disallows the use of REQUEST-scoped providers in certain situations](https://docs.nestjs.com/fundamentals/injection-scopes#scope-hierarchy)), but it rather injects a _SINGLETON_ [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) instance, which delegates access and calls to the actual instance, which is created for each request when the CLS context is set up. | ||
There are two kinds of Proxy providers - _Class_ and _Factory_. | ||
### Class Proxy Providers | ||
These providers look like your regular class providers, with the exception that is the `@InjectableProxy()` decorator to make them easily distinguishable. | ||
```ts | ||
@InjectableProxy() | ||
export class User { | ||
id: number; | ||
role: string; | ||
} | ||
``` | ||
To register the proxy provider, use the `ClsModule.forFeature()` registration | ||
```ts | ||
ClsModule.forFeature(User); | ||
``` | ||
It can be then injected using the class name. However, what will be actually injected _is not_ the instance of the class, but rather the Proxy which redirects all access to an unique instance in the CLS context. | ||
```ts | ||
@Injectable() | ||
export class UserInterceptor implements NestInterceptor { | ||
// we can inject the proxy here | ||
constructor(private readonly user: User) {} | ||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { | ||
const request = context.switchToHttp().getRequest(); | ||
// and assign or change values as it was a normal object | ||
this.user.id = request.user.id; | ||
this.user.role = 'admin'; | ||
return next.handle(); | ||
} | ||
} | ||
``` | ||
It is also possible to inject other providers into the Proxy Provider. | ||
For the convenience, the `CLS_REQ` and `CLS_RES` were also made into Proxy Providers and are exported from the `ClsModule`. | ||
```ts | ||
@InjectableProxy() | ||
export class AutoBootstrappingUser { | ||
id: number; | ||
role: string; | ||
constructor(@Inject(CLS_REQ) request: Request) { | ||
this.id = request.user.id; | ||
this.role = 'admin'; | ||
} | ||
} | ||
``` | ||
If you need to inject a provider from an external module, use the `ClsService.forFeatureAsync()` registration to import it first. | ||
```ts | ||
ClsModule.forFeatureAsync({ | ||
// say the DogsModule provides the DogsService | ||
import: [DogsModule], | ||
// now you can inject DogsService in the DogContext Proxy Provider | ||
useClass: DogContext, | ||
}); | ||
``` | ||
### Factory Proxy Providers | ||
Like your normal factory providers, Proxy factory providers look familiar. | ||
Here's an example of a hypothetical factory provider that dynamically resolves to a specific tenant database connection: | ||
```ts | ||
ClsModule.forFeature({ | ||
provide: TENANT_CONNECTION, | ||
import: [DatabaseConnectionModule], | ||
inject: [CLS_REQ, DatabaseConnectionService], | ||
useFactory: async (req: Request, dbService: DatabaseConnectionService) => { | ||
const tenantId = req.params['tenantId']; | ||
const connection = await dbService.getTenantConnection(tenantId); | ||
return connection; | ||
}, | ||
}); | ||
``` | ||
Again, the factory will be called on each request and the result will be stored in the CLS context. The `TENANT_CONNECTION` provider, however, will still be a singleton and will not affect the scope of whatever it is injected into. | ||
In the service, it can be injected using the `provide` token as usual: | ||
```ts | ||
@Injectable() | ||
class DogsService { | ||
constructor( | ||
@Inject(TENANT_CONNECTION) | ||
private readonly connection: TenantConnection, | ||
) {} | ||
getAll() { | ||
// | ||
return this.connection.dogs.getAll(); | ||
} | ||
} | ||
``` | ||
> **Please note**: 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 provides a function). | ||
### Proxy Providers outside of web request context | ||
For the time being ([until the this feature is implemented](https://github.com/Papooch/nestjs-cls/issues/19)), if you want to access Proxy Providers [outside the context of web request](#usage-outside-of-web-request), you need to make sure you call | ||
```ts | ||
await ClsServiceManager.resolveProxyProviders(); | ||
``` | ||
once you set up the context with `cls.run()`, to actually _instantiate_ the Proxy Providers and store them in the CLS context. Otherwise all access to an injected Proxy Providers will return `undefined`. | ||
# API | ||
@@ -496,3 +663,3 @@ | ||
The `ClsModule.register()` method takes the following `ClsModuleOptions`: | ||
The `ClsModule.forRoot()` method takes the following `ClsModuleOptions`: | ||
@@ -512,3 +679,3 @@ - **_`middleware:`_ `ClsMiddlewareOptions`** | ||
`ClsModule.registerAsync()` is also available. You can supply the usual `imports`, `inject` and `useFactory` parameters. | ||
`ClsModule.forRootAsync()` is also available. You can supply the usual `imports`, `inject` and `useFactory` parameters. | ||
@@ -555,7 +722,7 @@ All of the `Cls{Middleware,Guard,Interceptor}Options` take the following parameters (either in `ClsModuleOptions` or directly when instantiating them manually): | ||
| | REST | GQL | WS | Others | | ||
| :----------------------------------------------------------: | :------------------------------------------------------: | :-------------------------------------------------------------: | :----------------: | :----: | | ||
| **ClsMiddleware** | ✔ | ✔<br>must be _mounted manually_<br>and use `useEnterWith: true` | ✖ | ✖ | | ||
| **ClsGuard** <br>(uses `enterWith`) | ✔ | ✔ | ✔[\*](#websockets) | ? | | ||
| **ClsInterceptor** <br>(context inaccessible<br>in _Guards_) | ✔<br>context also inaccessible<br>in _Exception Filters_ | ✔ | ✔[\*](#websockets) | ? | | ||
| | REST | GQL | WS | Microservices | | ||
| :----------------------------------------------------------: | :------------------------------------------------------: | :-------------------------------------------------------------: | :----------------: | :-----------: | | ||
| **ClsMiddleware** | ✔ | ✔<br>must be _mounted manually_<br>and use `useEnterWith: true` | ✖ | ✖ | | ||
| **ClsGuard** <br>(uses `enterWith`) | ✔ | ✔ | ✔[\*](#websockets) | ✔ | | ||
| **ClsInterceptor** <br>(context inaccessible<br>in _Guards_) | ✔<br>context also inaccessible<br>in _Exception Filters_ | ✔ | ✔[\*](#websockets) | ✔ | | ||
@@ -592,62 +759,18 @@ ## REST | ||
_Websocket Gateways_ don't respect globally bound enhancers, therefore it is required to bind the `ClsGuard` or `ClsIntercetor` manually on the `WebscocketGateway`. (See [#8](https://github.com/Papooch/nestjs-cls/issues/8)) | ||
_Websocket Gateways_ don't respect globally bound enhancers, therefore it is required to bind the `ClsGuard` or `ClsInterceptor` manually on the `WebsocketGateway`. Special care is also needed for the `handleConnection` method (See [#8](https://github.com/Papooch/nestjs-cls/issues/8)) | ||
# ~~Namespaces~~ (deprecated) | ||
# Contributing | ||
> **Warning**: Namespace support will be dropped in v3.0 | ||
Contributing to a community project is always welcome, please see the [Contributing guide](./CONTRIBUTING.md) :) | ||
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. | ||
# Migration Guide | ||
To use custom namespace provider, use `ClsModule.forFeature('my-namespace')`. | ||
## `v2.x` ➡️ `v3.x` | ||
```ts | ||
@Module({ | ||
imports: [ClsModule.forFeature('hello-namespace')], | ||
providers: [HelloService], | ||
controllers: [HelloController], | ||
}) | ||
export class HelloModule {} | ||
``` | ||
This creates a namespaced `ClsService` provider that you can inject using `@InjectCls` | ||
```ts | ||
// hello.service.ts | ||
@Injectable() | ||
class HelloService { | ||
constructor( | ||
@InjectCls('hello-namespace') | ||
private readonly myCls: ClsService, | ||
) {} | ||
sayHello() { | ||
return this.myCls.run('hi'); | ||
} | ||
} | ||
// hello.controller.ts | ||
@Injectable() | ||
export class HelloController { | ||
constructor( | ||
@InjectCls('hello-namespace') | ||
private readonly myCls: ClsService, | ||
private readonly helloService: HelloService, | ||
); | ||
@Get('/hello') | ||
hello2() { | ||
// 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(getClsServiceToken('x'))`. If you don't pass an argument to `@InjectCls()`, the default ClsService will be injected and is equivalent to omitting the decorator altogether. | ||
# Contributing | ||
Contributing to a community project is always welcome, please see the [Contributing guide](./CONTRIBUTING.md) :) | ||
- The root registration method was _renamed_ from `register` (resp. `registerAsync`) to `forRoot` (resp. `forRootAsync`) to align with the convention. | ||
- Namespace injection support was dropped entirely, if you still have use case for it, you can still create a namespaced `ClsService` using | ||
```ts | ||
const als = new AsyncLocalStorage(); | ||
const namespacedClsService = new ClsService(als); | ||
``` | ||
and use a custom provider to inject it. |
@@ -1,75 +0,21 @@ | ||
import { ClassProvider, ValueProvider } from '@nestjs/common'; | ||
import { CLS_DEFAULT_NAMESPACE } from './cls.constants'; | ||
import { globalClsSevice } from './cls-service.globals'; | ||
import { ClsStore } from './cls.interfaces'; | ||
import { ClsService } from './cls.service'; | ||
import { AsyncLocalStorage } from 'async_hooks'; | ||
import { ProxyProviderManager } from './proxy-provider/proxy-provider-manager'; | ||
/** | ||
* Get ClsService injection token (as a string) | ||
*/ | ||
export function getClsServiceToken(): string; | ||
/** | ||
* Get namespaced ClsService injection token (as a string) | ||
* @param namespace name of the namespace | ||
* @deprecated Namespace support will be removed in v3.0 | ||
*/ | ||
export function getClsServiceToken(namespace: string): string; | ||
export function getClsServiceToken(namespace = CLS_DEFAULT_NAMESPACE) { | ||
return `ClsService-${namespace}`; | ||
} | ||
export class ClsServiceManager { | ||
private static namespaces: Record< | ||
string, | ||
AsyncLocalStorage<any> & { name?: string } | ||
> = {}; | ||
private static clsService = globalClsSevice; | ||
private static clsServices: Map<string | typeof ClsService, ClsService> = | ||
new Map([ | ||
[ | ||
ClsService, | ||
new ClsService(this.resolveNamespace(CLS_DEFAULT_NAMESPACE)), | ||
], | ||
]); | ||
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 = CLS_DEFAULT_NAMESPACE) { | ||
const service = new ClsService(this.resolveNamespace(name)); | ||
this.clsServices.set( | ||
getClsServiceToken(name), | ||
new ClsService(this.resolveNamespace(name)), | ||
); | ||
return service; | ||
} | ||
/** | ||
* Retrieve a ClsService outside of Nest's DI. | ||
* @param name namespace name, omit for default | ||
* @returns the ClsService with the given namespace | ||
* @returns the ClsService | ||
*/ | ||
static getClsService(name?: string) { | ||
const cls = this.clsServices.get( | ||
name ? getClsServiceToken(name) : ClsService, | ||
); | ||
if (!cls) | ||
throw new Error(`ClsService with namespace ${name} does not exist`); | ||
static getClsService<T extends ClsStore = ClsStore>(): ClsService<T> { | ||
const cls = this.clsService as ClsService<T>; | ||
return cls; | ||
} | ||
static getClsServicesAsProviders(): Array< | ||
ClassProvider<ClsService> | ValueProvider<ClsService> | ||
> { | ||
return Array.from(this.clsServices.entries()).map( | ||
([provide, service]) => ({ | ||
provide, | ||
useValue: service, | ||
}), | ||
); | ||
static async resolveProxyProviders() { | ||
return await ProxyProviderManager.resolveProxyProviders(); | ||
} | ||
} |
export const CLS_REQ = Symbol('CLS_REQUEST'); | ||
export const CLS_RES = Symbol('CLS_RESPONSE'); | ||
export const CLS_ID = Symbol('CLS_ID'); | ||
export const CLS_DEFAULT_NAMESPACE = 'CLS_DEFAULT_NAMESPACE'; | ||
export const CLS_MODULE_OPTIONS = 'ClsModuleOptions'; | ||
@@ -6,0 +5,0 @@ export const CLS_MIDDLEWARE_OPTIONS = 'ClsMiddlewareOptions'; |
@@ -1,4 +0,4 @@ | ||
import { Inject } from '@nestjs/common'; | ||
import { getClsServiceToken } from './cls-service-manager'; | ||
import { CLS_DEFAULT_NAMESPACE } from './cls.constants'; | ||
import { Inject, Injectable, SetMetadata } from '@nestjs/common'; | ||
import { ClsService } from './cls.service'; | ||
import { CLS_PROXY_METADATA_KEY } from './proxy-provider'; | ||
@@ -8,12 +8,12 @@ /** | ||
*/ | ||
export function InjectCls(): (target: any, key: string | symbol, index?: number) => void; | ||
export function InjectCls() { | ||
return Inject(ClsService); | ||
} | ||
/** | ||
* Use to inject a namespaced CLS service | ||
* @param namespace name of the namespace | ||
* @deprecated Namespace support will be removed in v3.0 | ||
* Mark a Proxy provider with this decorator to distinguis it from regular NestJS singleton providers | ||
*/ | ||
export function InjectCls(namespace: string): (target: any, key: string | symbol, index?: number) => void; | ||
export function InjectCls(namespace = CLS_DEFAULT_NAMESPACE) { | ||
return Inject(getClsServiceToken(namespace)); | ||
export function InjectableProxy() { | ||
return (target: any) => | ||
Injectable()(SetMetadata(CLS_PROXY_METADATA_KEY, true)(target)); | ||
} |
@@ -21,3 +21,3 @@ import { | ||
async canActivate(context: ExecutionContext): Promise<boolean> { | ||
const cls = ClsServiceManager.getClsService(this.options.namespaceName); | ||
const cls = ClsServiceManager.getClsService(); | ||
return cls.exit(async () => { | ||
@@ -32,2 +32,3 @@ cls.enter(); | ||
} | ||
await ClsServiceManager.resolveProxyProviders(); | ||
return true; | ||
@@ -34,0 +35,0 @@ }); |
@@ -23,3 +23,3 @@ import { | ||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { | ||
const cls = ClsServiceManager.getClsService(this.options.namespaceName); | ||
const cls = ClsServiceManager.getClsService(); | ||
return new Observable((subscriber) => { | ||
@@ -34,9 +34,14 @@ cls.run(async () => { | ||
} | ||
next.handle() | ||
.pipe() | ||
.subscribe({ | ||
next: (res) => subscriber.next(res), | ||
error: (err) => subscriber.error(err), | ||
complete: () => subscriber.complete(), | ||
}); | ||
try { | ||
await ClsServiceManager.resolveProxyProviders(); | ||
next.handle() | ||
.pipe() | ||
.subscribe({ | ||
next: (res) => subscriber.next(res), | ||
error: (err) => subscriber.error(err), | ||
complete: () => subscriber.complete(), | ||
}); | ||
} catch (e) { | ||
subscriber.error(e); | ||
} | ||
}); | ||
@@ -43,0 +48,0 @@ }); |
@@ -1,3 +0,2 @@ | ||
import { ExecutionContext, ModuleMetadata } from '@nestjs/common'; | ||
import { CLS_DEFAULT_NAMESPACE } from './cls.constants'; | ||
import { ExecutionContext, ModuleMetadata, Type } from '@nestjs/common'; | ||
import { ClsService } from './cls.service'; | ||
@@ -28,8 +27,5 @@ | ||
/** | ||
* 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) | ||
* @deprecated Namespace support will be removed in v3.0 | ||
* | ||
*/ | ||
namespaceName? = CLS_DEFAULT_NAMESPACE; | ||
proxyProviders?: Type[]; | ||
} | ||
@@ -39,3 +35,3 @@ | ||
ClsModuleOptions, | ||
'global' | 'namespaceName' | ||
'global' | 'providers' | ||
>; | ||
@@ -52,9 +48,7 @@ export interface ClsModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> { | ||
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) | ||
* @deprecated Namespace support will be removed in v3.0 | ||
* | ||
*/ | ||
namespaceName?: string; | ||
providers?: Type[]; | ||
} | ||
@@ -106,7 +100,2 @@ | ||
useEnterWith? = false; | ||
/** | ||
* @deprecated Namespace support will be removed in v3.0 | ||
*/ | ||
readonly namespaceName?: string; | ||
} | ||
@@ -139,7 +128,2 @@ | ||
) => void | Promise<void>; | ||
/** | ||
* @deprecated Namespace support will be removed in v3.0 | ||
*/ | ||
readonly namespaceName?: string; | ||
} | ||
@@ -172,7 +156,2 @@ | ||
) => void | Promise<void>; | ||
/** | ||
* @deprecated Namespace support will be removed in v3.0 | ||
*/ | ||
readonly namespaceName?: string; | ||
} | ||
@@ -179,0 +158,0 @@ |
import { Inject, Injectable, NestMiddleware } from '@nestjs/common'; | ||
import { ClsServiceManager } from './cls-service-manager'; | ||
import { CLS_ID, CLS_MIDDLEWARE_OPTIONS } from './cls.constants'; | ||
import { CLS_REQ, CLS_RES } from './cls.constants'; | ||
import { | ||
CLS_ID, | ||
CLS_MIDDLEWARE_OPTIONS, | ||
CLS_REQ, | ||
CLS_RES, | ||
} from './cls.constants'; | ||
import { ClsMiddlewareOptions } from './cls.interfaces'; | ||
@@ -18,4 +22,4 @@ import { ClsService } from './cls.service'; | ||
} | ||
use = async (req: any, res: any, next: () => any) => { | ||
const cls = ClsServiceManager.getClsService(this.options.namespaceName); | ||
use = async (req: any, res: any, next: (err?: any) => any) => { | ||
const cls = ClsServiceManager.getClsService(); | ||
const callback = async () => { | ||
@@ -32,3 +36,8 @@ this.options.useEnterWith && cls.enter(); | ||
} | ||
next(); | ||
try { | ||
await ClsServiceManager.resolveProxyProviders(); | ||
next(); | ||
} catch (e) { | ||
next(e); | ||
} | ||
}; | ||
@@ -35,0 +44,0 @@ const runner = this.options.useEnterWith |
@@ -10,2 +10,4 @@ import { | ||
Provider, | ||
Type, | ||
ValueProvider, | ||
} from '@nestjs/common'; | ||
@@ -18,3 +20,3 @@ import { | ||
} from '@nestjs/core'; | ||
import { ClsServiceManager, getClsServiceToken } from './cls-service-manager'; | ||
import { ClsServiceManager } from './cls-service-manager'; | ||
import { | ||
@@ -25,2 +27,4 @@ CLS_GUARD_OPTIONS, | ||
CLS_MODULE_OPTIONS, | ||
CLS_REQ, | ||
CLS_RES, | ||
} from './cls.constants'; | ||
@@ -36,9 +40,21 @@ import { ClsGuard } from './cls.guard'; | ||
} from './cls.interfaces'; | ||
import { ClsMiddleware } from './cls.middleware'; | ||
import { ClsService } from './cls.service'; | ||
import { ProxyProviderManager } from './proxy-provider/proxy-provider-manager'; | ||
import { ClsModuleProxyProviderOptions } from './proxy-provider/proxy-provider.interfaces'; | ||
const clsServiceProvider: ValueProvider<ClsService> = { | ||
provide: ClsService, | ||
useValue: ClsServiceManager.getClsService(), | ||
}; | ||
const commonProviders = [ | ||
clsServiceProvider, | ||
ProxyProviderManager.createProxyProviderFromExistingKey(CLS_REQ), | ||
ProxyProviderManager.createProxyProviderFromExistingKey(CLS_RES), | ||
]; | ||
@Module({ | ||
providers: [...ClsServiceManager.getClsServicesAsProviders()], | ||
exports: [...ClsServiceManager.getClsServicesAsProviders()], | ||
providers: [...commonProviders], | ||
exports: [...commonProviders], | ||
}) | ||
@@ -76,2 +92,46 @@ export class ClsModule implements NestModule { | ||
static forRoot(options?: ClsModuleOptions): DynamicModule { | ||
options = { ...new ClsModuleOptions(), ...options }; | ||
const { providers, exports } = this.getProviders(); | ||
const proxyProviders = | ||
options.proxyProviders?.map((providerClass) => | ||
ProxyProviderManager.createProxyProvider({ | ||
useClass: providerClass, | ||
}), | ||
) ?? []; | ||
return { | ||
module: ClsModule, | ||
providers: [ | ||
{ | ||
provide: CLS_MODULE_OPTIONS, | ||
useValue: options, | ||
}, | ||
...providers, | ||
...proxyProviders, | ||
], | ||
exports: [...exports, ...proxyProviders.map((p) => p.provide)], | ||
global: options.global, | ||
}; | ||
} | ||
static forRootAsync(asyncOptions: ClsModuleAsyncOptions): DynamicModule { | ||
const { providers, exports } = this.getProviders(); | ||
return { | ||
module: ClsModule, | ||
imports: asyncOptions.imports, | ||
providers: [ | ||
{ | ||
provide: CLS_MODULE_OPTIONS, | ||
inject: asyncOptions.inject, | ||
useFactory: asyncOptions.useFactory, | ||
}, | ||
...providers, | ||
], | ||
exports, | ||
global: asyncOptions.global, | ||
}; | ||
} | ||
/** | ||
@@ -81,18 +141,68 @@ * Registers the `ClsService` provider in the module | ||
static forFeature(): DynamicModule; | ||
/** | ||
* @param namespaceName | ||
* @deprecated usage with namespaceName is deprecated and will be | ||
* removed with namespace support in v3.0 | ||
* @returns | ||
*/ | ||
static forFeature(namespaceName: string): DynamicModule; | ||
static forFeature(namespaceName?: string): DynamicModule { | ||
const providers = ClsServiceManager.getClsServicesAsProviders().filter( | ||
(p) => | ||
p.provide === getClsServiceToken(namespaceName) || | ||
p.provide === ClsService, | ||
); | ||
static forFeature(...requestScopedProviders: Array<Type>): DynamicModule; | ||
static forFeature(...requestScopedProviders: Array<Type>): DynamicModule { | ||
const proxyProviders = | ||
requestScopedProviders.map((providerClass) => | ||
ProxyProviderManager.createProxyProvider({ | ||
useClass: providerClass, | ||
}), | ||
) ?? []; | ||
const providers = [...commonProviders]; | ||
return { | ||
module: ClsModule, | ||
providers, | ||
providers: [...providers, ...proxyProviders], | ||
exports: [...providers, ...proxyProviders.map((p) => p.provide)], | ||
}; | ||
} | ||
static forFeatureAsync( | ||
options: ClsModuleProxyProviderOptions, | ||
): DynamicModule { | ||
const proxyProvider = ProxyProviderManager.createProxyProvider(options); | ||
const providers = [ | ||
...commonProviders, | ||
...(options.extraProviders ?? []), | ||
]; | ||
return { | ||
module: ClsModule, | ||
imports: options.imports ?? [], | ||
providers: [...providers, proxyProvider], | ||
exports: [...commonProviders, proxyProvider.provide], | ||
}; | ||
} | ||
private static getProviders() { | ||
const providers: Provider[] = [ | ||
...commonProviders, | ||
{ | ||
provide: CLS_MIDDLEWARE_OPTIONS, | ||
inject: [CLS_MODULE_OPTIONS], | ||
useFactory: this.clsMiddlewareOptionsFactory, | ||
}, | ||
{ | ||
provide: CLS_GUARD_OPTIONS, | ||
inject: [CLS_MODULE_OPTIONS], | ||
useFactory: this.clsGuardOptionsFactory, | ||
}, | ||
{ | ||
provide: CLS_INTERCEPTOR_OPTIONS, | ||
inject: [CLS_MODULE_OPTIONS], | ||
useFactory: this.clsInterceptorOptionsFactory, | ||
}, | ||
]; | ||
const enhancerArr: Provider[] = [ | ||
{ | ||
provide: APP_GUARD, | ||
inject: [CLS_GUARD_OPTIONS], | ||
useFactory: this.clsGuardFactory, | ||
}, | ||
{ | ||
provide: APP_INTERCEPTOR, | ||
inject: [CLS_INTERCEPTOR_OPTIONS], | ||
useFactory: this.clsInterceptorFactory, | ||
}, | ||
]; | ||
return { | ||
providers: providers.concat(...enhancerArr), | ||
exports: providers, | ||
@@ -108,3 +218,2 @@ }; | ||
...options.middleware, | ||
namespaceName: options.namespaceName, | ||
}; | ||
@@ -120,3 +229,2 @@ return clsMiddlewareOptions; | ||
...options.guard, | ||
namespaceName: options.namespaceName, | ||
}; | ||
@@ -132,3 +240,2 @@ return clsGuardOptions; | ||
...options.interceptor, | ||
namespaceName: options.namespaceName, | ||
}; | ||
@@ -161,78 +268,2 @@ return clsInterceptorOptions; | ||
} | ||
private static getProviders(options: { namespaceName?: string }) { | ||
ClsServiceManager.addClsService(options.namespaceName); | ||
const providers: Provider[] = [ | ||
...ClsServiceManager.getClsServicesAsProviders(), | ||
{ | ||
provide: CLS_MIDDLEWARE_OPTIONS, | ||
inject: [CLS_MODULE_OPTIONS], | ||
useFactory: this.clsMiddlewareOptionsFactory, | ||
}, | ||
{ | ||
provide: CLS_GUARD_OPTIONS, | ||
inject: [CLS_MODULE_OPTIONS], | ||
useFactory: this.clsGuardOptionsFactory, | ||
}, | ||
{ | ||
provide: CLS_INTERCEPTOR_OPTIONS, | ||
inject: [CLS_MODULE_OPTIONS], | ||
useFactory: this.clsInterceptorOptionsFactory, | ||
}, | ||
]; | ||
const enhancerArr: Provider[] = [ | ||
{ | ||
provide: APP_GUARD, | ||
inject: [CLS_GUARD_OPTIONS], | ||
useFactory: this.clsGuardFactory, | ||
}, | ||
{ | ||
provide: APP_INTERCEPTOR, | ||
inject: [CLS_INTERCEPTOR_OPTIONS], | ||
useFactory: this.clsInterceptorFactory, | ||
}, | ||
]; | ||
return { | ||
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, | ||
}; | ||
} | ||
} |
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { Terminal } from '../types/terminal.type'; | ||
import { ClsServiceManager } from './cls-service-manager'; | ||
import { CLS_DEFAULT_NAMESPACE, CLS_ID } from './cls.constants'; | ||
import { CLS_ID } from './cls.constants'; | ||
import { ClsStore } from './cls.interfaces'; | ||
@@ -17,5 +17,3 @@ import { ClsService } from './cls.service'; | ||
provide: ClsService, | ||
useValue: ClsServiceManager.addClsService( | ||
CLS_DEFAULT_NAMESPACE, | ||
), | ||
useValue: ClsServiceManager.getClsService(), | ||
}, | ||
@@ -22,0 +20,0 @@ ], |
@@ -16,6 +16,3 @@ import { AsyncLocalStorage } from 'async_hooks'; | ||
export class ClsService<S extends ClsStore = ClsStore> { | ||
private readonly namespace: AsyncLocalStorage<any>; | ||
constructor(namespace: AsyncLocalStorage<any>) { | ||
this.namespace = namespace; | ||
} | ||
constructor(private readonly als: AsyncLocalStorage<any>) {} | ||
@@ -33,3 +30,3 @@ /** | ||
>(key: StringIfNever<T> | keyof ClsStore, value: AnyIfNever<P>): void { | ||
const store = this.namespace.getStore(); | ||
const store = this.als.getStore(); | ||
if (!store) { | ||
@@ -39,5 +36,3 @@ throw new Error( | ||
key, | ||
)}". No cls context available in namespace "${ | ||
this.namespace['name'] | ||
}", please make sure that a ClsMiddleware/Guard/Interceptor has set up the context, or wrap any calls that depend on cls with "ClsService#run"`, | ||
)}". No CLS context available, please make sure that a ClsMiddleware/Guard/Interceptor has set up the context, or wrap any calls that depend on CLS with "ClsService#run"`, | ||
); | ||
@@ -70,3 +65,3 @@ } | ||
get(key?: string | symbol): any { | ||
const store = this.namespace.getStore(); | ||
const store = this.als.getStore(); | ||
if (!key) return store; | ||
@@ -88,3 +83,3 @@ if (typeof key === 'symbol') { | ||
has(key: string | symbol): boolean { | ||
const store = this.namespace.getStore(); | ||
const store = this.als.getStore(); | ||
if (typeof key === 'symbol') { | ||
@@ -101,3 +96,3 @@ return !!store[key]; | ||
getId(): string { | ||
const store = this.namespace.getStore(); | ||
const store = this.als.getStore(); | ||
return store?.[CLS_ID]; | ||
@@ -112,3 +107,3 @@ } | ||
run<T = any>(callback: () => T) { | ||
return this.namespace.run({}, callback); | ||
return this.als.run({}, callback); | ||
} | ||
@@ -123,3 +118,3 @@ | ||
runWith<T = any>(store: S, callback: () => T) { | ||
return this.namespace.run(store ?? {}, callback); | ||
return this.als.run(store ?? {}, callback); | ||
} | ||
@@ -131,3 +126,3 @@ | ||
enter() { | ||
return this.namespace.enterWith({}); | ||
return this.als.enterWith({}); | ||
} | ||
@@ -140,3 +135,3 @@ | ||
enterWith(store?: S) { | ||
return this.namespace.enterWith(store ?? {}); | ||
return this.als.enterWith(store ?? {}); | ||
} | ||
@@ -150,3 +145,3 @@ | ||
exit<T = any>(callback: () => T): T { | ||
return this.namespace.exit(callback); | ||
return this.als.exit(callback); | ||
} | ||
@@ -159,4 +154,4 @@ | ||
isActive() { | ||
return !!this.namespace.getStore(); | ||
return !!this.als.getStore(); | ||
} | ||
} |
@@ -0,0 +0,0 @@ import { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
366925
12.41%102
37.84%2504
25.7%756
19.43%