node-cache-manager-redis-store icon indicating copy to clipboard operation
node-cache-manager-redis-store copied to clipboard

Incompatible with NestJS cache store

Open consult-kk opened this issue 1 year ago • 35 comments

V3.0.1 is incompatible with NestJS . Unable to register CacheModule using the RedisStore.

"message": "Type 'typeof import(\"cache-test/node_modules/cache-manager-redis-store/dist/index\")' is not assignable to type '(string | CacheStoreFactory | CacheStore) & typeof import(\"/Users/kk/dev/nodejs/cache-test/node_modules/cache-manager-redis-store/dist/index\")'.\n  ...

Here is the code I am using in the module.ts : ` import * as redisStore from "cache-manager-redis-store"; import { ConfigModule } from '@nestjs/config';

@Module({ imports: [ ConfigModule.forRoot(), CacheModule.register({ isGlobal: true, store: redisStore, url: "redis://localhost:6379", }), HttpModule, ], controllers: [AppController], providers: [AppService], }) export class AppModule {} `

consult-kk avatar Nov 03 '22 16:11 consult-kk

The same issue

dimaqw avatar Nov 06 '22 00:11 dimaqw

Same Issue

Alfagun74 avatar Nov 08 '22 20:11 Alfagun74

Same issue

belyaev-dev avatar Nov 12 '22 22:11 belyaev-dev

I found a way to fix this issue although it may not be nice.

import { redisStore } from 'cache-manager-redis-store'

CacheModule.registerAsync({
      isGlobal: true,
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        ttl: configService.get('CACHE_TTL'),
        store: (await redisStore({
          url: configService.get('REDIS_URL'),
        })) as unknown as CacheStore,
      }),
      inject: [ConfigService],
    }),

I'm not exactly sure what's going on outside of the fact the types don't match.

dougrocha avatar Nov 14 '22 22:11 dougrocha

I found a way to fix this issue although it may not be nice.

import { redisStore } from 'cache-manager-redis-store'

CacheModule.registerAsync({
      isGlobal: true,
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        ttl: configService.get('CACHE_TTL'),
        store: (await redisStore({
          url: configService.get('REDIS_URL'),
        })) as unknown as CacheStore,
      }),
      inject: [ConfigService],
    }),

I'm not exactly sure what's going on outside of the fact the types don't match.

Nah thanks imma stay on the old version until something proper gets released

Alfagun74 avatar Nov 15 '22 06:11 Alfagun74

@dougrocha You are effectively bypassing the Typescript type checks, which I would not recommend

I'm just stepping back into the Typescript and Node world again after a few years working in Go and Python, but I believe my #54 ⬆️ should get things working in NestJS by narrowing the type definitions provided by this package.

If @dabroek approves of the changes, then you should be unblocked.

MikeRossXYZ avatar Nov 16 '22 19:11 MikeRossXYZ

Also there is another issue. Here is my solution. The in memory provider supports milliseconds by default but the redis provider (this one) supports seconds.

And as others have mentioned, the 3rd argument on the standard in-memory provider takes a number but this provider needs an object with key "ttl" and a number.

More info here..

    // It supports seconds and NOT milliseconds
    if (this.configService.cacheProvider == CacheProvider.redis) {
      // support seconds!!
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      await this.cacheManager.set(apiKey, key, { ttl: this.configService.cacheTtl / 1000 } as any);
    } else {
      // rest of providers should support milliseconds
      await this.cacheManager.set(apiKey, key, this.configService.cacheTtl);
    }

iangregsondev avatar Nov 23 '22 13:11 iangregsondev

My solution above is a bit messy, first I had to use "as any" to bypass type issue but should not all providers support milliseconds? Otherwise you need to add some logic as my reply above ^^^

iangregsondev avatar Nov 23 '22 13:11 iangregsondev

@dabroek

luluhoc avatar Dec 07 '22 14:12 luluhoc

I managed to go through this error using require

