fix(product): Deep update data retrieval bottleneck (#12538)

* fix(product): Deep update data retrieval bottleneck

* Create shiny-spiders-matter.md

* fix(product): Deep update data retrieval bottleneck
This commit is contained in:
Adrien de Peretti
2025-05-20 16:42:42 +02:00
committed by GitHub
parent ebe5cc7acd
commit 41054a3419
3 changed files with 110 additions and 38 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/product": patch
---
fix(product): Deep update data retrieval bottleneck
@@ -33,7 +33,6 @@ import {
jest.setTimeout(300000) jest.setTimeout(300000)
moduleIntegrationTestRunner<IProductModuleService>({ moduleIntegrationTestRunner<IProductModuleService>({
debug: true,
moduleName: Modules.PRODUCT, moduleName: Modules.PRODUCT,
injectedDependencies: { injectedDependencies: {
[Modules.EVENT_BUS]: new MockEventBusService(), [Modules.EVENT_BUS]: new MockEventBusService(),
@@ -1,10 +1,14 @@
import { Product, ProductOption } from "@models" import { Product, ProductOption } from "@models"
import { Context, DAL, InferEntityType } from "@medusajs/framework/types" import { Context, DAL, InferEntityType } from "@medusajs/framework/types"
import { buildQuery, DALUtils } from "@medusajs/framework/utils" import {
arrayDifference,
buildQuery,
DALUtils,
MedusaError,
} from "@medusajs/framework/utils"
import { SqlEntityManager, wrap } from "@mikro-orm/postgresql" import { SqlEntityManager, wrap } from "@mikro-orm/postgresql"
// eslint-disable-next-line max-len
export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
Product Product
) { ) {
@@ -13,35 +17,102 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
super(...arguments) super(...arguments)
} }
/**
* Identify the relations to load for the given update.
* @param update
* @returns
*/
static #getProductDeepUpdateRelationsToLoad(
productsToUpdate: any[]
): string[] {
const relationsToLoad = new Set<string>()
productsToUpdate.forEach((productToUpdate) => {
if (productToUpdate.options) {
relationsToLoad.add("options")
relationsToLoad.add("options.values")
}
if (productToUpdate.variants) {
relationsToLoad.add("options")
relationsToLoad.add("options.values")
relationsToLoad.add("variants")
relationsToLoad.add("variants.options")
relationsToLoad.add("variants.options.option")
}
if (productToUpdate.tags) relationsToLoad.add("tags")
if (productToUpdate.categories) relationsToLoad.add("categories")
if (productToUpdate.images) relationsToLoad.add("images")
if (productToUpdate.collection) relationsToLoad.add("collection")
if (productToUpdate.type) relationsToLoad.add("type")
})
return Array.from(relationsToLoad)
}
// We should probably fix the column types in the database to avoid this
// It would also match the types in ProductVariant, which are already numbers
static #correctUpdateDTOTypes(productToUpdate: {
weight?: string | number
length?: string | number
height?: string | number
width?: string | number
}) {
productToUpdate.weight = productToUpdate.weight?.toString()
productToUpdate.length = productToUpdate.length?.toString()
productToUpdate.height = productToUpdate.height?.toString()
productToUpdate.width = productToUpdate.width?.toString()
}
async deepUpdate( async deepUpdate(
updates: any[], productsToUpdate: ({ id: string } & any)[],
validateVariantOptions: ( validateVariantOptions: (
variants: any[], variants: any[],
options: InferEntityType<typeof ProductOption>[] options: InferEntityType<typeof ProductOption>[]
) => void, ) => void,
context: Context = {} context: Context = {}
): Promise<InferEntityType<typeof Product>[]> { ): Promise<InferEntityType<typeof Product>[]> {
updates.forEach((update) => this.correctUpdateDTOTypes(update)) const productIdsToUpdate: string[] = []
productsToUpdate.forEach((productToUpdate) => {
ProductRepository.#correctUpdateDTOTypes(productToUpdate)
productIdsToUpdate.push(productToUpdate.id)
})
const products = await this.find( const relationsToLoad =
buildQuery({ id: updates.map((p) => p.id) }, { relations: ["*"] }), ProductRepository.#getProductDeepUpdateRelationsToLoad(productsToUpdate)
context
const findOptions = buildQuery(
{ id: productIdsToUpdate },
{
relations: relationsToLoad,
take: productsToUpdate.length,
}
) )
const products = await this.find(findOptions, context)
const productsMap = new Map(products.map((p) => [p.id, p])) const productsMap = new Map(products.map((p) => [p.id, p]))
for (const update of updates) { const productIds = Array.from(productsMap.keys())
const product = productsMap.get(update.id)! const productsNotFound = arrayDifference(productIdsToUpdate, productIds)
if (productsNotFound.length > 0) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Unable to update the products with ids: ${productsNotFound.join(", ")}`
)
}
for (const productToUpdate of productsToUpdate) {
const product = productsMap.get(productToUpdate.id)!
const wrappedProduct = wrap(product)
// Assign the options first, so they'll be available for the variants loop below // Assign the options first, so they'll be available for the variants loop below
if (update.options) { if (productToUpdate.options) {
wrap(product).assign({ options: update.options }) wrappedProduct.assign({ options: productToUpdate.options })
delete update.options // already assigned above, so no longer necessary delete productToUpdate.options // already assigned above, so no longer necessary
} }
if (update.variants) { if (productToUpdate.variants) {
validateVariantOptions(update.variants, product.options) validateVariantOptions(productToUpdate.variants, product.options)
update.variants.forEach((variant: any) => { productToUpdate.variants.forEach((variant: any) => {
if (variant.options) { if (variant.options) {
variant.options = Object.entries(variant.options).map( variant.options = Object.entries(variant.options).map(
([key, value]) => { ([key, value]) => {
@@ -58,37 +129,34 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
}) })
} }
if (update.tags) { if (productToUpdate.tags) {
update.tags = update.tags.map((t: { id: string }) => t.id) productToUpdate.tags = productToUpdate.tags.map(
(t: { id: string }) => t.id
)
} }
if (update.categories) { if (productToUpdate.categories) {
update.categories = update.categories.map((c: { id: string }) => c.id) productToUpdate.categories = productToUpdate.categories.map(
(c: { id: string }) => c.id
)
} }
if (update.images) { if (productToUpdate.images) {
update.images = update.images.map((image: any, index: number) => ({ productToUpdate.images = productToUpdate.images.map(
...image, (image: any, index: number) => ({
rank: index, ...image,
})) rank: index,
})
)
} }
wrap(product!).assign(update) wrappedProduct.assign(productToUpdate)
} }
// Doing this to ensure updates are returned in the same order they were provided, // Doing this to ensure updates are returned in the same order they were provided,
// since some core flows rely on this. // since some core flows rely on this.
// This is a high level of coupling though. // This is a high level of coupling though.
return updates return productsToUpdate.map(
.map((update) => productsMap.get(update.id)) (productToUpdate) => productsMap.get(productToUpdate.id)!
.filter((product) => product !== undefined) )
}
// We should probably fix the column types in the database to avoid this
// It would also match the types in ProductVariant, which are already numbers
protected correctUpdateDTOTypes(update: any) {
update.weight = update.weight?.toString()
update.length = update.length?.toString()
update.height = update.height?.toString()
update.width = update.width?.toString()
} }
/** /**