chore(): improve cart operations + Mikro orm 6.4.16 (#13712)

* chore(): Mikro orm 6.4.16

* Create small-ghosts-draw.md

* update config

* update config

* fix delete

* update config

* update workflows

* order improvements

* test pricing quuery

* test pricing quuery

* configurable connection options

* configurable connection options

* configurable connection options

* Update packages/modules/pricing/src/models/price.ts

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2025-10-10 08:58:19 +02:00
committed by GitHub
parent 76bf364440
commit c54c5ed6de
23 changed files with 481 additions and 288 deletions

View File

@@ -136,6 +136,15 @@
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_price_list_deleted_at\" ON \"price_list\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_price_list_id_status_starts_at_ends_at",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_price_list_id_status_starts_at_ends_at\" ON \"price_list\" (id, status, starts_at, ends_at) WHERE deleted_at IS NULL AND status = 'active'"
},
{
"keyName": "price_list_pkey",
"columnNames": [
@@ -252,6 +261,15 @@
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_price_list_rule_attribute\" ON \"price_list_rule\" (attribute) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_price_list_rule_value",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_price_list_rule_value\" ON \"price_list_rule\" USING gin (value) WHERE deleted_at IS NULL"
},
{
"keyName": "price_list_rule_pkey",
"columnNames": [
@@ -625,6 +643,15 @@
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_price_currency_code\" ON \"price\" (currency_code) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_price_currency_code_price_set_id_min_quantity",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_price_currency_code_price_set_id_min_quantity\" ON \"price\" (currency_code, price_set_id, min_quantity) WHERE deleted_at IS NULL"
},
{
"keyName": "price_pkey",
"columnNames": [
@@ -822,6 +849,15 @@
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_price_rule_operator_value\" ON \"price_rule\" (operator, value) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_price_rule_attribute_value_price_id",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_price_rule_attribute_value_price_id\" ON \"price_rule\" (attribute, value, price_id) WHERE deleted_at IS NULL"
},
{
"keyName": "price_rule_pkey",
"columnNames": [

View File

@@ -0,0 +1,28 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20251009110625 extends Migration {
override async up(): Promise<void> {
this.addSql(
`CREATE INDEX IF NOT EXISTS "IDX_price_list_id_status_starts_at_ends_at" ON "price_list" (id, status, starts_at, ends_at) WHERE deleted_at IS NULL AND status = 'active';`
)
this.addSql(
`CREATE INDEX IF NOT EXISTS "IDX_price_list_rule_value" ON "price_list_rule" USING gin (value) WHERE deleted_at IS NULL;`
)
this.addSql(
`CREATE INDEX IF NOT EXISTS "IDX_price_rule_attribute_value_price_id" ON "price_rule" (attribute, value, price_id) WHERE deleted_at IS NULL;`
)
}
override async down(): Promise<void> {
this.addSql(
`drop index if exists "IDX_price_list_id_status_starts_at_ends_at";`
)
this.addSql(`drop index if exists "IDX_price_list_rule_value";`)
this.addSql(
`drop index if exists "IDX_price_rule_attribute_value_price_id";`
)
}
}

View File

@@ -19,6 +19,11 @@ const PriceListRule = model
on: ["attribute"],
where: "deleted_at IS NULL",
},
{
on: ["value"],
where: "deleted_at IS NULL",
type: "gin",
},
])
export default PriceListRule

View File

@@ -26,5 +26,11 @@ const PriceList = model
.cascades({
delete: ["price_list_rules", "prices"],
})
.indexes([
{
on: ["id", "status", "starts_at", "ends_at"],
where: "deleted_at IS NULL AND status = 'active'",
},
])
export default PriceList

View File

@@ -30,6 +30,10 @@ const PriceRule = model
on: ["operator", "value"],
where: "deleted_at IS NULL",
},
{
on: ["attribute", "value", "price_id"],
where: "deleted_at IS NULL",
},
])
export default PriceRule

View File

