feat(medusa): invalidate price selection caching within update request (#3553)
* feat: invalidate price selection caching on update * feat: add `onVariantsPricesUpdate` to PriceSelectionStrategy * fix: update units * fix: import * Create .changeset/tame-pillows-heal.md * fix: address feedback * refactor: make `onVariantsPricesUpdate` optional --------- Co-authored-by: fPolic <frane@medusajs.com> Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
co-authored by
fPolic
Oliver Windall Juhl
parent
455c56c4b3
commit
1ce3cc5ae4
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(medusa): invalidate price selection caching within update request
|
||||||
@@ -44,7 +44,7 @@ describe("/admin/products", () => {
|
|||||||
dbConnection = await initDb({ cwd })
|
dbConnection = await initDb({ cwd })
|
||||||
medusaProcess = await setupServer({
|
medusaProcess = await setupServer({
|
||||||
cwd,
|
cwd,
|
||||||
env: { MEDUSA_FF_PRODUCT_CATEGORIES: true }
|
env: { MEDUSA_FF_PRODUCT_CATEGORIES: true },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1784,7 +1784,9 @@ describe("/admin/products", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
const variant = response.data.product.variants.find(v => v.id === variantId)
|
const variant = response.data.product.variants.find(
|
||||||
|
(v) => v.id === variantId
|
||||||
|
)
|
||||||
expect(variant.prices.length).toEqual(1)
|
expect(variant.prices.length).toEqual(1)
|
||||||
expect(variant.prices).toEqual(
|
expect(variant.prices).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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"
|
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379"
|
||||||
|
const cacheTTL = process.env.CACHE_TTL || 15
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [],
|
plugins: [],
|
||||||
@@ -17,11 +18,7 @@ module.exports = {
|
|||||||
modules: {
|
modules: {
|
||||||
cacheService: {
|
cacheService: {
|
||||||
resolve: "@medusajs/cache-inmemory",
|
resolve: "@medusajs/cache-inmemory",
|
||||||
// don't set cache since this is shared between tests
|
options: { ttl: cacheTTL },
|
||||||
// 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 },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export default async (req, res) => {
|
|||||||
currency_code: currencyCode,
|
currency_code: currencyCode,
|
||||||
customer_id: req.validatedQuery.customer_id,
|
customer_id: req.validatedQuery.customer_id,
|
||||||
include_discount_prices: true,
|
include_discount_prices: true,
|
||||||
|
ignore_cache: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -15,15 +15,21 @@ export interface IPriceSelectionStrategy {
|
|||||||
/**
|
/**
|
||||||
* Calculate the original and discount price for a given variant in a set of
|
* Calculate the original and discount price for a given variant in a set of
|
||||||
* circumstances described in the context.
|
* circumstances described in the context.
|
||||||
* @param variant The variant id of the variant for which to retrieve prices
|
* @param variantId The variant id of the variant for which to retrieve prices
|
||||||
* @param context Details relevant to determine the correct pricing of the variant
|
* @param context Details relevant to determine the correct pricing of the variant
|
||||||
* @return pricing details in an object containing the calculated lowest price,
|
* @return pricing details in an object containing the calculated lowest price,
|
||||||
* the default price an all valid prices for the given variant
|
* the default price an all valid prices for the given variant
|
||||||
*/
|
*/
|
||||||
calculateVariantPrice(
|
calculateVariantPrice(
|
||||||
variant_id: string,
|
variantId: string,
|
||||||
context: PriceSelectionContext
|
context: PriceSelectionContext
|
||||||
): Promise<PriceSelectionResult>
|
): Promise<PriceSelectionResult>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify price selection strategy that variants prices have been updated.
|
||||||
|
* @param variantIds The ids of the updated variants
|
||||||
|
*/
|
||||||
|
onVariantsPricesUpdate(variantIds: string[]): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class AbstractPriceSelectionStrategy
|
export abstract class AbstractPriceSelectionStrategy
|
||||||
@@ -34,9 +40,13 @@ export abstract class AbstractPriceSelectionStrategy
|
|||||||
): IPriceSelectionStrategy
|
): IPriceSelectionStrategy
|
||||||
|
|
||||||
public abstract calculateVariantPrice(
|
public abstract calculateVariantPrice(
|
||||||
variant_id: string,
|
variantId: string,
|
||||||
context: PriceSelectionContext
|
context: PriceSelectionContext
|
||||||
): Promise<PriceSelectionResult>
|
): Promise<PriceSelectionResult>
|
||||||
|
|
||||||
|
public async onVariantsPricesUpdate(variantIds: string[]): Promise<void> {
|
||||||
|
return void 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPriceSelectionStrategy(
|
export function isPriceSelectionStrategy(
|
||||||
@@ -57,6 +67,7 @@ export type PriceSelectionContext = {
|
|||||||
currency_code?: string
|
currency_code?: string
|
||||||
include_discount_prices?: boolean
|
include_discount_prices?: boolean
|
||||||
tax_rates?: TaxServiceRate[]
|
tax_rates?: TaxServiceRate[]
|
||||||
|
ignore_cache?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DefaultPriceType {
|
enum DefaultPriceType {
|
||||||
|
|||||||
@@ -649,10 +649,14 @@ describe("ProductVariantService", () => {
|
|||||||
.fn()
|
.fn()
|
||||||
.mockImplementation(() => Promise.resolve())
|
.mockImplementation(() => Promise.resolve())
|
||||||
|
|
||||||
const regionService = {
|
const priceSelectionStrategy = {
|
||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
onVariantsPricesUpdate: (variantIds) => Promise.resolve(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const regionService = {
|
||||||
list: jest.fn().mockImplementation((config) => {
|
list: jest.fn().mockImplementation((config) => {
|
||||||
const idOrIds = config.id
|
const idOrIds = config.id
|
||||||
|
|
||||||
@@ -679,6 +683,7 @@ describe("ProductVariantService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
regionService,
|
regionService,
|
||||||
|
priceSelectionStrategy,
|
||||||
moneyAmountRepository,
|
moneyAmountRepository,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -426,6 +426,7 @@ class PricingService extends TransactionBaseService {
|
|||||||
context: PriceSelectionContext = {}
|
context: PriceSelectionContext = {}
|
||||||
): Promise<PricedVariant[]> {
|
): Promise<PricedVariant[]> {
|
||||||
const pricingContext = await this.collectPricingContext(context)
|
const pricingContext = await this.collectPricingContext(context)
|
||||||
|
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
variants.map(async (variant) => {
|
variants.map(async (variant) => {
|
||||||
const variantPricing = await this.getProductVariantPricing(
|
const variantPricing = await this.getProductVariantPricing(
|
||||||
|
|||||||
@@ -367,6 +367,10 @@ class ProductVariantService extends TransactionBaseService {
|
|||||||
await this.setCurrencyPrice(variantId, price)
|
await this.setCurrencyPrice(variantId, price)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.priceSelectionStrategy_
|
||||||
|
.withTransaction(manager)
|
||||||
|
.onVariantsPricesUpdate([variantId])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,12 +51,14 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
|
|||||||
context: PriceSelectionContext
|
context: PriceSelectionContext
|
||||||
): Promise<PriceSelectionResult> {
|
): Promise<PriceSelectionResult> {
|
||||||
const cacheKey = this.getCacheKey(variant_id, context)
|
const cacheKey = this.getCacheKey(variant_id, context)
|
||||||
|
if (!context.ignore_cache) {
|
||||||
const cached = await this.cacheService_
|
const cached = await this.cacheService_
|
||||||
.get<PriceSelectionResult>(cacheKey)
|
.get<PriceSelectionResult>(cacheKey)
|
||||||
.catch(() => void 0)
|
.catch(() => void 0)
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return cached
|
return cached
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let result
|
let result
|
||||||
|
|
||||||
@@ -235,6 +237,14 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async onVariantsPricesUpdate(variantIds: string[]): Promise<void> {
|
||||||
|
await Promise.all(
|
||||||
|
variantIds.map(
|
||||||
|
async (id: string) => await this.cacheService_.invalidate(`ps:${id}:*`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private getCacheKey(
|
private getCacheKey(
|
||||||
variantId: string,
|
variantId: string,
|
||||||
context: PriceSelectionContext
|
context: PriceSelectionContext
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { ICacheService } from "@medusajs/types"
|
|
||||||
import { EventBusService, ProductVariantService } from "../services"
|
|
||||||
|
|
||||||
type ProductVariantUpdatedEventData = {
|
|
||||||
id: string
|
|
||||||
product_id: string
|
|
||||||
fields: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
class PricingSubscriber {
|
|
||||||
protected readonly eventBus_: EventBusService
|
|
||||||
protected readonly cacheService_: ICacheService
|
|
||||||
|
|
||||||
constructor({ eventBusService, cacheService }) {
|
|
||||||
this.eventBus_ = eventBusService
|
|
||||||
this.cacheService_ = cacheService
|
|
||||||
|
|
||||||
this.eventBus_.subscribe(
|
|
||||||
ProductVariantService.Events.UPDATED,
|
|
||||||
async (data) => {
|
|
||||||
const { id, fields } = data as ProductVariantUpdatedEventData
|
|
||||||
if (fields.includes("prices")) {
|
|
||||||
await this.cacheService_.invalidate(`ps:${id}:*`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PricingSubscriber
|
|
||||||
Reference in New Issue
Block a user