Skip to content

Commit 06ef919

Browse files
adrien2polivermrbl
andauthored
chore(pricing): Fix excessive db queries during price sets update (medusajs#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 <[email protected]>
1 parent 7f5b9fc commit 06ef919

File tree

3 files changed

+206
-56
lines changed

3 files changed

+206
-56
lines changed

.changeset/quiet-pumpkins-hang.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@medusajs/pricing": patch
3+
---
4+
5+
chore(pricing): Fix excessive db queries during price sets update

integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ medusaIntegrationTestRunner({
9999
`/admin/shipping-option-types`,
100100
{
101101
label: "Test",
102-
code: 'test',
102+
code: "test",
103103
},
104104
adminHeaders
105105
)
@@ -1025,7 +1025,7 @@ medusaIntegrationTestRunner({
10251025
const updateResponse = await api.post(
10261026
`/admin/shipping-options/${shippingOptionId}`,
10271027
{
1028-
name: "Updated shipping option"
1028+
name: "Updated shipping option",
10291029
},
10301030
adminHeaders
10311031
)
@@ -1034,7 +1034,7 @@ medusaIntegrationTestRunner({
10341034
expect(updateResponse.data.shipping_option).toEqual(
10351035
expect.objectContaining({
10361036
id: expect.any(String),
1037-
name: "Updated shipping option"
1037+
name: "Updated shipping option",
10381038
})
10391039
)
10401040
})
@@ -1129,7 +1129,7 @@ medusaIntegrationTestRunner({
11291129
description: "Test description",
11301130
code: "test-code",
11311131
},
1132-
type_id: "test_type_id"
1132+
type_id: "test_type_id",
11331133
}
11341134

11351135
const error = await api
@@ -1141,7 +1141,9 @@ medusaIntegrationTestRunner({
11411141
.catch((e) => e)
11421142

11431143
expect(error.response.status).toEqual(400)
1144-
expect(error.response.data.message).toEqual("Invalid request: Only one of 'type' or 'type_id' can be provided")
1144+
expect(error.response.data.message).toEqual(
1145+
"Invalid request: Only one of 'type' or 'type_id' can be provided"
1146+
)
11451147
})
11461148
})
11471149

packages/modules/pricing/src/services/pricing-module.ts

Lines changed: 194 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
groupBy,
3030
InjectManager,
3131
InjectTransactionManager,
32+
isDefined,
3233
isPresent,
3334
isString,
3435
MathBN,
@@ -370,7 +371,6 @@ export default class PricingModuleService
370371
}
371372
}
372373

