feat(workflows): update product workflow (#4982)
**What** - added "update product" workflow Co-authored-by: Riqwan Thamir <5105988+riqwan@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class ScopeLevelUnique1697708391459 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "UQ_inventory_level_inventory_item_id_location_id"`
|
||||
)
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX "UQ_inventory_level_inventory_item_id_location_id" ON "inventory_level" ("inventory_item_id", "location_id") WHERE deleted_at IS NULL;
|
||||
`)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX "UQ_inventory_level_inventory_item_id_location_id" ON "inventory_level" ("inventory_item_id", "location_id");
|
||||
`)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ export default class InventoryItemService {
|
||||
CREATED: "inventory-item.created",
|
||||
UPDATED: "inventory-item.updated",
|
||||
DELETED: "inventory-item.deleted",
|
||||
RESTORED: "inventory-item.restored",
|
||||
}
|
||||
|
||||
protected readonly manager_: EntityManager
|
||||
@@ -213,4 +214,27 @@ export default class InventoryItemService {
|
||||
ids: inventoryItemId,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inventoryItemId - The id of the inventory item to restore.
|
||||
* @param context
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
async restore(
|
||||
inventoryItemId: string | string[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<void> {
|
||||
const manager = context.transactionManager!
|
||||
const itemRepository = manager.getRepository(InventoryItem)
|
||||
|
||||
const ids = Array.isArray(inventoryItemId)
|
||||
? inventoryItemId
|
||||
: [inventoryItemId]
|
||||
|
||||
await itemRepository.restore({ id: In(ids) })
|
||||
|
||||
await this.eventBusService_?.emit?.(InventoryItemService.Events.RESTORED, {
|
||||
ids: inventoryItemId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export default class InventoryLevelService {
|
||||
CREATED: "inventory-level.created",
|
||||
UPDATED: "inventory-level.updated",
|
||||
DELETED: "inventory-level.deleted",
|
||||
RESTORED: "inventory-level.restored",
|
||||
}
|
||||
|
||||
protected readonly manager_: EntityManager
|
||||
@@ -231,13 +232,37 @@ export default class InventoryLevelService {
|
||||
const manager = context.transactionManager!
|
||||
const levelRepository = manager.getRepository(InventoryLevel)
|
||||
|
||||
await levelRepository.delete({ inventory_item_id: In(ids) })
|
||||
await levelRepository.softDelete({ inventory_item_id: In(ids) })
|
||||
|
||||
await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
|
||||
inventory_item_id: inventoryItemId,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores inventory levels by inventory Item ID.
|
||||
* @param inventoryItemId - The ID or IDs of the inventory item to restore inventory levels for.
|
||||
* @param context
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
async restoreByInventoryItemId(
|
||||
inventoryItemId: string | string[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<void> {
|
||||
const ids = Array.isArray(inventoryItemId)
|
||||
? inventoryItemId
|
||||
: [inventoryItemId]
|
||||
|
||||
const manager = context.transactionManager!
|
||||
const levelRepository = manager.getRepository(InventoryLevel)
|
||||
|
||||
await levelRepository.restore({ inventory_item_id: In(ids) })
|
||||
|
||||
await this.eventBusService_?.emit?.(InventoryLevelService.Events.RESTORED, {
|
||||
inventory_item_id: inventoryItemId,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an inventory level by ID.
|
||||
* @param inventoryLevelId - The ID or IDs of the inventory level to delete.
|
||||
@@ -255,7 +280,7 @@ export default class InventoryLevelService {
|
||||
const manager = context.transactionManager!
|
||||
const levelRepository = manager.getRepository(InventoryLevel)
|
||||
|
||||
await levelRepository.delete({ id: In(ids) })
|
||||
await levelRepository.softDelete({ id: In(ids) })
|
||||
|
||||
await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
|
||||
ids: inventoryLevelId,
|
||||
@@ -277,7 +302,7 @@ export default class InventoryLevelService {
|
||||
|
||||
const ids = Array.isArray(locationId) ? locationId : [locationId]
|
||||
|
||||
await levelRepository.delete({ location_id: In(ids) })
|
||||
await levelRepository.softDelete({ location_id: In(ids) })
|
||||
|
||||
await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
|
||||
location_ids: ids,
|
||||
|
||||
@@ -376,6 +376,27 @@ export default class InventoryService implements IInventoryService {
|
||||
return await this.inventoryItemService_.delete(inventoryItemId, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore an inventory item and levels
|
||||
* @param inventoryItemId - the id of the inventory item to delete
|
||||
* @param context
|
||||
*/
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async restoreInventoryItem(
|
||||
inventoryItemId: string | string[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<void> {
|
||||
await this.inventoryLevelService_.restoreByInventoryItemId(
|
||||
inventoryItemId,
|
||||
context
|
||||
)
|
||||
|
||||
return await this.inventoryItemService_.restore(inventoryItemId, context)
|
||||
}
|
||||
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { DistributedTransaction } from "@medusajs/orchestration"
|
||||
import { FlagRouter, MedusaError } from "@medusajs/utils"
|
||||
import { Workflows, updateProducts } from "@medusajs/workflows"
|
||||
import { Type } from "class-transformer"
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
@@ -11,7 +15,13 @@ import {
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
import {
|
||||
defaultAdminProductFields,
|
||||
defaultAdminProductRelations,
|
||||
defaultAdminProductRemoteQueryObject,
|
||||
} from "."
|
||||
import { ProductStatus, ProductVariant } from "../../../../models"
|
||||
import {
|
||||
PricingService,
|
||||
@@ -35,15 +45,13 @@ import {
|
||||
revertVariantTransaction,
|
||||
} from "./transaction/create-product-variant"
|
||||
|
||||
import { DistributedTransaction } from "@medusajs/orchestration"
|
||||
import { IInventoryService } from "@medusajs/types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { Type } from "class-transformer"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { IInventoryService, WorkflowTypes } from "@medusajs/types"
|
||||
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
|
||||
import { ProductVariantRepository } from "../../../../repositories/product-variant"
|
||||
import { Logger } from "../../../../types/global"
|
||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||
|
||||
import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
@@ -129,134 +137,192 @@ export default async (req, res) => {
|
||||
req.scope.resolve("inventoryService")
|
||||
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
const productServiceTx = productService.withTransaction(transactionManager)
|
||||
|
||||
const { variants } = validated
|
||||
delete validated.variants
|
||||
const productModuleService = req.scope.resolve("productModuleService")
|
||||
|
||||
const product = await productServiceTx.update(id, validated)
|
||||
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
|
||||
const isWorkflowEnabled = featureFlagRouter.isFeatureEnabled({
|
||||
workflows: Workflows.UpdateProducts,
|
||||
})
|
||||
|
||||
if (!variants) {
|
||||
return
|
||||
if (isWorkflowEnabled && !productModuleService) {
|
||||
logger.warn(
|
||||
`Cannot run ${Workflows.UpdateProducts} workflow without '@medusajs/product' installed`
|
||||
)
|
||||
}
|
||||
|
||||
if (isWorkflowEnabled && !!productModuleService) {
|
||||
const updateProductWorkflow = updateProducts(req.scope)
|
||||
|
||||
const input = {
|
||||
products: [
|
||||
{ id, ...validated },
|
||||
] as WorkflowTypes.ProductWorkflow.UpdateProductInputDTO[],
|
||||
}
|
||||
|
||||
const variantRepo = manager.withRepository(productVariantRepo)
|
||||
const productVariants = await productVariantService
|
||||
.withTransaction(transactionManager)
|
||||
.list(
|
||||
{ product_id: id },
|
||||
{
|
||||
select: variantRepo.metadata.columns.map(
|
||||
(c) => c.propertyName
|
||||
) as (keyof ProductVariant)[],
|
||||
const { result } = await updateProductWorkflow.run({
|
||||
input,
|
||||
context: {
|
||||
manager: manager,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
const productServiceTx =
|
||||
productService.withTransaction(transactionManager)
|
||||
|
||||
const { variants } = validated
|
||||
delete validated.variants
|
||||
|
||||
const product = await productServiceTx.update(id, validated)
|
||||
|
||||
if (!variants) {
|
||||
return
|
||||
}
|
||||
|
||||
const variantRepo = manager.withRepository(productVariantRepo)
|
||||
const productVariants = await productVariantService
|
||||
.withTransaction(transactionManager)
|
||||
.list(
|
||||
{ product_id: id },
|
||||
{
|
||||
select: variantRepo.metadata.columns.map(
|
||||
(c) => c.propertyName
|
||||
) as (keyof ProductVariant)[],
|
||||
}
|
||||
)
|
||||
|
||||
const productVariantMap = new Map(productVariants.map((v) => [v.id, v]))
|
||||
const variantWithIdSet = new Set()
|
||||
|
||||
const variantIdsNotBelongingToProduct: string[] = []
|
||||
const variantsToUpdate: {
|
||||
variant: ProductVariant
|
||||
updateData: UpdateProductVariantInput
|
||||
}[] = []
|
||||
const variantsToCreate: ProductVariantReq[] = []
|
||||
|
||||
// Preparing the data step
|
||||
for (const [variantRank, variant] of variants.entries()) {
|
||||
if (!variant.id) {
|
||||
Object.assign(variant, {
|
||||
variant_rank: variantRank,
|
||||
options: variant.options || [],
|
||||
prices: variant.prices || [],
|
||||
})
|
||||
variantsToCreate.push(variant)
|
||||
continue
|
||||
}
|
||||
)
|
||||
|
||||
const productVariantMap = new Map(productVariants.map((v) => [v.id, v]))
|
||||
const variantWithIdSet = new Set()
|
||||
// Will be used to find the variants that should be removed during the next steps
|
||||
variantWithIdSet.add(variant.id)
|
||||
|
||||
const variantIdsNotBelongingToProduct: string[] = []
|
||||
const variantsToUpdate: {
|
||||
variant: ProductVariant
|
||||
updateData: UpdateProductVariantInput
|
||||
}[] = []
|
||||
const variantsToCreate: ProductVariantReq[] = []
|
||||
if (!productVariantMap.has(variant.id)) {
|
||||
variantIdsNotBelongingToProduct.push(variant.id)
|
||||
continue
|
||||
}
|
||||
|
||||
// Preparing the data step
|
||||
for (const [variantRank, variant] of variants.entries()) {
|
||||
if (!variant.id) {
|
||||
const productVariant = productVariantMap.get(variant.id)!
|
||||
Object.assign(variant, {
|
||||
variant_rank: variantRank,
|
||||
options: variant.options || [],
|
||||
prices: variant.prices || [],
|
||||
product_id: productVariant.product_id,
|
||||
})
|
||||
variantsToCreate.push(variant)
|
||||
continue
|
||||
variantsToUpdate.push({ variant: productVariant, updateData: variant })
|
||||
}
|
||||
|
||||
// Will be used to find the variants that should be removed during the next steps
|
||||
variantWithIdSet.add(variant.id)
|
||||
|
||||
if (!productVariantMap.has(variant.id)) {
|
||||
variantIdsNotBelongingToProduct.push(variant.id)
|
||||
continue
|
||||
if (variantIdsNotBelongingToProduct.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Variants with id: ${variantIdsNotBelongingToProduct.join(
|
||||
", "
|
||||
)} are not associated with this product`
|
||||
)
|
||||
}
|
||||
|
||||
const productVariant = productVariantMap.get(variant.id)!
|
||||
Object.assign(variant, {
|
||||
variant_rank: variantRank,
|
||||
product_id: productVariant.product_id,
|
||||
})
|
||||
variantsToUpdate.push({ variant: productVariant, updateData: variant })
|
||||
}
|
||||
const allVariantTransactions: DistributedTransaction[] = []
|
||||
const transactionDependencies = {
|
||||
manager: transactionManager,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
productVariantService,
|
||||
}
|
||||
|
||||
if (variantIdsNotBelongingToProduct.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Variants with id: ${variantIdsNotBelongingToProduct.join(
|
||||
", "
|
||||
)} are not associated with this product`
|
||||
const productVariantServiceTx =
|
||||
productVariantService.withTransaction(transactionManager)
|
||||
|
||||
// Delete the variant that does not exist anymore from the provided variants
|
||||
const variantIdsToDelete = [...productVariantMap.keys()].filter(
|
||||
(variantId) => !variantWithIdSet.has(variantId)
|
||||
)
|
||||
}
|
||||
|
||||
const allVariantTransactions: DistributedTransaction[] = []
|
||||
const transactionDependencies = {
|
||||
manager: transactionManager,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
productVariantService,
|
||||
}
|
||||
|
||||
const productVariantServiceTx =
|
||||
productVariantService.withTransaction(transactionManager)
|
||||
|
||||
// Delete the variant that does not exist anymore from the provided variants
|
||||
const variantIdsToDelete = [...productVariantMap.keys()].filter(
|
||||
(variantId) => !variantWithIdSet.has(variantId)
|
||||
)
|
||||
|
||||
if (variantIdsToDelete) {
|
||||
await productVariantServiceTx.delete(variantIdsToDelete)
|
||||
}
|
||||
|
||||
if (variantsToUpdate.length) {
|
||||
await productVariantServiceTx.update(variantsToUpdate)
|
||||
}
|
||||
|
||||
if (variantsToCreate.length) {
|
||||
try {
|
||||
const varTransaction = await createVariantsTransaction(
|
||||
transactionDependencies,
|
||||
product.id,
|
||||
variantsToCreate as CreateProductVariantInput[]
|
||||
)
|
||||
allVariantTransactions.push(varTransaction)
|
||||
} catch (e) {
|
||||
await Promise.all(
|
||||
allVariantTransactions.map(async (transaction) => {
|
||||
await revertVariantTransaction(
|
||||
transactionDependencies,
|
||||
transaction
|
||||
).catch(() => logger.warn("Transaction couldn't be reverted."))
|
||||
})
|
||||
)
|
||||
|
||||
throw e
|
||||
if (variantIdsToDelete) {
|
||||
await productVariantServiceTx.delete(variantIdsToDelete)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const rawProduct = await productService.retrieve(id, {
|
||||
select: defaultAdminProductFields,
|
||||
relations: defaultAdminProductRelations,
|
||||
})
|
||||
if (variantsToUpdate.length) {
|
||||
await productVariantServiceTx.update(variantsToUpdate)
|
||||
}
|
||||
|
||||
if (variantsToCreate.length) {
|
||||
try {
|
||||
const varTransaction = await createVariantsTransaction(
|
||||
transactionDependencies,
|
||||
product.id,
|
||||
variantsToCreate as CreateProductVariantInput[]
|
||||
)
|
||||
allVariantTransactions.push(varTransaction)
|
||||
} catch (e) {
|
||||
await Promise.all(
|
||||
allVariantTransactions.map(async (transaction) => {
|
||||
await revertVariantTransaction(
|
||||
transactionDependencies,
|
||||
transaction
|
||||
).catch(() => logger.warn("Transaction couldn't be reverted."))
|
||||
})
|
||||
)
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let rawProduct
|
||||
|
||||
if (featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key)) {
|
||||
rawProduct = await getProductWithIsolatedProductModule(req, id)
|
||||
} else {
|
||||
rawProduct = await productService.retrieve(id, {
|
||||
select: defaultAdminProductFields,
|
||||
relations: defaultAdminProductRelations,
|
||||
})
|
||||
}
|
||||
|
||||
const [product] = await pricingService.setProductPrices([rawProduct])
|
||||
|
||||
res.json({ product })
|
||||
}
|
||||
|
||||
async function getProductWithIsolatedProductModule(req, id) {
|
||||
// TODO: Add support for fields/expands
|
||||
const remoteQuery = req.scope.resolve("remoteQuery")
|
||||
|
||||
const variables = { id }
|
||||
|
||||
const query = {
|
||||
product: {
|
||||
__args: variables,
|
||||
...defaultAdminProductRemoteQueryObject,
|
||||
},
|
||||
}
|
||||
|
||||
const [product] = await remoteQuery(query)
|
||||
|
||||
product.profile_id = product.profile?.id
|
||||
|
||||
return product
|
||||
}
|
||||
|
||||
class ProductVariantOptionReq {
|
||||
@IsString()
|
||||
value: string
|
||||
|
||||
@@ -998,7 +998,7 @@ export default class ProductModuleService<
|
||||
|
||||
if (productVariantIdsToDelete.length) {
|
||||
promises.push(
|
||||
this.productVariantService_.delete(
|
||||
this.productVariantService_.softDelete(
|
||||
productVariantIdsToDelete,
|
||||
sharedContext
|
||||
)
|
||||
@@ -1152,6 +1152,33 @@ export default class ProductModuleService<
|
||||
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async restoreVariants<
|
||||
TReturnableLinkableKeys extends string = Lowercase<
|
||||
keyof typeof LinkableKeys
|
||||
>
|
||||
>(
|
||||
variantIds: string[],
|
||||
{ returnLinkableKeys }: RestoreReturn<TReturnableLinkableKeys> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<Record<Lowercase<keyof typeof LinkableKeys>, string[]> | void> {
|
||||
const [_, cascadedEntitiesMap] = await this.productVariantService_.restore(
|
||||
variantIds,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
let mappedCascadedEntitiesMap
|
||||
if (returnLinkableKeys) {
|
||||
mappedCascadedEntitiesMap = mapObjectTo<
|
||||
Record<Lowercase<keyof typeof LinkableKeys>, string[]>
|
||||
>(cascadedEntitiesMap, entityNameToLinkableKeysMap, {
|
||||
pick: returnLinkableKeys,
|
||||
})
|
||||
}
|
||||
|
||||
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async restore_(
|
||||
productIds: string[],
|
||||
|
||||
@@ -154,4 +154,24 @@ export default class ProductVariantService<
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productVariantRepository_")
|
||||
async softDelete(
|
||||
ids: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
await this.productVariantRepository_.softDelete(ids, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productVariantRepository_")
|
||||
async restore(
|
||||
ids: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], Record<string, unknown[]>]> {
|
||||
return await this.productVariantRepository_.restore(ids, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,11 @@ export interface IInventoryService {
|
||||
context?: SharedContext
|
||||
): Promise<void>
|
||||
|
||||
restoreInventoryItem(
|
||||
inventoryItemId: string | string[],
|
||||
context?: SharedContext
|
||||
): Promise<void>
|
||||
|
||||
deleteInventoryItemLevelByLocationId(
|
||||
locationId: string | string[],
|
||||
context?: SharedContext
|
||||
|
||||
@@ -2433,4 +2433,10 @@ export interface IProductModuleService {
|
||||
config?: RestoreReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
|
||||
restoreVariants<TReturnableLinkableKeys extends string = string>(
|
||||
variantIds: string[],
|
||||
config?: RestoreReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface CreateProductSalesChannelInputDTO {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface CraeteProductProductCategoryInputDTO {
|
||||
export interface CreateProductProductCategoryInputDTO {
|
||||
id: string
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface CreateProductVariantPricesInputDTO {
|
||||
max_quantity?: number
|
||||
}
|
||||
|
||||
export interface CraeteProductVariantOptionInputDTO {
|
||||
export interface CreteProductVariantOptionInputDTO {
|
||||
value: string
|
||||
option_id: string
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export interface CreateProductVariantInputDTO {
|
||||
metadata?: Record<string, unknown>
|
||||
|
||||
prices?: CreateProductVariantPricesInputDTO[]
|
||||
options?: CraeteProductVariantOptionInputDTO[]
|
||||
options?: CreteProductVariantOptionInputDTO[]
|
||||
}
|
||||
|
||||
export interface CreateProductInputDTO {
|
||||
@@ -73,7 +73,7 @@ export interface CreateProductInputDTO {
|
||||
type?: CreateProductTypeInputDTO
|
||||
collection_id?: string
|
||||
tags?: CreateProductTagInputDTO[]
|
||||
categories?: CraeteProductProductCategoryInputDTO[]
|
||||
categories?: CreateProductProductCategoryInputDTO[]
|
||||
options?: CreateProductOptionInputDTO[]
|
||||
variants?: CreateProductVariantInputDTO[]
|
||||
weight?: number
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./create-products"
|
||||
export * from "./update-products"
|
||||
|
||||
88
packages/types/src/workflow/product/update-products.ts
Normal file
88
packages/types/src/workflow/product/update-products.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ProductStatus } from "../../product"
|
||||
|
||||
export interface UpdateProductTypeInputDTO {
|
||||
id?: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface UpdateProductTagInputDTO {
|
||||
id?: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface UpdateProductSalesChannelInputDTO {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface UpdateProductProductCategoryInputDTO {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface UpdateProductVariantPricesInputDTO {
|
||||
id?: string
|
||||
region_id?: string
|
||||
currency_code?: string
|
||||
amount: number
|
||||
min_quantity?: number
|
||||
max_quantity?: number
|
||||
}
|
||||
|
||||
export interface UpdateProductVariantOptionInputDTO {
|
||||
value: string
|
||||
option_id: string
|
||||
}
|
||||
|
||||
export interface UpdateProductVariantInputDTO {
|
||||
id?: string
|
||||
title?: string
|
||||
sku?: string
|
||||
ean?: string
|
||||
upc?: string
|
||||
barcode?: string
|
||||
hs_code?: string
|
||||
inventory_quantity?: number
|
||||
allow_backorder?: boolean
|
||||
manage_inventory?: boolean
|
||||
weight?: number
|
||||
length?: number
|
||||
height?: number
|
||||
width?: number
|
||||
origin_country?: string
|
||||
mid_code?: string
|
||||
material?: string
|
||||
metadata?: Record<string, unknown>
|
||||
|
||||
prices?: UpdateProductVariantPricesInputDTO[]
|
||||
options?: UpdateProductVariantOptionInputDTO[]
|
||||
}
|
||||
|
||||
export interface UpdateProductInputDTO {
|
||||
id: string
|
||||
title?: string
|
||||
subtitle?: string
|
||||
description?: string
|
||||
discountable?: boolean
|
||||
images?: string[]
|
||||
thumbnail?: string
|
||||
handle?: string
|
||||
status?: ProductStatus
|
||||
type?: UpdateProductTypeInputDTO
|
||||
collection_id?: string
|
||||
tags?: UpdateProductTagInputDTO[]
|
||||
sales_channels?: UpdateProductSalesChannelInputDTO[]
|
||||
categories?: UpdateProductProductCategoryInputDTO[]
|
||||
variants?: UpdateProductVariantInputDTO[]
|
||||
weight?: number
|
||||
length?: number
|
||||
width?: number
|
||||
height?: number
|
||||
hs_code?: string
|
||||
origin_country?: string
|
||||
mid_code?: string
|
||||
material?: string
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface UpdateProductsWorkflowInputDTO {
|
||||
products: UpdateProductInputDTO[]
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./create-products"
|
||||
export * from "./update-products"
|
||||
|
||||
364
packages/workflows/src/definition/product/update-products.ts
Normal file
364
packages/workflows/src/definition/product/update-products.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
import { ProductTypes, WorkflowTypes } from "@medusajs/types"
|
||||
|
||||
import { InputAlias, Workflows } from "../../definitions"
|
||||
import {
|
||||
TransactionStepsDefinition,
|
||||
WorkflowManager,
|
||||
} from "@medusajs/orchestration"
|
||||
import { exportWorkflow, pipe } from "../../helper"
|
||||
import { CreateProductsActions } from "./create-products"
|
||||
import { InventoryHandlers, ProductHandlers } from "../../handlers"
|
||||
import * as MiddlewareHandlers from "../../handlers/middlewares"
|
||||
import { detachSalesChannelFromProducts } from "../../handlers/product"
|
||||
import { prepareCreateInventoryItems } from "./prepare-create-inventory-items"
|
||||
|
||||
export enum UpdateProductsActions {
|
||||
prepare = "prepare",
|
||||
updateProducts = "updateProducts",
|
||||
|
||||
attachSalesChannels = "attachSalesChannels",
|
||||
detachSalesChannels = "detachSalesChannels",
|
||||
|
||||
createInventoryItems = "createInventoryItems",
|
||||
attachInventoryItems = "attachInventoryItems",
|
||||
detachInventoryItems = "detachInventoryItems",
|
||||
removeInventoryItems = "removeInventoryItems",
|
||||
}
|
||||
|
||||
export const updateProductsWorkflowSteps: TransactionStepsDefinition = {
|
||||
next: {
|
||||
action: CreateProductsActions.prepare,
|
||||
noCompensation: true,
|
||||
next: {
|
||||
action: UpdateProductsActions.updateProducts,
|
||||
next: [
|
||||
{
|
||||
action: UpdateProductsActions.attachSalesChannels,
|
||||
saveResponse: false,
|
||||
},
|
||||
{
|
||||
action: UpdateProductsActions.detachSalesChannels,
|
||||
saveResponse: false,
|
||||
},
|
||||
{
|
||||
// for created variants
|
||||
action: UpdateProductsActions.createInventoryItems,
|
||||
next: {
|
||||
action: UpdateProductsActions.attachInventoryItems,
|
||||
},
|
||||
},
|
||||
{
|
||||
// for deleted variants
|
||||
action: UpdateProductsActions.detachInventoryItems,
|
||||
next: {
|
||||
action: UpdateProductsActions.removeInventoryItems,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const handlers = new Map([
|
||||
[
|
||||
UpdateProductsActions.prepare,
|
||||
{
|
||||
invoke: pipe(
|
||||
{
|
||||
merge: true,
|
||||
inputAlias: InputAlias.ProductsInputData,
|
||||
invoke: {
|
||||
from: InputAlias.ProductsInputData,
|
||||
},
|
||||
},
|
||||
ProductHandlers.updateProductsPrepareData
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
UpdateProductsActions.updateProducts,
|
||||
{
|
||||
invoke: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: [
|
||||
{
|
||||
from: InputAlias.ProductsInputData,
|
||||
alias: ProductHandlers.updateProducts.aliases.products,
|
||||
},
|
||||
{
|
||||
from: UpdateProductsActions.prepare,
|
||||
},
|
||||
],
|
||||
},
|
||||
ProductHandlers.updateProducts
|
||||
),
|
||||
compensate: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: [
|
||||
{
|
||||
from: UpdateProductsActions.prepare,
|
||||
alias: ProductHandlers.revertUpdateProducts.aliases.preparedData,
|
||||
},
|
||||
{
|
||||
from: UpdateProductsActions.updateProducts,
|
||||
alias:
|
||||
MiddlewareHandlers.updateProductsExtractDeletedVariants.aliases
|
||||
.products,
|
||||
},
|
||||
],
|
||||
},
|
||||
MiddlewareHandlers.updateProductsExtractDeletedVariants,
|
||||
ProductHandlers.revertUpdateProducts
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
UpdateProductsActions.attachSalesChannels,
|
||||
{
|
||||
invoke: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: [
|
||||
{
|
||||
from: UpdateProductsActions.prepare,
|
||||
alias: "preparedData",
|
||||
},
|
||||
{
|
||||
from: UpdateProductsActions.updateProducts,
|
||||
alias:
|
||||
ProductHandlers.attachSalesChannelToProducts.aliases.products,
|
||||
},
|
||||
],
|
||||
},
|
||||
MiddlewareHandlers.mapData((d) => ({
|
||||
productsHandleSalesChannelsMap:
|
||||
d.preparedData.productHandleAddedChannelsMap,
|
||||
})),
|
||||
ProductHandlers.attachSalesChannelToProducts
|
||||
),
|
||||
compensate: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: [
|
||||
{
|
||||
from: UpdateProductsActions.prepare,
|
||||
alias: "preparedData",
|
||||
},
|
||||
{
|
||||
from: UpdateProductsActions.updateProducts,
|
||||
alias: detachSalesChannelFromProducts.aliases.products,
|
||||
},
|
||||
],
|
||||
},
|
||||
MiddlewareHandlers.mapData((d) => ({
|
||||
productsHandleSalesChannelsMap:
|
||||
d.preparedData.productHandleAddedChannelsMap,
|
||||
})),
|
||||
ProductHandlers.detachSalesChannelFromProducts
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
UpdateProductsActions.detachSalesChannels,
|
||||
{
|
||||
invoke: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: [
|
||||
{
|
||||
from: UpdateProductsActions.prepare,
|
||||
alias: "preparedData",
|
||||
},
|
||||
{
|
||||
from: UpdateProductsActions.updateProducts,
|
||||
alias:
|
||||
ProductHandlers.detachSalesChannelFromProducts.aliases.products,
|
||||
},
|
||||
],
|
||||
},
|
||||
MiddlewareHandlers.mapData((d) => ({
|
||||
productsHandleSalesChannelsMap:
|
||||
d.preparedData.productHandleRemovedChannelsMap,
|
||||
})),
|
||||
ProductHandlers.detachSalesChannelFromProducts
|
||||
),
|
||||
compensate: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: [
|
||||
{
|
||||
from: UpdateProductsActions.prepare,
|
||||
alias: "preparedData",
|
||||
},
|
||||
{
|
||||
from: UpdateProductsActions.updateProducts,
|
||||
alias:
|
||||
ProductHandlers.attachSalesChannelToProducts.aliases.products,
|
||||
},
|
||||
],
|
||||
},
|
||||
MiddlewareHandlers.mapData((d) => ({
|
||||
productsHandleSalesChannelsMap:
|
||||
d.preparedData.productHandleRemovedChannelsMap,
|
||||
})),
|
||||
ProductHandlers.attachSalesChannelToProducts
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
UpdateProductsActions.createInventoryItems,
|
||||
{
|
||||
invoke: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: [
|
||||
{
|
||||
from: UpdateProductsActions.prepare,
|
||||
alias:
|
||||
MiddlewareHandlers.updateProductsExtractCreatedVariants.aliases
|
||||
.preparedData,
|
||||
},
|
||||
{
|
||||
from: UpdateProductsActions.updateProducts,
|
||||
alias:
|
||||
MiddlewareHandlers.updateProductsExtractCreatedVariants.aliases
|
||||
.products,
|
||||
},
|
||||
],
|
||||
},
|
||||
MiddlewareHandlers.updateProductsExtractCreatedVariants,
|
||||
prepareCreateInventoryItems,
|
||||
InventoryHandlers.createInventoryItems
|
||||
),
|
||||
compensate: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: {
|
||||
from: UpdateProductsActions.createInventoryItems,
|
||||
alias:
|
||||
InventoryHandlers.removeInventoryItems.aliases.inventoryItems,
|
||||
},
|
||||
},
|
||||
InventoryHandlers.removeInventoryItems
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
UpdateProductsActions.attachInventoryItems,
|
||||
{
|
||||
invoke: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: {
|
||||
from: UpdateProductsActions.createInventoryItems,
|
||||
alias:
|
||||
InventoryHandlers.attachInventoryItems.aliases.inventoryItems,
|
||||
},
|
||||
},
|
||||
InventoryHandlers.attachInventoryItems
|
||||
),
|
||||
compensate: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: {
|
||||
from: UpdateProductsActions.attachInventoryItems,
|
||||
alias:
|
||||
InventoryHandlers.detachInventoryItems.aliases.inventoryItems,
|
||||
},
|
||||
},
|
||||
InventoryHandlers.detachInventoryItems
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
UpdateProductsActions.detachInventoryItems,
|
||||
{
|
||||
invoke: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: [
|
||||
{
|
||||
from: UpdateProductsActions.prepare,
|
||||
alias:
|
||||
MiddlewareHandlers.updateProductsExtractDeletedVariants.aliases
|
||||
.preparedData,
|
||||
},
|
||||
{
|
||||
from: UpdateProductsActions.updateProducts,
|
||||
alias:
|
||||
MiddlewareHandlers.updateProductsExtractDeletedVariants.aliases
|
||||
.products,
|
||||
},
|
||||
],
|
||||
},
|
||||
MiddlewareHandlers.updateProductsExtractDeletedVariants,
|
||||
MiddlewareHandlers.useVariantsInventoryItems,
|
||||
InventoryHandlers.detachInventoryItems
|
||||
),
|
||||
compensate: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: [
|
||||
{
|
||||
from: UpdateProductsActions.prepare,
|
||||
alias:
|
||||
MiddlewareHandlers.updateProductsExtractDeletedVariants.aliases
|
||||
.preparedData,
|
||||
},
|
||||
{
|
||||
from: UpdateProductsActions.updateProducts,
|
||||
alias:
|
||||
MiddlewareHandlers.updateProductsExtractDeletedVariants.aliases
|
||||
.products,
|
||||
},
|
||||
],
|
||||
},
|
||||
MiddlewareHandlers.updateProductsExtractDeletedVariants,
|
||||
MiddlewareHandlers.useVariantsInventoryItems,
|
||||
InventoryHandlers.attachInventoryItems
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
UpdateProductsActions.removeInventoryItems,
|
||||
{
|
||||
invoke: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: {
|
||||
from: UpdateProductsActions.detachInventoryItems,
|
||||
alias:
|
||||
InventoryHandlers.removeInventoryItems.aliases.inventoryItems,
|
||||
},
|
||||
},
|
||||
InventoryHandlers.removeInventoryItems
|
||||
),
|
||||
compensate: pipe(
|
||||
{
|
||||
merge: true,
|
||||
invoke: [
|
||||
{
|
||||
from: UpdateProductsActions.removeInventoryItems,
|
||||
alias:
|
||||
InventoryHandlers.restoreInventoryItems.aliases.inventoryItems,
|
||||
},
|
||||
],
|
||||
},
|
||||
InventoryHandlers.restoreInventoryItems
|
||||
),
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
WorkflowManager.register(
|
||||
Workflows.UpdateProducts,
|
||||
updateProductsWorkflowSteps,
|
||||
handlers
|
||||
)
|
||||
|
||||
export const updateProducts = exportWorkflow<
|
||||
WorkflowTypes.ProductWorkflow.UpdateProductsWorkflowInputDTO,
|
||||
ProductTypes.ProductDTO[]
|
||||
>(Workflows.UpdateProducts, UpdateProductsActions.updateProducts)
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum Workflows {
|
||||
// Product workflows
|
||||
CreateProducts = "create-products",
|
||||
UpdateProducts = "update-products",
|
||||
|
||||
// Cart workflows
|
||||
CreateCart = "create-cart",
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function attachInventoryItems({
|
||||
.withTransaction(manager)
|
||||
|
||||
if (!data?.inventoryItems?.length) {
|
||||
return
|
||||
return []
|
||||
}
|
||||
|
||||
const inventoryData = data.inventoryItems.map(({ tag, inventoryItem }) => ({
|
||||
@@ -25,7 +25,9 @@ export async function attachInventoryItems({
|
||||
inventoryItemId: inventoryItem.id,
|
||||
}))
|
||||
|
||||
return await productVariantInventoryService.attachInventoryItem(inventoryData)
|
||||
await productVariantInventoryService.attachInventoryItem(inventoryData)
|
||||
|
||||
return data.inventoryItems
|
||||
}
|
||||
|
||||
attachInventoryItems.aliases = {
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function createInventoryItems({
|
||||
return void 0
|
||||
}
|
||||
|
||||
const result = await Promise.all(
|
||||
return await Promise.all(
|
||||
data.inventoryItems.map(async (item) => {
|
||||
const inventoryItem = await inventoryService!.createInventoryItem({
|
||||
sku: item.sku!,
|
||||
@@ -40,8 +40,6 @@ export async function createInventoryItems({
|
||||
return { tag: item._associationTag ?? inventoryItem.id, inventoryItem }
|
||||
})
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
createInventoryItems.aliases = {
|
||||
|
||||
@@ -10,15 +10,15 @@ export async function detachInventoryItems({
|
||||
tag: string
|
||||
inventoryItem: InventoryItemDTO
|
||||
}[]
|
||||
}>): Promise<void> {
|
||||
}>) {
|
||||
const { manager } = context
|
||||
|
||||
const productVariantInventoryService = container
|
||||
.resolve("productVariantInventoryService")
|
||||
.withTransaction(manager)
|
||||
|
||||
if (!data?.inventoryItems.length) {
|
||||
return
|
||||
if (!data?.inventoryItems?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
@@ -29,6 +29,8 @@ export async function detachInventoryItems({
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
return data.inventoryItems
|
||||
}
|
||||
|
||||
detachInventoryItems.aliases = {
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./detach-inventory-items"
|
||||
export * from "./attach-inventory-items"
|
||||
export * from "./create-inventory-items"
|
||||
export * from "./remove-inventory-items"
|
||||
export * from "./restore-inventory-items"
|
||||
|
||||
@@ -14,12 +14,14 @@ export async function removeInventoryItems({
|
||||
logger.warn(
|
||||
`Inventory service not found. You should install the @medusajs/inventory package to use inventory. The 'removeInventoryItems' will be skipped.`
|
||||
)
|
||||
return
|
||||
return []
|
||||
}
|
||||
|
||||
return await inventoryService!.deleteInventoryItem(
|
||||
await inventoryService!.deleteInventoryItem(
|
||||
data.inventoryItems.map(({ inventoryItem }) => inventoryItem.id)
|
||||
)
|
||||
|
||||
return data.inventoryItems
|
||||
}
|
||||
|
||||
removeInventoryItems.aliases = {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
IInventoryService,
|
||||
InventoryItemDTO,
|
||||
SharedContext,
|
||||
} from "@medusajs/types"
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
export async function restoreInventoryItems({
|
||||
container,
|
||||
context,
|
||||
data,
|
||||
}: WorkflowArguments<{
|
||||
inventoryItems: { inventoryItem: InventoryItemDTO }[]
|
||||
}>) {
|
||||
const { manager } = context as SharedContext
|
||||
const inventoryService: IInventoryService =
|
||||
container.resolve("inventoryService")
|
||||
|
||||
if (!inventoryService) {
|
||||
const logger = container.resolve("logger")
|
||||
logger.warn(
|
||||
`Inventory service not found. You should install the @medusajs/inventory package to use inventory. The 'removeInventoryItems' will be skipped.`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
return await inventoryService!.restoreInventoryItem(
|
||||
data.inventoryItems.map(({ inventoryItem }) => inventoryItem.id),
|
||||
{ transactionManager: manager }
|
||||
)
|
||||
}
|
||||
|
||||
restoreInventoryItems.aliases = {
|
||||
inventoryItems: "inventoryItems",
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
export async function extractVariants({
|
||||
data,
|
||||
}: WorkflowArguments<{
|
||||
object: { variants?: ProductTypes.ProductVariantDTO[] }[]
|
||||
}>) {
|
||||
const variants = data.object.reduce((acc, object) => {
|
||||
if (object.variants?.length) {
|
||||
return acc.concat(object.variants)
|
||||
}
|
||||
return acc
|
||||
}, [] as ProductTypes.ProductVariantDTO[])
|
||||
|
||||
return {
|
||||
alias: extractVariants.aliases.output,
|
||||
value: {
|
||||
variants,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
extractVariants.aliases = {
|
||||
output: "extractVariantsFromProductOutput",
|
||||
object: "object",
|
||||
}
|
||||
@@ -1 +1,6 @@
|
||||
export * from "./create-products-prepare-create-prices-compensation"
|
||||
export * from "./update-products-extract-created-variants"
|
||||
export * from "./update-products-extract-deleted-variants"
|
||||
export * from "./use-variants-inventory-items"
|
||||
export * from "./extract-variants"
|
||||
export * from "./map-data"
|
||||
|
||||
16
packages/workflows/src/handlers/middlewares/map-data.ts
Normal file
16
packages/workflows/src/handlers/middlewares/map-data.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
/**
|
||||
* Middleware for map input data to a key/s.
|
||||
*
|
||||
* @param mapFn - apply function on the input data and return result as the middleware output
|
||||
* @param alias - key to save output under (if `merge === false`)
|
||||
*/
|
||||
export function mapData<T, S>(mapFn: (arg: T) => S, alias = "mapData") {
|
||||
return async function ({ data }: WorkflowArguments<T>) {
|
||||
return {
|
||||
alias,
|
||||
value: mapFn(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ProductTypes, ProductVariantDTO } from "@medusajs/types"
|
||||
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
import { UpdateProductsPreparedData } from "../product"
|
||||
|
||||
export async function updateProductsExtractCreatedVariants({
|
||||
data,
|
||||
}: WorkflowArguments<{
|
||||
preparedData: UpdateProductsPreparedData // products state before the update
|
||||
products: ProductTypes.ProductDTO[] // updated products
|
||||
}>) {
|
||||
const createdVariants: ProductVariantDTO[] = []
|
||||
|
||||
data.products.forEach((product) => {
|
||||
const addedVariants: ProductVariantDTO[] = []
|
||||
|
||||
const originalProduct = data.preparedData.originalProducts.find(
|
||||
(p) => p.id === product.id
|
||||
)!
|
||||
|
||||
product.variants.forEach((variant) => {
|
||||
if (!originalProduct.variants.find((v) => v.id === variant.id)) {
|
||||
addedVariants.push(variant)
|
||||
}
|
||||
})
|
||||
|
||||
createdVariants.push(...addedVariants)
|
||||
})
|
||||
|
||||
return {
|
||||
alias: updateProductsExtractCreatedVariants.aliases.output,
|
||||
value: [{ variants: createdVariants }],
|
||||
}
|
||||
}
|
||||
|
||||
updateProductsExtractCreatedVariants.aliases = {
|
||||
preparedData: "preparedData",
|
||||
products: "products",
|
||||
output: "products",
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ProductTypes, ProductVariantDTO } from "@medusajs/types"
|
||||
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
import { UpdateProductsPreparedData } from "../product"
|
||||
|
||||
export async function updateProductsExtractDeletedVariants({
|
||||
data,
|
||||
container,
|
||||
}: WorkflowArguments<{
|
||||
preparedData: UpdateProductsPreparedData // products state before the update
|
||||
products: ProductTypes.ProductDTO[] // updated products
|
||||
}>) {
|
||||
const deletedVariants: ProductVariantDTO[] = []
|
||||
|
||||
data.products.forEach((product) => {
|
||||
const removedVariants: ProductVariantDTO[] = []
|
||||
|
||||
const originalProduct = data.preparedData.originalProducts.find(
|
||||
(p) => p.id === product.id
|
||||
)!
|
||||
|
||||
originalProduct.variants.forEach((variant) => {
|
||||
if (!product.variants.find((v) => v.id === variant.id)) {
|
||||
removedVariants.push(variant)
|
||||
}
|
||||
})
|
||||
|
||||
deletedVariants.push(...removedVariants)
|
||||
})
|
||||
|
||||
return {
|
||||
alias: updateProductsExtractDeletedVariants.aliases.output,
|
||||
value: {
|
||||
variants: deletedVariants,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
updateProductsExtractDeletedVariants.aliases = {
|
||||
preparedData: "preparedData",
|
||||
products: "products",
|
||||
output: "updateProductsExtractDeletedVariantsOutput",
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
import { IInventoryService, ProductVariantDTO } from "@medusajs/types"
|
||||
|
||||
export async function useVariantsInventoryItems({
|
||||
data,
|
||||
container,
|
||||
}: WorkflowArguments<{
|
||||
updateProductsExtractDeletedVariantsOutput: { variants: ProductVariantDTO[] }
|
||||
}>) {
|
||||
const inventoryService: IInventoryService =
|
||||
container.resolve("inventoryService")
|
||||
|
||||
if (!inventoryService) {
|
||||
const logger = container.resolve("logger")
|
||||
logger.warn(
|
||||
`Inventory service not found. You should install the @medusajs/inventory package to use inventory. The 'useVariantsInventoryItems' will be skipped.`
|
||||
)
|
||||
return {
|
||||
alias: useVariantsInventoryItems.aliases.output,
|
||||
value: null,
|
||||
}
|
||||
}
|
||||
|
||||
const [inventoryItems] = await inventoryService!.listInventoryItems({
|
||||
sku: data.updateProductsExtractDeletedVariantsOutput.variants.map(
|
||||
(v) => v.id
|
||||
),
|
||||
})
|
||||
|
||||
const variantItems = inventoryItems.map((item) => ({
|
||||
inventoryItem: item,
|
||||
tag: data.updateProductsExtractDeletedVariantsOutput.variants.find(
|
||||
(variant) => variant.sku === item.sku
|
||||
)!.id,
|
||||
}))
|
||||
|
||||
return {
|
||||
alias: useVariantsInventoryItems.aliases.output,
|
||||
value: { inventoryItems: variantItems },
|
||||
}
|
||||
}
|
||||
|
||||
useVariantsInventoryItems.aliases = {
|
||||
variants: "variants",
|
||||
output: "useVariantsInventoryItemsOutput",
|
||||
}
|
||||
@@ -6,4 +6,7 @@ export * from "./detach-shipping-profile-from-products"
|
||||
export * from "./remove-products"
|
||||
export * from "./attach-shipping-profile-to-products"
|
||||
export * from "./list-products"
|
||||
export * from "./update-products"
|
||||
export * from "./update-products-prepare-data"
|
||||
export * from "./revert-update-products"
|
||||
export * from "./update-products-variants-prices"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ProductDTO,
|
||||
ProductTypes,
|
||||
ProductVariantDTO,
|
||||
UpdateProductDTO,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
import { UpdateProductsPreparedData } from "./update-products-prepare-data"
|
||||
|
||||
type HandlerInput = UpdateProductsPreparedData & {
|
||||
variants: ProductVariantDTO[]
|
||||
}
|
||||
|
||||
export async function revertUpdateProducts({
|
||||
container,
|
||||
data,
|
||||
}: WorkflowArguments<HandlerInput>): Promise<ProductDTO[]> {
|
||||
const productModuleService: ProductTypes.IProductModuleService =
|
||||
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
|
||||
|
||||
// restore variants that have been soft deleted during update products step
|
||||
await productModuleService.restoreVariants(data.variants.map((v) => v.id))
|
||||
data.originalProducts.forEach((product) => {
|
||||
// @ts-ignore
|
||||
product.variants = product.variants.map((v) => ({ id: v.id }))
|
||||
})
|
||||
|
||||
return await productModuleService.update(
|
||||
data.originalProducts as unknown as UpdateProductDTO[]
|
||||
)
|
||||
}
|
||||
|
||||
revertUpdateProducts.aliases = {
|
||||
preparedData: "preparedData",
|
||||
variants: "variants",
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { ProductDTO, SalesChannelDTO, WorkflowTypes } from "@medusajs/types"
|
||||
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
type ProductWithSalesChannelsDTO = ProductDTO & {
|
||||
sales_channels?: SalesChannelDTO[]
|
||||
}
|
||||
|
||||
export type UpdateProductsPreparedData = {
|
||||
originalProducts: ProductWithSalesChannelsDTO[]
|
||||
productHandleAddedChannelsMap: Map<string, string[]>
|
||||
productHandleRemovedChannelsMap: Map<string, string[]>
|
||||
}
|
||||
|
||||
export async function updateProductsPrepareData({
|
||||
container,
|
||||
context,
|
||||
data,
|
||||
}: WorkflowArguments<WorkflowTypes.ProductWorkflow.UpdateProductsWorkflowInputDTO>): Promise<UpdateProductsPreparedData> {
|
||||
const ids = data.products.map((product) => product.id)
|
||||
|
||||
const productHandleAddedChannelsMap = new Map<string, string[]>()
|
||||
const productHandleRemovedChannelsMap = new Map<string, string[]>()
|
||||
|
||||
const productService = container.resolve("productService")
|
||||
const productServiceTx = productService.withTransaction(context.manager)
|
||||
|
||||
const products = await productServiceTx.list(
|
||||
// TODO: use RemoteQuery - sales_channels needs to be added to the joiner config
|
||||
{ id: ids },
|
||||
{
|
||||
relations: [
|
||||
"variants",
|
||||
"variants.options",
|
||||
"images",
|
||||
"options",
|
||||
"tags",
|
||||
"collection",
|
||||
"sales_channels",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
data.products.forEach((productInput) => {
|
||||
const removedChannels: string[] = []
|
||||
const addedChannels: string[] = []
|
||||
|
||||
const currentProduct = products.find(
|
||||
(p) => p.id === productInput.id
|
||||
) as unknown as ProductWithSalesChannelsDTO
|
||||
|
||||
if (productInput.sales_channels) {
|
||||
productInput.sales_channels.forEach((channel) => {
|
||||
if (
|
||||
!currentProduct.sales_channels?.find((sc) => sc.id === channel.id)
|
||||
) {
|
||||
addedChannels.push(channel.id)
|
||||
}
|
||||
})
|
||||
|
||||
currentProduct.sales_channels?.forEach((channel) => {
|
||||
if (!productInput.sales_channels!.find((sc) => sc.id === channel.id)) {
|
||||
removedChannels.push(channel.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
productHandleAddedChannelsMap.set(currentProduct.handle!, addedChannels)
|
||||
productHandleRemovedChannelsMap.set(currentProduct.handle!, removedChannels)
|
||||
})
|
||||
|
||||
return {
|
||||
originalProducts: products,
|
||||
productHandleAddedChannelsMap,
|
||||
productHandleRemovedChannelsMap,
|
||||
}
|
||||
}
|
||||
|
||||
updateProductsPrepareData.aliases = {
|
||||
preparedData: "preparedData",
|
||||
}
|
||||
44
packages/workflows/src/handlers/product/update-products.ts
Normal file
44
packages/workflows/src/handlers/product/update-products.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
|
||||
import { ProductDTO, ProductTypes } from "@medusajs/types"
|
||||
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
type HandlerInput = {
|
||||
products: ProductTypes.UpdateProductDTO[]
|
||||
}
|
||||
|
||||
export async function updateProducts({
|
||||
container,
|
||||
context,
|
||||
data,
|
||||
}: WorkflowArguments<HandlerInput>): Promise<ProductDTO[]> {
|
||||
if (!data.products.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const productModuleService: ProductTypes.IProductModuleService =
|
||||
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
|
||||
|
||||
const products = await productModuleService.update(data.products)
|
||||
|
||||
return await productModuleService.list(
|
||||
{ id: products.map((p) => p.id) },
|
||||
{
|
||||
relations: [
|
||||
"variants",
|
||||
"variants.options",
|
||||
"images",
|
||||
"options",
|
||||
"tags",
|
||||
// "type",
|
||||
"collection",
|
||||
// "profiles",
|
||||
// "sales_channels",
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
updateProducts.aliases = {
|
||||
products: "products",
|
||||
}
|
||||
Reference in New Issue
Block a user