@@ -6,6 +6,10 @@ import {
PriceListStatus,
} from "@medusajs/framework/utils"
import {
Knex,
SqlEntityManager,
} from "@medusajs/framework/mikro-orm/postgresql"
import {
CalculatedPriceSetDTO,
Context,
@@ -13,10 +17,6 @@ import {
PricingFilters,
PricingRepositoryService,
} from "@medusajs/framework/types"
import {
Knex,
SqlEntityManager,
} from "@medusajs/framework/mikro-orm/postgresql"
export class PricingRepository
extends MikroOrmBase
@@ -172,101 +172,108 @@ export class PricingRepository
})
if (hasComplexContext) {
const priceRuleConditions = knex.raw(
`
(
price.rules_count = 0 OR
(
/* Count all matching rules and compare to total rule count */
SELECT COUNT(*)
FROM price_rule pr
WHERE pr.price_id = price.id
AND pr.deleted_at IS NULL
AND (
${flattenedContext
.map(([key, value]) => {
if (typeof value === "number") {
return `
(pr.attribute = ? AND (
(pr.operator = 'eq' AND pr.value = ?) OR
(pr.operator = 'gt' AND ? > pr.value::numeric) OR
(pr.operator = 'gte' AND ? >= pr.value::numeric) OR
(pr.operator = 'lt' AND ? < pr.value::numeric) OR
(pr.operator = 'lte' AND ? <= pr.value::numeric)
))
`
} else {
const normalizeValue = Array.isArray(value) ? value : [value]
const placeholders = normalizeValue.map(() => "?").join(",")
return `(pr.attribute = ? AND pr.value IN (${placeholders}))`
}
})
.join(" OR ")})
) = (
/* Get total rule count */
SELECT COUNT(*)
FROM price_rule pr
WHERE pr.price_id = price.id
AND pr.deleted_at IS NULL
)
)
`,
flattenedContext.flatMap(([key, value]) => {
// Build match conditions for LATERAL join
const priceRuleMatchConditions = flattenedContext
.map(([_key, value]) => {
if (typeof value === "number") {
return [key, value.toString(), value, value, value, value]
return `
(pr.attribute = ? AND (
(pr.operator = 'eq' AND pr.value = ?) OR
(pr.operator = 'gt' AND ? > pr.value::numeric) OR
(pr.operator = 'gte' AND ? >= pr.value::numeric) OR
(pr.operator = 'lt' AND ? < pr.value::numeric) OR
(pr.operator = 'lte' AND ? <= pr.value::numeric)
))
`
} else {
const normalizeValue = Array.isArray(value) ? value : [value]
return [key, ...normalizeValue]
const placeholders = normalizeValue.map(() => "?").join(",")
return `(pr.attribute = ? AND pr.value IN (${placeholders}))`
}
})
)
.join(" OR ")
const priceListRuleConditions = knex.raw(
`
(
pl.rules_count = 0 OR
(
/* Count all matching rules and compare to total rule count */
SELECT COUNT(*)
FROM price_list_rule plr
WHERE plr.price_list_id = pl.id
AND plr.deleted_at IS NULL
AND (
${flattenedContext
.map(([key, value]) => {
if (Array.isArray(value)) {
return value
.map((v) => `(plr.attribute = ? AND plr.value @> ?)`)
.join(" OR ")
}
return `(plr.attribute = ? AND plr.value @> ?)`
})
.join(" OR ")}
)
) = (
/* Get total rule count */
SELECT COUNT(*)
FROM price_list_rule plr
WHERE plr.price_list_id = pl.id
AND plr.deleted_at IS NULL
)
)
`,
flattenedContext.flatMap(([key, value]) => {
const priceRuleMatchParams = flattenedContext.flatMap(([key, value]) => {
if (typeof value === "number") {
return [key, value.toString(), value, value, value, value]
} else {
const normalizeValue = Array.isArray(value) ? value : [value]
return [key, ...normalizeValue]
}
})
const priceListRuleMatchConditions = flattenedContext
.map(([_key, value]) => {
if (Array.isArray(value)) {
return value
.map((_v) => `(plr.attribute = ? AND plr.value @> ?)`)
.join(" OR ")
}
return `(plr.attribute = ? AND plr.value @> ?)`
})
.join(" OR ")
const priceListRuleMatchParams = flattenedContext.flatMap(
([key, value]) => {
const valueAsArray = Array.isArray(value) ? value : [value]
return valueAsArray.flatMap((v) => [key, JSON.stringify(v)])
})
}
)
// Use LATERAL joins to compute matched and total counts in one go
query
.leftJoin(
knex.raw(
`LATERAL (
SELECT
COUNT(*) FILTER (WHERE ${priceRuleMatchConditions}) as matched_count,
COUNT(*) as total_count
FROM price_rule pr
WHERE pr.price_id = price.id
AND pr.deleted_at IS NULL
) pr_stats`,
priceRuleMatchParams
),
knex.raw("true")
)
.leftJoin(
knex.raw(
`LATERAL (
SELECT
COUNT(*) FILTER (WHERE ${priceListRuleMatchConditions}) as matched_count,
COUNT(*) as total_count
FROM price_list_rule plr
WHERE plr.price_list_id = pl.id
AND plr.deleted_at IS NULL
) plr_stats`,
priceListRuleMatchParams
),
knex.raw("true")
)
query.where((qb) => {
qb.whereNull("price.price_list_id")
.andWhereRaw(priceRuleConditions)
.orWhere((qb2) => {
qb2
.whereNotNull("price.price_list_id")
.whereRaw(priceListRuleConditions)
.andWhereRaw(priceRuleConditions)
qb.where((qb2) => {
// No price list: price rules must match or be zero
qb2.whereNull("price.price_list_id").andWhere((qb3) => {
qb3
.where("price.rules_count", 0)
.orWhereRaw("pr_stats.matched_count = price.rules_count")
})
}).orWhere((qb2) => {
// Has price list: both price rules and price list rules must match
qb2
.whereNotNull("price.price_list_id")
.andWhere((qb3) => {
qb3
.where("price.rules_count", 0)
.orWhereRaw("pr_stats.matched_count = price.rules_count")
})
.andWhere((qb3) => {
qb3
.where("pl.rules_count", 0)
.orWhereRaw("plr_stats.matched_count = pl.rules_count")
})
})
})
} else {
query.where(function (this: Knex.QueryBuilder) {