papooch / nestjs-cls Goto Github PK
View Code? Open in Web Editor NEWA continuation-local storage (async context) module compatible with NestJS's dependency injection.
Home Page: https://papooch.github.io/nestjs-cls/
License: MIT License
A continuation-local storage (async context) module compatible with NestJS's dependency injection.
Home Page: https://papooch.github.io/nestjs-cls/
License: MIT License
Hi, after implementing ClsModule within my application, I get this dependency error and I am unsure on how to fix. Does anyone have any guidance?
[Nest] 66508 - 07/07/2023, 11:57:53 AM ERROR [ExceptionHandler] Nest can't resolve dependencies of the ClsModule (?, ModuleRef). Please make sure that the argument HttpAdapterHost at index [0] is available in the ClsModule context.
Potential solutions:
- Is ClsModule a valid NestJS module?
- If HttpAdapterHost is a provider, is it part of the current ClsModule?
- If HttpAdapterHost is exported from a separate @Module, is that module imported within ClsModule?
@Module({
imports: [ /* the Module containing HttpAdapterHost */ ]
})
Error: Nest can't resolve dependencies of the ClsModule (?, ModuleRef). Please make sure that the argument HttpAdapterHost at index [0] is available in the ClsModule context.
Potential solutions:
- Is ClsModule a valid NestJS module?
- If HttpAdapterHost is a provider, is it part of the current ClsModule?
- If HttpAdapterHost is exported from a separate @Module, is that module imported within ClsModule?
@Module({
imports: [ /* the Module containing HttpAdapterHost */ ]
})
at Injector.lookupComponentInParentModules (/Users/linards/Documents/projects/fripro/apps/server/node_modules/@nestjs/core/injector/injector.js:248:19)
at Injector.resolveComponentInstance (/Users/linards/Documents/projects/fripro/apps/server/node_modules/@nestjs/core/injector/injector.js:202:33)
at resolveParam (/Users/linards/Documents/projects/fripro/apps/server/node_modules/@nestjs/core/injector/injector.js:123:38)
at async Promise.all (index 0)
at Injector.resolveConstructorParams (/Users/linards/Documents/projects/fripro/apps/server/node_modules/@nestjs/core/injector/injector.js:138:27)
at Injector.loadInstance (/Users/linards/Documents/projects/fripro/apps/server/node_modules/@nestjs/core/injector/injector.js:64:13)
at Injector.loadProvider (/Users/linards/Documents/projects/fripro/apps/server/node_modules/@nestjs/core/injector/injector.js:91:9)
at /Users/linards/Documents/projects/fripro/apps/server/node_modules/@nestjs/core/injector/instance-loader.js:56:13
at async Promise.all (index 0)
at InstanceLoader.createInstancesOfProviders (/Users/linards/Documents/projects/fripro/apps/server/node_modules/@nestjs/core/injector/instance-loader.js:55:9)
I implemented it just like in docs by adding module to AppModule imports
ClsModule.forRoot({
global: true,
middleware: { mount: true },
}),
```
The UseCls
decorator is very convenient; however, I need to use the IfNested
option and unfortunately the UseCls
decorator does not expose it.
It would be great if you could expose the IfNested
option (and any other options you may add to .run
in the future) on the UseCls
decorator options.
Thanks for this awesome library!
While trying to get rid of request-scope, that we are currently using to have RLS-scoped database connections, those Factory Proxy Providers looked very nice. Unfortunately, I cannot get them to work.
In the docs there is this example, but I just cannot get it to work ๐
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;
},
});
This works, but we want to get rid of:
@Global()
@Module({})
export class RLSModule {
static forRoot(
importModules: (
| DynamicModule
| Type<any>
| Promise<DynamicModule>
| ForwardReference<any>
)[],
// eslint-disable-next-line @typescript-eslint/ban-types
injectServices: (string | symbol | Function | Type<any> | Abstract<any>)[],
): DynamicModule {
const rlsProvider: Provider = {
provide: TENANT_CONNECTION,
inject: [REQUEST, DataSource, ...injectServices],
scope: Scope.REQUEST,
durable: true,
useFactory: async (request: Request, connection: DataSource, ...args) => {
const authInfo = request.auth.payload as Auth0JWTPayload;
return createScopedDataSource(
connection,
authInfo.sub,
authInfo.tenantId,
);
},
};
return {
module: RLSModule,
imports: importModules,
providers: [rlsProvider],
exports: [TENANT_CONNECTION],
};
}
}
Then, this global RLSModule is simply imported in app.module.ts.
This didnt work with errors like connection is undefined
@Global()
@Module({})
export class RLSModule {
static forRoot(
importModules: (
| DynamicModule
| Type<any>
| Promise<DynamicModule>
| ForwardReference<any>
)[],
// eslint-disable-next-line @typescript-eslint/ban-types
injectServices: (string | symbol | Function | Type<any> | Abstract<any>)[],
) {
// where should we put ClsModule.forFeatureAsync?
const dynamicModule = ClsModule.forFeatureAsync({
provide: TENANT_CONNECTION,
imports: [TypeOrmModule],
inject: [CLS_REQ, DataSource],
useFactory: async (request: Request, connection: DataSource) => {
const authInfo = request.auth.payload as Auth0JWTPayload;
return createScopedDataSource(
connection,
authInfo.sub,
authInfo.tenantId,
);
},
});
return {
module: RLSModule,
imports: importModules,
providers: [...dynamicModule.providers, RLSEnforcerService],
exports: [TENANT_CONNECTION],
};
}
}
here I run into dependency errors, where it cannot resolve the tenant connection:
@Module({
imports: [
ClsModule.forRoot({
global: true,
middleware: { mount: true },
}),
ClsModule.forFeatureAsync({
provide: TENANT_CONNECTION,
imports: [TypeOrmModule],
inject: [CLS_REQ, DataSource],
useFactory: async (request: Request, connection: DataSource) => {
const authInfo = request.auth.payload as Auth0JWTPayload;
return createScopedDataSource(
connection,
authInfo.sub,
authInfo.tenantId,
);
},
}),
ConfigModule.forRoot({ isGlobal: true }),
//...
});
Nest can't resolve dependencies of the Auth0Guard (ConfigService, ?, ContextService, TenantsService, PinoLogger). Please make sure that the argument TENANT_CONNECTION at index [1] is available in the AuthModule context.
As per the documentation here I chose to setup using the app.Use method and it looks like below
const app = await NestFactory.create<NestFastifyApplication>(AppModule,new FastifyAdapter());
//https://papooch.github.io/nestjs-cls/
const options: ClsMiddlewareOptions = {
mount: true,
setup: (context, req: Request, res: Response) => {
if (!req.url.startsWith('/api'))
context.set(SystemConstants.CurrentContext, new CurrentUserContext());
},
};
app.use(new ClsMiddleware(options).use);
Then I have registered the ClsService in my AppModule like below
@Module({
imports: [
AppSettingsModule
]
,controllers: [AppController]
,providers: [ClsService]
})
export class AppModule implements NestModule{
configure(consumer: MiddlewareConsumer) {
consumer.apply(SwaggerAuthMiddleware).forRoutes('api');
}
}
After that I inject the ClsService in one of my guards and use it like below
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly contextService:ClsService, private readonly authService:AuthService, private readonly reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const test = this.contextService.get(SystemConstants.CurrentContext)
return true;
}
}
and at this line const test = this.contextService.get(SystemConstants.CurrentContext) it gives this error Cannot read properties of undefined (reading 'getStore')
I can't figure out any missing configurations I may have done. Btw. I am using FastifyAdapter. So not sure whether it has something to do with it.
This is still just an idea, but I think it's very much possible to use a Proxy in combination of CLS to create a truly request-scoped providers in the Nest sense of the word but without the need to recreate the whole provider chain.
The implementation would be is similar to the one of request-scoped beans in the Spring framework for Java.
This would enable the following usage:
@Injectable()
export class UserService {
constructor(
// inject a singleton provider, that is actually a Proxy instance with correctly set up accessor traps
@InjectCls(RequestUser) user: RequestUser
) {}
getCurrentUserName() {
// when accessing the object, we the call is proxied to the current user object in the CLS
return this.user.name
}
}
or even
@Injectable()
export class UserService {
constructor(
// it could be possible to inject arbitrary provider - in this case a connection to the tenant database
@InjectCls('TENANT_CONNECTION') db: Knex
) {}
getCurrentUserFromDb() {
// the call would be again proxied to the current TENANT_CONNENCTION object in the CLS
return this.db.select('whatever').from('users') ...
}
}
The registration of such providers could be done as follows:
imports: [
ClsModule.forFeature(RequestUser)
]
and If we were to support the second use case - that is arbitrary factory providers, then even
imports: [
ClsModule.forFeature({
provide: 'TENANT_CONNENCTION',
import: [DatabaseModule],
inject: [ClsService, DatabaseService],
useFactory: async (cls: ClsService, databaseService: DatabaseService) => {
const tenantId = cls.get('tenantId');
const tenantConnection: Knex = await databaseService.getTenantConnection(tenantId);
return tenantConnection;
}
})
]
It would be needed to figure out where and when to populate the CLS store with the provider. It is too late to do it at the proxy access time, because the provider could be itself async so it would have to have been already constructed at the time of access.
Next issue is how to inject providers and such. All this would probably happen in the enhancer when the CLS context is being set up using moduleRef.
Also, what about situations where we're setting up the CLS store manually with cls.run()
?. We'd need to construct the providers there somehow and since the operation must be async, it can't be done in cls.run()
directly. We'll probably need an explicit call - something like await cls.boostrapProxyProviders()
.
Hello there!
I would like to inform that seems to have an issue regarding the docs for this lib (as of 2023-10-23 18:08 UTC)
https://downforeveryoneorjustme.com/papooch.github.io?proto=https
Recovered link from: https://www.npmjs.com/package/nestjs-cls?activeTab=readme
Original Url: https://papooch.github.io/nestjs-cls/
Hi, @Papooch thanks for this great library!
As I use this library, I find myself equating the API to that of Redis or node-lru-cache. While I know nestjs-cls
is not a general cache solution, I can see myself benefiting from further functions typically present in a cache manager. Here are some simple examples:
export interface ClsService {
// Check if a key is in the cache
has(key?: string | symbol): boolean
// Returns the values of all specified keys
mget<T>(...args: [string | symbol]): Promise<(T | null)[]>
// Sets the given keys to their respective values
mset<T>(args: [string, T][]): Promise<never[] | undefined>
// perhaps others...
}
Many properties and methods typically found on cache strategies are not relevant (delete, clear, TTL, etc.).
I would be happy to open a PR with this functionality if you think it is an appropriate inclusion!
Cheers,
Andrew
I'm new to Nestjs and I'm trying to use middleware to store both correlationId and contextLogger into cls to ease my services to log with context.
Here is my middleware code
@Injectable()
export class TestClsMiddleware implements NestMiddleware {
constructor(
private readonly clsService: ClsService,
@Inject(ILogger) private readonly loggerService: ILogger,
) {}
use(req: any, res: any, next: (err?: any) => any) {
const cls = this.clsService;
let {
'x-correlation-id': correlationId,
} = req.headers;
cls.set('correlationId', correlationId);
const contextLogger = this.loggerService.getContextLogger({correlationId, causationId, processId});
cls.set('logger', contextLogger);
}
}
@Module({
imports: [ClsModule.forRoot({
global : true,
middleware : {
mount : true,
setup : (cls: ClsService, req: Request) => {
}
}
}],
controllers : [AppController],
})
export class AppModule implements NestModule {
async configure(consumer: MiddlewareConsumer) {
const middlewares: any[] = [helmet()];
middlewares.push(SessionMiddleware);
consumer
.apply(TestClsMiddleware )
.forRoutes('*');
}
}
From above code, I get error Cannot set the key \"correlationId\". 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\"
when I it hit the line cls.set('correlationId', correlationId);
Please enlight me what is missing from my code. Or is there any better way to archive it?
Add documentation on how to use the module when the code runs out of the context of a request (i.e. there's no enhancer to set up the CLS context).
Add the following example to the documentation:
// This is currently supported
@Injectable()
export class CronController {
constructor(
private readonly someService: SomeService
private readonly cls: ClsService
)
@Cron('45 * * * * *')
handleCron() {
this.clsService.run(() => {
this.cls.set(CLS_ID, uuid())
this.someService.doTheThing(); // SomeService can now access CLS_ID from context
})
}
@Cron('90 * * * * *')
handleCron() {
this.clsService.runWith({
[CLS_ID]: uuid()
}, () => {
this.someService.doTheThing(); // SomeService can now access CLS_ID from context
})
}
}
Add github actions for automated testing and deployment to npm.
Instead of:
nestjs-cls/src/lib/cls.service.ts
Line 15 in f8bc850
nestjs-cls/src/lib/cls.service.ts
Line 30 in f8bc850
we could have this:
/**
* Evaluates to `true` if `T` is `any`. `false` otherwise.
* (c) https://stackoverflow.com/a/68633327/5290447
*/
type IsAny<T> = (
unknown extends T
? [keyof T] extends [never] ? false : true
: false
);
class ClsService<S extends Record<string, any> = Record<string, any>> {
// ...
set<T extends S[K] = any, K extends keyof S = keyof S>(key: K, value: T) {}
// ...
get<T = any, K extends keyof S = string>(key: K):
IsAny<T> extends true
? keyof S extends K
? undefined
: S[K]
: T
//
{}
}
Then we will get this:
One thing about my solution is that clsService.get<foo>('valid')
will always give the type foo
. I don't know if this is good or not, tbh. I've tried to implement a way on checking if foo
is assignable to S['valid']
but didn't succeed.
Also, we can't have non-string values as a key on ClsService#set
and ClsService#get
, which I think is expected due to the S = Record<string, any>
, but in the current version this can be bypassed.
Hi @Papooch !
I am considering implementing decorator-based database transactions in our NestJs application using AsyncLocalStorage. I'm reaching out to you, because looking at your CLS implementation, you seem to have extensive experience with this.
I'd like to merge the concept from this issue for Prisma, but using AsyncLocalStorage instead of cls-hooked. The idea is to introduce a @Transaction
decorator for service methods, that would store a transaction client in the storage and wrap the function s.t. the repositories within that call chain could retrieve it and execute their operations with it.
A given repository, when invoked, would try to first retrieve the transaction client from the store and use it if it's available. If not available, it would just use its own db-client (i.e. there is no composite transaction happening).
Do you think this is a sound approach? I'm not entirely sure about where the AsyncLocalStorage should be instantiated. Does it need to happen at bootstrap time or can it be spontaneous (when decorator is called)?
Any feedback would be much appreciated! ๐
PS.: I also considered to use your CLSService, but AFAIK injecting a custom service into a decorator would be challenging, so I'm not sure it'd be applicable, or would it? ๐ค
Currently, if the ClsStore
interface contains a complex object, all possible nested paths are generated.
push
, fill
, concat
)Issue 1 could be easily solved by adding any[]
to the list of terminal types so array methods are not expanded.
Issue 2 is tricky, but could be probably solved by introducing a terminal "branded type" that would stop generating the paths beyond a certain point,
Example:
interface IRequestContext extends ClsStore {
really: {
complex: Terminal<{
nested: {
type: string
}
}>
}
}
Issue 3 most likely cannot be automatically detected and can only be addressed by the solution from issue 2.
This should only generate the following type union: really | really.complex
, ignoring whatever is inside the Terminal
type.
Todo:
Proxy is defined as ()=>null
at https://github.com/Papooch/nestjs-cls/blob/main/packages/core/src/lib/proxy-provider/proxy-provider-manager.ts#L95C20-L95C25
Is there a specific reason for doing that instead of defining it as an empty object? It's causing object retrieved from cls has typeof proxiedObject
equal to function instead of object.
Instead of adding new features to the library, create a core library and provide extra functionality in the form of plugins. This would require breaking changes to the public API.
Proposed changes:
@nestjs-cls/core
with default functionality@nestjs-cls/proxy
I suggest adding additional sections to the README or FAQ:
This question sometimes arises because of early concerns with async_hooks performance.
See for example: iamolegga/nestjs-pino#322
Is there currently a concern? (I am not aware of any.)
According to Node documentation, AsyncLocalStorage is stable, even though the parent async_hooks is still experimental.
https://nodejs.org/api/async_context.html#class-asynclocalstorage
I wouldn't be so sure that using AsyncLocalStorage atm would be better (performance-wise) than properly designed request-scoped providers. - from nestjs/nest#1407 (comment)
In addition to performance, another problem with request-scoped providers is the scope hierarchy implementation in NestJS:
Scope bubbles up the injection chain. A controller that depends on a request-scoped provider will, itself, be request-scoped.
See https://stackoverflow.com/a/59681868/901597
This provider does not have this restriction.
Hey there, just came across this library while investigating my own issues with AsyncLocalStorage
losing context inside graphql interceptors.
I'm not (yet) using this library, I have a custom implementation, but I wanted to mention that I eventually got it working via this pattern in the interceptor:
intercept(...) {
return new Observable((observer) => {
storage.run(myContext, () => {
next
.handle()
.pipe(...)
.subscribe({
next: (res) => observer.next(res),
error: (error) => observer.error(error),
complete: () => observer.complete(),
});
});
});
}
Seems that wrapping the return in a cold observable is necessary, but this is all that I needed to do.
With this I can use run
instead of enterWith
, which I noticed was mentioned as a caveat in the readme about not being supported.
It should be enough to loosen the peer dep version, bump dev deps to use v9
Hello,
We recently added bullmq into our nestjs project.
Our project has ClsModule configured in AppModule imports as:
imports: [
ClsModule.forRoot({
guard: {
generateId: true,
mount: true,
},
global: true,
}),
],
And we have custom ClsStore interface:
export interface DataStore extends ClsStore {
testRunSuffix: string;
}
it is used in our service as:
@Injectable()
export class DataStoreService {
constructor(private readonly cls: ClsService<DataStore>) {}
public get testRunSuffix(): string {
return this.cls.get('testRunSuffix');
}
public set testRunSuffix(suffix: string) {
this.cls.set('testRunSuffix', suffix);
}
}
We have a controller with ClsGuard as:
@UseGuards(ClsGuard)
@Controller()
export class JobsController {
//...
}
We dont use decorators, or middlewares. We inject class DataStoreService
to other providers/services where we want to use store.
Before introducing BullMQ our flow was next:
Our listeners have many other services injected. Each service represents some feature domain.
In each domain feature service we have injected DataStoreService
too.
Listener can set in DataStoreService some data before executing feature services.
Feature services can read and set some data to store too so other feature services can access this data from the DataStoreService.
It worked well.
Problem was that with eventemmitter we were not able to control how many events we want to process concurrently.
We added BullMQ(not bull), implemented queues, processors(workers).
Flow now is next:
Processor has DataStoreService injected. Processor has all the feature services injected.
Processor sets data to DataStoreService.
Feature services try to read data from DataStoreService and it's undefined.
I tried to add @UseCls
decorator to the Processor->process method and also to all feature service method which executed from Processor but it didn't help.
We have another flow where instead of Controller endpoint we have a @Cron
decorator on specific method in Controller. On the schedule this method does the same as endpoint described above:
Non of the described flows work with DataStoreService.
Data which we set in Processor->process method is undefined when we try to read it from feature service.
Code how our processor and feature services look like:
@UseGuards(ClsGuard)
@Controller()
export class JobsController {
constructor(
private readonly jobService: JobsService,
) {}
@Cron(CronExpression.EVERY_DAY_AT_NOON)
async executeScheduledJobs(): Promise<void> {
await jobService.created({
id: job.id,
});
}
}
@Injectable()
export class JobService
extends BaseQueueConsumerService
implements JobPlugin
{
constructor(
@InjectQueue(JOB_QUEUE_NAME) protected queue: Queue,
) {
super();
}
async created(data: JobCreatedRequestPayload): Promise<void> {
await this.queue.add(
JobQueueTopics.JOB_CREATED,
data,
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
}
@Processor(JOB_QUEUE_NAME, {
concurrency: JOB_TOPIC_WORKERS_AMOUNT,
})
export class JobProcessor extends BaseJobProcessor<
JobQueueTopic,
JobTopicPayload
> {
constructor(
private readonly store: DataStoreService, // <----- store described above the code example
private readonly test: TeamsService, // <---- feature service
) {
super();
}
@UseCls()
async process({
data,
name,
}: Job): Promise<void> {
this.store.jobId = data.id;
this.logger.log(`????? ${this.store.jobId}`); // output: ????? 1234567890
await this.test.test();
}
}
@Injectable()
export class TeamsService {
constructor(
private readonly store: DataStoreService, // <----- store described above the code example
) {}
@UseCls()
async test(): Promise<void> {
this.logger.log(`????? ${this.store.jobId}`); // output: ????? undefined
}
}
const providers = [DataStoreService];
@Module({
providers,
exports: providers,
})
export class DataStoreModule {}
@Injectable()
export class DataStoreService {
constructor(private readonly cls: ClsService<DataStore>) {}
public get jobId(): string {
return this.cls.get('jobId');
}
public set jobId(jobId: Uuid) {
this.cls.set('jobId', jobId);
}
}
export interface DataStore extends ClsStore {
jobId: Uuid;
}
const providers = [TeamsService];
@Module({
imports: [
DataStoreModule, // <--- Module with DataStoreService
],
providers,
exports: providers,
})
export class TeamsModule {}
const providers = [
JobService,
JobProcessor,
];
@Module({
imports: [
BullModule.registerQueue({
name: JOB_QUEUE_NAME,
}),
DataStoreModule, // <--- Module with DataStoreService
TeamsModule, // <--- Module with feature service
],
providers,
exports: [JobService],
})
export class JobModule {}
Could you please advise what we are doing incorrectly? And if it's possible at all in this setup to have shared local storage
for the Processor and its dependencies?
Add a feature to reuse existing context instead of overwriting it. It might also be useful to add "nested" contexts that would inherit context values from the parent.
Relates to #53
I have trouble to get proxy provider working in my case:
// app.module.ts
@InjectableProxy()
export class MyAsyncCtx {
beforeRender: any[];
}
@Module({
imports: [
ClsModule.forRoot({
global: true,
interceptor: {
mount: true,
},
proxyProviders: [MyAsyncCtx],
}),
],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ResponseInterceptor,
},
],
})
export class AppModule {}
// response.interceptor.ts
@Injectable()
export class ResponseInterceptor<T>
implements NestInterceptor
{
constructor(
private readonly myAsyncCtx: MyAsyncCtx,
) {}
...
NestJS DI throws error:
Nest can't resolve dependencies of the ResponseInterceptor (?). Please make sure that the argument dependency at index [0] is available in the AppModule context.
Potential solutions:
- If dependency is a provider, is it part of the current AppModule?
- If dependency is exported from a separate @Module, is that module imported within AppModule?
@Module({
imports: [ /* the Module containing dependency */ ]
})
Per my understanding everything should be configured correctly. I've also took a look in nestjs-cls
source code and cofirmed that proxyProviders: [MyAsyncCtx]
are indeed exported from ClsModule
. Now I don't have any ideas more why this still doesn't work?
Update dev dependencies to new versions to make sure the library works with latest code.
Additionaly, Mercurius has got an official support from NestJS, so we can replace the nestjs-mercurius
package with it.
Log
Error: Cannot set the key "logTrace". 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"
at ClsService.set (/home/ian/templates/vite-nest/node_modules/.pnpm/[email protected]_77foi4w27ghy47yutmnzv7krjy/node_modules/nestjs-cls/dist/src/lib/cls.service.js:41:19)
at MiddlewareHost.use (/src/middlewares/authentication/authentication.middleware.ts:26:18)
at MiddlewareHost.use (/home/ian/templates/vite-nest/node_modules/.pnpm/@[email protected]_jrq2rdgfp2sx67wmylmrqliwxe/node_modules/@nestjs/core/middleware/utils.js:48:30)
at /home/ian/templates/vite-nest/node_modules/.pnpm/@[email protected]_jrq2rdgfp2sx67wmylmrqliwxe/node_modules/@nestjs/core/router/router-proxy.js:9:23
at Holder.done (/home/ian/templates/vite-nest/node_modules/.pnpm/@[email protected]/node_modules/@fastify/middie/engine.js:107:13)
at Object.run (/home/ian/templates/vite-nest/node_modules/.pnpm/@[email protected]/node_modules/@fastify/middie/engine.js:59:12)
To reproduce:
version 3.5.0
โ tools git:(master) โ npm i nestjs-cls
npm ERR! Cannot read properties of null (reading 'edgesOut')
npm ERR! A complete log of this run can be found in:
npm ERR! /Users/lastdoctor/.npm/_logs/2023-08-24T11_21_01_740Z-debug-0.log
Hi:
I use Factory Proxy Providers , tenant database connection example.
The provide "TENANT_CONNECTION" unavailable in my context.
Could you give some more detail with tenant database connection example, like Quick Start example๏ผ
Where to use ClsModule.forFeature๏ผ
Use ClsModule.forFeature or ClsModule.forFeatureAsync? ClsModule.forFeature 'provide' argument ERROR
Thank you
I just got this weird error when use your library in Nestjs
I set up it in my AppModule like below :
@Module({
imports: [
ClsModule.forRoot({
global: true,
middleware: {
mount: true,
generateId: true,
idGenerator: (req: Request) =>
req.headers[CORRELATION_ID_HEADER] ?? uuidv4(),
setup(cls, req) {
console.log('calling cls middleware');
const correlationId = cls.getId();
cls.set('correlationId', correlationId);
req.headers[CORRELATION_ID_HEADER] = correlationId;
},
},
}),
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
],
})
export class AppModule {}
In main.ts
I use prefix for API version
app.setGlobalPrefix('api', {
exclude: [
{
path: 'health',
method: RequestMethod.GET,
},
],
});
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
validationError: {
target: false,
value: false,
},
})
);
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
Steps I did :
/api/v1
=> Cls Middleware is not triggered ~ there is no log "calling cls middleware"But when I use that prefix, I see the log and also the correlationId is set in my exception response ๐
It seems like the mount: true
does not work like it is supposed to do ๐ค
Overview
Add rxjs
and reflect-metadata
to peerDependencies
of (I think) packages/core/package.json
.
Versions
nestjs-cls: 3.5.0
NodeJS: v16.16.0
Yarn: 3.3.1 (using Yarn PnP mode)
typescript: 4.9.4
Details
When trying to import nestjs-cls
, I get the following error
Cannot find module 'rxjs' from 'REDACTED/.yarn/__virtual__/nestjs-cls-virtual-c54616727a/0/cache/nestjs-cls-file-29ea52353d-10faf375e2.zip/node_modules/nestjs-cls/dist/src/lib/cls-initializers'
We use Yarn in PnP (Plug'n'Play) mode. I believe this is a bit more strict with module resolution, particularly around transitive dependencies. Since lib/cls-initializers/cls.interceptor.ts
uses rxjs
directly but this isn't declared as an explicit dependency of nestjs-cls
(except in "dev" mode) I think Yarn is blocking this being imported. Same applies for reflect-metadata
in lib/cls-initializers/use-cls.decorator.ts
.
Fix
For 3.5.0, I think the fix would be adding rxjs
and reflect-metadata
to the root package.json
in peerDependencies
would fix this (not sure what version constraints). With the yarn workspace refactor, I think the same fix should instead be applied to packages/core/package.json
Workaround
I was able to workaround this by downloading the v3.5.0 source code, making the above change to See reply below for much better workaround using peerDependencies
, and including this as a file:
dependency in our package.json
files. This works but isn't really practical to integrate into our build system, so this issue probably blocks us using this package.packageExtensions
N.B. thanks for the awesome package using AsyncLocalStorage, looking forward to being able to use it.
I am getting the following typing error when trying to use the package.
node_modules/nestjs-cls/dist/src/lib/cls.interfaces.d.ts:42:6 - error TS1023: An index signature parameter type must be either 'string' or 'number'.
42 [key: symbol]: any;
~~~
Reproduction repository: https://github.com/ruslanguns/cqrs-with-cls
System information
[System Information]
OS Version : Linux 5.10
NodeJS Version : v16.13.1
NPM Version : 8.3.0
[Nest CLI]
Nest CLI Version : 8.0.2
[Nest Platform Information]
platform-express version : 8.0.0
common version : 8.0.0
core version : 8.0.0
cqrs version : 8.0.0
How does this allow me to get access to the request object in a singleton service - or better yet, how does it allow a singleton service to have dependencies or access to request scoped services?
I have a service which has a dependency on HttpService from nestjs HttpModule. This was working fine and being injected until I gave the class the @InjectableProxy attribute. Now I get this error
Cannot create Proxy provider RequestScopedService (?, RequestHelper). The argument HttpService at index [0] was not found in the ClsModule Context.
Potential solutions:
- If HttpService is a provider from a separate module, make sure to import the module in "ClsModule.forFeatureAsync()" registration
Hi,
unfortunately, Mercurius also requires enterWith: true
to be true. The issue occurs not if using fastify.inject
- only with "real clients". I have absolutely no idea why, but that seems to fix it. I opened a PR to replace fastify.inject
with supertest in the mercurius e2e test and to set useEnterWith: true
. I also updated the documentation accordingly.
I would like to mount CLS in middleware, since it guarantees one mount per request context (in graphql if you use interceptor, the context will be per resolver instead).
However, middleware doesn't yet have access to user and stuff, since that gets resolved in Guards. So I would like to add that in an interceptor (and not directly in a guard, for separation of concerns). Setting up such an interceptor myself would be kinda trivial, but I wonder if it's a good enough case to include in the library anyway?
UPD: not that trivial, since a global interceptor for setup can't inject ClsService, lol. Oh well, there are workarounds
http
and ws
, within one application.cls.set('tenant', tenant)
. This information will then be used by the custom logger to add tenant information for each log statement created in websocket context.I'm not familiar with this repository, neither with cls nor the different NestJs communication protocols. Therefore, at the moment, I can't provide an ideas. From the perspective of a consumer of nestjs-cls, it would be great to extend the existing functionality in such a way, that cls for websockt context can be added by setting a property or by an additional import in the module that provides the websocket gateway.
It might be possible to emulate the behavior of Durable Providers with CLS as well, where we could retrieve the same context instance in multiple subsequent requests.
A proper solution should also include a configurable cache timeout for the "durable" instances.
I just create a new app with npx @nestjs/cli new
that has the following src/app.service.ts
:
import { Injectable } from '@nestjs/common';
import { ClsService, ClsStore } from 'nestjs-cls';
interface ClsStoreFoo extends ClsStore {
foo: string;
bar: AbortController; // If I drop this line, it works
}
@Injectable()
export class AppService {
constructor(
private readonly cls: ClsService<ClsStoreFoo>,
) {}
getHello(): string {
return this.cls.set('foo', '');
}
}
src/app.service.ts:16:5 - error TS2589: Type instantiation is excessively deep and possibly infinite.
16 this.cls.set('foo', '');
~~~~~~~~~~~~~~~~~~~~~~~
When the store has some entry with the type AbortController, I got that a tsc error on ClsService#get
and ClsService#set
typescript
: v4.7 & v4.8.4nestjs-cls
: v3.0.3// app.module.ts
@Module({
imports: [
ClsModule.register({
global: true,
interceptor: {
generateId: true,
mount: true,
setup: (cls: ClsService, context: ExecutionContext) => {
cls.set('startTime', +new Date());
},
},
}),
TypeOrmModule.forRoot({
...config.mysql,
maxQueryExecutionTime: 0.1,
logger: new OrmLogger(),
}),
],
})
export class AppModule { }
// controller.ts
@ApiOperation({ summary: 'log ๆต่ฏ' })
@Get('log')
async log(@Query('id', new ParseIntPipe()) id: number): Promise<any> {
return await this.userRepository.find({
where: {
id,
}
});
}
// OrmLogger.ts
import { ClsServiceManager } from 'nestjs-cls';
import { Log } from 'src/common/logger/';
import { Logger, QueryRunner } from "typeorm";
export class OrmLogger implements Logger {
logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) {
const cls = ClsServiceManager.getClsService();
console.log(`${cls.get('startTime')} Executed: ${query}`);
}
logQuerySlow(time: number, query: string, parameters?: any[], queryRunner?: QueryRunner) {
const cls = ClsServiceManager.getClsService();
console.log(`${cls.get('startTime')} Executed: ${query} time: ${time}ms`);
}
}
// console
1649417986277 Executed: SELECT `User`.`id` AS `User_id`, `User`.`truename` AS `User_truename`, `User`.`studentid` AS `User_studentid` FROM `user` `User` WHERE `User`.`id` IN (?)
undefined Executed: SELECT `User`.`id` AS `User_id`, `User`.`truename` AS `User_truename`, `User`.`studentid` AS `User_studentid` FROM `user` `User` WHERE `User`.`id` IN (?) time: 19ms
I have two services, one exposes REST apis and the other one grpc apis. I was able to successfully add the ClsModule as a middleware for the first one but not able to do it with the second one. I can see that the interceptor is initialized properly when the app starts up with this message
[Nest] 28817 - 10/23/2023, 6:56:34 PM DEBUG [ClsModule] ClsInterceptor will be automatically mounted
But, in my controller, when I access the ClsService, I get this error
[Nest] 28817 - 10/23/2023, 6:56:37 PM ERROR [RpcExceptionsHandler] Cannot set the key "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"
This is what i added to the app.module
ClsModule.forRoot({
interceptor: {
mount: true,
setup: (cls, context) => {
const req = context.getArgs();
console.log(`request is ${req}`);
cls.set('TENANT_ID', 'tenant');
},
},
}),
Hi folks, in this library we have an opportunity to write an interceptor to use on the controller, what do you think about making a globalClsInterceptor? is it possible?
sometimes the interceptor doesn't work very well, I think we do not have an injection of ClsService in the current interceptor when we do not use it on the same module, I'm not totally sure and confident about it.
I have one situation in my application to use that, but I think if I put the if condition to verify the ClsService on interceptor probably it will return an undefined value and not work so well.
In this application we do not have a bootstrap function, because it is a library and we will put it in other projects, I test the Cls library in one full project NestJs with dependencies like the documentation and it works fine.
Now I will write the sample, the minimal example:
The service
@Injectable()
export class LoggerService {
constructor(private readonly cls: ClsService) {}
private addIp(param: string): void {
param = this.cls.get('ip');
}
}
The interceptor:
@Injectable()
export class IpInterceptor {
// I put it that way but if I change for standard of documentation it produces the same error
constructor(private readonly cls?: ClsService) {
if (!(cls instanceof ClsService)) {
this.cls = ClsServiceManager.getClsService();
}
}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
request.setHeader('ip', ''ip-bla-bla-bla');
this.cls.set('ip', request.headers['ip']);
return next.handle();
}
}
The Module:
@Module({
imports: [
ClsModule.forRoot({
global: true,
middleware: { mount: true },
}),
],
providers: [LoggerService],
exports: [LoggerService],
})
export class NewClsModule {}
And now I will use it in other applications, (e.g: another nest application):
@Module({
imports: [
NewClsModule
],
providers: [],
exports: [],
})
export class InfraModule {}
And add the interceptor to the bootstrap function
app.useGlobalInterceptors(new IpInterceptor());
And I receive the following error:
Nest can't resolve dependencies of the ClsModule (?, ModuleRef). Please make sure that the argument HttpAdapterHost at index [0] is available in the ClsModule context.
In my case, I need this approach because I use a global log and need a global interceptor.
Thank you for your answers ;)
After running npm install nestjs-cls, it seems decoarators file is getting compiled correctly in the node_modules folder.
Importing in App Module as follows:
ClsModule.register({
global: true,
middleware: { mount: true },
guard: { mount: true, generateId: true },
})
Hey @Papooch! Thanks for an amazing library! I've been trying to fit it properly in my Nest.js + TypeORM project, where I am trying to implement RLS-based multi-tenancy for PostgresDB.
I am using rls library to achieve that. One downside is that it uses Request.SCOPE
. I wanted to work around it using nestjs-cls
which seems like a perfectly doable thing.
This part from nestjs-cls
use cases examples
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;
},
});
Looks like this part of the code from cls
library.
So as I tried just replacing Request
with CLS_RQS
there in my own fork, but it didn't work, because it was trying to instantiate the rls
module too early, at the application bootstrap time, when I don't have access to the request yet.
Now when when I am trying to do it with ClsModule.forFeature
as in the example above โ I am struggling with making TENANT_CONNECTION
avialble in RLSModule
as well.
Can you please nudge me in the right direction of how to do it properly? I am sure it could be done really easily, but couldn't just wrap my head around it.
I'm using a common setup function for all my unit tests. However when running several test suites together only the first one to use the ClsService succeeds.
Repro url: https://github.com/lazaroofarrill/nestjs-cls-error-across-multiple-test-runs
I have a external module (in a private repository) using nestjs-cls
MyModuleUsingNestjsCls
imports
ClsModule.forRoot({
global: true,
middleware: { mount: true },
})
provide: MyServiceUsingClsService
exports: MyServiceUsingClsService
This module has a provider that uses ClsService:
class MyService constructor(private readonly clsService: ClsService) {}
This app works fine when i start this module, the cls service also works.
If i import MyModuleUsingNestjsCls in another nestjs app like this:
MyAnotherAppModule imports MyModuleUsingNestjsCls (from an repository like import { MyModuleUsingNestjsCls } 'my-private-lib')
I have the following error: Nest can't resolve dependencies of the ClsModule (?, ModuleRef). Please make sure that the argument HttpAdapterHost at index [0] is available in the ClsModule context.
Any idea why this happens?
Hi,
I am trying to setup the context to holds my sessionId as well but the ClsModule setup is always running before my SessionModule setup (hence session is not available in ClsModule context).
SessionModule.forRoot({
session: {
secret: 'session secret...',
resave: false,
cookie: {
maxAge: 60000,
},
genid: (req) => {
console.log(`generate session id now`);
return uuidv4();
},
saveUninitialized: true,
},
}),
// Register the ClsModule and automatically mount the ClsMiddleware
ClsModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
return {
global: true,
middleware: {
mount: true,
setup: (cls: ClsService, request: any) => {
const userIp = getClientIp(request);
const originalUrl = request['originalUrl'];
const requestId = request['id'] + '';
const user = request['user'] as User;
let userId, email;
if (user) {
userId = user.id;
email = user.email;
}
const trackingInfo = {
requestId,
originalUrl,
userIp,
userId,
email,
};
console.log(`setting up context`, trackingInfo);
cls.set(GLOBALS.CLS_TRACKING, trackingInfo);
},
},
};
},
}),
If I look at the log I get:
setting up context...
generate session id now...
Any recommendation to fix this issue?
Currently I create a separate interceptor that complements the setup and adds the session attribute to my context.
(relates to #18)
Consider adding a @UseCls()
method decorator to set up the context for a method automagically, when it is run outside of a request context.
// This is currently NOT supported
@Injectable()
export class CronController {
constructor(
private readonly cls: ClsService,
private readonly someService: SomeService
)
@Cron('45 * * * * *')
@UseCls()
handleCron() {
this.cls.set(CLS_ID, uuid())
this.someService.doTheThing();
}
}
The decorator would need to replace the method's implementation by wrapping the actual call with the cls.run()
method. This would be the prefered way of entering the CLS context for invoking CLS-aware code that does not go through a controller, where a middleware or enhancer take care of setting up the context.
Namespaces are a remnant from the early implementation based on cls-hooked
, but I've never found an useful application for them, therefore I've decided to remove them to clean up the code base and re-use the API in favor of #30
Add support for async configuration. This could be useful if anyone wants to inject services for use in idGenerator
or setup
methods.
Greetings!
While trying out this module within an app using GraphQL, I have come across an odd behavior:
I have defined a simple interceptor that logs the requestId (csl.getId()
) and applied it to a GQL resolver, which resolves a bunch of fields for an object type (@ResolveField).
When I configure the module to use the middleware:
ClsModule.forRoot({
middleware: {
generateId: true,
mount: true,
},
});
I get, as expected, all log statements showing the same requestId.
(sidenote: documentation states for GQL I must set useEnterWith: true
, but practically things work even without - I then assumed that this was somehow being automatically set, but debugging revealed that the code was still hitting ClsService.run
, so that's another mystery to me...).
When I configure the module to use the interceptor/guard:
ClsModule.forRoot({
interceptor | guard: {
generateId: true,
mount: true,
},
});
all the log statements show different requestId values.
By the way, the reason I was investigating the other two approaches was, aside from the security concerns of EnterWith
, to test out suppport for GraphQL over websocket (GraphQL suscriptions), which, as per documentation, turns out to not be working with the middleware approach.
If the project grows, we'll need contributing guidelines -> create a CONTRIBUTING.md
file with steps.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.