When i used import and bypass using some weird thing in typescript i cant get anything in Redis container and was receiveng some errors in cache service. by using the code above, i managed to do things work. const redisStore = require('cache-manager-redis-store').redisStore;

GabSnow24 avatar Dec 07 '22 16:12 GabSnow24

For NestJs 8, I wasn't able to get any of the solutions to work.

One solution shown in the NestJs does work

CacheModule.register<ClientOpts>({
      store: redisStore,

      // Store-specific configuration:
      host: process.env.REDIS_HOST,
      port: parseInt(process.env.REDIS_PORT),
      password: process.env.REDIS_PASSWORD,
    }),

But you need to ensure that this is using cache-manager-redis-store 2.0.

cgat avatar Dec 11 '22 06:12 cgat

The problem is redisStore is of type Store, and CacheStore is required. My solution is casting redisStore as CacheStore to fix it.

CacheModule.register({ store: redisStore as unknown as CacheStore, host: 'localhost', port: 6379, }),

marianolinares avatar Jan 05 '23 23:01 marianolinares

Hello i solve this problem with down grade version of some packages, go check here https://github.com/Pangeran29/testing-nestjs/blob/master/src/testing-cache-redis/testing-cahce-redis.MD

Pangeran29 avatar Feb 07 '23 08:02 Pangeran29

Referring to this comment, I think this issue can be solved if you use node-cache-manager-redis-yet instead.

import { CacheModule, Module } from '@nestjs/common';
import { redisStore } from 'cache-manager-redis-yet';

@Module({
  imports: [
    CacheModule.register({
      store: redisStore,
      url: 'redis://localhost:6379',
    }),
  ],
})

yusuftaufiq avatar Feb 14 '23 06:02 yusuftaufiq

Hello i solve this problem with down grade version of some packages, go check here https://github.com/Pangeran29/testing-nestjs/blob/master/src/testing-cache-redis/testing-cahce-redis.MD

Thank you king

puckj avatar Feb 16 '23 03:02 puckj

Hello i solve this problem with down grade version of some packages, go check here https://github.com/Pangeran29/testing-nestjs/blob/master/src/testing-cache-redis/testing-cahce-redis.MD

Thanks

AhmedHdeawy avatar Feb 16 '23 12:02 AhmedHdeawy

This package is riddled with issues, bad typing, missing methods that are not exposed and so on. I appreciate it for bridging nest's cache manager and redis but at this point I think it's easier to use something like ioredis with very basic abstractions for the set and get methods, and just be done with it.

This issue prevents me from properly closing the connection after running my end to end tests. Previously I couldn't set redis clusters properly because of the way the client is set up and exposed through this additional layer. It works great for simpler projects anyway.

Exilz avatar Feb 17 '23 17:02 Exilz

The solution mentioned by @cgat is working. If we use cache-manager-redis-store 2.0. It fixes the problem

pratyush-prateek avatar Feb 19 '23 06:02 pratyush-prateek

Hello i solve this problem with down grade version of some packages, go check here https://github.com/Pangeran29/testing-nestjs/blob/master/src/testing-cache-redis/testing-cahce-redis.MD

Thanks It works.

dev-whoan avatar Feb 28 '23 02:02 dev-whoan

My solution with a dummy store fallback

import {
  CacheModule,
  CacheModuleOptions,
  CacheStore,
  Module
} from '@nestjs/common'
import { redisStore } from 'cache-manager-redis-store'

class DummyFallBackStore implements CacheStore {
  get(key: string) {
    console.debug(`Dummy Cache :: gets nothing for key - ${key}`)
    return Promise.resolve(undefined)
  }
  set(key: string) {
    console.debug(`Dummy Cache :: sets nothing for key - ${key}`)
    return Promise.resolve(undefined)
  }
  del(key: string) {
    logger.debug(`Dummy Cache :: deletes nothing for key - ${key}`)
    return Promise.resolve(undefined)
  }
}

