feat(medusa): Cache modules (#3187)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
/dist
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
.env
|
||||
*.sql
|
||||
@@ -0,0 +1,26 @@
|
||||
# Medusa Cache Redis
|
||||
|
||||
Use Redis as a Medusa cache store.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
yarn add @medusajs/cache-redis
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
{
|
||||
ttl?: number // Time to keep data in cache (in seconds)
|
||||
|
||||
redisUrl?: string // Redis instance connection string
|
||||
|
||||
redisOptions?: RedisOptions // Redis client options
|
||||
|
||||
namespace?: string // Prefix for event keys (the default is `medusa:`)
|
||||
}
|
||||
```
|
||||
|
||||
### Other caching modules
|
||||
- [Medusa Cache In-Memory](../cache-inmemory/README.md)
|
||||
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@medusajs/cache-redis",
|
||||
"version": "1.0.0",
|
||||
"description": "Redis Cache Module for Medusa",
|
||||
"main": "dist/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/cache-redis"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"author": "Medusa",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@medusajs/medusa": "*",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4",
|
||||
"ts-jest": "^25.5.1",
|
||||
"typescript": "^4.4.4"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "tsc --build --watch",
|
||||
"prepare": "cross-env NODE_ENV=production yarn run build",
|
||||
"build": "tsc --build",
|
||||
"test": "jest --passWithNoTests",
|
||||
"test:unit": "jest --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@medusajs/medusa": "^1.7.11"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ModuleExports } from "@medusajs/modules-sdk"
|
||||
|
||||
import { RedisCacheService } from "./services"
|
||||
import Loader from "./loaders"
|
||||
|
||||
const service = RedisCacheService
|
||||
const loaders = [Loader]
|
||||
|
||||
const moduleDefinition: ModuleExports = {
|
||||
service,
|
||||
loaders,
|
||||
}
|
||||
|
||||
export default moduleDefinition
|
||||
@@ -0,0 +1,38 @@
|
||||
import Redis from "ioredis"
|
||||
import { asValue } from "awilix"
|
||||
import { LoaderOptions } from "@medusajs/modules-sdk"
|
||||
|
||||
import { RedisCacheModuleOptions } from "../types"
|
||||
|
||||
export default async ({
|
||||
container,
|
||||
logger,
|
||||
options,
|
||||
}: LoaderOptions): Promise<void> => {
|
||||
const { redisUrl, redisOptions } = options as RedisCacheModuleOptions
|
||||
|
||||
if (!redisUrl) {
|
||||
throw Error(
|
||||
"No `redisUrl` provided in `cacheService` module options. It is required for the Redis Cache Module."
|
||||
)
|
||||
}
|
||||
|
||||
const connection = new Redis(redisUrl, {
|
||||
// Lazy connect to properly handle connection errors
|
||||
lazyConnect: true,
|
||||
...(redisOptions ?? {}),
|
||||
})
|
||||
|
||||
try {
|
||||
await connection.connect()
|
||||
logger?.info(`Connection to Redis in module 'cache-redis' established`)
|
||||
} catch (err) {
|
||||
logger?.error(
|
||||
`An error occurred while connecting to Redis in module 'cache-redis': ${err}`
|
||||
)
|
||||
}
|
||||
|
||||
container.register({
|
||||
cacheRedisConnection: asValue(connection),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { RedisCacheService } from "../index"
|
||||
|
||||
const redisClientMock = {
|
||||
set: jest.fn(),
|
||||
get: jest.fn(),
|
||||
}
|
||||
|
||||
describe("RedisCacheService", () => {
|
||||
let cacheService
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("Underlying client methods are called", async () => {
|
||||
cacheService = new RedisCacheService(
|
||||
{
|
||||
cacheRedisConnection: redisClientMock,
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
await cacheService.set("test-key", "value")
|
||||
expect(redisClientMock.set).toBeCalled()
|
||||
|
||||
await cacheService.get("test-key")
|
||||
expect(redisClientMock.get).toBeCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export { default as RedisCacheService } from "./redis-cache"
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Redis } from "ioredis"
|
||||
import { ICacheService } from "@medusajs/medusa"
|
||||
|
||||
import { RedisCacheModuleOptions } from "../types"
|
||||
|
||||
const DEFAULT_NAMESPACE = "medusa"
|
||||
const DEFAULT_CACHE_TIME = 30 // 30 seconds
|
||||
const EXPIRY_MODE = "EX" // "EX" stands for an expiry time in second
|
||||
|
||||
type InjectedDependencies = {
|
||||
cacheRedisConnection: Redis
|
||||
}
|
||||
|
||||
class RedisCacheService implements ICacheService {
|
||||
protected readonly TTL: number
|
||||
protected readonly redis: Redis
|
||||
private readonly namespace: string
|
||||
|
||||
constructor(
|
||||
{ cacheRedisConnection }: InjectedDependencies,
|
||||
options: RedisCacheModuleOptions = {}
|
||||
) {
|
||||
this.redis = cacheRedisConnection
|
||||
this.TTL = options.ttl ?? DEFAULT_CACHE_TIME
|
||||
this.namespace = options.namespace || DEFAULT_NAMESPACE
|
||||
}
|
||||
/**
|
||||
* Set a key/value pair to the cache.
|
||||
* If the ttl is 0 it will act like the value should not be cached at all.
|
||||
* @param key
|
||||
* @param data
|
||||
* @param ttl
|
||||
*/
|
||||
async set(
|
||||
key: string,
|
||||
data: Record<string, unknown>,
|
||||
ttl: number = this.TTL
|
||||
): Promise<void> {
|
||||
await this.redis.set(
|
||||
this.getCacheKey(key),
|
||||
JSON.stringify(data),
|
||||
EXPIRY_MODE,
|
||||
ttl
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a cached value belonging to the given key.
|
||||
* @param cacheKey
|
||||
*/
|
||||
async get<T>(cacheKey: string): Promise<T | null> {
|
||||
cacheKey = this.getCacheKey(cacheKey)
|
||||
try {
|
||||
const cached = await this.redis.get(cacheKey)
|
||||
if (cached) {
|
||||
return JSON.parse(cached)
|
||||
}
|
||||
} catch (err) {
|
||||
await this.redis.del(cacheKey)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache for a specific key. a key can be either a specific key or more global such as "ps:*".
|
||||
* @param key
|
||||
*/
|
||||
async invalidate(key: string): Promise<void> {
|
||||
const keys = await this.redis.keys(this.getCacheKey(key))
|
||||
const pipeline = this.redis.pipeline()
|
||||
|
||||
keys.forEach(function (key) {
|
||||
pipeline.del(key)
|
||||
})
|
||||
|
||||
await pipeline.exec()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns namespaced cache key
|
||||
* @param key
|
||||
*/
|
||||
private getCacheKey(key: string) {
|
||||
return this.namespace ? `${this.namespace}:${key}` : key
|
||||
}
|
||||
}
|
||||
|
||||
export default RedisCacheService
|
||||
@@ -0,0 +1,27 @@
|
||||
import { RedisOptions } from "ioredis"
|
||||
|
||||
/**
|
||||
* Module config type
|
||||
*/
|
||||
export type RedisCacheModuleOptions = {
|
||||
/**
|
||||
* Time to keep data in cache (in seconds)
|
||||
*/
|
||||
ttl?: number
|
||||
|
||||
/**
|
||||
* Redis connection string
|
||||
*/
|
||||
redisUrl?: string
|
||||
|
||||
/**
|
||||
* Redis client options
|
||||
*/
|
||||
redisOptions?: RedisOptions
|
||||
|
||||
/**
|
||||
* Prefix for event keys
|
||||
* @default `medusa:`
|
||||
*/
|
||||
namespace?: string
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es5",
|
||||
"es6",
|
||||
"es2019"
|
||||
],
|
||||
"target": "es5",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitReturns": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"downlevelIteration": true // to use ES5 specific tooling
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"./src/**/__tests__",
|
||||
"./src/**/__mocks__",
|
||||
"./src/**/__fixtures__",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user