Code Monkey home page Code Monkey logo

nestjs-cls's People

Contributors

agmoss avatar alexandre-abrioux avatar almontasser avatar bmeverett avatar giosuedelgado avatar jangdaljin avatar joebowbeer avatar maheegamage avatar micalevisk avatar moofoo avatar nicobuzeta avatar papooch avatar psteinroe avatar sam-artuso avatar zyles avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

nestjs-cls's Issues

Cannot resolve dependencies of ClsModule

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 },
    }),
    ```

Expose run options in UseCls decorator

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.

Question: How to use Factory Proxy Providers?

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;
    },
});
  • forFeature does not seem to accept the config, probably forFeatureAsync is meant?
  • with forFeatureAsync I run into dependency errors

The old request scope approach

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.

embed ClsModule in RlsModule

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],
    };
  }
}

standalone ClsModule

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.

bootstrap issue - Cannot read properties of undefined (reading 'getStore')

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.

Add support for request-scoped providers based on CLS

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

Additional ClsService methods

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

Example on how to use ClsMiddleware with dependency injection

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?

Usage when no request context is available (e.g. cron jobs)

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

Improve type inference and type safety

Instead of:

set<T = any>(key: string, value: T) {

get<T = any>(key: string): T {

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:

image

image

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.

Design Question: Storing Prisma transaction client in AsyncLocalStorage

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? ๐Ÿค”

Generated path type sometimes contains unnecessary paths

Currently, if the ClsStore interface contains a complex object, all possible nested paths are generated.

image
image
This causes some issues:

  1. It generates unnecessary paths for methods of arrays (push, fill, concat)
  2. It generates unnecessary paths for some complex user-defined types
  3. It completely breaks if the type recursively references itself

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:

Split into core and plugins

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:

  • create a core package @nestjs-cls/core with default functionality
  • create a plugin API in core
  • remove Proxy providers from core and move them to @nestjs-cls/proxy

docs: Address performance and stability concerns

I suggest adding additional sections to the README or FAQ:

Performance concerns with AsyncLocalStorage?

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

Stability of AsyncLocalStorage?

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

Could a more performant request-scoped provider implementation solve this problem?

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.

Add ClsInterceptor to avoid having to use enterWith with GraphQL

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.

Support NestJS 9

It should be enough to loosen the peer dep version, bump dev deps to use v9

Cannot setup correctly to use with nestjs + bullmq

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:

  • controller receives request from api
  • method in controller uses EventEmitter2 to emit specific to logic event
  • listener of event recieves payload and starts execution of logic

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:

  • Controller receives request through endpoint
  • controller runs service method which adds topic to queue
  • processor(worker) gets topic+body from queue
  • processor runns injected feature services

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:

  • run method in the injected service
  • service adds topic+payload to queue
  • processor reacts to the topic
  • processor executes methods in feature services

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?

Feature: Reuse existing context

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

Can't resolve proxy provider

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

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.

can not access to CLS in e2e test after the first sub-test

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:

  1. degit https://github.com/ianzone/vite-nest-fastify to-check
  2. cd to-check
  3. pnpm i
  4. pnpm run test

Cannot install

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

Factory Proxy Providers doesn't work

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

Cls middleware is not triggered in all routes

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 :

  • Get 404 route without the prefix /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 ๐Ÿค”

Add 'rxjs' and 'reflect-metadata' as peer dependencies

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 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. See reply below for much better workaround using packageExtensions

N.B. thanks for the awesome package using AsyncLocalStorage, looking forward to being able to use it.

Getting typing error with ClsStore interface

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

Can't resolve HttpService

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

Execution context is also lost with mercurius without enterWith: true

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.

Setup in a different enhancer than mounting?

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

Cls support for websockets

Problem statement

  • Summary: At the moment, there seems to be no way to get cls working for websocket communication. It would be great to have cls support for both, http and ws, within one application.
  • Background: I have a tenant interceptor for my websocket gateway that reads the tenant from the namespace of the websocket object. This information shall be used in the interceptor to do 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.

Solution

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.

Minimum reproduction

Consider durable CLS-based providers

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.

tsc error 'Type instantiation is excessively deep and possibly infinite' when using AbortController

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


  • OS: Linux
  • nodejs: v16.15
  • typescript: v4.7 & v4.8.4
  • nestjs-cls: v3.0.3

Why can't I get context data in Typeorm's logQuerySlow when logQuery does

// 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

What is the cause of this, and what solutions are available

Adding ClsModule as an interceptor is not working

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');
        },
      },
    }),

Suport for a global interceptor or other idea

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

Error: Cannot find module './lib/cls.decorators'

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 },
})

Integrating with RLS

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.

Error using nestjs-cls in an external module

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?

adding session id

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.

Add @UseCls decorator

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

Remove Namespaces support in `v3.0`

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

Different requestIds for same request in GraphQL (when using an Interceptor)

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.

Add contributing guide

If the project grows, we'll need contributing guidelines -> create a CONTRIBUTING.md file with steps.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.