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:
@@ -9,7 +9,10 @@ import productSeeder from "../../../../helpers/product-seeder"
|
|||||||
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
|
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
|
||||||
import { Workflows } from "@medusajs/workflows"
|
import { Workflows } from "@medusajs/workflows"
|
||||||
import { AxiosInstance } from "axios"
|
import { AxiosInstance } from "axios"
|
||||||
import { simpleSalesChannelFactory } from "../../../../factories"
|
import {
|
||||||
|
simpleProductFactory,
|
||||||
|
simpleSalesChannelFactory,
|
||||||
|
} from "../../../../factories"
|
||||||
|
|
||||||
jest.setTimeout(5000000)
|
jest.setTimeout(5000000)
|
||||||
|
|
||||||
@@ -428,4 +431,223 @@ describe("/admin/products", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/products/:id", () => {
|
||||||
|
const toUpdateWithSalesChannels = "to-update-with-sales-channels"
|
||||||
|
const toUpdateWithVariants = "to-update-with-variants"
|
||||||
|
const toUpdate = "to-update"
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await productSeeder(dbConnection)
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "Default channel",
|
||||||
|
id: "default-channel",
|
||||||
|
is_default: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "Channel 3",
|
||||||
|
id: "channel-3",
|
||||||
|
is_default: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
await simpleProductFactory(dbConnection, {
|
||||||
|
title: "To update product",
|
||||||
|
id: toUpdate,
|
||||||
|
})
|
||||||
|
|
||||||
|
await simpleProductFactory(dbConnection, {
|
||||||
|
title: "To update product with channels",
|
||||||
|
id: toUpdateWithSalesChannels,
|
||||||
|
sales_channels: [
|
||||||
|
{ name: "channel 1", id: "channel-1" },
|
||||||
|
{ name: "channel 2", id: "channel-2" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "To be added",
|
||||||
|
id: "to-be-added",
|
||||||
|
})
|
||||||
|
|
||||||
|
await simpleProductFactory(dbConnection, {
|
||||||
|
title: "To update product with variants",
|
||||||
|
id: toUpdateWithVariants,
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "variant-1",
|
||||||
|
title: "Variant 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "variant-2",
|
||||||
|
title: "Variant 2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should do a basic product update", async () => {
|
||||||
|
const api = useApi()! as AxiosInstance
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
title: "New title",
|
||||||
|
description: "test-product-description",
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.post(`/admin/products/${toUpdate}`, payload, adminHeaders)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response?.status).toEqual(200)
|
||||||
|
expect(response?.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: toUpdate,
|
||||||
|
title: "New title",
|
||||||
|
description: "test-product-description",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should update product and also update a variant and create a variant", async () => {
|
||||||
|
const api = useApi()! as AxiosInstance
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
title: "New title",
|
||||||
|
description: "test-product-description",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "variant-1",
|
||||||
|
title: "Variant 1 updated",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Variant 3",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.post(`/admin/products/${toUpdateWithVariants}`, payload, adminHeaders)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response?.status).toEqual(200)
|
||||||
|
expect(response?.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: toUpdateWithVariants,
|
||||||
|
title: "New title",
|
||||||
|
description: "test-product-description",
|
||||||
|
variants: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "variant-1",
|
||||||
|
title: "Variant 1 updated",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Variant 3",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should update product's sales channels", async () => {
|
||||||
|
const api = useApi()! as AxiosInstance
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
title: "New title",
|
||||||
|
description: "test-product-description",
|
||||||
|
sales_channels: [{ id: "channel-2" }, { id: "channel-3" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.post(
|
||||||
|
`/admin/products/${toUpdateWithSalesChannels}`,
|
||||||
|
payload,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response?.status).toEqual(200)
|
||||||
|
expect(response?.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: toUpdateWithSalesChannels,
|
||||||
|
sales_channels: [
|
||||||
|
expect.objectContaining({ id: "channel-2" }),
|
||||||
|
expect.objectContaining({ id: "channel-3" }),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should update inventory when variants are updated", async () => {
|
||||||
|
const api = useApi()! as AxiosInstance
|
||||||
|
|
||||||
|
const variantInventoryService = medusaContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
title: "New title",
|
||||||
|
description: "test-product-description",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "variant-1",
|
||||||
|
title: "Variant 1 updated",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Variant 3",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.post(`/admin/products/${toUpdateWithVariants}`, payload, adminHeaders)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
let inventory = await variantInventoryService.listInventoryItemsByVariant(
|
||||||
|
"variant-2"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response?.status).toEqual(200)
|
||||||
|
expect(response?.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: toUpdateWithVariants,
|
||||||
|
title: "New title",
|
||||||
|
description: "test-product-description",
|
||||||
|
variants: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "variant-1",
|
||||||
|
title: "Variant 1 updated",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Variant 3",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(inventory).toEqual([]) // no inventory items for removed variant
|
||||||
|
|
||||||
|
inventory = await variantInventoryService.listInventoryItemsByVariant(
|
||||||
|
response?.data.product.variants.find((v) => v.title === "Variant 3").id
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(inventory).toEqual([
|
||||||
|
expect.objectContaining({ id: expect.any(String) }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import {
|
||||||
|
Handlers,
|
||||||
|
pipe,
|
||||||
|
updateProducts,
|
||||||
|
UpdateProductsActions,
|
||||||
|
} from "@medusajs/workflows"
|
||||||
|
import { WorkflowTypes } from "@medusajs/types"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
import { initDb, useDb } from "../../../../environment-helpers/use-db"
|
||||||
|
import { bootstrapApp } from "../../../../environment-helpers/bootstrap-app"
|
||||||
|
import { simpleProductFactory } from "../../../../factories"
|
||||||
|
|
||||||
|
describe("UpdateProduct workflow", function () {
|
||||||
|
let medusaProcess
|
||||||
|
let dbConnection
|
||||||
|
let medusaContainer
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd } as any)
|
||||||
|
const { container } = await bootstrapApp({ cwd })
|
||||||
|
medusaContainer = container
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
|
||||||
|
medusaProcess.kill()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await simpleProductFactory(dbConnection, {
|
||||||
|
title: "Original title",
|
||||||
|
id: "to-update",
|
||||||
|
variants: [{ id: "original-variant" }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should compensate all the invoke if something fails", async () => {
|
||||||
|
const workflow = updateProducts(medusaContainer)
|
||||||
|
|
||||||
|
workflow.appendAction(
|
||||||
|
"fail_step",
|
||||||
|
UpdateProductsActions.removeInventoryItems,
|
||||||
|
{
|
||||||
|
invoke: pipe({}, async function failStep() {
|
||||||
|
throw new Error(`Failed to update products`)
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
noCompensation: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const input: WorkflowTypes.ProductWorkflow.UpdateProductsWorkflowInputDTO =
|
||||||
|
{
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
id: "to-update",
|
||||||
|
title: "Updated title",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Should be deleted with revert variant",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = medusaContainer.resolve("manager")
|
||||||
|
const context = {
|
||||||
|
manager,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { errors, transaction } = await workflow.run({
|
||||||
|
input,
|
||||||
|
context,
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
action: "fail_step",
|
||||||
|
handlerType: "invoke",
|
||||||
|
error: new Error(`Failed to update products`),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(transaction.getState()).toEqual("reverted")
|
||||||
|
|
||||||
|
let [product] = await Handlers.ProductHandlers.listProducts({
|
||||||
|
container: medusaContainer,
|
||||||
|
context,
|
||||||
|
data: {
|
||||||
|
ids: ["to-update"],
|
||||||
|
config: { listConfig: { relations: ["variants"] } },
|
||||||
|
},
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
expect(product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Original title",
|
||||||
|
id: "to-update",
|
||||||
|
variants: [expect.objectContaining({ id: "original-variant" })],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -35,6 +35,7 @@ module.exports = {
|
|||||||
featureFlags: {
|
featureFlags: {
|
||||||
workflows: {
|
workflows: {
|
||||||
[Workflows.CreateProducts]: true,
|
[Workflows.CreateProducts]: true,
|
||||||
|
[Workflows.UpdateProducts]: true,
|
||||||
[Workflows.CreateCart]: true,
|
[Workflows.CreateCart]: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
CREATED: "inventory-item.created",
|
||||||
UPDATED: "inventory-item.updated",
|
UPDATED: "inventory-item.updated",
|
||||||
DELETED: "inventory-item.deleted",
|
DELETED: "inventory-item.deleted",
|
||||||
|
RESTORED: "inventory-item.restored",
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly manager_: EntityManager
|
protected readonly manager_: EntityManager
|
||||||
@@ -213,4 +214,27 @@ export default class InventoryItemService {
|
|||||||
ids: inventoryItemId,
|
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",
|
CREATED: "inventory-level.created",
|
||||||
UPDATED: "inventory-level.updated",
|
UPDATED: "inventory-level.updated",
|
||||||
DELETED: "inventory-level.deleted",
|
DELETED: "inventory-level.deleted",
|
||||||
|
RESTORED: "inventory-level.restored",
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly manager_: EntityManager
|
protected readonly manager_: EntityManager
|
||||||
@@ -231,13 +232,37 @@ export default class InventoryLevelService {
|
|||||||
const manager = context.transactionManager!
|
const manager = context.transactionManager!
|
||||||
const levelRepository = manager.getRepository(InventoryLevel)
|
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, {
|
await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
|
||||||
inventory_item_id: inventoryItemId,
|
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.
|
* Deletes an inventory level by ID.
|
||||||
* @param inventoryLevelId - The ID or IDs of the inventory level to delete.
|
* @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 manager = context.transactionManager!
|
||||||
const levelRepository = manager.getRepository(InventoryLevel)
|
const levelRepository = manager.getRepository(InventoryLevel)
|
||||||
|
|
||||||
await levelRepository.delete({ id: In(ids) })
|
await levelRepository.softDelete({ id: In(ids) })
|
||||||
|
|
||||||
await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
|
await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
|
||||||
ids: inventoryLevelId,
|
ids: inventoryLevelId,
|
||||||
@@ -277,7 +302,7 @@ export default class InventoryLevelService {
|
|||||||
|
|
||||||
const ids = Array.isArray(locationId) ? locationId : [locationId]
|
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, {
|
await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
|
||||||
location_ids: ids,
|
location_ids: ids,
|
||||||
|
|||||||
@@ -376,6 +376,27 @@ export default class InventoryService implements IInventoryService {
|
|||||||
return await this.inventoryItemService_.delete(inventoryItemId, context)
|
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(
|
@InjectEntityManager(
|
||||||
(target) =>
|
(target) =>
|
||||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
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 {
|
import {
|
||||||
IsArray,
|
IsArray,
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
@@ -11,7 +15,13 @@ import {
|
|||||||
ValidateIf,
|
ValidateIf,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
import { EntityManager } from "typeorm"
|
||||||
|
|
||||||
|
import {
|
||||||
|
defaultAdminProductFields,
|
||||||
|
defaultAdminProductRelations,
|
||||||
|
defaultAdminProductRemoteQueryObject,
|
||||||
|
} from "."
|
||||||
import { ProductStatus, ProductVariant } from "../../../../models"
|
import { ProductStatus, ProductVariant } from "../../../../models"
|
||||||
import {
|
import {
|
||||||
PricingService,
|
PricingService,
|
||||||
@@ -35,15 +45,13 @@ import {
|
|||||||
revertVariantTransaction,
|
revertVariantTransaction,
|
||||||
} from "./transaction/create-product-variant"
|
} from "./transaction/create-product-variant"
|
||||||
|
|
||||||
import { DistributedTransaction } from "@medusajs/orchestration"
|
import { IInventoryService, WorkflowTypes } from "@medusajs/types"
|
||||||
import { IInventoryService } from "@medusajs/types"
|
|
||||||
import { MedusaError } from "@medusajs/utils"
|
|
||||||
import { Type } from "class-transformer"
|
|
||||||
import { EntityManager } from "typeorm"
|
|
||||||
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
|
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
|
||||||
import { ProductVariantRepository } from "../../../../repositories/product-variant"
|
import { ProductVariantRepository } from "../../../../repositories/product-variant"
|
||||||
import { Logger } from "../../../../types/global"
|
import { Logger } from "../../../../types/global"
|
||||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||||
|
|
||||||
|
import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain"
|
||||||
import { validator } from "../../../../utils/validator"
|
import { validator } from "../../../../utils/validator"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,134 +137,192 @@ export default async (req, res) => {
|
|||||||
req.scope.resolve("inventoryService")
|
req.scope.resolve("inventoryService")
|
||||||
|
|
||||||
const manager: EntityManager = req.scope.resolve("manager")
|
const manager: EntityManager = req.scope.resolve("manager")
|
||||||
await manager.transaction(async (transactionManager) => {
|
|
||||||
const productServiceTx = productService.withTransaction(transactionManager)
|
|
||||||
|
|
||||||
const { variants } = validated
|
const productModuleService = req.scope.resolve("productModuleService")
|
||||||
delete validated.variants
|
|
||||||
|
|
||||||
const product = await productServiceTx.update(id, validated)
|
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
|
||||||
|
const isWorkflowEnabled = featureFlagRouter.isFeatureEnabled({
|
||||||
|
workflows: Workflows.UpdateProducts,
|
||||||
|
})
|
||||||
|
|
||||||
if (!variants) {
|
if (isWorkflowEnabled && !productModuleService) {
|
||||||
return
|
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 { result } = await updateProductWorkflow.run({
|
||||||
const productVariants = await productVariantService
|
input,
|
||||||
.withTransaction(transactionManager)
|
context: {
|
||||||
.list(
|
manager: manager,
|
||||||
{ product_id: id },
|
},
|
||||||
{
|
})
|
||||||
select: variantRepo.metadata.columns.map(
|
} else {
|
||||||
(c) => c.propertyName
|
await manager.transaction(async (transactionManager) => {
|
||||||
) as (keyof ProductVariant)[],
|
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]))
|
// Will be used to find the variants that should be removed during the next steps
|
||||||
const variantWithIdSet = new Set()
|
variantWithIdSet.add(variant.id)
|
||||||
|
|
||||||
const variantIdsNotBelongingToProduct: string[] = []
|
if (!productVariantMap.has(variant.id)) {
|
||||||
const variantsToUpdate: {
|
variantIdsNotBelongingToProduct.push(variant.id)
|
||||||
variant: ProductVariant
|
continue
|
||||||
updateData: UpdateProductVariantInput
|
}
|
||||||
}[] = []
|
|
||||||
const variantsToCreate: ProductVariantReq[] = []
|
|
||||||
|
|
||||||
// Preparing the data step
|
const productVariant = productVariantMap.get(variant.id)!
|
||||||
for (const [variantRank, variant] of variants.entries()) {
|
|
||||||
if (!variant.id) {
|
|
||||||
Object.assign(variant, {
|
Object.assign(variant, {
|
||||||
variant_rank: variantRank,
|
variant_rank: variantRank,
|
||||||
options: variant.options || [],
|
product_id: productVariant.product_id,
|
||||||
prices: variant.prices || [],
|
|
||||||
})
|
})
|
||||||
variantsToCreate.push(variant)
|
variantsToUpdate.push({ variant: productVariant, updateData: variant })
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Will be used to find the variants that should be removed during the next steps
|
if (variantIdsNotBelongingToProduct.length) {
|
||||||
variantWithIdSet.add(variant.id)
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
if (!productVariantMap.has(variant.id)) {
|
`Variants with id: ${variantIdsNotBelongingToProduct.join(
|
||||||
variantIdsNotBelongingToProduct.push(variant.id)
|
", "
|
||||||
continue
|
)} are not associated with this product`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const productVariant = productVariantMap.get(variant.id)!
|
const allVariantTransactions: DistributedTransaction[] = []
|
||||||
Object.assign(variant, {
|
const transactionDependencies = {
|
||||||
variant_rank: variantRank,
|
manager: transactionManager,
|
||||||
product_id: productVariant.product_id,
|
inventoryService,
|
||||||
})
|
productVariantInventoryService,
|
||||||
variantsToUpdate.push({ variant: productVariant, updateData: variant })
|
productVariantService,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (variantIdsNotBelongingToProduct.length) {
|
const productVariantServiceTx =
|
||||||
throw new MedusaError(
|
productVariantService.withTransaction(transactionManager)
|
||||||
MedusaError.Types.NOT_FOUND,
|
|
||||||
`Variants with id: ${variantIdsNotBelongingToProduct.join(
|
// Delete the variant that does not exist anymore from the provided variants
|
||||||
", "
|
const variantIdsToDelete = [...productVariantMap.keys()].filter(
|
||||||
)} are not associated with this product`
|
(variantId) => !variantWithIdSet.has(variantId)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
const allVariantTransactions: DistributedTransaction[] = []
|
if (variantIdsToDelete) {
|
||||||
const transactionDependencies = {
|
await productVariantServiceTx.delete(variantIdsToDelete)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const rawProduct = await productService.retrieve(id, {
|
if (variantsToUpdate.length) {
|
||||||
select: defaultAdminProductFields,
|
await productVariantServiceTx.update(variantsToUpdate)
|
||||||
relations: defaultAdminProductRelations,
|
}
|
||||||
})
|
|
||||||
|
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])
|
const [product] = await pricingService.setProductPrices([rawProduct])
|
||||||
|
|
||||||
res.json({ product })
|
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 {
|
class ProductVariantOptionReq {
|
||||||
@IsString()
|
@IsString()
|
||||||
value: string
|
value: string
|
||||||
|
|||||||
@@ -998,7 +998,7 @@ export default class ProductModuleService<
|
|||||||
|
|
||||||
if (productVariantIdsToDelete.length) {
|
if (productVariantIdsToDelete.length) {
|
||||||
promises.push(
|
promises.push(
|
||||||
this.productVariantService_.delete(
|
this.productVariantService_.softDelete(
|
||||||
productVariantIdsToDelete,
|
productVariantIdsToDelete,
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
@@ -1152,6 +1152,33 @@ export default class ProductModuleService<
|
|||||||
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
|
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_")
|
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||||
async restore_(
|
async restore_(
|
||||||
productIds: string[],
|
productIds: string[],
|
||||||
|
|||||||
@@ -154,4 +154,24 @@ export default class ProductVariantService<
|
|||||||
transactionManager: sharedContext.transactionManager,
|
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
|
context?: SharedContext
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
|
||||||
|
restoreInventoryItem(
|
||||||
|
inventoryItemId: string | string[],
|
||||||
|
context?: SharedContext
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
deleteInventoryItemLevelByLocationId(
|
deleteInventoryItemLevelByLocationId(
|
||||||
locationId: string | string[],
|
locationId: string | string[],
|
||||||
context?: SharedContext
|
context?: SharedContext
|
||||||
|
|||||||
@@ -2433,4 +2433,10 @@ export interface IProductModuleService {
|
|||||||
config?: RestoreReturn<TReturnableLinkableKeys>,
|
config?: RestoreReturn<TReturnableLinkableKeys>,
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<Record<string, string[]> | void>
|
): 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
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CraeteProductProductCategoryInputDTO {
|
export interface CreateProductProductCategoryInputDTO {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export interface CreateProductVariantPricesInputDTO {
|
|||||||
max_quantity?: number
|
max_quantity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CraeteProductVariantOptionInputDTO {
|
export interface CreteProductVariantOptionInputDTO {
|
||||||
value: string
|
value: string
|
||||||
option_id: string
|
option_id: string
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ export interface CreateProductVariantInputDTO {
|
|||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
|
|
||||||
prices?: CreateProductVariantPricesInputDTO[]
|
prices?: CreateProductVariantPricesInputDTO[]
|
||||||
options?: CraeteProductVariantOptionInputDTO[]
|
options?: CreteProductVariantOptionInputDTO[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateProductInputDTO {
|
export interface CreateProductInputDTO {
|
||||||
@@ -73,7 +73,7 @@ export interface CreateProductInputDTO {
|
|||||||
type?: CreateProductTypeInputDTO
|
type?: CreateProductTypeInputDTO
|
||||||
collection_id?: string
|
collection_id?: string
|
||||||
tags?: CreateProductTagInputDTO[]
|
tags?: CreateProductTagInputDTO[]
|
||||||
categories?: CraeteProductProductCategoryInputDTO[]
|
categories?: CreateProductProductCategoryInputDTO[]
|
||||||
options?: CreateProductOptionInputDTO[]
|
options?: CreateProductOptionInputDTO[]
|
||||||
variants?: CreateProductVariantInputDTO[]
|
variants?: CreateProductVariantInputDTO[]
|
||||||
weight?: number
|
weight?: number
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from "./create-products"
|
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 "./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 {
|
export enum Workflows {
|
||||||
// Product workflows
|
// Product workflows
|
||||||
CreateProducts = "create-products",
|
CreateProducts = "create-products",
|
||||||
|
UpdateProducts = "update-products",
|
||||||
|
|
||||||
// Cart workflows
|
// Cart workflows
|
||||||
CreateCart = "create-cart",
|
CreateCart = "create-cart",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export async function attachInventoryItems({
|
|||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
|
|
||||||
if (!data?.inventoryItems?.length) {
|
if (!data?.inventoryItems?.length) {
|
||||||
return
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const inventoryData = data.inventoryItems.map(({ tag, inventoryItem }) => ({
|
const inventoryData = data.inventoryItems.map(({ tag, inventoryItem }) => ({
|
||||||
@@ -25,7 +25,9 @@ export async function attachInventoryItems({
|
|||||||
inventoryItemId: inventoryItem.id,
|
inventoryItemId: inventoryItem.id,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return await productVariantInventoryService.attachInventoryItem(inventoryData)
|
await productVariantInventoryService.attachInventoryItem(inventoryData)
|
||||||
|
|
||||||
|
return data.inventoryItems
|
||||||
}
|
}
|
||||||
|
|
||||||
attachInventoryItems.aliases = {
|
attachInventoryItems.aliases = {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function createInventoryItems({
|
|||||||
return void 0
|
return void 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await Promise.all(
|
return await Promise.all(
|
||||||
data.inventoryItems.map(async (item) => {
|
data.inventoryItems.map(async (item) => {
|
||||||
const inventoryItem = await inventoryService!.createInventoryItem({
|
const inventoryItem = await inventoryService!.createInventoryItem({
|
||||||
sku: item.sku!,
|
sku: item.sku!,
|
||||||
@@ -40,8 +40,6 @@ export async function createInventoryItems({
|
|||||||
return { tag: item._associationTag ?? inventoryItem.id, inventoryItem }
|
return { tag: item._associationTag ?? inventoryItem.id, inventoryItem }
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createInventoryItems.aliases = {
|
createInventoryItems.aliases = {
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ export async function detachInventoryItems({
|
|||||||
tag: string
|
tag: string
|
||||||
inventoryItem: InventoryItemDTO
|
inventoryItem: InventoryItemDTO
|
||||||
}[]
|
}[]
|
||||||
}>): Promise<void> {
|
}>) {
|
||||||
const { manager } = context
|
const { manager } = context
|
||||||
|
|
||||||
const productVariantInventoryService = container
|
const productVariantInventoryService = container
|
||||||
.resolve("productVariantInventoryService")
|
.resolve("productVariantInventoryService")
|
||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
|
|
||||||
if (!data?.inventoryItems.length) {
|
if (!data?.inventoryItems?.length) {
|
||||||
return
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -29,6 +29,8 @@ export async function detachInventoryItems({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return data.inventoryItems
|
||||||
}
|
}
|
||||||
|
|
||||||
detachInventoryItems.aliases = {
|
detachInventoryItems.aliases = {
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from "./detach-inventory-items"
|
|||||||
export * from "./attach-inventory-items"
|
export * from "./attach-inventory-items"
|
||||||
export * from "./create-inventory-items"
|
export * from "./create-inventory-items"
|
||||||
export * from "./remove-inventory-items"
|
export * from "./remove-inventory-items"
|
||||||
|
export * from "./restore-inventory-items"
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ export async function removeInventoryItems({
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
`Inventory service not found. You should install the @medusajs/inventory package to use inventory. The 'removeInventoryItems' will be skipped.`
|
`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)
|
data.inventoryItems.map(({ inventoryItem }) => inventoryItem.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return data.inventoryItems
|
||||||
}
|
}
|
||||||
|
|
||||||
removeInventoryItems.aliases = {
|
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 "./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 "./remove-products"
|
||||||
export * from "./attach-shipping-profile-to-products"
|
export * from "./attach-shipping-profile-to-products"
|
||||||
export * from "./list-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"
|
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