@Module({
  imports: [
    CacheModule.registerAsync({
      useFactory: async () : Promise<CacheModuleOptions> => {
        try {
          const store = await redisStore({
            url: '<ConnectionString>'
          })
          return { store }
        } catch (error) {
          console.error(error)
          return { store: new DummyFallBackStore() }
        }
      }
    })
  ],
  controllers: [<MyController>]
})
export class MyModule {}

ram95krishh avatar Feb 28 '23 22:02 ram95krishh

Can't take credit, but chiming in, this solution worked for me in app.module.

import { redisStore } from 'cache-manager-redis-store';
...
CacheModule.registerAsync < any > ({
    isGlobal: true,
    imports: [ConfigModule],
    useFactory: async (configService: ConfigService) => {
        const store = await redisStore({
            url: configService.get('REDIS_URL'),
            ttl: 60,
        });
        return {
            store: () => store,
        };
    },
    inject: [ConfigService],
}),

Do note, for some reason ttl changed from milliseconds to seconds with this change, despite the in-memory caching being in milliseconds with my environment

CarsonRoscoe avatar Mar 02 '23 20:03 CarsonRoscoe

Problem

Example from official docs works only for cache-manager-redis-store@^2.0.0

cache-manager-redis-store@^2.0.0 incorrectly implement CacheManager interface and accepts TTL as object where ttl property should be in seconds { ttl: TTL_IN_SECONDS } see: https://github.com/dabroek/node-cache-manager-redis-store/issues/53#issuecomment-1325108666

Solution

The node-cache-manager have now official redis/ioredis cache stores.

npm i --save cache-manager-redis-yet

import { CacheModule, Module } from '@nestjs/common';
import { redisStore } from 'cache-manager-redis-yet';

@Module({
  imports: [
    CacheModule.registerAsync({
      isGlobal: true,
      useFactory: async () => ({
        store: await redisStore({
          host: process.env.REDIS_HOST,
          port: parseInt(process.env.REDIS_PORT!),
        }),
      }),
    }),
  ],
})
export class AppModule {}

MaksimKiselev avatar Mar 17 '23 11:03 MaksimKiselev

Thanks @MaksimKiselev I tried your solution and it works But, I just want to note that the TTL when I set, it is miliseconds not seconds

alexphamhp avatar Apr 03 '23 09:04 alexphamhp

@alexphamhp I meant that cache-manager-redis-store@^2.0.0 incorrectly implement CacheManager interface and accept ttl parameter as object with ttl property where ttl in seconds.

CacheManager interface define ttl parameter as number where ttl must be in microseconds.

I've update original comment. Thx. 🍻

MaksimKiselev avatar Apr 03 '23 10:04 MaksimKiselev

this correct solution:

CacheModule.registerAsync<any>({
      isGlobal: true,
      imports: [ConfigModule],
      useFactory: async () => {
        const store = await redisStore({
          url: 'redis://redis_listings_service:6379',
          ttl: 0,
        });
        return {
          store: () => store,
        };
      },
    }),

diariowebsquad avatar Apr 13 '23 21:04 diariowebsquad

My solution for cache-manager v5 + cache-manager-redis-yet: https://github.com/node-cache-manager/node-cache-manager/issues/210#issuecomment-1519127408

suoncha avatar Apr 23 '23 18:04 suoncha

Here is my solution. Node v:16.18.0 npm v:8.19.2

"@nestjs/cache-manager": "^1.0.0", "@nestjs/common": "^9.0.0", "@nestjs/core": "^9.0.0", "@nestjs/jwt": "^10.0.3", "@nestjs/platform-express": "^9.0.0", "@nestjs/typeorm": "^9.0.1", "bcrypt": "^5.1.0", "bcryptjs": "^2.4.3", "cache-manager": "^5.2.1", "cache-manager-redis-store": "^2.0.0", "cache-manager-redis-yet": "^4.1.1",

