chore: Cache available price rule attributes (#12144)

**What**
We found out that the pricing context from the cart always contains the entire cart, even though it is kind of wrong. The issue is that even though we improve the performances of the query, it will cost a lot to have hundreds of constraint for nothing potentially. For that reason, we cache the attributes in memory with the best possible query we can do to gather them and we renew them when we perform a calculate prices if it has been reset. That way, we ensure we don't have unnecessary checks on attributes that does not have rules.

Since we don't have the type table anymore which was doing that for us and until we have a proper caching layer it would do IMO. But the rules type table was very useful for these attributes findings
This commit is contained in:
Adrien de Peretti
2025-04-10 15:55:35 +00:00
committed by GitHub
parent d87b25203c
commit 3a1cf2212a
3 changed files with 176 additions and 36 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/pricing": patch
---
Chore/pricing cache available keys
@@ -19,12 +19,46 @@ export class PricingRepository
extends MikroOrmBase extends MikroOrmBase
implements PricingRepositoryService implements PricingRepositoryService
{ {
#availableAttributes: Set<string> = new Set()
constructor() { constructor() {
// @ts-ignore // @ts-ignore
// eslint-disable-next-line prefer-rest-params // eslint-disable-next-line prefer-rest-params
super(...arguments) super(...arguments)
} }
clearAvailableAttributes() {
this.#availableAttributes.clear()
}
async #cacheAvailableAttributes() {
const manager = this.getActiveManager<SqlEntityManager>()
const knex = manager.getKnex()
const { rows } = await knex.raw(
`
SELECT DISTINCT attribute
FROM (
SELECT attribute
FROM price_rule
UNION ALL
SELECT attribute
FROM price_list_rule
) as combined_rules_attributes
`
)
this.#availableAttributes.clear()
rows.forEach(({ attribute }) => {
this.#availableAttributes.add(attribute)
})
}
async #cacheAvailableAttributesIfNecessary() {
if (this.#availableAttributes.size === 0) {
await this.#cacheAvailableAttributes()
}
}
async calculatePrices( async calculatePrices(
pricingFilters: PricingFilters, pricingFilters: PricingFilters,
pricingContext: PricingContext = { context: {} }, pricingContext: PricingContext = { context: {} },
@@ -53,7 +87,7 @@ export class PricingRepository
const flattenedKeyValuePairs = flattenObjectToKeyValuePairs(context) const flattenedKeyValuePairs = flattenObjectToKeyValuePairs(context)
// First filter by value presence // First filter by value presence
const flattenedContext = Object.entries(flattenedKeyValuePairs).filter( let flattenedContext = Object.entries(flattenedKeyValuePairs).filter(
([, value]) => { ([, value]) => {
const isValuePresent = !Array.isArray(value) && isPresent(value) const isValuePresent = !Array.isArray(value) && isPresent(value)
const isArrayPresent = Array.isArray(value) && value.flat(1).length const isArrayPresent = Array.isArray(value) && value.flat(1).length
@@ -62,6 +96,13 @@ export class PricingRepository
} }
) )
if (flattenedContext.length > 10) {
await this.#cacheAvailableAttributesIfNecessary()
flattenedContext = flattenedContext.filter(([key]) =>
this.#availableAttributes.has(key)
)
}
const hasComplexContext = flattenedContext.length > 0 const hasComplexContext = flattenedContext.length > 0
const query = knex const query = knex
@@ -227,7 +268,6 @@ export class PricingRepository
.orderBy("pl.id", "asc") .orderBy("pl.id", "asc")
.orderBy("price.amount", "asc") .orderBy("price.amount", "asc")
console.log(query.toString())
return await query return await query
} }
} }
@@ -77,24 +77,28 @@ const generateMethodForModels = {
PricePreference, PricePreference,
} }
const BaseClass = ModulesSdkUtils.MedusaService<{
PriceSet: { dto: PricingTypes.PriceSetDTO }
Price: { dto: PricingTypes.PriceDTO }
PriceRule: {
dto: PricingTypes.PriceRuleDTO
create: PricingTypes.CreatePriceRuleDTO
update: PricingTypes.UpdatePriceRuleDTO
}
PriceList: { dto: PricingTypes.PriceListDTO }
PriceListRule: { dto: PricingTypes.PriceListRuleDTO }
// PricePreference: { dto: PricingTypes.PricePreferenceDTO }
PricePreference: { dto: any }
}>(generateMethodForModels)
export default class PricingModuleService export default class PricingModuleService
extends ModulesSdkUtils.MedusaService<{ extends BaseClass
PriceSet: { dto: PricingTypes.PriceSetDTO }
Price: { dto: PricingTypes.PriceDTO }
PriceRule: {
dto: PricingTypes.PriceRuleDTO
create: PricingTypes.CreatePriceRuleDTO
update: PricingTypes.UpdatePriceRuleDTO
}
PriceList: { dto: PricingTypes.PriceListDTO }
PriceListRule: { dto: PricingTypes.PriceListRuleDTO }
// PricePreference: { dto: PricingTypes.PricePreferenceDTO }
PricePreference: { dto: any }
}>(generateMethodForModels)
implements PricingTypes.IPricingModuleService implements PricingTypes.IPricingModuleService
{ {
protected baseRepository_: DAL.RepositoryService protected baseRepository_: DAL.RepositoryService
protected readonly pricingRepository_: PricingRepositoryService protected readonly pricingRepository_: PricingRepositoryService & {
clearAvailableAttributes?: () => Promise<void>
}
protected readonly priceSetService_: ModulesSdkTypes.IMedusaInternalService< protected readonly priceSetService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof PriceSet> InferEntityType<typeof PriceSet>
> >
@@ -164,6 +168,52 @@ export default class PricingModuleService
return pricingContext return pricingContext
} }
// @ts-expect-error
async createPriceRules(
...args: Parameters<PricingTypes.IPricingModuleService["createPriceRules"]>
): Promise<PricingTypes.PriceRuleDTO | PricingTypes.PriceRuleDTO[]> {
try {
return await super.createPriceRules(...args)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
// @ts-expect-error
async updatePriceRules(
...args: Parameters<PricingTypes.IPricingModuleService["updatePriceRules"]>
): Promise<PricingTypes.PriceRuleDTO | PricingTypes.PriceRuleDTO[]> {
try {
return await super.updatePriceRules(...args)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
// @ts-expect-error
async createPriceListRules(
...args: any[]
): Promise<PricingTypes.PriceListRuleDTO | PricingTypes.PriceListRuleDTO[]> {
try {
// @ts-ignore
return await super.createPriceListRules(...args)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
// @ts-expect-error
async updatePriceListRules(
...args: any[]
): Promise<PricingTypes.PriceListRuleDTO | PricingTypes.PriceListRuleDTO[]> {
try {
// @ts-ignore
return await super.updatePriceListRules(...args)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectManager() @InjectManager()
// @ts-expect-error // @ts-expect-error
async retrievePriceSet( async retrievePriceSet(
@@ -458,9 +508,13 @@ export default class PricingModuleService
return dbPriceSets.find((p) => p.id === priceSet.id)! return dbPriceSets.find((p) => p.id === priceSet.id)!
}) })
return await this.baseRepository_.serialize<PriceSetDTO[] | PriceSetDTO>( try {
Array.isArray(data) ? results : results[0] return await this.baseRepository_.serialize<PriceSetDTO[] | PriceSetDTO>(
) Array.isArray(data) ? results : results[0]
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
} }
async upsertPriceSets( async upsertPriceSets(
@@ -496,9 +550,14 @@ export default class PricingModuleService
} }
const result = (await promiseAll(operations)).flat() const result = (await promiseAll(operations)).flat()
return await this.baseRepository_.serialize<PriceSetDTO[] | PriceSetDTO>(
Array.isArray(data) ? result : result[0] try {
) return await this.baseRepository_.serialize<PriceSetDTO[] | PriceSetDTO>(
Array.isArray(data) ? result : result[0]
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
} }
// @ts-expect-error // @ts-expect-error
@@ -548,7 +607,11 @@ export default class PricingModuleService
PriceSetDTO[] | PriceSetDTO PriceSetDTO[] | PriceSetDTO
>(updateResult) >(updateResult)
return isString(idOrSelector) ? priceSets[0] : priceSets try {
return isString(idOrSelector) ? priceSets[0] : priceSets
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
} }
@InjectTransactionManager() @InjectTransactionManager()
@@ -744,7 +807,11 @@ export default class PricingModuleService
return dbPrices.find((p) => p.id === inputItem.priceSetId)! return dbPrices.find((p) => p.id === inputItem.priceSetId)!
}) })
return Array.isArray(data) ? orderedPriceSets : orderedPriceSets[0] try {
return Array.isArray(data) ? orderedPriceSets : orderedPriceSets[0]
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
} }
@InjectManager() @InjectManager()
@@ -756,9 +823,13 @@ export default class PricingModuleService
): Promise<PricingTypes.PriceListDTO[]> { ): Promise<PricingTypes.PriceListDTO[]> {
const priceLists = await this.createPriceLists_(data, sharedContext) const priceLists = await this.createPriceLists_(data, sharedContext)
return await this.baseRepository_.serialize<PricingTypes.PriceListDTO[]>( try {
priceLists return await this.baseRepository_.serialize<PricingTypes.PriceListDTO[]>(
) priceLists
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
} }
@InjectTransactionManager() @InjectTransactionManager()
@@ -770,9 +841,13 @@ export default class PricingModuleService
): Promise<PricingTypes.PriceListDTO[]> { ): Promise<PricingTypes.PriceListDTO[]> {
const priceLists = await this.updatePriceLists_(data, sharedContext) const priceLists = await this.updatePriceLists_(data, sharedContext)
return await this.baseRepository_.serialize<PricingTypes.PriceListDTO[]>( try {
priceLists return await this.baseRepository_.serialize<PricingTypes.PriceListDTO[]>(
) priceLists
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
} }
@InjectManager() @InjectManager()
@@ -783,7 +858,13 @@ export default class PricingModuleService
): Promise<PricingTypes.PriceDTO[]> { ): Promise<PricingTypes.PriceDTO[]> {
const prices = await this.updatePriceListPrices_(data, sharedContext) const prices = await this.updatePriceListPrices_(data, sharedContext)
return await this.baseRepository_.serialize<PricingTypes.PriceDTO[]>(prices) try {
return await this.baseRepository_.serialize<PricingTypes.PriceDTO[]>(
prices
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
} }
@InjectManager() @InjectManager()
@@ -792,7 +873,11 @@ export default class PricingModuleService
ids: string[], ids: string[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<void> { ): Promise<void> {
await this.removePrices_(ids, sharedContext) try {
await this.removePrices_(ids, sharedContext)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
} }
@InjectManager() @InjectManager()
@@ -803,7 +888,13 @@ export default class PricingModuleService
): Promise<PricingTypes.PriceDTO[]> { ): Promise<PricingTypes.PriceDTO[]> {
const prices = await this.addPriceListPrices_(data, sharedContext) const prices = await this.addPriceListPrices_(data, sharedContext)
return await this.baseRepository_.serialize<PricingTypes.PriceDTO[]>(prices) try {
return await this.baseRepository_.serialize<PricingTypes.PriceDTO[]>(
prices
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
} }
@InjectManager() @InjectManager()
@@ -814,9 +905,13 @@ export default class PricingModuleService
): Promise<PricingTypes.PriceListDTO> { ): Promise<PricingTypes.PriceListDTO> {
const [priceList] = await this.setPriceListRules_([data], sharedContext) const [priceList] = await this.setPriceListRules_([data], sharedContext)
return await this.baseRepository_.serialize<PricingTypes.PriceListDTO>( try {
priceList return await this.baseRepository_.serialize<PricingTypes.PriceListDTO>(
) priceList
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
} }
@InjectManager() @InjectManager()