fix: move product update logic to repository

This commit is contained in:
Pedro Guzman committed 2025-05-08 12:03:29 +02:00
1 parent d9c8608e51
commit 1ff5c4bf86
2 files changed
+74 -78

No files matched your search

@@ -1,8 +1,8 @@
import { Product } from "@models" import { Product, ProductOption } from "@models"
import { Context, DAL } from "@medusajs/framework/types" import { Context, DAL, InferEntityType } from "@medusajs/framework/types"
import { DALUtils } from "@medusajs/framework/utils" import { buildQuery, DALUtils } from "@medusajs/framework/utils"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager, wrap } from "@mikro-orm/postgresql"
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
@@ -13,6 +13,68 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
super(...arguments) super(...arguments)
} }
async deepUpdate(
updates: any[],
validateVariantOptions: (
variants: any[],
options: InferEntityType<typeof ProductOption>[]
) => void,
context: Context = {}
): Promise<InferEntityType<typeof Product>[]> {
const products = await this.find(
buildQuery({ id: updates.map((p) => p.id) }, { relations: ["*"] }),
context
)
const productsMap = new Map(products.map((p) => [p.id, p]))
for (const update of updates) {
const product = productsMap.get(update.id)!
// Assign the options first, so they'll be available for the variants loop below
if (update.options) {
wrap(product).assign({ options: update.options })
delete update.options // already assigned above, so no longer necessary
}
if (update.variants) {
validateVariantOptions(update.variants, product.options)
update.variants.forEach((variant: any) => {
if (variant.options) {
variant.options = Object.entries(variant.options).map(
([key, value]) => {
const productOption = product.options.find(
(option) => option.title === key
)!
const productOptionValue = productOption.values?.find(
(optionValue) => optionValue.value === value
)!
return productOptionValue.id
}
)
}
})
}
if (update.tags) {
update.tags = update.tags.map((t: { id: string }) => t.id)
}
if (update.categories) {
update.categories = update.categories.map((c: { id: string }) => c.id)
}
if (update.images) {
update.images = update.images.map((image: any, index: number) => ({
...image,
rank: index,
}))
}
wrap(product!).assign(update)
}
return products
}
/** /**
* In order to be able to have a strict not in categories, and prevent a product * In order to be able to have a strict not in categories, and prevent a product
* to be return in the case it also belongs to other categories, we need to * to be return in the case it also belongs to other categories, we need to
@@ -42,6 +42,7 @@ import {
removeUndefined, removeUndefined,
toHandle, toHandle,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { ProductRepository } from "../repositories"
import { import {
UpdateCategoryInput, UpdateCategoryInput,
UpdateCollectionInput, UpdateCollectionInput,
@@ -56,6 +57,7 @@ import { joinerConfig } from "./../joiner-config"
type InjectedDependencies = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
productRepository: ProductRepository
productService: ModulesSdkTypes.IMedusaInternalService<any, any> productService: ModulesSdkTypes.IMedusaInternalService<any, any>
productVariantService: ModulesSdkTypes.IMedusaInternalService<any, any> productVariantService: ModulesSdkTypes.IMedusaInternalService<any, any>
productTagService: ModulesSdkTypes.IMedusaInternalService<any> productTagService: ModulesSdkTypes.IMedusaInternalService<any>
@@ -112,6 +114,7 @@ export default class ProductModuleService
implements ProductTypes.IProductModuleService implements ProductTypes.IProductModuleService
{ {
protected baseRepository_: DAL.RepositoryService protected baseRepository_: DAL.RepositoryService
protected readonly productRepository_: ProductRepository
protected readonly productService_: ModulesSdkTypes.IMedusaInternalService< protected readonly productService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof Product> InferEntityType<typeof Product>
> >
@@ -142,6 +145,7 @@ export default class ProductModuleService
constructor( constructor(
{ {
baseRepository, baseRepository,
productRepository,
productService, productService,
productVariantService, productVariantService,
productTagService, productTagService,
@@ -160,6 +164,7 @@ export default class ProductModuleService
super(...arguments) super(...arguments)
this.baseRepository_ = baseRepository this.baseRepository_ = baseRepository
this.productRepository_ = productRepository
this.productService_ = productService this.productService_ = productService
this.productVariantService_ = productVariantService this.productVariantService_ = productVariantService
this.productTagService_ = productTagService this.productTagService_ = productTagService
@@ -1660,82 +1665,11 @@ export default class ProductModuleService
this.validateProductUpdatePayload(product) this.validateProductUpdatePayload(product)
} }
const products = await this.productService_.list( return this.productRepository_.deepUpdate(
{ id: normalizedProducts.map((p) => p.id) }, normalizedProducts,
{ ProductModuleService.validateVariantOptions,
relations: [
"images",
"variants",
"variants.options",
"options",
"options.values",
],
}, // loading all relations is the only way for productService_.update to update deep relations, otherwise it triggers INSERTS for all relations
sharedContext sharedContext
) )
const productsMap = new Map(products.map((p) => [p.id, p]))
await this.productService_.update(
normalizedProducts.map((normalizedProduct: any) => {
const update = { ...normalizedProduct }
if (update.tags) {
update.tags = update.tags.map((t: { id: string }) => t.id)
}
if (update.categories) {
update.categories = update.categories.map((c: { id: string }) => c.id)
}
if (update.images) {
update.images = update.images.map((image: any, index: number) => ({
...image,
rank: index,
}))
}
// There's an integration test that checks that metadata updates are all-or-nothing
// but productService_.update merges instead, so we update the field directly
productsMap.get(normalizedProduct.id)!.metadata = update.metadata
delete update.metadata
delete update.variants // variants are updated in the next step
return update
}),
sharedContext
)
// We update variants in a second step because we need the options to be updated first
await this.productService_.update(
normalizedProducts
.filter((p) => p.variants)
.map((normalizedProduct: any) => {
const update = {
id: normalizedProduct.id,
variants: normalizedProduct.variants,
}
const options = productsMap.get(normalizedProduct.id)!.options
ProductModuleService.validateVariantOptions(update.variants, options)
update.variants.forEach((variant: any) => {
if (variant.options) {
variant.options = Object.entries(variant.options).map(
([key, value]) => {
const option = options.find((option) => option.title === key)!
const optionValue = option.values?.find(
(optionValue) => optionValue.value === value
)!
return optionValue.id
}
)
}
})
return update
}),
sharedContext
)
return products
} }
// @ts-expect-error // @ts-expect-error