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:
Philip Korsholm
2023-11-23 14:35:01 +00:00
committed by GitHub
co-authored by Riqwan Thamir
parent b25b29fe7b
commit a39ce125cc
16 changed files with 617 additions and 44 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@medusajs/workflows": patch
"@medusajs/product": patch
"@medusajs/types": patch
---
fix(workflows, product, types): Fix issues relating to update-variant workflow and options
+2 -1
View File
@@ -1,9 +1,10 @@
--- ---
"@medusajs/workflows": patch "@medusajs/workflows": patch
"@medusajs/product": patch "@medusajs/product": patch
"@medusajs/pricing": patch
"@medusajs/medusa": patch "@medusajs/medusa": patch
"@medusajs/types": patch "@medusajs/types": patch
"@medusajs/utils": patch "@medusajs/utils": patch
--- ---
feat(medusa,types,workflows,utils,product): PricingModule Integration of PriceLists into Core feat(medusa,types,workflows,utils,product,pricing): PricingModule Integration of PriceLists into Core
+4 -2
View File
@@ -125,7 +125,6 @@ jobs:
env: env:
DB_PASSWORD: postgres DB_PASSWORD: postgres
DB_USERNAME: postgres DB_USERNAME: postgres
SPLIT: ${{ steps['split-tests'].outputs['split'] }}
integration-tests-api: integration-tests-api:
needs: setup needs: setup
@@ -186,7 +185,8 @@ jobs:
run: yarn test:integration:api run: yarn test:integration:api
env: env:
DB_PASSWORD: postgres DB_PASSWORD: postgres
SPLIT: ${{ steps['split-tests'].outputs['split'] }} DB_USERNAME: postgres
integration-tests-plugins: integration-tests-plugins:
needs: setup needs: setup
@@ -237,6 +237,7 @@ jobs:
- name: Run plugin integration tests - name: Run plugin integration tests
run: yarn test:integration:plugins run: yarn test:integration:plugins
env: env:
DB_USERNAME: postgres
DB_PASSWORD: postgres DB_PASSWORD: postgres
NODE_OPTIONS: "--max_old_space_size=4096" NODE_OPTIONS: "--max_old_space_size=4096"
@@ -286,4 +287,5 @@ jobs:
- name: Run repository integration tests - name: Run repository integration tests
run: yarn test:integration:repositories run: yarn test:integration:repositories
env: env:
DB_USERNAME: postgres
DB_PASSWORD: postgres DB_PASSWORD: postgres
@@ -1,5 +1,3 @@
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db" import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { import {
simpleProductFactory, simpleProductFactory,
@@ -7,11 +5,13 @@ import {
} from "../../../../factories" } from "../../../../factories"
import { AxiosInstance } from "axios" import { AxiosInstance } from "axios"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import adminSeeder from "../../../../helpers/admin-seeder" import adminSeeder from "../../../../helpers/admin-seeder"
import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types"
import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" import { createVariantPriceSet } from "../../../helpers/create-variant-price-set"
import { getContainer } from "../../../../environment-helpers/use-container"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
jest.setTimeout(50000) jest.setTimeout(50000)
@@ -62,6 +62,9 @@ describe("POST /admin/products/:id/variants/:id", () => {
{ {
options: [{ option_id: "test-product-option-1", value: "test" }], options: [{ option_id: "test-product-option-1", value: "test" }],
}, },
{
options: [{ option_id: "test-product-option-1", value: "test 2" }],
},
], ],
options: [ options: [
{ {
@@ -250,4 +253,231 @@ describe("POST /admin/products/:id/variants/:id", () => {
}) })
) )
}) })
it("should update variant option value", async () => {
const api = useApi()! as AxiosInstance
const data = {
options: [
{
option_id: "test-product-option-1",
value: "updated",
},
],
}
await api.post(
`/admin/products/${product.id}/variants/${variant.id}`,
data,
adminHeaders
)
const response = await api.get(
`/admin/products/${product.id}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
id: expect.any(String),
variants: expect.arrayContaining([
expect.objectContaining({
id: variant.id,
options: [
expect.objectContaining({
option_id: "test-product-option-1",
value: "updated",
}),
],
}),
expect.objectContaining({
id: product.variants[1].id,
options: [
expect.objectContaining({
option_id: "test-product-option-1",
value: "test 2",
}),
],
}),
]),
})
)
})
it("should update variant metadata", async () => {
const api = useApi()! as AxiosInstance
const data = {
metadata: {
test: "string",
},
}
await api.post(
`/admin/products/${product.id}/variants/${variant.id}`,
data,
adminHeaders
)
const response = await api.get(
`/admin/products/${product.id}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
id: expect.any(String),
variants: expect.arrayContaining([
expect.objectContaining({
id: variant.id,
metadata: {
test: "string",
},
}),
]),
})
)
})
it("should remove options not present in update", async () => {
const api = useApi()! as AxiosInstance
product = await simpleProductFactory(dbConnection, {
id: "test-product-with-multiple-options",
variants: [
{
options: [
{ option_id: "test-product-multi-option-1", value: "test" },
{ option_id: "test-product-multi-option-2", value: "test value" },
],
},
],
options: [
{
id: "test-product-multi-option-1",
title: "Test option 1",
},
{
id: "test-product-multi-option-2",
title: "Test option 2",
},
],
})
variant = product.variants[0]
const data = {
options: [
{
option_id: "test-product-multi-option-1",
value: "updated",
},
],
}
await api.post(
`/admin/products/${product.id}/variants/${variant.id}`,
data,
adminHeaders
)
const response = await api.get(
`/admin/products/${product.id}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
id: expect.any(String),
variants: [
expect.objectContaining({
id: variant.id,
options: [
expect.objectContaining({
option_id: "test-product-multi-option-1",
value: "updated",
}),
],
}),
],
})
)
})
it("should update several options in the same api call", async () => {
const api = useApi()! as AxiosInstance
product = await simpleProductFactory(dbConnection, {
id: "test-product-with-multiple-options",
variants: [
{
options: [
{ option_id: "test-product-multi-option-1", value: "test" },
{ option_id: "test-product-multi-option-2", value: "test value" },
],
},
],
options: [
{
id: "test-product-multi-option-1",
title: "Test option 1",
},
{
id: "test-product-multi-option-2",
title: "Test option 2",
},
],
})
variant = product.variants[0]
const data = {
options: [
{
option_id: "test-product-multi-option-1",
value: "updated",
},
{
option_id: "test-product-multi-option-2",
value: "updated 2",
},
],
}
await api.post(
`/admin/products/${product.id}/variants/${variant.id}`,
data,
adminHeaders
)
const response = await api.get(
`/admin/products/${product.id}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
id: expect.any(String),
variants: [
expect.objectContaining({
id: variant.id,
options: [
expect.objectContaining({
option_id: "test-product-multi-option-1",
value: "updated",
}),
expect.objectContaining({
option_id: "test-product-multi-option-2",
value: "updated 2",
}),
],
}),
],
})
)
})
}) })
@@ -5,6 +5,7 @@ import {
ProductCollectionRepository, ProductCollectionRepository,
ProductImageRepository, ProductImageRepository,
ProductOptionRepository, ProductOptionRepository,
ProductOptionValueRepository,
ProductRepository, ProductRepository,
ProductTagRepository, ProductTagRepository,
ProductTypeRepository, ProductTypeRepository,
@@ -17,6 +18,7 @@ import {
ProductImageService, ProductImageService,
ProductModuleService, ProductModuleService,
ProductOptionService, ProductOptionService,
ProductOptionValueService,
ProductService, ProductService,
ProductTagService, ProductTagService,
ProductTypeService, ProductTypeService,
@@ -48,6 +50,7 @@ export default async ({
productImageService: asClass(ProductImageService).singleton(), productImageService: asClass(ProductImageService).singleton(),
productTypeService: asClass(ProductTypeService).singleton(), productTypeService: asClass(ProductTypeService).singleton(),
productOptionService: asClass(ProductOptionService).singleton(), productOptionService: asClass(ProductOptionService).singleton(),
productOptionValueService: asClass(ProductOptionValueService).singleton(),
}) })
if (customRepositories) { if (customRepositories) {
@@ -69,6 +72,9 @@ function loadDefaultRepositories({ container }) {
productTagRepository: asClass(ProductTagRepository).singleton(), productTagRepository: asClass(ProductTagRepository).singleton(),
productTypeRepository: asClass(ProductTypeRepository).singleton(), productTypeRepository: asClass(ProductTypeRepository).singleton(),
productOptionRepository: asClass(ProductOptionRepository).singleton(), productOptionRepository: asClass(ProductOptionRepository).singleton(),
productOptionValueRepository: asClass(
ProductOptionValueRepository
).singleton(),
productVariantRepository: asClass(ProductVariantRepository).singleton(), productVariantRepository: asClass(ProductVariantRepository).singleton(),
}) })
} }
+1
View File
@@ -5,4 +5,5 @@ export { default as ProductTag } from "./product-tag"
export { default as ProductType } from "./product-type" export { default as ProductType } from "./product-type"
export { default as ProductVariant } from "./product-variant" export { default as ProductVariant } from "./product-variant"
export { default as ProductOption } from "./product-option" export { default as ProductOption } from "./product-option"
export { default as ProductOptionValue } from "./product-option-value"
export { default as Image } from "./product-image" export { default as Image } from "./product-image"
@@ -7,3 +7,4 @@ export { ProductCategoryRepository } from "./product-category"
export { ProductImageRepository } from "./product-image" export { ProductImageRepository } from "./product-image"
export { ProductTypeRepository } from "./product-type" export { ProductTypeRepository } from "./product-type"
export { ProductOptionRepository } from "./product-option" 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 } }, {})
}
}
+1
View File
@@ -7,3 +7,4 @@ export { default as ProductVariantService } from "./product-variant"
export { default as ProductTypeService } from "./product-type" export { default as ProductTypeService } from "./product-type"
export { default as ProductOptionService } from "./product-option" export { default as ProductOptionService } from "./product-option"
export { default as ProductImageService } from "./product-image" export { default as ProductImageService } from "./product-image"
export { default as ProductOptionValueService } from "./product-option-value"
@@ -16,6 +16,7 @@ import {
ProductCategory, ProductCategory,
ProductCollection, ProductCollection,
ProductOption, ProductOption,
ProductOptionValue,
ProductTag, ProductTag,
ProductType, ProductType,
ProductVariant, ProductVariant,
@@ -24,6 +25,7 @@ import {
ProductCategoryService, ProductCategoryService,
ProductCollectionService, ProductCollectionService,
ProductOptionService, ProductOptionService,
ProductOptionValueService,
ProductService, ProductService,
ProductTagService, ProductTagService,
ProductTypeService, ProductTypeService,
@@ -53,6 +55,8 @@ import {
} from "../types/services/product" } from "../types/services/product"
import { import {
arrayDifference,
groupBy,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
isDefined, isDefined,
@@ -68,6 +72,10 @@ import {
joinerConfig, joinerConfig,
LinkableKeys, LinkableKeys,
} from "./../joiner-config" } from "./../joiner-config"
import {
CreateProductOptionValueDTO,
UpdateProductOptionValueDTO,
} from "../types/services/product-option-value"
type InjectedDependencies = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
@@ -79,6 +87,7 @@ type InjectedDependencies = {
productImageService: ProductImageService<any> productImageService: ProductImageService<any>
productTypeService: ProductTypeService<any> productTypeService: ProductTypeService<any>
productOptionService: ProductOptionService<any> productOptionService: ProductOptionService<any>
productOptionValueService: ProductOptionValueService<any>
eventBusModuleService?: IEventBusModuleService eventBusModuleService?: IEventBusModuleService
} }
@@ -90,7 +99,8 @@ export default class ProductModuleService<
TProductCategory extends ProductCategory = ProductCategory, TProductCategory extends ProductCategory = ProductCategory,
TProductImage extends Image = Image, TProductImage extends Image = Image,
TProductType extends ProductType = ProductType, TProductType extends ProductType = ProductType,
TProductOption extends ProductOption = ProductOption TProductOption extends ProductOption = ProductOption,
TProductOptionValue extends ProductOptionValue = ProductOptionValue
> implements ProductTypes.IProductModuleService > implements ProductTypes.IProductModuleService
{ {
protected baseRepository_: DAL.RepositoryService protected baseRepository_: DAL.RepositoryService
@@ -108,6 +118,8 @@ export default class ProductModuleService<
protected readonly productImageService_: ProductImageService<TProductImage> protected readonly productImageService_: ProductImageService<TProductImage>
protected readonly productTypeService_: ProductTypeService<TProductType> protected readonly productTypeService_: ProductTypeService<TProductType>
protected readonly productOptionService_: ProductOptionService<TProductOption> protected readonly productOptionService_: ProductOptionService<TProductOption>
// eslint-disable-next-line max-len
protected readonly productOptionValueService_: ProductOptionValueService<TProductOptionValue>
protected readonly eventBusModuleService_?: IEventBusModuleService protected readonly eventBusModuleService_?: IEventBusModuleService
constructor( constructor(
@@ -121,6 +133,7 @@ export default class ProductModuleService<
productImageService, productImageService,
productTypeService, productTypeService,
productOptionService, productOptionService,
productOptionValueService,
eventBusModuleService, eventBusModuleService,
}: InjectedDependencies, }: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration protected readonly moduleDeclaration: InternalModuleDeclaration
@@ -134,6 +147,7 @@ export default class ProductModuleService<
this.productImageService_ = productImageService this.productImageService_ = productImageService
this.productTypeService_ = productTypeService this.productTypeService_ = productTypeService
this.productOptionService_ = productOptionService this.productOptionService_ = productOptionService
this.productOptionValueService_ = productOptionValueService
this.eventBusModuleService_ = eventBusModuleService 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_") @InjectManager("baseRepository_")
async retrieveTag( async retrieveTag(
tagId: string, tagId: string,
@@ -1098,7 +1237,11 @@ export default class ProductModuleService<
if (!productData.thumbnail && productData.images?.length) { if (!productData.thumbnail && productData.images?.length) {
productData.thumbnail = isString(productData.images[0]) productData.thumbnail = isString(productData.images[0])
? (productData.images[0] as string) ? (productData.images[0] as string)
: (productData.images[0] as { url: string }).url : (
productData.images[0] as {
url: string
}
).url
} }
if (productData.images?.length) { 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 { export interface UpdateProductVariantDTO {
id: string id: string
product_id: string
title?: string title?: string
sku?: string sku?: string
barcode?: string barcode?: string
@@ -18,6 +19,6 @@ export interface UpdateProductVariantDTO {
length?: number length?: number
height?: number height?: number
width?: number width?: number
options?: CreateProductVariantOptionDTO[] options?: (CreateProductVariantOptionDTO & { id?: string })[]
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
} }
+22 -21
View File
@@ -83,43 +83,43 @@ export interface ProductDTO {
material?: string | null material?: string | null
/** /**
* The associated product collection. * The associated product collection.
* *
* @expandable * @expandable
*/ */
collection: ProductCollectionDTO collection: ProductCollectionDTO
/** /**
* The associated product categories. * The associated product categories.
* *
* @expandable * @expandable
*/ */
categories?: ProductCategoryDTO[] | null categories?: ProductCategoryDTO[] | null
/** /**
* The associated product type. * The associated product type.
* *
* @expandable * @expandable
*/ */
type: ProductTypeDTO[] type: ProductTypeDTO[]
/** /**
* The associated product tags. * The associated product tags.
* *
* @expandable * @expandable
*/ */
tags: ProductTagDTO[] tags: ProductTagDTO[]
/** /**
* The associated product variants. * The associated product variants.
* *
* @expandable * @expandable
*/ */
variants: ProductVariantDTO[] variants: ProductVariantDTO[]
/** /**
* The associated product options. * The associated product options.
* *
* @expandable * @expandable
*/ */
options: ProductOptionDTO[] options: ProductOptionDTO[]
/** /**
* The associated product images. * The associated product images.
* *
* @expandable * @expandable
*/ */
images: ProductImageDTO[] images: ProductImageDTO[]
@@ -226,17 +226,17 @@ export interface ProductVariantDTO {
width?: number | null width?: number | null
/** /**
* The associated product options. * The associated product options.
* *
* @expandable * @expandable
*/ */
options: ProductOptionValueDTO options: ProductOptionValueDTO[]
/** /**
* Holds custom data in key-value pairs. * Holds custom data in key-value pairs.
*/ */
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
/** /**
* The associated product. * The associated product.
* *
* @expandable * @expandable
*/ */
product: ProductDTO product: ProductDTO
@@ -298,13 +298,13 @@ export interface ProductCategoryDTO {
rank?: number rank?: number
/** /**
* The associated parent category. * The associated parent category.
* *
* @expandable * @expandable
*/ */
parent_category?: ProductCategoryDTO parent_category?: ProductCategoryDTO
/** /**
* The associated child categories. * The associated child categories.
* *
* @expandable * @expandable
*/ */
category_children: ProductCategoryDTO[] category_children: ProductCategoryDTO[]
@@ -410,7 +410,7 @@ export interface ProductTagDTO {
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
/** /**
* The associated products. * The associated products.
* *
* @expandable * @expandable
*/ */
products?: ProductDTO[] products?: ProductDTO[]
@@ -444,7 +444,7 @@ export interface ProductCollectionDTO {
deleted_at?: string | Date deleted_at?: string | Date
/** /**
* The associated products. * The associated products.
* *
* @expandable * @expandable
*/ */
products?: ProductDTO[] products?: ProductDTO[]
@@ -478,7 +478,7 @@ export interface ProductTypeDTO {
* @interface * @interface
* *
* A product option's data. * A product option's data.
* *
*/ */
export interface ProductOptionDTO { export interface ProductOptionDTO {
/** /**
@@ -491,13 +491,13 @@ export interface ProductOptionDTO {
title: string title: string
/** /**
* The associated product. * The associated product.
* *
* @expandable * @expandable
*/ */
product: ProductDTO product: ProductDTO
/** /**
* The associated product option values. * The associated product option values.
* *
* @expandable * @expandable
*/ */
values: ProductOptionValueDTO[] values: ProductOptionValueDTO[]
@@ -563,13 +563,13 @@ export interface ProductOptionValueDTO {
value: string value: string
/** /**
* The associated product option. * The associated product option.
* *
* @expandable * @expandable
*/ */
option: ProductOptionDTO option: ProductOptionDTO
/** /**
* The associated product variant. * The associated product variant.
* *
* @expandable * @expandable
*/ */
variant: ProductVariantDTO variant: ProductVariantDTO
@@ -612,7 +612,7 @@ export interface FilterableProductProps
/** /**
* Filters on a product's tags. * Filters on a product's tags.
*/ */
tags?: { tags?: {
/** /**
* Values to filter product tags by. * Values to filter product tags by.
*/ */
@@ -761,7 +761,7 @@ export interface FilterableProductVariantProps
/** /**
* Filter product variants by their associated options. * Filter product variants by their associated options.
*/ */
options?: { options?: {
/** /**
* IDs to filter options by. * IDs to filter options by.
*/ */
@@ -986,6 +986,7 @@ export interface CreateProductVariantOptionDTO {
* The value of a product variant option. * The value of a product variant option.
*/ */
value: string value: string
option_id?: string option_id?: string
} }
+9 -3
View File
@@ -26,12 +26,13 @@ import {
UpdateProductOptionDTO, UpdateProductOptionDTO,
UpdateProductTagDTO, UpdateProductTagDTO,
UpdateProductTypeDTO, UpdateProductTypeDTO,
UpdateProductVariantDTO,
} from "./common" } from "./common"
import { FindConfig } from "../common"
import { RestoreReturn, SoftDeleteReturn } from "../dal" import { RestoreReturn, SoftDeleteReturn } from "../dal"
import { ModuleJoinerConfig } from "../modules-sdk"
import { Context } from "../shared-context" import { Context } from "../shared-context"
import { FindConfig } from "../common"
import { ModuleJoinerConfig } from "../modules-sdk"
export interface IProductModuleService { export interface IProductModuleService {
/** /**
@@ -1490,6 +1491,11 @@ export interface IProductModuleService {
sharedContext?: Context sharedContext?: Context
): Promise<ProductVariantDTO[]> ): Promise<ProductVariantDTO[]>
updateVariants(
data: UpdateProductVariantDTO[],
sharedContext?: Context
): Promise<ProductVariantDTO[]>
createVariants( createVariants(
data: CreateProductVariantDTO[], data: CreateProductVariantDTO[],
sharedContext?: Context sharedContext?: Context
@@ -1,5 +1,5 @@
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
import { ProductTypes } from "@medusajs/types" import { ProductTypes, UpdateProductVariantOnlyDTO } from "@medusajs/types"
import { WorkflowArguments } from "../../helper" import { WorkflowArguments } from "../../helper"
type HandlerInput = { type HandlerInput = {
@@ -14,21 +14,22 @@ export async function updateProductVariants({
> { > {
const { productVariantsMap } = data const { productVariantsMap } = data
const productsVariants: ProductTypes.UpdateProductVariantDTO[] = [] const productsVariants: ProductTypes.UpdateProductVariantDTO[] = []
const updateProductsData: ProductTypes.UpdateProductDTO[] = [] const updateVariantsData: ProductTypes.UpdateProductVariantOnlyDTO[] = []
const productModuleService: ProductTypes.IProductModuleService = const productModuleService: ProductTypes.IProductModuleService =
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName) container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
for (const [productId, variantsData = []] of productVariantsMap) { for (const [product_id, variantsUpdateData = []] of productVariantsMap) {
updateProductsData.push({ updateVariantsData.push(
id: productId, ...(variantsUpdateData as unknown as UpdateProductVariantOnlyDTO[]).map(
variants: variantsData, (update) => ({ ...update, product_id })
}) )
)
productsVariants.push(...variantsData) productsVariants.push(...variantsUpdateData)
} }
if (updateProductsData.length) { if (updateVariantsData.length) {
await productModuleService.update(updateProductsData) await productModuleService.updateVariants(updateVariantsData)
} }
return productsVariants return productsVariants