feat(medusa): Cache modules (#3187)
This commit is contained in:
9
.changeset/two-cherries-enjoy.md
Normal file
9
.changeset/two-cherries-enjoy.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@medusajs/cache-inmemory": minor
|
||||
"@medusajs/cache-redis": minor
|
||||
"@medusajs/medusa": minor
|
||||
"medusa-plugin-contentful": patch
|
||||
"medusa-source-shopify": patch
|
||||
---
|
||||
|
||||
feat(medusa, cache-redis, cache-inmemory): Added cache modules
|
||||
@@ -3,13 +3,25 @@ const DB_USERNAME = process.env.DB_USERNAME
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD
|
||||
const DB_NAME = process.env.DB_TEMP_NAME
|
||||
|
||||
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379"
|
||||
|
||||
module.exports = {
|
||||
plugins: [],
|
||||
projectConfig: {
|
||||
redis_url: process.env.REDIS_URL,
|
||||
redis_url: redisUrl,
|
||||
database_url: `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}`,
|
||||
database_type: "postgres",
|
||||
jwt_secret: "test",
|
||||
cookie_secret: "test",
|
||||
},
|
||||
modules: {
|
||||
cacheService: {
|
||||
resolve: "@medusajs/cache-inmemory",
|
||||
// don't set cache since this is shared between tests
|
||||
// and since we have "test-product" / "test-variant" as ids
|
||||
// in a bunch of tests, this could cause that incorrect data is returned
|
||||
// (e.g. price selection caches calculations under `ps:${variantId}`)
|
||||
options: { ttl: 0 },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"build": "babel src -d dist --extensions \".ts,.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/cache-inmemory": "*",
|
||||
"@medusajs/medusa": "*",
|
||||
"faker": "^5.5.3",
|
||||
"medusa-interfaces": "*",
|
||||
|
||||
@@ -23,7 +23,6 @@ module.exports = ({ cwd, redisUrl, uploadDir, verbose, env }) => {
|
||||
COOKIE_SECRET: "test",
|
||||
REDIS_URL: redisUrl ? redisUrlWithDatabase : undefined, // If provided, will use a real instance, otherwise a fake instance
|
||||
UPLOAD_DIR: uploadDir, // If provided, will be used for the fake local file service
|
||||
CACHE_TTL: 0, // By default the cache service is disabled and 0 means that none of the cache key/value will be stored.
|
||||
...env,
|
||||
},
|
||||
stdio: verbose
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
const path = require("path")
|
||||
|
||||
const Redis = require("ioredis")
|
||||
const { GenericContainer } = require("testcontainers")
|
||||
|
||||
require("dotenv").config({ path: path.join(__dirname, "../.env") })
|
||||
|
||||
const workerId = parseInt(process.env.JEST_WORKER_ID || "1")
|
||||
|
||||
const DB_USERNAME = process.env.DB_USERNAME || "postgres"
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD || ""
|
||||
|
||||
const DbTestUtil = {
|
||||
db_: null,
|
||||
|
||||
setDb: function (connection) {
|
||||
this.db_ = connection
|
||||
},
|
||||
|
||||
clear: async function () {
|
||||
/* noop */
|
||||
},
|
||||
|
||||
teardown: async function () {
|
||||
/* noop */
|
||||
},
|
||||
|
||||
shutdown: async function () {
|
||||
/* noop */
|
||||
// TODO: stop container
|
||||
},
|
||||
}
|
||||
|
||||
const instance = DbTestUtil
|
||||
|
||||
module.exports = {
|
||||
initRedis: async function ({ cwd }) {
|
||||
// const configPath = path.resolve(path.join(cwd, `medusa-config.js`))
|
||||
// const { projectConfig } = require(configPath)
|
||||
|
||||
const container = await new GenericContainer("redis")
|
||||
.withExposedPorts(6379)
|
||||
.start()
|
||||
|
||||
const redisClient = new Redis({
|
||||
host: container.getHost(),
|
||||
port: container.getMappedPort(6379),
|
||||
db: workerId,
|
||||
})
|
||||
|
||||
instance.setDb(redisClient)
|
||||
|
||||
return redisClient
|
||||
},
|
||||
useRedis: function () {
|
||||
return instance
|
||||
},
|
||||
}
|
||||
@@ -39,5 +39,9 @@ module.exports = {
|
||||
resources: "shared",
|
||||
resolve: "@medusajs/inventory",
|
||||
},
|
||||
cacheService: {
|
||||
resolve: "@medusajs/cache-inmemory",
|
||||
options: { ttl: 5 },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"build": "babel src -d dist --extensions \".ts,.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/cache-inmemory": "*",
|
||||
"@medusajs/medusa": "*",
|
||||
"faker": "^5.5.3",
|
||||
"medusa-fulfillment-webshipper": "*",
|
||||
|
||||
6
packages/cache-inmemory/.gitignore
vendored
Normal file
6
packages/cache-inmemory/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/dist
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
.env
|
||||
*.sql
|
||||
23
packages/cache-inmemory/README.md
Normal file
23
packages/cache-inmemory/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Medusa Cache In-memory
|
||||
|
||||
Medusa in-memory cache module. Use plain JS Map as a cache store.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
yarn add @medusajs/cache-inmemory
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
{
|
||||
ttl?: number // Time to keep data in cache (in seconds)
|
||||
}
|
||||
```
|
||||
|
||||
### Note
|
||||
Recommended for testing and development. For production, use Redis cache module.
|
||||
|
||||
### Other caching modules
|
||||
- [Medusa Cache Redis](../cache-redis/README.md)
|
||||
13
packages/cache-inmemory/jest.config.js
Normal file
13
packages/cache-inmemory/jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
}
|
||||
36
packages/cache-inmemory/package.json
Normal file
36
packages/cache-inmemory/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@medusajs/cache-inmemory",
|
||||
"version": "1.0.0",
|
||||
"description": "In-memory Cache Module for Medusa",
|
||||
"main": "dist/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/cache-inmemory"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
13
packages/cache-inmemory/src/index.ts
Normal file
13
packages/cache-inmemory/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ModuleExports } from "@medusajs/modules-sdk"
|
||||
|
||||
import InMemoryCacheService from "./services/inmemory-cache"
|
||||
|
||||
const loaders = []
|
||||
const service = InMemoryCacheService
|
||||
|
||||
const moduleDefinition: ModuleExports = {
|
||||
service,
|
||||
loaders,
|
||||
}
|
||||
|
||||
export default moduleDefinition
|
||||
@@ -0,0 +1,96 @@
|
||||
import { InMemoryCacheService } from "../index"
|
||||
|
||||
jest.setTimeout(40000)
|
||||
|
||||
describe("InMemoryCacheService", () => {
|
||||
let inMemoryCache
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("Stores and retrieves data", async () => {
|
||||
inMemoryCache = new InMemoryCacheService({}, {})
|
||||
|
||||
await inMemoryCache.set("cache-key", { data: "value" })
|
||||
|
||||
expect(await inMemoryCache.get("cache-key")).toEqual({ data: "value" })
|
||||
})
|
||||
|
||||
it("Invalidates single record", async () => {
|
||||
inMemoryCache = new InMemoryCacheService({}, {})
|
||||
|
||||
await inMemoryCache.set("cache-key", { data: "value" })
|
||||
|
||||
await inMemoryCache.invalidate("cache-key")
|
||||
|
||||
expect(await inMemoryCache.get("cache-key")).toEqual(null)
|
||||
})
|
||||
|
||||
it("Invalidates multiple keys with wildcard (end matching)", async () => {
|
||||
inMemoryCache = new InMemoryCacheService({}, {})
|
||||
|
||||
await inMemoryCache.set("cache-key:id_1:x:y", { data: "value" })
|
||||
await inMemoryCache.set("cache-key:id_2:x:y", { data: "value" })
|
||||
await inMemoryCache.set("cache-key:id_3:x:y", { data: "value" })
|
||||
await inMemoryCache.set("cache-key-old", { data: "value" })
|
||||
|
||||
await inMemoryCache.invalidate("cache-key:*")
|
||||
|
||||
expect(await inMemoryCache.get("cache-key:id1:x:y")).toEqual(null)
|
||||
expect(await inMemoryCache.get("cache-key:id2:x:y")).toEqual(null)
|
||||
expect(await inMemoryCache.get("cache-key:id3:x:y")).toEqual(null)
|
||||
expect(await inMemoryCache.get("cache-key-old")).toEqual({ data: "value" })
|
||||
})
|
||||
|
||||
it("Invalidates multiple keys with wildcard (middle matching)", async () => {
|
||||
inMemoryCache = new InMemoryCacheService({}, {})
|
||||
|
||||
await inMemoryCache.set("cache-key:1:new", { data: "value" })
|
||||
await inMemoryCache.set("cache-key:2:new", { data: "value" })
|
||||
await inMemoryCache.set("cache-key:3:new", { data: "value" })
|
||||
await inMemoryCache.set("cache-key:4:old", { data: "value" })
|
||||
|
||||
await inMemoryCache.invalidate("cache-key:*:new")
|
||||
|
||||
expect(await inMemoryCache.get("cache-key:1:new")).toEqual(null)
|
||||
expect(await inMemoryCache.get("cache-key:2:new")).toEqual(null)
|
||||
expect(await inMemoryCache.get("cache-key:3:new")).toEqual(null)
|
||||
expect(await inMemoryCache.get("cache-key:4:old")).toEqual({
|
||||
data: "value",
|
||||
})
|
||||
})
|
||||
|
||||
it("Removes data after TTL", async () => {
|
||||
inMemoryCache = new InMemoryCacheService({}, {})
|
||||
|
||||
await inMemoryCache.set("cache-key", { data: "value" }, 2)
|
||||
expect(await inMemoryCache.get("cache-key")).toEqual({ data: "value" })
|
||||
|
||||
await new Promise((res) => setTimeout(res, 3000))
|
||||
|
||||
expect(await inMemoryCache.get("cache-key")).toEqual(null)
|
||||
})
|
||||
|
||||
it("Removes data after default TTL if TTL params isn't passed", async () => {
|
||||
inMemoryCache = new InMemoryCacheService({})
|
||||
|
||||
await inMemoryCache.set("cache-key", { data: "value" })
|
||||
expect(await inMemoryCache.get("cache-key")).toEqual({ data: "value" })
|
||||
|
||||
await new Promise((res) => setTimeout(res, 33000))
|
||||
|
||||
expect(await inMemoryCache.get("cache-key")).toEqual(null)
|
||||
})
|
||||
|
||||
it("Removes data after TTL from the config if TTL params isn't passed", async () => {
|
||||
inMemoryCache = new InMemoryCacheService({}, { ttl: 1 })
|
||||
|
||||
await inMemoryCache.set("cache-key", { data: "value" })
|
||||
expect(await inMemoryCache.get("cache-key")).toEqual({ data: "value" })
|
||||
|
||||
await new Promise((res) => setTimeout(res, 2000))
|
||||
|
||||
expect(await inMemoryCache.get("cache-key")).toEqual(null)
|
||||
})
|
||||
})
|
||||
1
packages/cache-inmemory/src/services/index.ts
Normal file
1
packages/cache-inmemory/src/services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as InMemoryCacheService } from "./inmemory-cache"
|
||||
101
packages/cache-inmemory/src/services/inmemory-cache.ts
Normal file
101
packages/cache-inmemory/src/services/inmemory-cache.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { ICacheService } from "@medusajs/medusa"
|
||||
|
||||
import { CacheRecord, InMemoryCacheModuleOptions } from "../types"
|
||||
|
||||
const DEFAULT_TTL = 30 // seconds
|
||||
|
||||
type InjectedDependencies = {}
|
||||
|
||||
/**
|
||||
* Class represents basic, in-memory, cache store.
|
||||
*/
|
||||
class InMemoryCacheService implements ICacheService {
|
||||
protected readonly TTL: number
|
||||
|
||||
protected readonly store = new Map<string, CacheRecord<any>>()
|
||||
protected readonly timoutRefs = new Map<string, NodeJS.Timeout>()
|
||||
|
||||
constructor(
|
||||
deps: InjectedDependencies,
|
||||
options: InMemoryCacheModuleOptions = {}
|
||||
) {
|
||||
this.TTL = options.ttl ?? DEFAULT_TTL
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve data from the cache.
|
||||
* @param key - cache key
|
||||
*/
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
const now = Date.now()
|
||||
const record: CacheRecord<T> | undefined = this.store.get(key)
|
||||
|
||||
const recordExpire = record?.expire ?? Infinity
|
||||
|
||||
if (!record || recordExpire < now) {
|
||||
return null
|
||||
}
|
||||
|
||||
return record.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data to the cache.
|
||||
* @param key - cache key under which the data is stored
|
||||
* @param data - data to be stored in the cache
|
||||
* @param ttl - expiration time in seconds
|
||||
*/
|
||||
async set<T>(key: string, data: T, ttl: number = this.TTL): Promise<void> {
|
||||
const record: CacheRecord<T> = { data, expire: ttl * 1000 + Date.now() }
|
||||
|
||||
const oldRecord = this.store.get(key)
|
||||
|
||||
if (oldRecord) {
|
||||
clearTimeout(this.timoutRefs.get(key))
|
||||
this.timoutRefs.delete(key)
|
||||
}
|
||||
|
||||
const ref = setTimeout(() => {
|
||||
this.invalidate(key)
|
||||
}, ttl * 1000)
|
||||
|
||||
this.timoutRefs.set(key, ref)
|
||||
this.store.set(key, record)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete data from the cache.
|
||||
* Could use wildcard (*) matcher e.g. `invalidate("ps:*")` to delete all keys that start with "ps:"
|
||||
*
|
||||
* @param key - cache key
|
||||
*/
|
||||
async invalidate(key: string): Promise<void> {
|
||||
let keys = [key]
|
||||
|
||||
if (key.includes("*")) {
|
||||
const regExp = new RegExp(key.replace("*", ".*"))
|
||||
keys = Array.from(this.store.keys()).filter((k) => k.match(regExp))
|
||||
}
|
||||
|
||||
keys.forEach((key) => {
|
||||
const timeoutRef = this.timoutRefs.get(key)
|
||||
if (timeoutRef) {
|
||||
clearTimeout(timeoutRef)
|
||||
this.timoutRefs.delete(key)
|
||||
}
|
||||
this.store.delete(key)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entire cache.
|
||||
*/
|
||||
async clear() {
|
||||
this.timoutRefs.forEach((ref) => clearTimeout(ref))
|
||||
this.timoutRefs.clear()
|
||||
|
||||
this.store.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export default InMemoryCacheService
|
||||
17
packages/cache-inmemory/src/types/index.ts
Normal file
17
packages/cache-inmemory/src/types/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Shape of a record saved in `in-memory` cache
|
||||
*/
|
||||
export type CacheRecord<T> = {
|
||||
data: T
|
||||
/**
|
||||
* Timestamp in milliseconds
|
||||
*/
|
||||
expire: number
|
||||
}
|
||||
|
||||
export type InMemoryCacheModuleOptions = {
|
||||
/**
|
||||
* Time to keep data in cache (in seconds)
|
||||
*/
|
||||
ttl?: number
|
||||
}
|
||||
33
packages/cache-inmemory/tsconfig.json
Normal file
33
packages/cache-inmemory/tsconfig.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
6
packages/cache-redis/.gitignore
vendored
Normal file
6
packages/cache-redis/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/dist
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
.env
|
||||
*.sql
|
||||
26
packages/cache-redis/README.md
Normal file
26
packages/cache-redis/README.md
Normal file
@@ -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)
|
||||
13
packages/cache-redis/jest.config.js
Normal file
13
packages/cache-redis/jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
}
|
||||
36
packages/cache-redis/package.json
Normal file
36
packages/cache-redis/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
14
packages/cache-redis/src/index.ts
Normal file
14
packages/cache-redis/src/index.ts
Normal file
@@ -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
|
||||
38
packages/cache-redis/src/loaders/index.ts
Normal file
38
packages/cache-redis/src/loaders/index.ts
Normal file
@@ -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),
|
||||
})
|
||||
}
|
||||
29
packages/cache-redis/src/services/__tests__/redis-cache.js
Normal file
29
packages/cache-redis/src/services/__tests__/redis-cache.js
Normal file
@@ -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()
|
||||
})
|
||||
})
|
||||
1
packages/cache-redis/src/services/index.ts
Normal file
1
packages/cache-redis/src/services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as RedisCacheService } from "./redis-cache"
|
||||
88
packages/cache-redis/src/services/redis-cache.ts
Normal file
88
packages/cache-redis/src/services/redis-cache.ts
Normal file
@@ -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
|
||||
27
packages/cache-redis/src/types/index.ts
Normal file
27
packages/cache-redis/src/types/index.ts
Normal file
@@ -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
|
||||
}
|
||||
33
packages/cache-redis/tsconfig.json
Normal file
33
packages/cache-redis/tsconfig.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -18,7 +18,7 @@ describe("ContentfulService", () => {
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
}
|
||||
const redisClient = {
|
||||
const cacheService = {
|
||||
get: async (id) => {
|
||||
// const key = `${id}_ignore_${side}`
|
||||
if (id === `ignored_ignore_contentful`) {
|
||||
@@ -44,7 +44,7 @@ describe("ContentfulService", () => {
|
||||
{
|
||||
regionService,
|
||||
productService,
|
||||
redisClient,
|
||||
cacheService,
|
||||
productVariantService,
|
||||
eventBusService,
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ class ContentfulService extends BaseService {
|
||||
{
|
||||
regionService,
|
||||
productService,
|
||||
redisClient,
|
||||
cacheService,
|
||||
productVariantService,
|
||||
eventBusService,
|
||||
},
|
||||
@@ -31,24 +31,23 @@ class ContentfulService extends BaseService {
|
||||
accessToken: options.access_token,
|
||||
})
|
||||
|
||||
this.redis_ = redisClient
|
||||
this.cacheService_ = cacheService
|
||||
|
||||
this.capab_ = {}
|
||||
}
|
||||
|
||||
async addIgnore_(id, side) {
|
||||
const key = `${id}_ignore_${side}`
|
||||
return await this.redis_.set(
|
||||
return await this.cacheService_.set(
|
||||
key,
|
||||
1,
|
||||
"EX",
|
||||
this.options_.ignore_threshold || IGNORE_THRESHOLD
|
||||
)
|
||||
}
|
||||
|
||||
async shouldIgnore_(id, side) {
|
||||
const key = `${id}_ignore_${side}`
|
||||
return await this.redis_.get(key)
|
||||
return await this.cacheService_.get(key)
|
||||
}
|
||||
|
||||
async getContentfulEnvironment_() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const ShopifyRedisServiceMock = {
|
||||
export const ShopifyCacheServiceMock = {
|
||||
addIgnore: jest.fn().mockImplementation((_id, _event) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
@@ -4,7 +4,7 @@ import { ProductServiceMock } from "../__mocks__/product-service"
|
||||
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
||||
import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile"
|
||||
import { ShopifyClientServiceMock } from "../__mocks__/shopify-client"
|
||||
import { ShopifyRedisServiceMock } from "../__mocks__/shopify-redis"
|
||||
import { ShopifyCacheServiceMock } from "../__mocks__/shopify-cache"
|
||||
import { medusaProducts, shopifyProducts } from "../__mocks__/test-products"
|
||||
|
||||
describe("ShopifyProductService", () => {
|
||||
@@ -32,7 +32,7 @@ describe("ShopifyProductService", () => {
|
||||
manager: MockManager,
|
||||
shopifyClientService: ShopifyClientServiceMock,
|
||||
productService: ProductServiceMock,
|
||||
shopifyRedisService: ShopifyRedisServiceMock,
|
||||
shopifyCacheService: ShopifyCacheServiceMock,
|
||||
shippingProfileService: ShippingProfileServiceMock,
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
})
|
||||
@@ -46,8 +46,8 @@ describe("ShopifyProductService", () => {
|
||||
|
||||
const product = await shopifyProductService.create(data)
|
||||
|
||||
expect(ShopifyRedisServiceMock.shouldIgnore).toHaveBeenCalledTimes(1)
|
||||
expect(ShopifyRedisServiceMock.addIgnore).toHaveBeenCalledTimes(1)
|
||||
expect(ShopifyCacheServiceMock.shouldIgnore).toHaveBeenCalledTimes(1)
|
||||
expect(ShopifyCacheServiceMock.addIgnore).toHaveBeenCalledTimes(1)
|
||||
expect(ShippingProfileServiceMock.retrieveDefault).toHaveBeenCalledTimes(
|
||||
1
|
||||
)
|
||||
@@ -66,7 +66,7 @@ describe("ShopifyProductService", () => {
|
||||
manager: MockManager,
|
||||
shopifyClientService: ShopifyClientServiceMock,
|
||||
productService: ProductServiceMock,
|
||||
shopifyRedisService: ShopifyRedisServiceMock,
|
||||
shopifyCacheService: ShopifyCacheServiceMock,
|
||||
shippingProfileService: ShippingProfileServiceMock,
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
})
|
||||
|
||||
@@ -1,41 +1,40 @@
|
||||
// shopify-redis
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
|
||||
import { IGNORE_THRESHOLD } from "../utils/const"
|
||||
|
||||
class shopifyRedisService extends BaseService {
|
||||
constructor({ redisClient }, options) {
|
||||
class ShopifyCacheService extends BaseService {
|
||||
constructor({ cacheService }, options) {
|
||||
super()
|
||||
|
||||
this.options_ = options
|
||||
|
||||
/** @private @const {RedisClient} */
|
||||
this.redis_ = redisClient
|
||||
/** @private @const {ICacheService} */
|
||||
this.cacheService_ = cacheService
|
||||
}
|
||||
|
||||
async addIgnore(id, side) {
|
||||
const key = `sh_${id}_ignore_${side}`
|
||||
return await this.redis_.set(
|
||||
return await this.cacheService_.set(
|
||||
key,
|
||||
1,
|
||||
"EX",
|
||||
this.options_.ignore_threshold || IGNORE_THRESHOLD
|
||||
)
|
||||
}
|
||||
|
||||
async shouldIgnore(id, action) {
|
||||
const key = `sh_${id}_ignore_${action}`
|
||||
return await this.redis_.get(key)
|
||||
return await this.cacheService_.get(key)
|
||||
}
|
||||
|
||||
async addUniqueValue(uniqueVal, type) {
|
||||
const key = `sh_${uniqueVal}_${type}`
|
||||
return await this.redis_.set(key, 1, "EX", 60 * 5)
|
||||
return await this.cacheService_.set(key, 1, 60 * 5)
|
||||
}
|
||||
|
||||
async getUniqueValue(uniqueVal, type) {
|
||||
const key = `sh_${uniqueVal}_${type}`
|
||||
return await this.redis_.get(key)
|
||||
return await this.cacheService_.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
export default shopifyRedisService
|
||||
export default ShopifyCacheService
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DataType } from "@shopify/shopify-api"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
|
||||
import { createClient } from "../utils/create-client"
|
||||
import { pager } from "../utils/pager"
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class ShopifyProductService extends BaseService {
|
||||
productVariantService,
|
||||
shippingProfileService,
|
||||
shopifyClientService,
|
||||
shopifyRedisService,
|
||||
shopifyCacheService,
|
||||
},
|
||||
options
|
||||
) {
|
||||
@@ -32,8 +32,8 @@ class ShopifyProductService extends BaseService {
|
||||
this.shippingProfileService_ = shippingProfileService
|
||||
/** @private @const {ShopifyRestClient} */
|
||||
this.shopify_ = shopifyClientService
|
||||
|
||||
this.redis_ = shopifyRedisService
|
||||
/** @private @const {ICacheService} */
|
||||
this.cacheService_ = shopifyCacheService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
@@ -41,15 +41,17 @@ class ShopifyProductService extends BaseService {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new ShopifyProductService({
|
||||
manager: transactionManager,
|
||||
options: this.options,
|
||||
shippingProfileService: this.shippingProfileService_,
|
||||
productVariantService: this.productVariantService_,
|
||||
productService: this.productService_,
|
||||
shopifyClientService: this.shopify_,
|
||||
shopifyRedisService: this.redis_,
|
||||
})
|
||||
const cloned = new ShopifyProductService(
|
||||
{
|
||||
manager: transactionManager,
|
||||
shippingProfileService: this.shippingProfileService_,
|
||||
productVariantService: this.productVariantService_,
|
||||
productService: this.productService_,
|
||||
shopifyClientService: this.shopify_,
|
||||
shopifyCacheService: this.cacheService_,
|
||||
},
|
||||
this.options
|
||||
)
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
@@ -65,7 +67,10 @@ class ShopifyProductService extends BaseService {
|
||||
*/
|
||||
async create(data) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const ignore = await this.redis_.shouldIgnore(data.id, "product.created")
|
||||
const ignore = await this.cacheService_.shouldIgnore(
|
||||
data.id,
|
||||
"product.created"
|
||||
)
|
||||
if (ignore) {
|
||||
return
|
||||
}
|
||||
@@ -106,7 +111,7 @@ class ShopifyProductService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
await this.redis_.addIgnore(data.id, "product.created")
|
||||
await this.cacheService_.addIgnore(data.id, "product.created")
|
||||
|
||||
return product
|
||||
})
|
||||
@@ -114,7 +119,7 @@ class ShopifyProductService extends BaseService {
|
||||
|
||||
async update(existing, shopifyUpdate) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const ignore = await this.redis_.shouldIgnore(
|
||||
const ignore = await this.cacheService_.shouldIgnore(
|
||||
shopifyUpdate.id,
|
||||
"product.updated"
|
||||
)
|
||||
@@ -140,7 +145,7 @@ class ShopifyProductService extends BaseService {
|
||||
}
|
||||
|
||||
if (!isEmpty(update)) {
|
||||
await this.redis_.addIgnore(shopifyUpdate.id, "product.updated")
|
||||
await this.cacheService_.addIgnore(shopifyUpdate.id, "product.updated")
|
||||
return await this.productService_
|
||||
.withTransaction(manager)
|
||||
.update(existing.id, update)
|
||||
@@ -219,7 +224,7 @@ class ShopifyProductService extends BaseService {
|
||||
)
|
||||
})
|
||||
|
||||
await this.redis_.addIgnore(product.external_id, "product.updated")
|
||||
await this.cacheService_.addIgnore(product.external_id, "product.updated")
|
||||
}
|
||||
|
||||
async shopifyVariantUpdate(id, fields) {
|
||||
@@ -273,7 +278,7 @@ class ShopifyProductService extends BaseService {
|
||||
)
|
||||
})
|
||||
|
||||
await this.redis_.addIgnore(
|
||||
await this.cacheService_.addIgnore(
|
||||
variant.metadata.sh_id,
|
||||
"product-variant.updated"
|
||||
)
|
||||
@@ -298,7 +303,10 @@ class ShopifyProductService extends BaseService {
|
||||
)
|
||||
})
|
||||
|
||||
await this.redis_.addIgnore(metadata.sh_id, "product-variant.deleted")
|
||||
await this.cacheService_.addIgnore(
|
||||
metadata.sh_id,
|
||||
"product-variant.deleted"
|
||||
)
|
||||
}
|
||||
|
||||
async updateCollectionId(productId, collectionId) {
|
||||
@@ -314,11 +322,11 @@ class ShopifyProductService extends BaseService {
|
||||
const { id, variants, options } = product
|
||||
for (let variant of updateVariants) {
|
||||
const ignore =
|
||||
(await this.redis_.shouldIgnore(
|
||||
(await this.cacheService_.shouldIgnore(
|
||||
variant.metadata.sh_id,
|
||||
"product-variant.updated"
|
||||
)) ||
|
||||
(await this.redis_.shouldIgnore(
|
||||
(await this.cacheService_.shouldIgnore(
|
||||
variant.metadata.sh_id,
|
||||
"product-variant.created"
|
||||
))
|
||||
@@ -349,7 +357,7 @@ class ShopifyProductService extends BaseService {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const { variants } = product
|
||||
for (const variant of variants) {
|
||||
const ignore = await this.redis_.shouldIgnore(
|
||||
const ignore = await this.cacheService_.shouldIgnore(
|
||||
variant.metadata.sh_id,
|
||||
"product-variant.deleted"
|
||||
)
|
||||
@@ -551,15 +559,15 @@ class ShopifyProductService extends BaseService {
|
||||
|
||||
async testUnique_(uniqueVal, type) {
|
||||
// Test if the unique value has already been added, if it was then pass the value onto the duplicate handler and return the new value
|
||||
const exists = await this.redis_.getUniqueValue(uniqueVal, type)
|
||||
const exists = await this.cacheService_.getUniqueValue(uniqueVal, type)
|
||||
|
||||
if (exists) {
|
||||
const dupValue = this.handleDuplicateConstraint_(uniqueVal)
|
||||
await this.redis_.addUniqueValue(dupValue, type)
|
||||
await this.cacheService_.addUniqueValue(dupValue, type)
|
||||
return dupValue
|
||||
}
|
||||
// If it doesn't exist, we return the value
|
||||
await this.redis_.addUniqueValue(uniqueVal, type)
|
||||
await this.cacheService_.addUniqueValue(uniqueVal, type)
|
||||
return uniqueVal
|
||||
}
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Redis } from "ioredis"
|
||||
import { ICacheService } from "../interfaces"
|
||||
|
||||
const DEFAULT_CACHE_TIME = 30 // 30 seconds
|
||||
const EXPIRY_MODE = "EX" // "EX" stands for an expiry time in second
|
||||
|
||||
export default class CacheService implements ICacheService {
|
||||
protected readonly redis_: Redis
|
||||
|
||||
constructor({ redisClient }) {
|
||||
this.redis_ = redisClient
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a key/value pair to the cache.
|
||||
* It is also possible to manage the ttl through environment variable using CACHE_TTL. 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 = DEFAULT_CACHE_TIME
|
||||
): Promise<void> {
|
||||
ttl = Number(process.env.CACHE_TTL ?? ttl)
|
||||
if (ttl === 0) {
|
||||
// No need to call redis set without expiry time
|
||||
return
|
||||
}
|
||||
|
||||
await this.redis_.set(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> {
|
||||
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(key)
|
||||
const pipeline = this.redis_.pipeline()
|
||||
|
||||
keys.forEach(function (key) {
|
||||
pipeline.del(key)
|
||||
})
|
||||
|
||||
await pipeline.exec()
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ export { default as AnalyticsConfigService } from "./analytics-config"
|
||||
export { default as AuthService } from "./auth"
|
||||
export { default as BatchJobService } from "./batch-job"
|
||||
export { default as CartService } from "./cart"
|
||||
export { default as CacheService } from "./cache"
|
||||
export { default as ClaimService } from "./claim"
|
||||
export { default as ClaimItemService } from "./claim-item"
|
||||
export { default as CurrencyService } from "./currency"
|
||||
|
||||
@@ -57,7 +57,7 @@ class PricingService extends TransactionBaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects additional information neccessary for completing the price
|
||||
* Collects additional information necessary for completing the price
|
||||
* selection.
|
||||
* @param context - the price selection context to use
|
||||
* @return The pricing context
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager, In } from "typeorm"
|
||||
import {
|
||||
ICacheService,
|
||||
IInventoryService,
|
||||
IStockLocationService,
|
||||
TransactionBaseService,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
} from "../types/inventory"
|
||||
import { PricedProduct, PricedVariant } from "../types/pricing"
|
||||
import {
|
||||
CacheService,
|
||||
ProductVariantService,
|
||||
SalesChannelInventoryService,
|
||||
SalesChannelLocationService,
|
||||
@@ -35,7 +35,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
||||
protected readonly productVariantService_: ProductVariantService
|
||||
protected readonly stockLocationService_: IStockLocationService
|
||||
protected readonly inventoryService_: IInventoryService
|
||||
protected readonly cacheService_: CacheService
|
||||
protected readonly cacheService_: ICacheService
|
||||
|
||||
constructor({
|
||||
stockLocationService,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import {
|
||||
CacheService,
|
||||
EventBusService,
|
||||
ProductVariantService,
|
||||
} from "../services"
|
||||
import { EventBusService, ProductVariantService } from "../services"
|
||||
import { ICacheService } from "../interfaces"
|
||||
|
||||
type ProductVariantUpdatedEventData = {
|
||||
id: string
|
||||
@@ -12,7 +9,7 @@ type ProductVariantUpdatedEventData = {
|
||||
|
||||
class PricingSubscriber {
|
||||
protected readonly eventBus_: EventBusService
|
||||
protected readonly cacheService_: CacheService
|
||||
protected readonly cacheService_: ICacheService
|
||||
|
||||
constructor({ eventBusService, cacheService }) {
|
||||
this.eventBus_ = eventBusService
|
||||
@@ -23,7 +20,7 @@ class PricingSubscriber {
|
||||
async (data) => {
|
||||
const { id, fields } = data as ProductVariantUpdatedEventData
|
||||
if (fields.includes("prices")) {
|
||||
await this.cacheService_.invalidate(`ps:${id}*`)
|
||||
await this.cacheService_.invalidate(`ps:${id}:*`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -25,6 +25,18 @@ export const MODULE_DEFINITIONS: ModuleDefinition[] = [
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "cacheService",
|
||||
registrationName: "cacheService",
|
||||
defaultPackage: "@medusajs/cache-inmemory",
|
||||
label: "CacheService",
|
||||
isRequired: true,
|
||||
canOverride: true,
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default MODULE_DEFINITIONS
|
||||
|
||||
30
yarn.lock
30
yarn.lock
@@ -5694,6 +5694,34 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@medusajs/cache-inmemory@*, @medusajs/cache-inmemory@workspace:packages/cache-inmemory":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@medusajs/cache-inmemory@workspace:packages/cache-inmemory"
|
||||
dependencies:
|
||||
"@medusajs/medusa": "*"
|
||||
cross-env: ^5.2.1
|
||||
jest: ^25.5.4
|
||||
ts-jest: ^25.5.1
|
||||
typescript: ^4.4.4
|
||||
peerDependencies:
|
||||
"@medusajs/medusa": ^1.7.11
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@medusajs/cache-redis@workspace:packages/cache-redis":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@medusajs/cache-redis@workspace:packages/cache-redis"
|
||||
dependencies:
|
||||
"@medusajs/medusa": "*"
|
||||
cross-env: ^5.2.1
|
||||
jest: ^25.5.4
|
||||
ts-jest: ^25.5.1
|
||||
typescript: ^4.4.4
|
||||
peerDependencies:
|
||||
"@medusajs/medusa": ^1.7.11
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@medusajs/inventory@workspace:packages/inventory":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@medusajs/inventory@workspace:packages/inventory"
|
||||
@@ -23220,6 +23248,7 @@ __metadata:
|
||||
"@babel/cli": ^7.12.10
|
||||
"@babel/core": ^7.12.10
|
||||
"@babel/node": ^7.12.10
|
||||
"@medusajs/cache-inmemory": "*"
|
||||
"@medusajs/medusa": "*"
|
||||
babel-preset-medusa-package: "*"
|
||||
faker: ^5.5.3
|
||||
@@ -23237,6 +23266,7 @@ __metadata:
|
||||
"@babel/cli": ^7.12.10
|
||||
"@babel/core": ^7.12.10
|
||||
"@babel/node": ^7.12.10
|
||||
"@medusajs/cache-inmemory": "*"
|
||||
"@medusajs/medusa": "*"
|
||||
babel-preset-medusa-package: "*"
|
||||
faker: ^5.5.3
|
||||
|
||||
Reference in New Issue
Block a user