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:
Frane Polić
2023-03-28 11:18:13 +02:00
committed by GitHub
parent 455c56c4b3
commit 1ce3cc5ae4
10 changed files with 53 additions and 47 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
feat(medusa): invalidate price selection caching within update request

View File

@@ -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([

View File

@@ -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 },
},
},
}

View File

@@ -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({

View File

@@ -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 {

View File

@@ -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,
})

View File

@@ -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(

View File

@@ -367,6 +367,10 @@ class ProductVariantService extends TransactionBaseService {
await this.setCurrencyPrice(variantId, price)
}
}
await this.priceSelectionStrategy_
.withTransaction(manager)
.onVariantsPricesUpdate([variantId])
})
}

View File

@@ -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

View File

@@ -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