fix(product, types, workflows): Update product variant workflow (#5668)
**What** - Fix issues with update-variant workflow: - other variants than the updated variant are no longer removed - options are updated properly Co-authored-by: Riqwan Thamir <5105988+riqwan@users.noreply.github.com>
This commit is contained in:
co-authored by
Riqwan Thamir
parent
b25b29fe7b
commit
a39ce125cc
@@ -5,6 +5,7 @@ import {
|
||||
ProductCollectionRepository,
|
||||
ProductImageRepository,
|
||||
ProductOptionRepository,
|
||||
ProductOptionValueRepository,
|
||||
ProductRepository,
|
||||
ProductTagRepository,
|
||||
ProductTypeRepository,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
ProductImageService,
|
||||
ProductModuleService,
|
||||
ProductOptionService,
|
||||
ProductOptionValueService,
|
||||
ProductService,
|
||||
ProductTagService,
|
||||
ProductTypeService,
|
||||
@@ -48,6 +50,7 @@ export default async ({
|
||||
productImageService: asClass(ProductImageService).singleton(),
|
||||
productTypeService: asClass(ProductTypeService).singleton(),
|
||||
productOptionService: asClass(ProductOptionService).singleton(),
|
||||
productOptionValueService: asClass(ProductOptionValueService).singleton(),
|
||||
})
|
||||
|
||||
if (customRepositories) {
|
||||
@@ -69,6 +72,9 @@ function loadDefaultRepositories({ container }) {
|
||||
productTagRepository: asClass(ProductTagRepository).singleton(),
|
||||
productTypeRepository: asClass(ProductTypeRepository).singleton(),
|
||||
productOptionRepository: asClass(ProductOptionRepository).singleton(),
|
||||
productOptionValueRepository: asClass(
|
||||
ProductOptionValueRepository
|
||||
).singleton(),
|
||||
productVariantRepository: asClass(ProductVariantRepository).singleton(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ export { default as ProductTag } from "./product-tag"
|
||||
export { default as ProductType } from "./product-type"
|
||||
export { default as ProductVariant } from "./product-variant"
|
||||
export { default as ProductOption } from "./product-option"
|
||||
export { default as ProductOptionValue } from "./product-option-value"
|
||||
export { default as Image } from "./product-image"
|
||||
|
||||
@@ -7,3 +7,4 @@ export { ProductCategoryRepository } from "./product-category"
|
||||
export { ProductImageRepository } from "./product-image"
|
||||
export { ProductTypeRepository } from "./product-type"
|
||||
export { ProductOptionRepository } from "./product-option"
|
||||
export { ProductOptionValueRepository } from "./product-option-value"
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
import {
|
||||
CreateProductOptionValueDTO,
|
||||
UpdateProductOptionValueDTO,
|
||||
} from "../types/services/product-option-value"
|
||||
|
||||
import { DALUtils } from "@medusajs/utils"
|
||||
import { FilterQuery as MikroFilterQuery } from "@mikro-orm/core/typings"
|
||||
import { FindOptions as MikroOptions } from "@mikro-orm/core/drivers/IDatabaseDriver"
|
||||
import { ProductOptionValue } from "@models"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
|
||||
export class ProductOptionValueRepository extends DALUtils.MikroOrmBaseRepository {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductOptionValue> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductOptionValue[]> {
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
|
||||
return await manager.find(
|
||||
ProductOptionValue,
|
||||
findOptions_.where as MikroFilterQuery<ProductOptionValue>,
|
||||
findOptions_.options as MikroOptions<ProductOptionValue>
|
||||
)
|
||||
}
|
||||
|
||||
async upsert(
|
||||
optionValues: (UpdateProductOptionValueDTO | CreateProductOptionValueDTO)[],
|
||||
context: Context = {}
|
||||
): Promise<ProductOptionValue[]> {
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
|
||||
const optionValueIds: string[] = []
|
||||
|
||||
for (const optionValue of optionValues) {
|
||||
if (optionValue.id) {
|
||||
optionValueIds.push(optionValue.id)
|
||||
}
|
||||
}
|
||||
|
||||
const existingOptionValues = await this.find(
|
||||
{
|
||||
where: {
|
||||
id: {
|
||||
$in: optionValueIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
const existingOptionValuesMap = new Map(
|
||||
existingOptionValues.map<[string, ProductOptionValue]>((optionValue) => [
|
||||
optionValue.id,
|
||||
optionValue,
|
||||
])
|
||||
)
|
||||
|
||||
const upsertedOptionValues: ProductOptionValue[] = []
|
||||
const optionValuesToCreate: ProductOptionValue[] = []
|
||||
const optionValuesToUpdate: ProductOptionValue[] = []
|
||||
|
||||
optionValues.forEach(({ option_id, ...optionValue }) => {
|
||||
const existingOptionValue = optionValue.id
|
||||
? existingOptionValuesMap.get(optionValue.id)
|
||||
: undefined
|
||||
|
||||
if (optionValue.id && existingOptionValue) {
|
||||
const updatedOptionValue = manager.assign(existingOptionValue, {
|
||||
option: option_id,
|
||||
...optionValue,
|
||||
})
|
||||
optionValuesToUpdate.push(updatedOptionValue)
|
||||
return
|
||||
}
|
||||
|
||||
const newOptionValue = manager.create(ProductOptionValue, {
|
||||
option: option_id,
|
||||
variant: (optionValue as CreateProductOptionValueDTO).variant_id,
|
||||
...optionValue,
|
||||
})
|
||||
optionValuesToCreate.push(newOptionValue)
|
||||
})
|
||||
|
||||
if (optionValuesToCreate.length) {
|
||||
manager.persist(optionValuesToCreate)
|
||||
upsertedOptionValues.push(...optionValuesToCreate)
|
||||
}
|
||||
|
||||
if (optionValuesToUpdate.length) {
|
||||
manager.persist(optionValuesToUpdate)
|
||||
upsertedOptionValues.push(...optionValuesToUpdate)
|
||||
}
|
||||
|
||||
return upsertedOptionValues
|
||||
}
|
||||
|
||||
async delete(ids: string[], context: Context = {}): Promise<void> {
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
await manager.nativeDelete(ProductOptionValue, { id: { $in: ids } }, {})
|
||||
}
|
||||
}
|
||||
@@ -7,3 +7,4 @@ export { default as ProductVariantService } from "./product-variant"
|
||||
export { default as ProductTypeService } from "./product-type"
|
||||
export { default as ProductOptionService } from "./product-option"
|
||||
export { default as ProductImageService } from "./product-image"
|
||||
export { default as ProductOptionValueService } from "./product-option-value"
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ProductCategory,
|
||||
ProductCollection,
|
||||
ProductOption,
|
||||
ProductOptionValue,
|
||||
ProductTag,
|
||||
ProductType,
|
||||
ProductVariant,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
ProductCategoryService,
|
||||
ProductCollectionService,
|
||||
ProductOptionService,
|
||||
ProductOptionValueService,
|
||||
ProductService,
|
||||
ProductTagService,
|
||||
ProductTypeService,
|
||||
@@ -53,6 +55,8 @@ import {
|
||||
} from "../types/services/product"
|
||||
|
||||
import {
|
||||
arrayDifference,
|
||||
groupBy,
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
isDefined,
|
||||
@@ -68,6 +72,10 @@ import {
|
||||
joinerConfig,
|
||||
LinkableKeys,
|
||||
} from "./../joiner-config"
|
||||
import {
|
||||
CreateProductOptionValueDTO,
|
||||
UpdateProductOptionValueDTO,
|
||||
} from "../types/services/product-option-value"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
@@ -79,6 +87,7 @@ type InjectedDependencies = {
|
||||
productImageService: ProductImageService<any>
|
||||
productTypeService: ProductTypeService<any>
|
||||
productOptionService: ProductOptionService<any>
|
||||
productOptionValueService: ProductOptionValueService<any>
|
||||
eventBusModuleService?: IEventBusModuleService
|
||||
}
|
||||
|
||||
@@ -90,7 +99,8 @@ export default class ProductModuleService<
|
||||
TProductCategory extends ProductCategory = ProductCategory,
|
||||
TProductImage extends Image = Image,
|
||||
TProductType extends ProductType = ProductType,
|
||||
TProductOption extends ProductOption = ProductOption
|
||||
TProductOption extends ProductOption = ProductOption,
|
||||
TProductOptionValue extends ProductOptionValue = ProductOptionValue
|
||||
> implements ProductTypes.IProductModuleService
|
||||
{
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
@@ -108,6 +118,8 @@ export default class ProductModuleService<
|
||||
protected readonly productImageService_: ProductImageService<TProductImage>
|
||||
protected readonly productTypeService_: ProductTypeService<TProductType>
|
||||
protected readonly productOptionService_: ProductOptionService<TProductOption>
|
||||
// eslint-disable-next-line max-len
|
||||
protected readonly productOptionValueService_: ProductOptionValueService<TProductOptionValue>
|
||||
protected readonly eventBusModuleService_?: IEventBusModuleService
|
||||
|
||||
constructor(
|
||||
@@ -121,6 +133,7 @@ export default class ProductModuleService<
|
||||
productImageService,
|
||||
productTypeService,
|
||||
productOptionService,
|
||||
productOptionValueService,
|
||||
eventBusModuleService,
|
||||
}: InjectedDependencies,
|
||||
protected readonly moduleDeclaration: InternalModuleDeclaration
|
||||
@@ -134,6 +147,7 @@ export default class ProductModuleService<
|
||||
this.productImageService_ = productImageService
|
||||
this.productTypeService_ = productTypeService
|
||||
this.productOptionService_ = productOptionService
|
||||
this.productOptionValueService_ = productOptionValueService
|
||||
this.eventBusModuleService_ = eventBusModuleService
|
||||
}
|
||||
|
||||
@@ -307,6 +321,131 @@ export default class ProductModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async updateVariants(
|
||||
data: ProductTypes.UpdateProductVariantOnlyDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<ProductTypes.ProductVariantDTO[]> {
|
||||
const productVariants = await this.updateVariants_(data, sharedContext)
|
||||
|
||||
const updatedVariants = await this.baseRepository_.serialize<
|
||||
ProductTypes.ProductVariantDTO[]
|
||||
>(productVariants, {
|
||||
populate: true,
|
||||
})
|
||||
|
||||
return updatedVariants
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
protected async updateVariants_(
|
||||
data: ProductTypes.UpdateProductVariantOnlyDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TProductVariant[]> {
|
||||
const variantIdsToUpdate = data.map(({ id }) => id)
|
||||
const variants = await this.listVariants(
|
||||
{ id: variantIdsToUpdate },
|
||||
{ relations: ["options", "options.option"] },
|
||||
sharedContext
|
||||
)
|
||||
const variantsMap = new Map(
|
||||
variants.map((variant) => [variant.id, variant])
|
||||
)
|
||||
|
||||
if (variants.length !== data.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Cannot update non-existing variants with ids: ${arrayDifference(
|
||||
variantIdsToUpdate,
|
||||
[...variantsMap.keys()]
|
||||
).join(", ")}`
|
||||
)
|
||||
}
|
||||
|
||||
const optionValuesToUpsert: (
|
||||
| CreateProductOptionValueDTO
|
||||
| UpdateProductOptionValueDTO
|
||||
)[] = []
|
||||
const optionsValuesToDelete: string[] = []
|
||||
|
||||
const toUpdate = data.map(({ id, options, ...rest }) => {
|
||||
const variant = variantsMap.get(id)!
|
||||
|
||||
const toUpdate: UpdateProductVariantDTO = {
|
||||
id,
|
||||
product_id: variant.product_id,
|
||||
}
|
||||
|
||||
if (options?.length) {
|
||||
const optionIdToUpdateValueMap = new Map(
|
||||
options.map(({ option, option_id, value }) => {
|
||||
const computedOptionId = option_id ?? option.id ?? option
|
||||
return [computedOptionId, value]
|
||||
})
|
||||
)
|
||||
|
||||
for (const existingOptionValue of variant.options) {
|
||||
if (!optionIdToUpdateValueMap.has(existingOptionValue.option.id)) {
|
||||
optionsValuesToDelete.push(existingOptionValue.id)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
optionValuesToUpsert.push({
|
||||
id: existingOptionValue.id,
|
||||
option_id: existingOptionValue.option.id,
|
||||
value: optionIdToUpdateValueMap.get(existingOptionValue.option.id)!,
|
||||
})
|
||||
optionIdToUpdateValueMap.delete(existingOptionValue.option.id)
|
||||
}
|
||||
|
||||
for (const [option_id, value] of optionIdToUpdateValueMap.entries()) {
|
||||
optionValuesToUpsert.push({
|
||||
option_id,
|
||||
value,
|
||||
variant_id: id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(rest)) {
|
||||
if (variant[key] !== value) {
|
||||
toUpdate[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return toUpdate
|
||||
})
|
||||
|
||||
const groups = groupBy(toUpdate, "product_id")
|
||||
|
||||
const [, , productVariants]: [
|
||||
void,
|
||||
TProductOptionValue[],
|
||||
TProductVariant[][]
|
||||
] = await promiseAll([
|
||||
await this.productOptionValueService_.delete(
|
||||
optionsValuesToDelete,
|
||||
sharedContext
|
||||
),
|
||||
await this.productOptionValueService_.upsert(
|
||||
optionValuesToUpsert,
|
||||
sharedContext
|
||||
),
|
||||
await promiseAll(
|
||||
[...groups.entries()].map(async ([product_id, update]) => {
|
||||
return await this.productVariantService_.update(
|
||||
product_id,
|
||||
update.map(({ product_id, ...update }) => update),
|
||||
sharedContext
|
||||
)
|
||||
})
|
||||
),
|
||||
])
|
||||
|
||||
return productVariants.flat()
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async retrieveTag(
|
||||
tagId: string,
|
||||
@@ -1098,7 +1237,11 @@ export default class ProductModuleService<
|
||||
if (!productData.thumbnail && productData.images?.length) {
|
||||
productData.thumbnail = isString(productData.images[0])
|
||||
? (productData.images[0] as string)
|
||||
: (productData.images[0] as { url: string }).url
|
||||
: (
|
||||
productData.images[0] as {
|
||||
url: string
|
||||
}
|
||||
).url
|
||||
}
|
||||
|
||||
if (productData.images?.length) {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ProductOptionValue } from "@models"
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
import {
|
||||
ProductOptionRepository,
|
||||
ProductOptionValueRepository,
|
||||
} from "@repositories"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
import {
|
||||
CreateProductOptionValueDTO,
|
||||
UpdateProductOptionValueDTO,
|
||||
} from "../types/services/product-option-value"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productOptionValueRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class ProductOptionValueService<
|
||||
TEntity extends ProductOptionValue = ProductOptionValue
|
||||
> {
|
||||
protected readonly productOptionValueRepository_: DAL.RepositoryService
|
||||
|
||||
constructor({ productOptionValueRepository }: InjectedDependencies) {
|
||||
this.productOptionValueRepository_ =
|
||||
productOptionValueRepository as ProductOptionRepository
|
||||
}
|
||||
|
||||
@InjectTransactionManager("productOptionValueRepository_")
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
return await this.productOptionValueRepository_.delete(ids, sharedContext)
|
||||
}
|
||||
|
||||
@InjectTransactionManager("productOptionValueRepository_")
|
||||
async upsert(
|
||||
data: (UpdateProductOptionValueDTO | CreateProductOptionValueDTO)[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return (await (
|
||||
this.productOptionValueRepository_ as ProductOptionValueRepository
|
||||
).upsert!(data, sharedContext)) as TEntity[]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface UpdateProductOptionValueDTO {
|
||||
id: string
|
||||
value: string
|
||||
option_id: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface CreateProductOptionValueDTO {
|
||||
id?: string
|
||||
value: string
|
||||
option_id: string
|
||||
variant_id: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { CreateProductVariantOptionDTO } from "@medusajs/types"
|
||||
|
||||
export interface UpdateProductVariantDTO {
|
||||
id: string
|
||||
product_id: string
|
||||
title?: string
|
||||
sku?: string
|
||||
barcode?: string
|
||||
@@ -18,6 +19,6 @@ export interface UpdateProductVariantDTO {
|
||||
length?: number
|
||||
height?: number
|
||||
width?: number
|
||||
options?: CreateProductVariantOptionDTO[]
|
||||
options?: (CreateProductVariantOptionDTO & { id?: string })[]
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user