373-
374374
pricesSetPricesMap.set(key, { calculatedPrice, originalPrice })
375375
priceIds.push(
376376
...(deduplicate(
@@ -535,6 +535,7 @@ export default class PricingModuleService
535535
@MedusaContext() sharedContext: Context = {}
536536
): Promise<PriceSetDTO | PriceSetDTO[]> {
537537
const input = Array.isArray(data) ? data : [data]
538+
538539
const forUpdate = input.filter(
539540
(priceSet): priceSet is ServiceTypes.UpdatePriceSetInput => !!priceSet.id
540541
)
@@ -621,73 +622,215 @@ export default class PricingModuleService
621622
data: ServiceTypes.UpdatePriceSetInput[],
622623
@MedusaContext() sharedContext: Context = {}
623624
): Promise<InferEntityType<typeof PriceSet>[]> {
624-
// TODO: Since money IDs are rarely passed, this will delete all previous data and insert new entries.
625-
// We can make the `insert` inside upsertWithReplace do an `upsert` instead to avoid this
626625
const normalizedData = await this.normalizeUpdateData(data)
627626

628-
const priceListPrices = await this.priceService_.list({
629-
price_set_id: normalizedData.map(({ id }) => id),
630-
price_list_id: { $ne: null },
631-
})
627+
const priceSetIds = normalizedData.map(({ id }) => id)
628+
const existingPrices = await this.priceService_.list(
629+
{
630+
price_set_id: priceSetIds,
631+
price_list_id: null,
632+
},
633+
{
634+
relations: ["price_rules"],
635+
take: null,
636+
},
637+
sharedContext
638+
)
639+
640+
const existingPricesMap = new Map<string, InferEntityType<typeof Price>>(
641+
existingPrices.map((p) => [p.id, p])
642+
)
632643

633644
const prices = normalizedData.flatMap((priceSet) => priceSet.prices || [])
634-
const { entities: upsertedPrices, performedActions } =
635-
await this.priceService_.upsertWithReplace(
636-
prices,
637-
{ relations: ["price_rules"] },
645+
646+
const pricesToCreate = prices.filter(
647+
(price) => !price.id || !existingPricesMap.has(price.id)
648+
)
649+
const pricesToUpdate = prices.filter(
650+
(price) => price.id && existingPricesMap.has(price.id)
651+
)
652+
653+
const incomingPriceIds = new Set(prices.map((p) => p.id).filter(Boolean))
654+
const pricesToDelete = existingPrices
655+
.filter((existingPrice) => !incomingPriceIds.has(existingPrice.id))
656+
.map((p) => p.id)
657+
658+
let createdPrices: InferEntityType<typeof Price>[] = []
659+
let updatedPrices: InferEntityType<typeof Price>[] = []
660+
661+
if (pricesToCreate.length > 0) {
662+
createdPrices = await this.priceService_.create(
663+
pricesToCreate.map((price) => {
664+
price.price_rules ??= []
665+
return price
666+
}),
638667
sharedContext
639668
)
669+
}
640670

641-
composeAllEvents({
642-
eventBuilders,
643-
performedActions,
644-
sharedContext,
645-
})
671+
if (pricesToUpdate.length > 0) {
672+
// Handle price rules for updated prices
673+
for (const priceToUpdate of pricesToUpdate) {
674+
const existingPrice = existingPricesMap.get(priceToUpdate.id!)
675+
676+
if (priceToUpdate.price_rules?.length) {
677+
const existingPriceRules = existingPrice?.price_rules || []
678+
679+
// Separate price rules for create, update, delete
680+
const priceRulesToCreate = priceToUpdate.price_rules.filter(
681+
(rule) => !("id" in rule)
682+
)
683+
const priceRulesToUpdate = priceToUpdate.price_rules.filter(
684+
(rule) => "id" in rule
685+
)
686+
687+
const incomingPriceRuleIds = new Set(
688+
priceToUpdate.price_rules
689+
.map((r) => "id" in r && r.id)
690+
.filter(Boolean)
691+
)
692+
const priceRulesToDelete = existingPriceRules
693+
.filter(
694+
(existingRule) => !incomingPriceRuleIds.has(existingRule.id)
695+
)
696+
.map((r) => r.id)
697+
698+
let createdPriceRules: InferEntityType<typeof PriceRule>[] = []
699+
let updatedPriceRules: InferEntityType<typeof PriceRule>[] = []
700+
701+
// Bulk operations for price rules
702+
if (priceRulesToCreate.length > 0) {
703+
createdPriceRules = await this.priceRuleService_.create(
704+
priceRulesToCreate.map((rule) => ({
705+
...rule,
706+
price_id: priceToUpdate.id,
707+
})),
708+
sharedContext
709+
)
710+
eventBuilders.createdPriceRule({
711+
data: createdPriceRules.map((r) => ({ id: r.id })),
712+
sharedContext,
713+
})
714+
}
646715

647-
const priceSetsToUpsert = normalizedData.map((priceSet) => {
648-
const { prices, ...rest } = priceSet
649-
return {
650-
...rest,
651-
prices: [
652-
...upsertedPrices
653-
.filter((p) => p.price_set_id === priceSet.id)
654-
.map((price) => {
655-
// @ts-ignore
656-
delete price.price_rules
657-
return price
658-
}),
659-
...priceListPrices
660-
.filter((p) => p.price_set_id === priceSet.id)
661-
.map((price) => ({
662-
id: price.id,
663-
amount: price.amount,
664-
price_set_id: price.price_set_id,
665-
price_list_id: price.price_list_id,
666-
})),
667-
],
716+
if (priceRulesToUpdate.length > 0) {
717+
updatedPriceRules = await this.priceRuleService_.update(
718+
priceRulesToUpdate,
719+
sharedContext
720+
)
721+
eventBuilders.updatedPriceRule({
722+
data: updatedPriceRules.map((r) => ({ id: r.id })),
723+
sharedContext,
724+
})
725+
}
726+
727+
if (priceRulesToDelete.length > 0) {
728+
await this.priceRuleService_.delete(
729+
priceRulesToDelete,
730+
sharedContext
731+
)
732+
eventBuilders.deletedPriceRule({
733+
data: priceRulesToDelete.map((id) => ({ id })),
734+
sharedContext,
735+
})
736+
}
737+
738+
const upsertedPriceRules = [
739+
...createdPriceRules,
740+
...updatedPriceRules,
741+
]
742+
743+
priceToUpdate.price_rules = upsertedPriceRules
744+
;(priceToUpdate as InferEntityType<typeof Price>).rules_count =
745+
upsertedPriceRules.length
746+
} else if (
747+
// In the case price_rules is provided but without any rules, we delete the existing rules
748+
isDefined(priceToUpdate.price_rules) &&
749+
priceToUpdate.price_rules.length === 0
750+
) {
751+
const priceRuleToDelete = existingPrice?.price_rules?.map((r) => r.id)
752+
753+
if (priceRuleToDelete?.length) {
754+
await this.priceRuleService_.delete(
755+
priceRuleToDelete,
756+
sharedContext
757+
)
758+
eventBuilders.deletedPriceRule({
759+
data: priceRuleToDelete.map((id) => ({ id })),
760+
sharedContext,
761+
})
762+
}
763+
764+
;(priceToUpdate as InferEntityType<typeof Price>).rules_count = 0
765+
} else {
766+
// @ts-expect-error - we want to delete the rules_count property in any case even if provided by mistake
767+
delete (priceToUpdate as InferEntityType<typeof Price>).rules_count
768+
}
769+
// We don't want to persist the price_rules in the database through the price service as it would not work
770+
delete priceToUpdate.price_rules
668771
}
669-
})
670772

671-
const { entities: priceSets, performedActions: priceSetPerformedActions } =
672-
await this.priceSetService_.upsertWithReplace(
673-
priceSetsToUpsert,
674-
{ relations: ["prices"] },
773+
updatedPrices = await this.priceService_.update(
774+
pricesToUpdate,
675775
sharedContext
676776
)
777+
}
677778

678-
composeAllEvents({
679-
eventBuilders,
680-
performedActions: priceSetPerformedActions,
681-
sharedContext,
779+
if (pricesToDelete.length > 0) {
780+
await this.priceService_.delete(pricesToDelete, sharedContext)
781+
}
782+
783+
if (createdPrices.length > 0) {
784+
eventBuilders.createdPrice({
785+
data: createdPrices.map((p) => ({ id: p.id })),
786+
sharedContext,
787+
})
788+
}
789+
790+
if (updatedPrices.length > 0) {
791+
eventBuilders.updatedPrice({
792+
data: updatedPrices.map((p) => ({ id: p.id })),
793+
sharedContext,
794+
})
795+
}
796+
797+
if (pricesToDelete.length > 0) {
798+
eventBuilders.deletedPrice({
799+
data: pricesToDelete.map((id) => ({ id })),
800+
sharedContext,
801+
})
802+
}
803+
804+
const priceSets = await this.priceSetService_.list(
805+
{ id: normalizedData.map(({ id }) => id) },
806+
{
807+
relations: ["prices", "prices.price_rules"],
808+
},
809+
sharedContext
810+
)
811+
812+
const upsertedPricesMap = new Map<string, InferEntityType<typeof Price>[]>()
813+
814+
const upsertedPrices = [...createdPrices, ...updatedPrices]
815+
upsertedPrices.forEach((price) => {
816+
upsertedPricesMap.set(price.price_set_id, [
817+
...(upsertedPricesMap.get(price.price_set_id) || []),
818+
price,
819+
])
682820
})
683821

684-
return priceSets.map((ps) => {
685-
if (ps.prices) {
686-
ps.prices = (ps.prices as any).filter((p) => !p.price_list_id)
687-
}
822+
// 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
823+
824+
priceSets.forEach((ps) => {
825+
ps.prices = upsertedPricesMap.get(ps.id) || []
826+
})
688827

689-
return ps
828+
eventBuilders.updatedPriceSet({
829+
data: priceSets.map((ps) => ({ id: ps.id })),
830+
sharedContext,
690831
})
832+
833+
return priceSets
691834
}
692835

693836
private async normalizeUpdateData(data: ServiceTypes.UpdatePriceSetInput[]) {

0 commit comments

Comments
 (0)