feat(medusa): Cache modules (#3187)
This commit is contained in:
1 parent
f43f03badb
commit
f97b3d7cce
42 files changed
+778
-181
No files matched your search
@@ -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_PASSWORD = process.env.DB_PASSWORD
|
||||||
const DB_NAME = process.env.DB_TEMP_NAME
|
const DB_NAME = process.env.DB_TEMP_NAME
|
||||||
|
|
||||||
|
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379"
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [],
|
plugins: [],
|
||||||
projectConfig: {
|
projectConfig: {
|
||||||
redis_url: process.env.REDIS_URL,
|
redis_url: redisUrl,
|
||||||
database_url: `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}`,
|
database_url: `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}`,
|
||||||
database_type: "postgres",
|
database_type: "postgres",
|
||||||
jwt_secret: "test",
|
jwt_secret: "test",
|
||||||
cookie_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\""
|
"build": "babel src -d dist --extensions \".ts,.js\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@medusajs/cache-inmemory": "*",
|
||||||
"@medusajs/medusa": "*",
|
"@medusajs/medusa": "*",
|
||||||
"faker": "^5.5.3",
|
"faker": "^5.5.3",
|
||||||
"medusa-interfaces": "*",
|
"medusa-interfaces": "*",
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ module.exports = ({ cwd, redisUrl, uploadDir, verbose, env }) => {
|
|||||||
COOKIE_SECRET: "test",
|
COOKIE_SECRET: "test",
|
||||||
REDIS_URL: redisUrl ? redisUrlWithDatabase : undefined, // If provided, will use a real instance, otherwise a fake instance
|
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
|
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,
|
...env,
|
||||||
},
|
},
|
||||||
stdio: verbose
|
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",
|
resources: "shared",
|
||||||
resolve: "@medusajs/inventory",
|
resolve: "@medusajs/inventory",
|
||||||
},
|
},
|
||||||
|
cacheService: {
|
||||||
|
resolve: "@medusajs/cache-inmemory",
|
||||||
|
options: { ttl: 5 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"build": "babel src -d dist --extensions \".ts,.js\""
|
"build": "babel src -d dist --extensions \".ts,.js\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@medusajs/cache-inmemory": "*",
|
||||||
"@medusajs/medusa": "*",
|
"@medusajs/medusa": "*",
|
||||||
"faker": "^5.5.3",
|
"faker": "^5.5.3",
|
||||||
"medusa-fulfillment-webshipper": "*",
|
"medusa-fulfillment-webshipper": "*",
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/dist
|
||||||
|
node_modules
|
||||||
|
.DS_store
|
||||||
|
.env*
|
||||||
|
.env
|
||||||
|
*.sql
|
||||||
@@ -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)
|
||||||
@@ -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-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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as InMemoryCacheService } from "./inmemory-cache"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ describe("ContentfulService", () => {
|
|||||||
return Promise.resolve(undefined)
|
return Promise.resolve(undefined)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
const redisClient = {
|
const cacheService = {
|
||||||
get: async (id) => {
|
get: async (id) => {
|
||||||
// const key = `${id}_ignore_${side}`
|
// const key = `${id}_ignore_${side}`
|
||||||
if (id === `ignored_ignore_contentful`) {
|
if (id === `ignored_ignore_contentful`) {
|
||||||
@@ -44,7 +44,7 @@ describe("ContentfulService", () => {
|
|||||||
{
|
{
|
||||||
regionService,
|
regionService,
|
||||||
productService,
|
productService,
|
||||||
redisClient,
|
cacheService,
|
||||||
productVariantService,
|
productVariantService,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class ContentfulService extends BaseService {
|
|||||||
{
|
{
|
||||||
regionService,
|
regionService,
|
||||||
productService,
|
productService,
|
||||||
redisClient,
|
cacheService,
|
||||||
productVariantService,
|
productVariantService,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
},
|
},
|
||||||
@@ -31,24 +31,23 @@ class ContentfulService extends BaseService {
|
|||||||
accessToken: options.access_token,
|
accessToken: options.access_token,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.redis_ = redisClient
|
this.cacheService_ = cacheService
|
||||||
|
|
||||||
this.capab_ = {}
|
this.capab_ = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addIgnore_(id, side) {
|
async addIgnore_(id, side) {
|
||||||
const key = `${id}_ignore_${side}`
|
const key = `${id}_ignore_${side}`
|
||||||
return await this.redis_.set(
|
return await this.cacheService_.set(
|
||||||
key,
|
key,
|
||||||
1,
|
1,
|
||||||
"EX",
|
|
||||||
this.options_.ignore_threshold || IGNORE_THRESHOLD
|
this.options_.ignore_threshold || IGNORE_THRESHOLD
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async shouldIgnore_(id, side) {
|
async shouldIgnore_(id, side) {
|
||||||
const key = `${id}_ignore_${side}`
|
const key = `${id}_ignore_${side}`
|
||||||
return await this.redis_.get(key)
|
return await this.cacheService_.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getContentfulEnvironment_() {
|
async getContentfulEnvironment_() {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
export const ShopifyRedisServiceMock = {
|
export const ShopifyCacheServiceMock = {
|
||||||
addIgnore: jest.fn().mockImplementation((_id, _event) => {
|
addIgnore: jest.fn().mockImplementation((_id, _event) => {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}),
|
}),
|
||||||
@@ -4,7 +4,7 @@ import { ProductServiceMock } from "../__mocks__/product-service"
|
|||||||
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
||||||
import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile"
|
import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile"
|
||||||
import { ShopifyClientServiceMock } from "../__mocks__/shopify-client"
|
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"
|
import { medusaProducts, shopifyProducts } from "../__mocks__/test-products"
|
||||||
|
|
||||||
describe("ShopifyProductService", () => {
|
describe("ShopifyProductService", () => {
|
||||||
@@ -32,7 +32,7 @@ describe("ShopifyProductService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
shopifyClientService: ShopifyClientServiceMock,
|
shopifyClientService: ShopifyClientServiceMock,
|
||||||
productService: ProductServiceMock,
|
productService: ProductServiceMock,
|
||||||
shopifyRedisService: ShopifyRedisServiceMock,
|
shopifyCacheService: ShopifyCacheServiceMock,
|
||||||
shippingProfileService: ShippingProfileServiceMock,
|
shippingProfileService: ShippingProfileServiceMock,
|
||||||
productVariantService: ProductVariantServiceMock,
|
productVariantService: ProductVariantServiceMock,
|
||||||
})
|
})
|
||||||
@@ -46,8 +46,8 @@ describe("ShopifyProductService", () => {
|
|||||||
|
|
||||||
const product = await shopifyProductService.create(data)
|
const product = await shopifyProductService.create(data)
|
||||||
|
|
||||||
expect(ShopifyRedisServiceMock.shouldIgnore).toHaveBeenCalledTimes(1)
|
expect(ShopifyCacheServiceMock.shouldIgnore).toHaveBeenCalledTimes(1)
|
||||||
expect(ShopifyRedisServiceMock.addIgnore).toHaveBeenCalledTimes(1)
|
expect(ShopifyCacheServiceMock.addIgnore).toHaveBeenCalledTimes(1)
|
||||||
expect(ShippingProfileServiceMock.retrieveDefault).toHaveBeenCalledTimes(
|
expect(ShippingProfileServiceMock.retrieveDefault).toHaveBeenCalledTimes(
|
||||||
1
|
1
|
||||||
)
|
)
|
||||||
@@ -66,7 +66,7 @@ describe("ShopifyProductService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
shopifyClientService: ShopifyClientServiceMock,
|
shopifyClientService: ShopifyClientServiceMock,
|
||||||
productService: ProductServiceMock,
|
productService: ProductServiceMock,
|
||||||
shopifyRedisService: ShopifyRedisServiceMock,
|
shopifyCacheService: ShopifyCacheServiceMock,
|
||||||
shippingProfileService: ShippingProfileServiceMock,
|
shippingProfileService: ShippingProfileServiceMock,
|
||||||
productVariantService: ProductVariantServiceMock,
|
productVariantService: ProductVariantServiceMock,
|
||||||
})
|
})
|
||||||
|
|||||||
+10
-11
@@ -1,41 +1,40 @@
|
|||||||
// shopify-redis
|
|
||||||
import { BaseService } from "medusa-interfaces"
|
import { BaseService } from "medusa-interfaces"
|
||||||
|
|
||||||
import { IGNORE_THRESHOLD } from "../utils/const"
|
import { IGNORE_THRESHOLD } from "../utils/const"
|
||||||
|
|
||||||
class shopifyRedisService extends BaseService {
|
class ShopifyCacheService extends BaseService {
|
||||||
constructor({ redisClient }, options) {
|
constructor({ cacheService }, options) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.options_ = options
|
this.options_ = options
|
||||||
|
|
||||||
/** @private @const {RedisClient} */
|
/** @private @const {ICacheService} */
|
||||||
this.redis_ = redisClient
|
this.cacheService_ = cacheService
|
||||||
}
|
}
|
||||||
|
|
||||||
async addIgnore(id, side) {
|
async addIgnore(id, side) {
|
||||||
const key = `sh_${id}_ignore_${side}`
|
const key = `sh_${id}_ignore_${side}`
|
||||||
return await this.redis_.set(
|
return await this.cacheService_.set(
|
||||||
key,
|
key,
|
||||||
1,
|
1,
|
||||||
"EX",
|
|
||||||
this.options_.ignore_threshold || IGNORE_THRESHOLD
|
this.options_.ignore_threshold || IGNORE_THRESHOLD
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async shouldIgnore(id, action) {
|
async shouldIgnore(id, action) {
|
||||||
const key = `sh_${id}_ignore_${action}`
|
const key = `sh_${id}_ignore_${action}`
|
||||||
return await this.redis_.get(key)
|
return await this.cacheService_.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
async addUniqueValue(uniqueVal, type) {
|
async addUniqueValue(uniqueVal, type) {
|
||||||
const key = `sh_${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) {
|
async getUniqueValue(uniqueVal, type) {
|
||||||
const key = `sh_${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 { DataType } from "@shopify/shopify-api"
|
||||||
import { BaseService } from "medusa-interfaces"
|
import { BaseService } from "medusa-interfaces"
|
||||||
|
|
||||||
import { createClient } from "../utils/create-client"
|
import { createClient } from "../utils/create-client"
|
||||||
import { pager } from "../utils/pager"
|
import { pager } from "../utils/pager"
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class ShopifyProductService extends BaseService {
|
|||||||
productVariantService,
|
productVariantService,
|
||||||
shippingProfileService,
|
shippingProfileService,
|
||||||
shopifyClientService,
|
shopifyClientService,
|
||||||
shopifyRedisService,
|
shopifyCacheService,
|
||||||
},
|
},
|
||||||
options
|
options
|
||||||
) {
|
) {
|
||||||
@@ -32,8 +32,8 @@ class ShopifyProductService extends BaseService {
|
|||||||
this.shippingProfileService_ = shippingProfileService
|
this.shippingProfileService_ = shippingProfileService
|
||||||
/** @private @const {ShopifyRestClient} */
|
/** @private @const {ShopifyRestClient} */
|
||||||
this.shopify_ = shopifyClientService
|
this.shopify_ = shopifyClientService
|
||||||
|
/** @private @const {ICacheService} */
|
||||||
this.redis_ = shopifyRedisService
|
this.cacheService_ = shopifyCacheService
|
||||||
}
|
}
|
||||||
|
|
||||||
withTransaction(transactionManager) {
|
withTransaction(transactionManager) {
|
||||||
@@ -41,15 +41,17 @@ class ShopifyProductService extends BaseService {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
const cloned = new ShopifyProductService({
|
const cloned = new ShopifyProductService(
|
||||||
|
{
|
||||||
manager: transactionManager,
|
manager: transactionManager,
|
||||||
options: this.options,
|
|
||||||
shippingProfileService: this.shippingProfileService_,
|
shippingProfileService: this.shippingProfileService_,
|
||||||
productVariantService: this.productVariantService_,
|
productVariantService: this.productVariantService_,
|
||||||
productService: this.productService_,
|
productService: this.productService_,
|
||||||
shopifyClientService: this.shopify_,
|
shopifyClientService: this.shopify_,
|
||||||
shopifyRedisService: this.redis_,
|
shopifyCacheService: this.cacheService_,
|
||||||
})
|
},
|
||||||
|
this.options
|
||||||
|
)
|
||||||
|
|
||||||
cloned.transactionManager_ = transactionManager
|
cloned.transactionManager_ = transactionManager
|
||||||
|
|
||||||
@@ -65,7 +67,10 @@ class ShopifyProductService extends BaseService {
|
|||||||
*/
|
*/
|
||||||
async create(data) {
|
async create(data) {
|
||||||
return this.atomicPhase_(async (manager) => {
|
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) {
|
if (ignore) {
|
||||||
return
|
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
|
return product
|
||||||
})
|
})
|
||||||
@@ -114,7 +119,7 @@ class ShopifyProductService extends BaseService {
|
|||||||
|
|
||||||
async update(existing, shopifyUpdate) {
|
async update(existing, shopifyUpdate) {
|
||||||
return this.atomicPhase_(async (manager) => {
|
return this.atomicPhase_(async (manager) => {
|
||||||
const ignore = await this.redis_.shouldIgnore(
|
const ignore = await this.cacheService_.shouldIgnore(
|
||||||
shopifyUpdate.id,
|
shopifyUpdate.id,
|
||||||
"product.updated"
|
"product.updated"
|
||||||
)
|
)
|
||||||
@@ -140,7 +145,7 @@ class ShopifyProductService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isEmpty(update)) {
|
if (!isEmpty(update)) {
|
||||||
await this.redis_.addIgnore(shopifyUpdate.id, "product.updated")
|
await this.cacheService_.addIgnore(shopifyUpdate.id, "product.updated")
|
||||||
return await this.productService_
|
return await this.productService_
|
||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
.update(existing.id, update)
|
.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) {
|
async shopifyVariantUpdate(id, fields) {
|
||||||
@@ -273,7 +278,7 @@ class ShopifyProductService extends BaseService {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.redis_.addIgnore(
|
await this.cacheService_.addIgnore(
|
||||||
variant.metadata.sh_id,
|
variant.metadata.sh_id,
|
||||||
"product-variant.updated"
|
"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) {
|
async updateCollectionId(productId, collectionId) {
|
||||||
@@ -314,11 +322,11 @@ class ShopifyProductService extends BaseService {
|
|||||||
const { id, variants, options } = product
|
const { id, variants, options } = product
|
||||||
for (let variant of updateVariants) {
|
for (let variant of updateVariants) {
|
||||||
const ignore =
|
const ignore =
|
||||||
(await this.redis_.shouldIgnore(
|
(await this.cacheService_.shouldIgnore(
|
||||||
variant.metadata.sh_id,
|
variant.metadata.sh_id,
|
||||||
"product-variant.updated"
|
"product-variant.updated"
|
||||||
)) ||
|
)) ||
|
||||||
(await this.redis_.shouldIgnore(
|
(await this.cacheService_.shouldIgnore(
|
||||||
variant.metadata.sh_id,
|
variant.metadata.sh_id,
|
||||||
"product-variant.created"
|
"product-variant.created"
|
||||||
))
|
))
|
||||||
@@ -349,7 +357,7 @@ class ShopifyProductService extends BaseService {
|
|||||||
return this.atomicPhase_(async (manager) => {
|
return this.atomicPhase_(async (manager) => {
|
||||||
const { variants } = product
|
const { variants } = product
|
||||||
for (const variant of variants) {
|
for (const variant of variants) {
|
||||||
const ignore = await this.redis_.shouldIgnore(
|
const ignore = await this.cacheService_.shouldIgnore(
|
||||||
variant.metadata.sh_id,
|
variant.metadata.sh_id,
|
||||||
"product-variant.deleted"
|
"product-variant.deleted"
|
||||||
)
|
)
|
||||||
@@ -551,15 +559,15 @@ class ShopifyProductService extends BaseService {
|
|||||||
|
|
||||||
async testUnique_(uniqueVal, type) {
|
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
|
// 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) {
|
if (exists) {
|
||||||
const dupValue = this.handleDuplicateConstraint_(uniqueVal)
|
const dupValue = this.handleDuplicateConstraint_(uniqueVal)
|
||||||
await this.redis_.addUniqueValue(dupValue, type)
|
await this.cacheService_.addUniqueValue(dupValue, type)
|
||||||
return dupValue
|
return dupValue
|
||||||
}
|
}
|
||||||
// If it doesn't exist, we return the value
|
// If it doesn't exist, we return the value
|
||||||
await this.redis_.addUniqueValue(uniqueVal, type)
|
await this.cacheService_.addUniqueValue(uniqueVal, type)
|
||||||
return uniqueVal
|
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 AuthService } from "./auth"
|
||||||
export { default as BatchJobService } from "./batch-job"
|
export { default as BatchJobService } from "./batch-job"
|
||||||
export { default as CartService } from "./cart"
|
export { default as CartService } from "./cart"
|
||||||
export { default as CacheService } from "./cache"
|
|
||||||
export { default as ClaimService } from "./claim"
|
export { default as ClaimService } from "./claim"
|
||||||
export { default as ClaimItemService } from "./claim-item"
|
export { default as ClaimItemService } from "./claim-item"
|
||||||
export { default as CurrencyService } from "./currency"
|
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.
|
* selection.
|
||||||
* @param context - the price selection context to use
|
* @param context - the price selection context to use
|
||||||
* @return The pricing context
|
* @return The pricing context
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||||
import { EntityManager, In } from "typeorm"
|
import { EntityManager, In } from "typeorm"
|
||||||
import {
|
import {
|
||||||
|
ICacheService,
|
||||||
IInventoryService,
|
IInventoryService,
|
||||||
IStockLocationService,
|
IStockLocationService,
|
||||||
TransactionBaseService,
|
TransactionBaseService,
|
||||||
@@ -14,7 +15,6 @@ import {
|
|||||||
} from "../types/inventory"
|
} from "../types/inventory"
|
||||||
import { PricedProduct, PricedVariant } from "../types/pricing"
|
import { PricedProduct, PricedVariant } from "../types/pricing"
|
||||||
import {
|
import {
|
||||||
CacheService,
|
|
||||||
ProductVariantService,
|
ProductVariantService,
|
||||||
SalesChannelInventoryService,
|
SalesChannelInventoryService,
|
||||||
SalesChannelLocationService,
|
SalesChannelLocationService,
|
||||||
@@ -35,7 +35,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
protected readonly productVariantService_: ProductVariantService
|
protected readonly productVariantService_: ProductVariantService
|
||||||
protected readonly stockLocationService_: IStockLocationService
|
protected readonly stockLocationService_: IStockLocationService
|
||||||
protected readonly inventoryService_: IInventoryService
|
protected readonly inventoryService_: IInventoryService
|
||||||
protected readonly cacheService_: CacheService
|
protected readonly cacheService_: ICacheService
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
stockLocationService,
|
stockLocationService,
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import {
|
import { EventBusService, ProductVariantService } from "../services"
|
||||||
CacheService,
|
import { ICacheService } from "../interfaces"
|
||||||
EventBusService,
|
|
||||||
ProductVariantService,
|
|
||||||
} from "../services"
|
|
||||||
|
|
||||||
type ProductVariantUpdatedEventData = {
|
type ProductVariantUpdatedEventData = {
|
||||||
id: string
|
id: string
|
||||||
@@ -12,7 +9,7 @@ type ProductVariantUpdatedEventData = {
|
|||||||
|
|
||||||
class PricingSubscriber {
|
class PricingSubscriber {
|
||||||
protected readonly eventBus_: EventBusService
|
protected readonly eventBus_: EventBusService
|
||||||
protected readonly cacheService_: CacheService
|
protected readonly cacheService_: ICacheService
|
||||||
|
|
||||||
constructor({ eventBusService, cacheService }) {
|
constructor({ eventBusService, cacheService }) {
|
||||||
this.eventBus_ = eventBusService
|
this.eventBus_ = eventBusService
|
||||||
@@ -23,7 +20,7 @@ class PricingSubscriber {
|
|||||||
async (data) => {
|
async (data) => {
|
||||||
const { id, fields } = data as ProductVariantUpdatedEventData
|
const { id, fields } = data as ProductVariantUpdatedEventData
|
||||||
if (fields.includes("prices")) {
|
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,
|
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
|
export default MODULE_DEFINITIONS
|
||||||
@@ -5694,6 +5694,34 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
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":
|
"@medusajs/inventory@workspace:packages/inventory":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@medusajs/inventory@workspace:packages/inventory"
|
resolution: "@medusajs/inventory@workspace:packages/inventory"
|
||||||
@@ -23220,6 +23248,7 @@ __metadata:
|
|||||||
"@babel/cli": ^7.12.10
|
"@babel/cli": ^7.12.10
|
||||||
"@babel/core": ^7.12.10
|
"@babel/core": ^7.12.10
|
||||||
"@babel/node": ^7.12.10
|
"@babel/node": ^7.12.10
|
||||||
|
"@medusajs/cache-inmemory": "*"
|
||||||
"@medusajs/medusa": "*"
|
"@medusajs/medusa": "*"
|
||||||
babel-preset-medusa-package: "*"
|
babel-preset-medusa-package: "*"
|
||||||
faker: ^5.5.3
|
faker: ^5.5.3
|
||||||
@@ -23237,6 +23266,7 @@ __metadata:
|
|||||||
"@babel/cli": ^7.12.10
|
"@babel/cli": ^7.12.10
|
||||||
"@babel/core": ^7.12.10
|
"@babel/core": ^7.12.10
|
||||||
"@babel/node": ^7.12.10
|
"@babel/node": ^7.12.10
|
||||||
|
"@medusajs/cache-inmemory": "*"
|
||||||
"@medusajs/medusa": "*"
|
"@medusajs/medusa": "*"
|
||||||
babel-preset-medusa-package: "*"
|
babel-preset-medusa-package: "*"
|
||||||
faker: ^5.5.3
|
faker: ^5.5.3
|
||||||
|
|||||||
Reference in new issue
Block a user