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:
5
.changeset/tame-pillows-heal.md
Normal file
5
.changeset/tame-pillows-heal.md
Normal file
@@ -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 })
|
||||
medusaProcess = await setupServer({
|
||||
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)
|
||||
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).toEqual(
|
||||
expect.arrayContaining([
|
||||
|
||||
@@ -4,6 +4,7 @@ const DB_PASSWORD = process.env.DB_PASSWORD
|
||||
const DB_NAME = process.env.DB_TEMP_NAME
|
||||
|
||||
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379"
|
||||
const cacheTTL = process.env.CACHE_TTL || 15
|
||||
|
||||
module.exports = {
|
||||
plugins: [],
|
||||
@@ -17,11 +18,7 @@ module.exports = {
|
||||
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 },
|
||||
options: { ttl: cacheTTL },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ export default async (req, res) => {
|
||||
currency_code: currencyCode,
|
||||
customer_id: req.validatedQuery.customer_id,
|
||||
include_discount_prices: true,
|
||||
ignore_cache: true,
|
||||
})
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface IPriceSelectionStrategy {
|
||||
/**
|
||||
* Instantiate a new price selection strategy with the active transaction in
|
||||
* order to ensure reads are accurate.
|
||||
* @param manager EntityManager with the queryrunner of the active transaction
|
||||
* @param manager EntityManager with the query runner of the active transaction
|
||||
* @returns a new price selection strategy
|
||||
*/
|
||||
withTransaction(manager: EntityManager): IPriceSelectionStrategy
|
||||
@@ -15,15 +15,21 @@ export interface IPriceSelectionStrategy {
|
||||
/**
|
||||
* Calculate the original and discount price for a given variant in a set of
|
||||
* 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
|
||||
* @return pricing details in an object containing the calculated lowest price,
|
||||
* the default price an all valid prices for the given variant
|
||||
*/
|
||||
calculateVariantPrice(
|
||||
variant_id: string,
|
||||
variantId: string,
|
||||
context: PriceSelectionContext
|
||||
): 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
|
||||
@@ -34,9 +40,13 @@ export abstract class AbstractPriceSelectionStrategy
|
||||
): IPriceSelectionStrategy
|
||||
|
||||
public abstract calculateVariantPrice(
|
||||
variant_id: string,
|
||||
variantId: string,
|
||||
context: PriceSelectionContext
|
||||
): Promise<PriceSelectionResult>
|
||||
|
||||
public async onVariantsPricesUpdate(variantIds: string[]): Promise<void> {
|
||||
return void 0
|
||||
}
|
||||
}
|
||||
|
||||
export function isPriceSelectionStrategy(
|
||||
@@ -57,6 +67,7 @@ export type PriceSelectionContext = {
|
||||
currency_code?: string
|
||||
include_discount_prices?: boolean
|
||||
tax_rates?: TaxServiceRate[]
|
||||
ignore_cache?: boolean
|
||||
}
|
||||
|
||||
enum DefaultPriceType {
|
||||
|
||||
@@ -649,10 +649,14 @@ describe("ProductVariantService", () => {
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve())
|
||||
|
||||
const regionService = {
|
||||
const priceSelectionStrategy = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
onVariantsPricesUpdate: (variantIds) => Promise.resolve(),
|
||||
}
|
||||
|
||||
const regionService = {
|
||||
list: jest.fn().mockImplementation((config) => {
|
||||
const idOrIds = config.id
|
||||
|
||||
@@ -679,6 +683,7 @@ describe("ProductVariantService", () => {
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
regionService,
|
||||
priceSelectionStrategy,
|
||||
moneyAmountRepository,
|
||||
})
|
||||
|
||||
|
||||
@@ -426,6 +426,7 @@ class PricingService extends TransactionBaseService {
|
||||
context: PriceSelectionContext = {}
|
||||
): Promise<PricedVariant[]> {
|
||||
const pricingContext = await this.collectPricingContext(context)
|
||||
|
||||
return await Promise.all(
|
||||
variants.map(async (variant) => {
|
||||
const variantPricing = await this.getProductVariantPricing(
|
||||
|
||||
@@ -367,6 +367,10 @@ class ProductVariantService extends TransactionBaseService {
|
||||
await this.setCurrencyPrice(variantId, price)
|
||||
}
|
||||
}
|
||||
|
||||
await this.priceSelectionStrategy_
|
||||
.withTransaction(manager)
|
||||
.onVariantsPricesUpdate([variantId])
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -51,11 +51,13 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
|
||||
context: PriceSelectionContext
|
||||
): Promise<PriceSelectionResult> {
|
||||
const cacheKey = this.getCacheKey(variant_id, context)
|
||||
const cached = await this.cacheService_
|
||||
.get<PriceSelectionResult>(cacheKey)
|
||||
.catch(() => void 0)
|
||||
if (cached) {
|
||||
return cached
|
||||
if (!context.ignore_cache) {
|
||||
const cached = await this.cacheService_
|
||||
.get<PriceSelectionResult>(cacheKey)
|
||||
.catch(() => void 0)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
let result
|
||||
@@ -235,6 +237,14 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy {
|
||||
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(
|
||||
variantId: string,
|
||||
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