chore(pricing): Pricing retrieval improvements (#12128)

**What**
I have removed the check for the context key where it was fetching all attributes available and then stripping out the one that does not exists.. On big dataset these would remove multiple hundreds of ms of query execution
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2025-04-10 09:39:21 +00:00
1 parent 6ae1e7b708
commit 07252691c5
8 files changed
+671 -127

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/pricing": patch
---
chore(pricing): Pricing retrieval improvements
@@ -18,7 +18,14 @@ describe("flattenObjectToKeyValuePairs", function () {
}, },
{ {
product_id: "product-2", product_id: "product-2",
product: { id: "product-2" }, product: {
id: "product-2",
something: [
{
id: "test",
},
],
},
}, },
], ],
} }
@@ -31,6 +38,217 @@ describe("flattenObjectToKeyValuePairs", function () {
"customer.groups.name": ["test", "test 2"], "customer.groups.name": ["test", "test 2"],
"items.product_id": ["product-1", "product-2"], "items.product_id": ["product-1", "product-2"],
"items.product.id": ["product-1", "product-2"], "items.product.id": ["product-1", "product-2"],
"items.product.something.id": ["test"],
})
})
it("should handle complex nested objects", function () {
const cart = {
id: "cart_01JRDH08QD8CZ0KJDVE410KM1J",
currency_code: "usd",
email: "tony@stark-industries.com",
region_id: "reg_01JRDH08ENY3276P6133BVXGWJ",
created_at: "2025-04-09T14:59:24.526Z",
updated_at: "2025-04-09T14:59:24.526Z",
completed_at: null,
total: 1500,
subtotal: 1428.5714285714287,
tax_total: 71.42857142857143,
discount_total: 0,
discount_subtotal: 0,
discount_tax_total: 0,
original_total: 1500,
original_tax_total: 71.42857142857143,
item_total: 1500,
item_subtotal: 1428.5714285714287,
item_tax_total: 71.42857142857143,
original_item_total: 1500,
original_item_subtotal: 1428.5714285714287,
original_item_tax_total: 71.42857142857143,
shipping_total: 0,
shipping_subtotal: 0,
shipping_tax_total: 0,
original_shipping_tax_total: 0,
original_shipping_subtotal: 0,
original_shipping_total: 0,
credit_line_subtotal: 0,
credit_line_tax_total: 0,
credit_line_total: 0,
metadata: null,
sales_channel_id: "sc_01JRDH08KWX1AR5SB0A3THWWQQ",
shipping_address_id: "caaddr_01JRDH08QDXHV9SJXKHT04TXK0",
customer_id: "cus_01JRDH08ATYB5AMFEZDTWCQWNK",
items: [
{
id: "cali_01JRDH08QDQH3CB1DE4S79HREC",
thumbnail: null,
variant_id: "variant_01JRDH08GJCZQB4GZCDDTYMD1V",
product_id: "prod_01JRDH08FPZ6QBZQ096B310RM7",
product_type_id: null,
product_title: "Medusa T-Shirt",
product_description: null,
product_subtitle: null,
product_type: null,
product_collection: null,
product_handle: "t-shirt",
variant_sku: "SHIRT-S-BLACK",
variant_barcode: null,
variant_title: "S / Black",
requires_shipping: true,
metadata: {},
created_at: "2025-04-09T14:59:24.526Z",
updated_at: "2025-04-09T14:59:24.526Z",
title: "S / Black",
quantity: 1,
unit_price: 1500,
compare_at_unit_price: null,
is_tax_inclusive: true,
tax_lines: [
{
id: "calitxl_01JRDH08RJEQ4WXXDTJYWV7B4M",
description: "CA Default Rate",
code: "CADEFAULT",
rate: 5,
provider_id: "system",
},
],
adjustments: [],
product: {
id: "prod_01JRDH08FPZ6QBZQ096B310RM7",
collection_id: null,
type_id: null,
categories: [],
tags: [],
},
},
],
shipping_methods: [],
shipping_address: {
id: "caaddr_01JRDH08QDXHV9SJXKHT04TXK0",
first_name: null,
last_name: null,
company: null,
address_1: "test address 1",
address_2: "test address 2",
city: "SF",
postal_code: "94016",
country_code: "US",
province: "CA",
phone: null,
},
billing_address: null,
credit_lines: [],
customer: {
id: "cus_01JRDH08ATYB5AMFEZDTWCQWNK",
email: "tony@stark-industries.com",
groups: [],
},
region: {
id: "reg_01JRDH08ENY3276P6133BVXGWJ",
name: "US",
currency_code: "usd",
automatic_taxes: true,
countries: [
{
iso_2: "us",
iso_3: "usa",
num_code: "840",
name: "UNITED STATES",
display_name: "United States",
region_id: "reg_01JRDH08ENY3276P6133BVXGWJ",
metadata: null,
created_at: "2025-04-09T14:59:20.275Z",
updated_at: "2025-04-09T14:59:24.250Z",
deleted_at: null,
},
],
},
promotions: [],
}
const keyValueParis = flattenObjectToKeyValuePairs(cart)
console.log(JSON.stringify(keyValueParis, null, 2))
expect(keyValueParis).toEqual({
id: "cart_01JRDH08QD8CZ0KJDVE410KM1J",
currency_code: "usd",
email: "tony@stark-industries.com",
region_id: "reg_01JRDH08ENY3276P6133BVXGWJ",
created_at: "2025-04-09T14:59:24.526Z",
updated_at: "2025-04-09T14:59:24.526Z",
total: 1500,
subtotal: 1428.5714285714287,
tax_total: 71.42857142857143,
discount_total: 0,
discount_subtotal: 0,
discount_tax_total: 0,
original_total: 1500,
original_tax_total: 71.42857142857143,
item_total: 1500,
"items.adjustments": [],
item_subtotal: 1428.5714285714287,
item_tax_total: 71.42857142857143,
original_item_total: 1500,
original_item_subtotal: 1428.5714285714287,
original_item_tax_total: 71.42857142857143,
shipping_total: 0,
shipping_subtotal: 0,
shipping_tax_total: 0,
original_shipping_tax_total: 0,
original_shipping_subtotal: 0,
original_shipping_total: 0,
credit_line_subtotal: 0,
credit_line_tax_total: 0,
credit_line_total: 0,
sales_channel_id: "sc_01JRDH08KWX1AR5SB0A3THWWQQ",
shipping_address_id: "caaddr_01JRDH08QDXHV9SJXKHT04TXK0",
customer_id: "cus_01JRDH08ATYB5AMFEZDTWCQWNK",
"items.id": ["cali_01JRDH08QDQH3CB1DE4S79HREC"],
"items.variant_id": ["variant_01JRDH08GJCZQB4GZCDDTYMD1V"],
"items.product_id": ["prod_01JRDH08FPZ6QBZQ096B310RM7"],
"items.product_title": ["Medusa T-Shirt"],
"items.product_handle": ["t-shirt"],
"items.variant_sku": ["SHIRT-S-BLACK"],
"items.variant_title": ["S / Black"],
"items.requires_shipping": [true],
"items.created_at": ["2025-04-09T14:59:24.526Z"],
"items.updated_at": ["2025-04-09T14:59:24.526Z"],
"items.title": ["S / Black"],
"items.quantity": [1],
"items.unit_price": [1500],
"items.is_tax_inclusive": [true],
"items.tax_lines.id": ["calitxl_01JRDH08RJEQ4WXXDTJYWV7B4M"],
"items.tax_lines.description": ["CA Default Rate"],
"items.tax_lines.code": ["CADEFAULT"],
"items.tax_lines.rate": [5],
"items.tax_lines.provider_id": ["system"],
"items.product.id": ["prod_01JRDH08FPZ6QBZQ096B310RM7"],
"items.product.categories": [],
"items.product.tags": [],
shipping_methods: [],
"shipping_address.id": "caaddr_01JRDH08QDXHV9SJXKHT04TXK0",
"shipping_address.address_1": "test address 1",
"shipping_address.address_2": "test address 2",
"shipping_address.city": "SF",
"shipping_address.postal_code": "94016",
"shipping_address.country_code": "US",
"shipping_address.province": "CA",
credit_lines: [],
"customer.id": "cus_01JRDH08ATYB5AMFEZDTWCQWNK",
"customer.email": "tony@stark-industries.com",
"customer.groups": [],
"region.id": "reg_01JRDH08ENY3276P6133BVXGWJ",
"region.name": "US",
"region.currency_code": "usd",
"region.automatic_taxes": true,
"region.countries.iso_2": ["us"],
"region.countries.iso_3": ["usa"],
"region.countries.num_code": ["840"],
"region.countries.name": ["UNITED STATES"],
"region.countries.display_name": ["United States"],
"region.countries.region_id": ["reg_01JRDH08ENY3276P6133BVXGWJ"],
"region.countries.created_at": ["2025-04-09T14:59:20.275Z"],
"region.countries.updated_at": ["2025-04-09T14:59:24.250Z"],
promotions: [],
}) })
}) })
}) })
@@ -27,116 +27,284 @@ type NestedObject = {
"product.categories.id": ["test-category"], "product.categories.id": ["test-category"],
"product.categories.name": ["Test Category"] "product.categories.name": ["Test Category"]
} }
Null and undefined values are excluded from the result.
*/ */
export function flattenObjectToKeyValuePairs(obj: NestedObject): NestedObject { export function flattenObjectToKeyValuePairs(obj: NestedObject): NestedObject {
const result: NestedObject = {} const result: NestedObject = {}
// Find all paths that contain arrays of objects function shouldPreserveArray(value: any[], path: string[]): boolean {
function findArrayPaths( if (!Array.isArray(value) || value.length === 0) {
obj: unknown, return false
currentPath: string[] = []
): string[][] {
const paths: string[][] = []
if (!obj || typeof obj !== "object") {
return paths
} }
// If it's an array of objects, add this path if (value.some((item) => isObject(item) && !Array.isArray(item))) {
if (Array.isArray(obj) && obj.length > 0 && isObject(obj[0])) { return true
paths.push(currentPath)
} }
// Check all properties if (value.some((item) => Array.isArray(item))) {
if (isObject(obj)) { return true
Object.entries(obj as Record<string, unknown>).forEach(([key, value]) => {
const newPath = [...currentPath, key]
paths.push(...findArrayPaths(value, newPath))
})
} }
return paths if (path.length > 1) {
} return true
}
// Extract array values at a specific path if (value.length > 1) {
function getArrayValues(obj: unknown, path: string[]): unknown[] { const firstType = typeof value[0]
const arrayObj = path.reduce((acc: unknown, key: string) => { const allSameType = value.every(
if (acc && isObject(acc)) { (item) =>
return (acc as Record<string, unknown>)[key] typeof item === firstType && !isObject(item) && !Array.isArray(item)
)
if (allSameType) {
return true
} }
return undefined }
}, obj)
if (!Array.isArray(arrayObj)) return [] return false
return arrayObj
} }
// Process non-array paths /**
function processRegularPaths(obj: unknown, prefix = ""): void { * Normalize array values - unwrap single-item arrays and handle empty arrays
if (!obj || typeof obj !== "object") { */
result[prefix] = obj function normalizeArrayValue(value: any, path: string[]): any {
if (!Array.isArray(value)) {
return value
}
if (
value.length === 1 &&
Array.isArray(value[0]) &&
value[0].length === 0
) {
return []
}
if (shouldPreserveArray(value, path)) {
return value
}
if (value.length === 1 && !isObject(value[0]) && !Array.isArray(value[0])) {
return value[0]
}
return value
}
/**
* Recursively process an object/array and flatten it
*/
function processPath(value: any, currentPath: string[] = []): void {
// Handle null, undefined, or primitive values
if (!value || typeof value !== "object") {
if (value !== null && value !== undefined && currentPath.length > 0) {
result[currentPath.join(".")] = value
}
return return
} }
if (Array.isArray(obj)) return if (Array.isArray(value)) {
if (value.length === 0) {
if (currentPath.length > 0) {
result[currentPath.join(".")] = []
}
return
}
Object.entries(obj as Record<string, unknown>).forEach(([key, value]) => { if (value.some((item) => isObject(item) && !Array.isArray(item))) {
const newPrefix = prefix ? `${prefix}.${key}` : key extractPropertiesFromArray(value, currentPath)
if (value && isObject(value) && !Array.isArray(value)) { } else if (value.some((item) => Array.isArray(item))) {
processRegularPaths(value, newPrefix) const allValues: any[] = []
} else if (!Array.isArray(value)) { const flattenedObjects: Record<string, any>[] = []
result[newPrefix] = value
const flattenArray = (arr: any[], collector: any[] = []): void => {
arr.forEach((item) => {
if (Array.isArray(item)) {
flattenArray(item, collector)
} else if (isObject(item)) {
collector.push(item)
} else if (item !== null && item !== undefined) {
allValues.push(item)
}
})
}
flattenArray(value, flattenedObjects)
if (flattenedObjects.length > 0) {
extractPropertiesFromArray(flattenedObjects, currentPath)
} else if (allValues.length > 0) {
result[currentPath.join(".")] = normalizeArrayValue(
allValues,
currentPath
)
}
} else {
const cleanedValues = value.filter((v) => v !== null && v !== undefined)
if (cleanedValues.length > 0) {
result[currentPath.join(".")] = normalizeArrayValue(
cleanedValues,
currentPath
)
}
}
return
}
Object.entries(value).forEach(([key, propValue]) => {
const newPath = [...currentPath, key]
if (propValue === null || propValue === undefined) {
return
} else if (Array.isArray(propValue)) {
if (propValue.length === 0) {
result[newPath.join(".")] = []
} else {
processPath(propValue, newPath)
}
} else if (isObject(propValue)) {
processPath(propValue, newPath)
} else {
result[newPath.join(".")] = propValue
} }
}) })
} }
// Process the object /**
processRegularPaths(obj) * Extract all properties from an array of objects and store them
*/
function extractPropertiesFromArray(
array: any[],
basePath: string[] = []
): void {
if (!array.length) return
// Find and process array paths // Collect all unique keys from all objects in the array
const arrayPaths = findArrayPaths(obj) const allKeys = new Set<string>()
arrayPaths.forEach((path) => { array.forEach((item) => {
const pathStr = path.join(".") if (isObject(item) && !Array.isArray(item)) {
const arrayObjects = getArrayValues(obj, path) Object.keys(item).forEach((key) => allKeys.add(key))
}
})
if (Array.isArray(arrayObjects) && arrayObjects.length > 0) { allKeys.forEach((key) => {
// Get all possible keys from the array objects const valuePath = [...basePath, key]
const keys = new Set<string>() const values: any[] = []
arrayObjects.forEach((item) => {
if (item && isObject(item)) {
Object.keys(item as object).forEach((k) => keys.add(k))
}
})
// Process each key array.forEach((item) => {
keys.forEach((key) => { if (isObject(item) && !Array.isArray(item) && key in item) {
const values = arrayObjects const itemValue = item[key]
.map((item) => { if (itemValue !== null && itemValue !== undefined) {
if (item && isObject(item)) { values.push(itemValue)
return (item as Record<string, unknown>)[key]
}
return undefined
})
.filter((v) => v !== undefined)
if (values.length > 0) {
const newPath = `${pathStr}.${key}`
if (values.every((v) => isObject(v) && !Array.isArray(v))) {
// If these are all objects, recursively process them
const subObj = { [key]: values }
const subResult = flattenObjectToKeyValuePairs(subObj)
Object.entries(subResult).forEach(([k, v]) => {
const finalPath = `${pathStr}.${k}`
result[finalPath] = v
})
} else {
result[newPath] = values
} }
} }
}) })
}
}) if (values.length === 0) return
if (values.every((v) => isObject(v) && !Array.isArray(v))) {
extractNestedObjectProperties(values, valuePath)
} else if (values.some((v) => Array.isArray(v))) {
if (values.every((v) => Array.isArray(v) && v.length === 0)) {
result[valuePath.join(".")] = []
} else {
const flattenedArray: any[] = []
for (const arrayValue of values) {
if (Array.isArray(arrayValue)) {
if (arrayValue.some((v) => isObject(v) && !Array.isArray(v))) {
extractPropertiesFromArray(arrayValue, valuePath)
} else {
flattenedArray.push(...arrayValue)
}
} else {
flattenedArray.push(arrayValue)
}
}
if (
flattenedArray.length > 0 &&
!flattenedArray.some((v) => isObject(v) && !Array.isArray(v))
) {
result[valuePath.join(".")] = normalizeArrayValue(
flattenedArray,
valuePath
)
}
}
} else {
result[valuePath.join(".")] = normalizeArrayValue(values, valuePath)
}
})
}
/**
* Extract properties from nested objects and add them to the result
*/
function extractNestedObjectProperties(
objects: any[],
basePath: string[] = []
): void {
if (!objects.length) return
// Collect all unique keys from all objects
const allNestedKeys = new Set<string>()
objects.forEach((obj) => {
if (isObject(obj) && !Array.isArray(obj)) {
Object.keys(obj).forEach((key) => allNestedKeys.add(key))
}
})
allNestedKeys.forEach((nestedKey) => {
const nestedPath = [...basePath, nestedKey]
const nestedValues: any[] = []
objects.forEach((obj) => {
if (isObject(obj) && !Array.isArray(obj) && nestedKey in obj) {
const nestedValue = obj[nestedKey]
if (nestedValue !== null && nestedValue !== undefined) {
nestedValues.push(nestedValue)
}
}
})
if (nestedValues.length === 0) return
if (nestedValues.every((v) => isObject(v) && !Array.isArray(v))) {
extractNestedObjectProperties(nestedValues, nestedPath)
} else if (nestedValues.some((v) => Array.isArray(v))) {
if (nestedValues.every((v) => Array.isArray(v) && v.length === 0)) {
result[nestedPath.join(".")] = []
} else {
const allArrayItems: any[] = []
for (const arrayValue of nestedValues) {
if (Array.isArray(arrayValue)) {
allArrayItems.push(...arrayValue)
} else {
allArrayItems.push(arrayValue)
}
}
if (
allArrayItems.some((item) => isObject(item) && !Array.isArray(item))
) {
extractPropertiesFromArray(allArrayItems, nestedPath)
} else {
result[nestedPath.join(".")] = normalizeArrayValue(
allArrayItems,
nestedPath
)
}
}
} else {
result[nestedPath.join(".")] = normalizeArrayValue(
nestedValues,
nestedPath
)
}
})
}
processPath(obj)
return result return result
} }
@@ -286,6 +286,171 @@ moduleIntegrationTestRunner<IPricingModuleService>({
}) })
}) })
it("should successfully calculate prices with complex context", async () => {
const context = {
id: "cart_01JRDH08QD8CZ0KJDVE410KM1J",
currency_code: "PLN",
email: "tony@stark-industries.com",
region_id: "reg_01JRDH08ENY3276P6133BVXGWJ",
created_at: "2025-04-09T14:59:24.526Z",
updated_at: "2025-04-09T14:59:24.526Z",
completed_at: null,
total: 1500,
subtotal: 1428.5714285714287,
tax_total: 71.42857142857143,
discount_total: 0,
discount_subtotal: 0,
discount_tax_total: 0,
original_total: 1500,
original_tax_total: 71.42857142857143,
item_total: 1500,
item_subtotal: 1428.5714285714287,
item_tax_total: 71.42857142857143,
original_item_total: 1500,
original_item_subtotal: 1428.5714285714287,
original_item_tax_total: 71.42857142857143,
shipping_total: 0,
shipping_subtotal: 0,
shipping_tax_total: 0,
original_shipping_tax_total: 0,
original_shipping_subtotal: 0,
original_shipping_total: 0,
credit_line_subtotal: 0,
credit_line_tax_total: 0,
credit_line_total: 0,
metadata: null,
sales_channel_id: "sc_01JRDH08KWX1AR5SB0A3THWWQQ",
shipping_address_id: "caaddr_01JRDH08QDXHV9SJXKHT04TXK0",
customer_id: "cus_01JRDH08ATYB5AMFEZDTWCQWNK",
items: [
{
id: "cali_01JRDH08QDQH3CB1DE4S79HREC",
thumbnail: null,
variant_id: "variant_01JRDH08GJCZQB4GZCDDTYMD1V",
product_id: "prod_01JRDH08FPZ6QBZQ096B310RM7",
product_type_id: null,
product_title: "Medusa T-Shirt",
product_description: null,
product_subtitle: null,
product_type: null,
product_collection: null,
product_handle: "t-shirt",
variant_sku: "SHIRT-S-BLACK",
variant_barcode: null,
variant_title: "S / Black",
requires_shipping: true,
metadata: {},
created_at: "2025-04-09T14:59:24.526Z",
updated_at: "2025-04-09T14:59:24.526Z",
title: "S / Black",
quantity: 1,
unit_price: 1500,
compare_at_unit_price: null,
is_tax_inclusive: true,
tax_lines: [
{
id: "calitxl_01JRDH08RJEQ4WXXDTJYWV7B4M",
description: "CA Default Rate",
code: "CADEFAULT",
rate: 5,
provider_id: "system",
},
],
adjustments: [],
product: {
id: "prod_01JRDH08FPZ6QBZQ096B310RM7",
collection_id: null,
type_id: null,
categories: [],
tags: [],
},
},
],
shipping_methods: [],
shipping_address: {
id: "caaddr_01JRDH08QDXHV9SJXKHT04TXK0",
first_name: null,
last_name: null,
company: null,
address_1: "test address 1",
address_2: "test address 2",
city: "SF",
postal_code: "94016",
country_code: "US",
province: "CA",
phone: null,
},
billing_address: null,
credit_lines: [],
customer: {
id: "cus_01JRDH08ATYB5AMFEZDTWCQWNK",
email: "tony@stark-industries.com",
groups: [],
},
region: {
id: "reg_01JRDH08ENY3276P6133BVXGWJ",
name: "US",
currency_code: "usd",
automatic_taxes: true,
countries: [
{
iso_2: "us",
iso_3: "usa",
num_code: "840",
name: "UNITED STATES",
display_name: "United States",
region_id: "reg_01JRDH08ENY3276P6133BVXGWJ",
metadata: null,
created_at: "2025-04-09T14:59:20.275Z",
updated_at: "2025-04-09T14:59:24.250Z",
deleted_at: null,
},
],
},
promotions: [],
}
const calculatedPrice = await service.calculatePrices(
{ id: ["price-set-EUR", "price-set-PLN"] },
{ context: context as any }
)
expect(calculatedPrice).toEqual([
{
id: "price-set-PLN",
is_calculated_price_price_list: false,
is_calculated_price_tax_inclusive: false,
calculated_amount: 1000,
raw_calculated_amount: {
value: "1000",
precision: 20,
},
is_original_price_price_list: false,
is_original_price_tax_inclusive: false,
original_amount: 1000,
raw_original_amount: {
value: "1000",
precision: 20,
},
currency_code: "PLN",
calculated_price: {
id: "price-PLN",
price_list_id: null,
price_list_type: null,
min_quantity: 1,
max_quantity: 10,
},
original_price: {
id: "price-PLN",
price_list_id: null,
price_list_type: null,
min_quantity: 1,
max_quantity: 10,
},
},
])
})
it("should throw an error when currency code is not set", async () => { it("should throw an error when currency code is not set", async () => {
let result = service.calculatePrices( let result = service.calculatePrices(
{ id: ["price-set-EUR", "price-set-PLN"] }, { id: ["price-set-EUR", "price-set-PLN"] },
@@ -795,6 +795,15 @@
"unique": false, "unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_price_rule_price_id_attribute_operator_unique\" ON \"price_rule\" (price_id, attribute, operator) WHERE deleted_at IS NULL" "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_price_rule_price_id_attribute_operator_unique\" ON \"price_rule\" (price_id, attribute, operator) WHERE deleted_at IS NULL"
}, },
{
"keyName": "IDX_price_rule_attribute",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_price_rule_attribute\" ON \"price_rule\" (attribute) WHERE deleted_at IS NULL"
},
{ {
"keyName": "IDX_price_rule_attribute_value", "keyName": "IDX_price_rule_attribute_value",
"columnNames": [], "columnNames": [],
@@ -0,0 +1,13 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20250409122219 extends Migration {
override async up(): Promise<void> {
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_price_rule_attribute" ON "price_rule" (attribute) WHERE deleted_at IS NULL;`);
}
override async down(): Promise<void> {
this.addSql(`drop index if exists "IDX_price_rule_attribute";`);
}
}
@@ -18,6 +18,10 @@ const PriceRule = model
where: "deleted_at IS NULL", where: "deleted_at IS NULL",
unique: true, unique: true,
}, },
{
on: ["attribute"],
where: "deleted_at IS NULL",
},
{ {
on: ["attribute", "value"], on: ["attribute", "value"],
where: "deleted_at IS NULL", where: "deleted_at IS NULL",
@@ -4,7 +4,6 @@ import {
MedusaError, MedusaError,
MikroOrmBase, MikroOrmBase,
PriceListStatus, PriceListStatus,
promiseAll,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { import {
@@ -16,8 +15,6 @@ import {
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { Knex, SqlEntityManager } from "@mikro-orm/postgresql" import { Knex, SqlEntityManager } from "@mikro-orm/postgresql"
// Simple cache implementation
export class PricingRepository export class PricingRepository
extends MikroOrmBase extends MikroOrmBase
implements PricingRepositoryService implements PricingRepositoryService
@@ -53,31 +50,20 @@ export class PricingRepository
} }
// Generate flatten key-value pairs for rule matching // Generate flatten key-value pairs for rule matching
const [ruleAttributes, priceListRuleAttributes] =
await this.getAttributesFromRuleTables(knex)
const allowedRuleAttributes = [
...ruleAttributes,
...priceListRuleAttributes,
]
const flattenedKeyValuePairs = flattenObjectToKeyValuePairs(context) const flattenedKeyValuePairs = flattenObjectToKeyValuePairs(context)
// First filter by value presence
const flattenedContext = Object.entries(flattenedKeyValuePairs).filter( const flattenedContext = Object.entries(flattenedKeyValuePairs).filter(
([key, 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
return ( return isValuePresent || isArrayPresent
allowedRuleAttributes.includes(key) &&
(isValuePresent || isArrayPresent)
)
} }
) )
const hasComplexContext = flattenedContext.length > 0 const hasComplexContext = flattenedContext.length > 0
// Base query with efficient index lookups
const query = knex const query = knex
.select({ .select({
id: "price.id", id: "price.id",
@@ -97,7 +83,6 @@ export class PricingRepository
.andWhere("price.currency_code", currencyCode) .andWhere("price.currency_code", currencyCode)
.whereNull("price.deleted_at") .whereNull("price.deleted_at")
// Apply quantity filter
if (quantity !== undefined) { if (quantity !== undefined) {
query.andWhere(function (this: Knex.QueryBuilder) { query.andWhere(function (this: Knex.QueryBuilder) {
this.where(function (this: Knex.QueryBuilder) { this.where(function (this: Knex.QueryBuilder) {
@@ -118,7 +103,6 @@ export class PricingRepository
}) })
} }
// Efficient price list join with index usage
query.leftJoin("price_list as pl", function (this: Knex.JoinClause) { query.leftJoin("price_list as pl", function (this: Knex.JoinClause) {
this.on("pl.id", "=", "price.price_list_id") this.on("pl.id", "=", "price.price_list_id")
.andOn("pl.status", "=", knex.raw("?", [PriceListStatus.ACTIVE])) .andOn("pl.status", "=", knex.raw("?", [PriceListStatus.ACTIVE]))
@@ -133,9 +117,7 @@ export class PricingRepository
}) })
}) })
// OPTIMIZATION: Only add complex rule filtering when necessary
if (hasComplexContext) { if (hasComplexContext) {
// For price rules - direct check that ALL rules match
const priceRuleConditions = knex.raw( const priceRuleConditions = knex.raw(
` `
( (
@@ -188,7 +170,6 @@ export class PricingRepository
}) })
) )
// For price list rules - direct check that ALL rules match
const priceListRuleConditions = knex.raw( const priceListRuleConditions = knex.raw(
` `
( (
@@ -231,7 +212,6 @@ export class PricingRepository
}) })
}) })
} else { } else {
// Simple case - just get prices with no rules or price lists with no rules
query.where(function (this: Knex.QueryBuilder) { query.where(function (this: Knex.QueryBuilder) {
this.where("price.rules_count", 0).orWhere(function ( this.where("price.rules_count", 0).orWhere(function (
this: Knex.QueryBuilder this: Knex.QueryBuilder
@@ -241,31 +221,13 @@ export class PricingRepository
}) })
} }
// Optimized ordering to help query planner and preserve price list precedence
query query
.orderByRaw("price.price_list_id IS NOT NULL DESC") .orderByRaw("price.price_list_id IS NOT NULL DESC")
.orderByRaw("price.rules_count + COALESCE(pl.rules_count, 0) DESC") // More specific rules first .orderByRaw("price.rules_count + COALESCE(pl.rules_count, 0) DESC")
.orderBy("pl.id", "asc") // Order by price list ID to ensure first created price list takes precedence .orderBy("pl.id", "asc")
.orderBy("price.amount", "asc") // For non-price list prices, cheaper ones first .orderBy("price.amount", "asc")
// Execute the optimized query console.log(query.toString())
return await query return await query
} }
// Helper method to get attributes from rule tables
private async getAttributesFromRuleTables(knex: Knex) {
// Using distinct queries for better performance
const priceRuleAttributesQuery = knex("price_rule")
.distinct("attribute")
.pluck("attribute")
const priceListRuleAttributesQuery = knex("price_list_rule")
.distinct("attribute")
.pluck("attribute")
return await promiseAll([
priceRuleAttributesQuery,
priceListRuleAttributesQuery,
])
}
} }