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:
Frane Polić
2023-10-19 14:02:40 +02:00
committed by GitHub
parent 3aba6269ed
commit aba9ded2a3
34 changed files with 1511 additions and 126 deletions

View File

@@ -1 +1,2 @@
export * from "./create-products"
export * from "./update-products"

View 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)

View File

@@ -1,6 +1,7 @@
export enum Workflows {
// Product workflows
CreateProducts = "create-products",
UpdateProducts = "update-products",
// Cart workflows
CreateCart = "create-cart",

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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"

View File

@@ -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 = {

View File

@@ -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",
}

View File

@@ -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",
}

View File

@@ -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"

View 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),
}
}
}

View File

@@ -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",
}

View File

@@ -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",
}

View File

@@ -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",
}

View File

@@ -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"

View File

@@ -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",
}

View File

@@ -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",
}

View 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",
}