chore(pricing): Fix excessive db queries during price sets update (#13258)
* chore(pricing): Fix excessive db queries during price sets update * chore(pricing): Fix excessive db queries during price sets update * finalize upsert with replace of the rules * fix limit * Create quiet-pumpkins-hang.md --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
7f5b9fc5fa
commit
06ef9197f8
@@ -29,6 +29,7 @@ import {
|
||||
groupBy,
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
isDefined,
|
||||
isPresent,
|
||||
isString,
|
||||
MathBN,
|
||||
@@ -370,7 +371,6 @@ export default class PricingModuleService
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pricesSetPricesMap.set(key, { calculatedPrice, originalPrice })
|
||||
priceIds.push(
|
||||
...(deduplicate(
|
||||
@@ -535,6 +535,7 @@ export default class PricingModuleService
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<PriceSetDTO | PriceSetDTO[]> {
|
||||
const input = Array.isArray(data) ? data : [data]
|
||||
|
||||
const forUpdate = input.filter(
|
||||
(priceSet): priceSet is ServiceTypes.UpdatePriceSetInput => !!priceSet.id
|
||||
)
|
||||
@@ -621,73 +622,215 @@ export default class PricingModuleService
|
||||
data: ServiceTypes.UpdatePriceSetInput[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<InferEntityType<typeof PriceSet>[]> {
|
||||
// TODO: Since money IDs are rarely passed, this will delete all previous data and insert new entries.
|
||||
// We can make the `insert` inside upsertWithReplace do an `upsert` instead to avoid this
|
||||
const normalizedData = await this.normalizeUpdateData(data)
|
||||
|
||||
const priceListPrices = await this.priceService_.list({
|
||||
price_set_id: normalizedData.map(({ id }) => id),
|
||||
price_list_id: { $ne: null },
|
||||
})
|
||||
const priceSetIds = normalizedData.map(({ id }) => id)
|
||||
const existingPrices = await this.priceService_.list(
|
||||
{
|
||||
price_set_id: priceSetIds,
|
||||
price_list_id: null,
|
||||
},
|
||||
{
|
||||
relations: ["price_rules"],
|
||||
take: null,
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const existingPricesMap = new Map<string, InferEntityType<typeof Price>>(
|
||||
existingPrices.map((p) => [p.id, p])
|
||||
)
|
||||
|
||||
const prices = normalizedData.flatMap((priceSet) => priceSet.prices || [])
|
||||
const { entities: upsertedPrices, performedActions } =
|
||||
await this.priceService_.upsertWithReplace(
|
||||
prices,
|
||||
{ relations: ["price_rules"] },
|
||||
|
||||
const pricesToCreate = prices.filter(
|
||||
(price) => !price.id || !existingPricesMap.has(price.id)
|
||||
)
|
||||
const pricesToUpdate = prices.filter(
|
||||
(price) => price.id && existingPricesMap.has(price.id)
|
||||
)
|
||||
|
||||
const incomingPriceIds = new Set(prices.map((p) => p.id).filter(Boolean))
|
||||
const pricesToDelete = existingPrices
|
||||
.filter((existingPrice) => !incomingPriceIds.has(existingPrice.id))
|
||||
.map((p) => p.id)
|
||||
|
||||
let createdPrices: InferEntityType<typeof Price>[] = []
|
||||
let updatedPrices: InferEntityType<typeof Price>[] = []
|
||||
|
||||
if (pricesToCreate.length > 0) {
|
||||
createdPrices = await this.priceService_.create(
|
||||
pricesToCreate.map((price) => {
|
||||
price.price_rules ??= []
|
||||
return price
|
||||
}),
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
composeAllEvents({
|
||||
eventBuilders,
|
||||
performedActions,
|
||||
if (pricesToUpdate.length > 0) {
|
||||
// Handle price rules for updated prices
|
||||
for (const priceToUpdate of pricesToUpdate) {
|
||||
const existingPrice = existingPricesMap.get(priceToUpdate.id!)
|
||||
|
||||
if (priceToUpdate.price_rules?.length) {
|
||||
const existingPriceRules = existingPrice?.price_rules || []
|
||||
|
||||
// Separate price rules for create, update, delete
|
||||
const priceRulesToCreate = priceToUpdate.price_rules.filter(
|
||||
(rule) => !("id" in rule)
|
||||
)
|
||||
const priceRulesToUpdate = priceToUpdate.price_rules.filter(
|
||||
(rule) => "id" in rule
|
||||
)
|
||||
|
||||
const incomingPriceRuleIds = new Set(
|
||||
priceToUpdate.price_rules
|
||||
.map((r) => "id" in r && r.id)
|
||||
.filter(Boolean)
|
||||
)
|
||||
const priceRulesToDelete = existingPriceRules
|
||||
.filter(
|
||||
(existingRule) => !incomingPriceRuleIds.has(existingRule.id)
|
||||
)
|
||||
.map((r) => r.id)
|
||||
|
||||
let createdPriceRules: InferEntityType<typeof PriceRule>[] = []
|
||||
let updatedPriceRules: InferEntityType<typeof PriceRule>[] = []
|
||||
|
||||
// Bulk operations for price rules
|
||||
if (priceRulesToCreate.length > 0) {
|
||||
createdPriceRules = await this.priceRuleService_.create(
|
||||
priceRulesToCreate.map((rule) => ({
|
||||
...rule,
|
||||
price_id: priceToUpdate.id,
|
||||
})),
|
||||
sharedContext
|
||||
)
|
||||
eventBuilders.createdPriceRule({
|
||||
data: createdPriceRules.map((r) => ({ id: r.id })),
|
||||
sharedContext,
|
||||
})
|
||||
}
|
||||
|
||||
if (priceRulesToUpdate.length > 0) {
|
||||
updatedPriceRules = await this.priceRuleService_.update(
|
||||
priceRulesToUpdate,
|
||||
sharedContext
|
||||
)
|
||||
eventBuilders.updatedPriceRule({
|
||||
data: updatedPriceRules.map((r) => ({ id: r.id })),
|
||||
sharedContext,
|
||||
})
|
||||
}
|
||||
|
||||
if (priceRulesToDelete.length > 0) {
|
||||
await this.priceRuleService_.delete(
|
||||
priceRulesToDelete,
|
||||
sharedContext
|
||||
)
|
||||
eventBuilders.deletedPriceRule({
|
||||
data: priceRulesToDelete.map((id) => ({ id })),
|
||||
sharedContext,
|
||||
})
|
||||
}
|
||||
|
||||
const upsertedPriceRules = [
|
||||
...createdPriceRules,
|
||||
...updatedPriceRules,
|
||||
]
|
||||
|
||||
priceToUpdate.price_rules = upsertedPriceRules
|
||||
;(priceToUpdate as InferEntityType<typeof Price>).rules_count =
|
||||
upsertedPriceRules.length
|
||||
} else if (
|
||||
// In the case price_rules is provided but without any rules, we delete the existing rules
|
||||
isDefined(priceToUpdate.price_rules) &&
|
||||
priceToUpdate.price_rules.length === 0
|
||||
) {
|
||||
const priceRuleToDelete = existingPrice?.price_rules?.map((r) => r.id)
|
||||
|
||||
if (priceRuleToDelete?.length) {
|
||||
await this.priceRuleService_.delete(
|
||||
priceRuleToDelete,
|
||||
sharedContext
|
||||
)
|
||||
eventBuilders.deletedPriceRule({
|
||||
data: priceRuleToDelete.map((id) => ({ id })),
|
||||
sharedContext,
|
||||
})
|
||||
}
|
||||
|
||||
;(priceToUpdate as InferEntityType<typeof Price>).rules_count = 0
|
||||
} else {
|
||||
// @ts-expect-error - we want to delete the rules_count property in any case even if provided by mistake
|
||||
delete (priceToUpdate as InferEntityType<typeof Price>).rules_count
|
||||
}
|
||||
// We don't want to persist the price_rules in the database through the price service as it would not work
|
||||
delete priceToUpdate.price_rules
|
||||
}
|
||||
|
||||
updatedPrices = await this.priceService_.update(
|
||||
pricesToUpdate,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
if (pricesToDelete.length > 0) {
|
||||
await this.priceService_.delete(pricesToDelete, sharedContext)
|
||||
}
|
||||
|
||||
if (createdPrices.length > 0) {
|
||||
eventBuilders.createdPrice({
|
||||
data: createdPrices.map((p) => ({ id: p.id })),
|
||||
sharedContext,
|
||||
})
|
||||
}
|
||||
|
||||
if (updatedPrices.length > 0) {
|
||||
eventBuilders.updatedPrice({
|
||||
data: updatedPrices.map((p) => ({ id: p.id })),
|
||||
sharedContext,
|
||||
})
|
||||
}
|
||||
|
||||
if (pricesToDelete.length > 0) {
|
||||
eventBuilders.deletedPrice({
|
||||
data: pricesToDelete.map((id) => ({ id })),
|
||||
sharedContext,
|
||||
})
|
||||
}
|
||||
|
||||
const priceSets = await this.priceSetService_.list(
|
||||
{ id: normalizedData.map(({ id }) => id) },
|
||||
{
|
||||
relations: ["prices", "prices.price_rules"],
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const upsertedPricesMap = new Map<string, InferEntityType<typeof Price>[]>()
|
||||
|
||||
const upsertedPrices = [...createdPrices, ...updatedPrices]
|
||||
upsertedPrices.forEach((price) => {
|
||||
upsertedPricesMap.set(price.price_set_id, [
|
||||
...(upsertedPricesMap.get(price.price_set_id) || []),
|
||||
price,
|
||||
])
|
||||
})
|
||||
|
||||
// re assign the prices to the price sets to not have to refetch after the transaction and keep the bahaviour the same as expected. If the user needs more data, he can still re list the price set with the expected fields and relations that he needs
|
||||
|
||||
priceSets.forEach((ps) => {
|
||||
ps.prices = upsertedPricesMap.get(ps.id) || []
|
||||
})
|
||||
|
||||
eventBuilders.updatedPriceSet({
|
||||
data: priceSets.map((ps) => ({ id: ps.id })),
|
||||
sharedContext,
|
||||
})
|
||||
|
||||
const priceSetsToUpsert = normalizedData.map((priceSet) => {
|
||||
const { prices, ...rest } = priceSet
|
||||
return {
|
||||
...rest,
|
||||
prices: [
|
||||
...upsertedPrices
|
||||
.filter((p) => p.price_set_id === priceSet.id)
|
||||
.map((price) => {
|
||||
// @ts-ignore
|
||||
delete price.price_rules
|
||||
return price
|
||||
}),
|
||||
...priceListPrices
|
||||
.filter((p) => p.price_set_id === priceSet.id)
|
||||
.map((price) => ({
|
||||
id: price.id,
|
||||
amount: price.amount,
|
||||
price_set_id: price.price_set_id,
|
||||
price_list_id: price.price_list_id,
|
||||
})),
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
const { entities: priceSets, performedActions: priceSetPerformedActions } =
|
||||
await this.priceSetService_.upsertWithReplace(
|
||||
priceSetsToUpsert,
|
||||
{ relations: ["prices"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
composeAllEvents({
|
||||
eventBuilders,
|
||||
performedActions: priceSetPerformedActions,
|
||||
sharedContext,
|
||||
})
|
||||
|
||||
return priceSets.map((ps) => {
|
||||
if (ps.prices) {
|
||||
ps.prices = (ps.prices as any).filter((p) => !p.price_list_id)
|
||||
}
|
||||
|
||||
return ps
|
||||
})
|
||||
return priceSets
|
||||
}
|
||||
|
||||
private async normalizeUpdateData(data: ServiceTypes.UpdatePriceSetInput[]) {
|
||||
|
||||
Reference in New Issue
Block a user