import {  Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { JwtModule } from '@nestjs/jwt';
import * as redisStore from 'cache-manager-redis-store';
@Module({
    imports:[JwtModule.register({ secret: 'secret', signOptions:{expiresIn:'1d'} }), CacheModule.register({
        store: redisStore as any,
        host: 'redis',
        port: 6379,
    })],
    exports:[JwtModule, CacheModule]
})
export class SharedModule {}

import { Body, Controller, Get, Post, Param, ParseIntPipe, Put, Delete, UseGuards, UseInterceptors, Inject } from '@nestjs/common'; import { AuthGuard } from 'src/auth/auth.guard'; import { ProductCreateDto } from './dtos/product-create.dto'; import { ProductService } from './product.service'; import { CacheKey,CacheInterceptor,CacheTTL, CACHE_MANAGER } from '@nestjs/cache-manager'; import {Cache} from 'cache-manager'

@Controller() export class ProductController { constructor(private productService:ProductService, @Inject(CACHE_MANAGER) private cacheManager:Cache){ }

@UseGuards(AuthGuard)
@Get('admin/products')
async getAllProducts(){
    return await this.productService.findAll({});
}

@UseGuards(AuthGuard)
@Post('admin/products')
async create(@Body() body:ProductCreateDto){
    return await this.productService.save(body)
}

@UseGuards(AuthGuard)
@Get('admin/products/:id')
async getProduct(@Param('id', ParseIntPipe) id:number){
    return await this.productService.findById(id)
}

@UseGuards(AuthGuard)
@Put('admin/products/:id')
async update(@Param('id',ParseIntPipe) id:number, @Body() body:ProductCreateDto){
    await this.productService.update(id, body);
    return await this.productService.findById(id);
}

@UseGuards(AuthGuard)
@Delete('admin/products/:id')
async delete(@Param('id') id:number){
    await this.productService.delete(id);
}

@CacheKey('products_frontend')
@CacheTTL(30*60)
@UseInterceptors(CacheInterceptor)
@Get('ambassador/products/frontend')
async frontend(){
    return await this.productService.findAll({})
}

@Get('ambassador/products/backend')
async backend(){
    let products = await this.cacheManager.get('products_backend');
    if(!products) {
        products = await this.productService.findAll({})
        await this.cacheManager.set('products_backend',products, 1800)
    }
    return products;
}

}

tushermahmud avatar May 15 '23 17:05 tushermahmud

CacheModule.registerAsync({
  isGlobal: true,
  useFactory: async () => {

    try {
      const store = await redisStore({
        url: process.env.REDIS,
        ttl: 60000,
        fallback: 'memory' -- it would be interesting to fall back
      });
      return {store};
    } catch (error) {
      return {
        store: 'memory',
      };
    }
  },

jongomes avatar Jul 20 '23 18:07 jongomes

cache.module.ts

I personally don't like having my configs directly in my App Module. I separate my configs into different modules and import them into App module.

You'd also notice that I prefer to read my environment variables from the ConfigService instead of reading directly from process.env (This way you have more control for where env files are being read from)

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { CacheModule, CacheModuleAsyncOptions } from '@nestjs/cache-manager';
import { redisStore } from 'cache-manager-redis-store';

@Module({
  imports: [
    CacheModule.registerAsync({
      isGlobal: true,
      imports: [ConfigModule], // No need to call ConfigModule.forRoot again
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => {
        const isActive = ['true', '1', 'yes'].includes(
          configService.get<string>('API_REDIS_STORE_IS_ACTIVE'),
        );
        return {
          store:
            isActive &&
            (await redisStore({
              // Store-specific configuration:
              socket: {
                host: configService.get<string>('API_REDIS_HOST'),
                port: +configService.get<number>('API_REDIS_PORT'),
              },
            })), // use redis when available or default to cache store
          ttl: 5000, // milliseconds
          max: 10, // maximum number of items in cache
        } as CacheModuleAsyncOptions;
      },
    }),
  ],
})
export class CacheConfigModule {}

alhajee avatar Jul 27 '23 14:07 alhajee

none of this works for me, I'm trying to get information from redis but I. don't get anything from the Redis database

arlan85 avatar Jul 31 '23 18:07 arlan85