diff --git a/integration-tests/plugins/__tests__/product/admin/index.ts b/integration-tests/plugins/__tests__/product/admin/index.ts index 17af7f87b4..826c55e107 100644 --- a/integration-tests/plugins/__tests__/product/admin/index.ts +++ b/integration-tests/plugins/__tests__/product/admin/index.ts @@ -10,7 +10,7 @@ import { simpleSalesChannelFactory } from "../../../../factories" import { AxiosInstance } from "axios" import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" -jest.setTimeout(50000) +jest.setTimeout(5000000) const adminHeaders = { headers: { diff --git a/packages/medusa/package.json b/packages/medusa/package.json index 0cea6c7958..3685ee5257 100644 --- a/packages/medusa/package.json +++ b/packages/medusa/package.json @@ -51,6 +51,7 @@ "@medusajs/modules-sdk": "^1.9.0", "@medusajs/orchestration": "^0.1.0", "@medusajs/utils": "^1.9.3", + "@medusajs/workflows": "workspace:^", "awilix": "^8.0.0", "body-parser": "^1.19.0", "boxen": "^5.0.1", diff --git a/packages/medusa/src/api/routes/admin/products/create-product.ts b/packages/medusa/src/api/routes/admin/products/create-product.ts index 606e6a9518..693b57c769 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.ts +++ b/packages/medusa/src/api/routes/admin/products/create-product.ts @@ -36,13 +36,13 @@ import { DistributedTransaction } from "@medusajs/orchestration" import { EntityManager } from "typeorm" import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators" import { FlagRouter } from "../../../../utils/flag-router" -import { IInventoryService } from "@medusajs/types" +import { IInventoryService, WorkflowTypes } from "@medusajs/types" import { Logger } from "../../../../types/global" import { ProductStatus } from "../../../../models" import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels" import { Type } from "class-transformer" -import { createProductsWorkflow } from "../../../../workflows/admin/create-products" -import { validator } from "../../../../utils/validator" +import { validator } from "../../../../utils" +import { createProducts } from "@medusajs/workflows" /** * @oas [post] /admin/products @@ -133,13 +133,26 @@ export default async (req, res) => { const productModuleService = req.scope.resolve("productModuleService") if (productModuleService) { - const products = await createProductsWorkflow( - { - container: req.scope, + const createProductWorkflow = createProducts(req.scope) + + const input = { + products: [ + validated, + ] as WorkflowTypes.ProductWorkflow.CreateProductInputDTO[], + config: { + listConfig: { + select: defaultAdminProductFields, + relations: defaultAdminProductRelations, + }, + }, + } + + const { result: products } = await createProductWorkflow.run({ + input, + context: { manager: entityManager, }, - [validated] - ) + }) return res.json({ product: products[0] }) } diff --git a/packages/medusa/src/workflows/admin/create-products/definition.ts b/packages/medusa/src/workflows/admin/create-products/definition.ts deleted file mode 100644 index 4003cecfb8..0000000000 --- a/packages/medusa/src/workflows/admin/create-products/definition.ts +++ /dev/null @@ -1,421 +0,0 @@ -import { - TransactionHandlerType, - TransactionPayload, - TransactionStepHandler, - TransactionStepsDefinition, -} from "@medusajs/orchestration" -import { - IInventoryService, - MedusaContainer, - ProductTypes, -} from "@medusajs/types" -import { - defaultAdminProductFields, - defaultAdminProductRelations, -} from "../../../api" -import { - attachInventoryItems, - attachSalesChannelToProducts, - attachShippingProfileToProducts, - createInventoryItems, - createProducts, - CreateProductsData, - CreateProductsPreparedData, - detachInventoryItems, - detachSalesChannelFromProducts, - detachShippingProfileFromProducts, - prepareCreateProductsData, - removeInventoryItems, - removeProducts, - updateProductsVariantsPrices, -} from "../../functions" -import { PricingService, ProductService } from "../../../services" -import { CreateProductsWorkflowInputData, InjectedDependencies } from "./types" - -export enum CreateProductsWorkflowActions { - prepare = "prepare", - createProducts = "createProducts", - attachToSalesChannel = "attachToSalesChannel", - attachShippingProfile = "attachShippingProfile", - createPrices = "createPrices", - createInventoryItems = "createInventoryItems", - attachInventoryItems = "attachInventoryItems", - result = "result", -} - -export const workflowSteps: TransactionStepsDefinition = { - next: { - action: CreateProductsWorkflowActions.prepare, - noCompensation: true, - next: { - action: CreateProductsWorkflowActions.createProducts, - next: [ - { - action: CreateProductsWorkflowActions.attachShippingProfile, - saveResponse: false, - }, - { - action: CreateProductsWorkflowActions.attachToSalesChannel, - saveResponse: false, - }, - { - action: CreateProductsWorkflowActions.createPrices, - next: { - action: CreateProductsWorkflowActions.createInventoryItems, - next: { - action: CreateProductsWorkflowActions.attachInventoryItems, - next: { - action: CreateProductsWorkflowActions.result, - noCompensation: true, - }, - }, - }, - }, - ], - }, - }, -} - -const shouldSkipInventoryStep = ( - container: MedusaContainer, - stepName: string -) => { - const inventoryService = container.resolve( - "inventoryService" - ) as IInventoryService - if (!inventoryService) { - const logger = container.resolve("logger") - logger.warn( - `Inventory service not found. You should install the @medusajs/inventory package to use inventory. The '${stepName}' will be skipped.` - ) - return true - } - - return false -} - -export function transactionHandler( - dependencies: InjectedDependencies -): TransactionStepHandler { - const { manager, container } = dependencies - - const command = { - [CreateProductsWorkflowActions.prepare]: { - [TransactionHandlerType.INVOKE]: async ( - data: CreateProductsWorkflowInputData - ) => { - return await prepareCreateProductsData({ - container, - manager, - data, - }) - }, - }, - - [CreateProductsWorkflowActions.createProducts]: { - [TransactionHandlerType.INVOKE]: async ( - data: CreateProductsData - ): Promise => { - return await createProducts({ - container, - data, - }) - }, - [TransactionHandlerType.COMPENSATE]: async ( - data: CreateProductsWorkflowInputData, - { invoke } - ) => { - const products = invoke[ - CreateProductsWorkflowActions.createProducts - ] as ProductTypes.ProductDTO[] - - if (!products?.length) { - return - } - - return await removeProducts({ - container, - data: products, - }) - }, - }, - - [CreateProductsWorkflowActions.attachShippingProfile]: { - [TransactionHandlerType.INVOKE]: async ( - data: CreateProductsWorkflowInputData, - { invoke } - ) => { - const products = invoke[ - CreateProductsWorkflowActions.createProducts - ] as ProductTypes.ProductDTO[] - const { productsHandleShippingProfileIdMap } = invoke[ - CreateProductsWorkflowActions.prepare - ] as CreateProductsPreparedData - - return await attachShippingProfileToProducts({ - container, - manager, - data: { - productsHandleShippingProfileIdMap, - products, - }, - }) - }, - [TransactionHandlerType.COMPENSATE]: async ( - data: CreateProductsWorkflowInputData, - { invoke } - ) => { - const products = invoke[ - CreateProductsWorkflowActions.createProducts - ] as ProductTypes.ProductDTO[] - const { productsHandleShippingProfileIdMap } = invoke[ - CreateProductsWorkflowActions.prepare - ] as CreateProductsPreparedData - - return await detachShippingProfileFromProducts({ - container, - manager, - data: { - productsHandleShippingProfileIdMap, - products, - }, - }) - }, - }, - - [CreateProductsWorkflowActions.attachToSalesChannel]: { - [TransactionHandlerType.INVOKE]: async ( - data: CreateProductsWorkflowInputData, - { invoke } - ) => { - const products = invoke[ - CreateProductsWorkflowActions.createProducts - ] as ProductTypes.ProductDTO[] - const { productsHandleSalesChannelsMap } = invoke[ - CreateProductsWorkflowActions.prepare - ] as CreateProductsPreparedData - - return await attachSalesChannelToProducts({ - container, - manager, - data: { - productsHandleSalesChannelsMap, - products, - }, - }) - }, - [TransactionHandlerType.COMPENSATE]: async ( - data: CreateProductsWorkflowInputData, - { invoke } - ) => { - const products = invoke[ - CreateProductsWorkflowActions.createProducts - ] as ProductTypes.ProductDTO[] - const { productsHandleSalesChannelsMap } = invoke[ - CreateProductsWorkflowActions.prepare - ] as CreateProductsPreparedData - - return await detachSalesChannelFromProducts({ - container, - manager, - data: { - productsHandleSalesChannelsMap, - products, - }, - }) - }, - }, - - [CreateProductsWorkflowActions.createInventoryItems]: { - [TransactionHandlerType.INVOKE]: async ( - data: CreateProductsWorkflowInputData, - { invoke } - ) => { - const shouldSkipStep_ = shouldSkipInventoryStep( - container, - CreateProductsWorkflowActions.createInventoryItems - ) - if (shouldSkipStep_) { - return - } - - const { [CreateProductsWorkflowActions.createProducts]: products } = - invoke - - return await createInventoryItems({ - container, - manager, - data: products, - }) - }, - [TransactionHandlerType.COMPENSATE]: async (_, { invoke }) => { - const shouldSkipStep_ = shouldSkipInventoryStep( - container, - CreateProductsWorkflowActions.createInventoryItems - ) - - const variantInventoryItemsData = - invoke[CreateProductsWorkflowActions.createInventoryItems] - - if (shouldSkipStep_ || !variantInventoryItemsData?.length) { - return - } - - await removeInventoryItems({ - container, - manager, - data: variantInventoryItemsData, - }) - }, - }, - - [CreateProductsWorkflowActions.attachInventoryItems]: { - [TransactionHandlerType.INVOKE]: async ( - data: CreateProductsWorkflowInputData, - { invoke } - ) => { - const shouldSkipStep_ = shouldSkipInventoryStep( - container, - CreateProductsWorkflowActions.attachInventoryItems - ) - if (shouldSkipStep_) { - return - } - - const { - [CreateProductsWorkflowActions.createInventoryItems]: - inventoryItemsResult, - } = invoke - - return await attachInventoryItems({ - container, - manager, - data: inventoryItemsResult, - }) - }, - [TransactionHandlerType.COMPENSATE]: async ( - data: CreateProductsWorkflowInputData, - { invoke } - ) => { - const shouldSkipStep_ = shouldSkipInventoryStep( - container, - CreateProductsWorkflowActions.attachInventoryItems - ) - - const { - [CreateProductsWorkflowActions.createInventoryItems]: - inventoryItemsResult, - } = invoke - - if (shouldSkipStep_ || !inventoryItemsResult?.length) { - return - } - - return await detachInventoryItems({ - container, - manager, - data: inventoryItemsResult, - }) - }, - }, - - [CreateProductsWorkflowActions.createPrices]: { - [TransactionHandlerType.INVOKE]: async ( - data: CreateProductsWorkflowInputData, - { invoke } - ) => { - const { productsHandleVariantsIndexPricesMap } = invoke[ - CreateProductsWorkflowActions.prepare - ] as CreateProductsPreparedData - const products = invoke[ - CreateProductsWorkflowActions.createProducts - ] as ProductTypes.ProductDTO[] - - return await updateProductsVariantsPrices({ - container, - manager, - data: { - products, - productsHandleVariantsIndexPricesMap, - }, - }) - }, - [TransactionHandlerType.COMPENSATE]: async ( - data: CreateProductsWorkflowInputData, - { invoke } - ) => { - const { productsHandleVariantsIndexPricesMap } = invoke[ - CreateProductsWorkflowActions.prepare - ] as CreateProductsPreparedData - const products = invoke[ - CreateProductsWorkflowActions.createProducts - ] as ProductTypes.ProductDTO[] - - if (!productsHandleVariantsIndexPricesMap?.size) { - return - } - - const updatedProductsHandleVariantsIndexPricesMap = new Map() - productsHandleVariantsIndexPricesMap.forEach((items, productHandle) => { - const existingItems = - updatedProductsHandleVariantsIndexPricesMap.get(productHandle) ?? [] - - items.forEach(({ index }) => { - existingItems.push({ - index, - prices: [], - }) - }) - - updatedProductsHandleVariantsIndexPricesMap.set(productHandle, items) - }) - - return await updateProductsVariantsPrices({ - container, - manager, - data: { - products, - productsHandleVariantsIndexPricesMap: - updatedProductsHandleVariantsIndexPricesMap, - }, - }) - }, - }, - - [CreateProductsWorkflowActions.result]: { - [TransactionHandlerType.INVOKE]: async ( - data: CreateProductsWorkflowInputData, - { invoke } - ) => { - const { [CreateProductsWorkflowActions.createProducts]: products } = - invoke - - const productService = container.resolve( - "productService" - ) as ProductService - const pricingService = container.resolve( - "pricingService" - ) as PricingService - - const rawProduct = await productService - .withTransaction(manager) - .retrieve(products[0].id, { - select: defaultAdminProductFields, - relations: defaultAdminProductRelations, - }) - - const res = await pricingService - .withTransaction(manager) - .setProductPrices([rawProduct]) - - return res - }, - }, - } - - return ( - actionId: string, - type: TransactionHandlerType, - payload: TransactionPayload - ) => command[actionId][type](payload.data, payload.context) -} diff --git a/packages/medusa/src/workflows/admin/create-products/index.ts b/packages/medusa/src/workflows/admin/create-products/index.ts deleted file mode 100644 index 485ab5a4d9..0000000000 --- a/packages/medusa/src/workflows/admin/create-products/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ulid } from "ulid" -import { MedusaError } from "@medusajs/utils" -import { - TransactionOrchestrator, - TransactionState, -} from "@medusajs/orchestration" -import { AdminPostProductsReq } from "../../../api" -import { Product } from "../../../models" -import { PricedProduct } from "../../../types/pricing" -import { - CreateProductsWorkflowActions, - transactionHandler, - workflowSteps, -} from "./definition" -import { InjectedDependencies } from "./types" - -const createProductsOrchestrator = new TransactionOrchestrator( - "create-products", - workflowSteps -) - -export async function createProductsWorkflow( - dependencies: InjectedDependencies, - input: AdminPostProductsReq[] -): Promise<(Product | PricedProduct)[]> { - const transaction = await createProductsOrchestrator.beginTransaction( - ulid(), - transactionHandler(dependencies), - input - ) - - await createProductsOrchestrator.resume(transaction) - - if (transaction.getState() !== TransactionState.DONE) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - transaction - .getErrors() - .map((err) => err.error?.message) - .join("\n") - ) - } - - return transaction.getContext().invoke[ - CreateProductsWorkflowActions.result - ] as (Product | PricedProduct)[] -} diff --git a/packages/medusa/src/workflows/admin/create-products/types.ts b/packages/medusa/src/workflows/admin/create-products/types.ts deleted file mode 100644 index 2526460d77..0000000000 --- a/packages/medusa/src/workflows/admin/create-products/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { EntityManager } from "typeorm" -import { MedusaContainer } from "@medusajs/types" -import { AdminPostProductsReq } from "../../../api" - -export type InjectedDependencies = { - manager: EntityManager - container: MedusaContainer -} - -export type CreateProductsWorkflowInputData = AdminPostProductsReq[] diff --git a/packages/medusa/src/workflows/functions/attach-inventory-items.ts b/packages/medusa/src/workflows/functions/attach-inventory-items.ts deleted file mode 100644 index e2f749681b..0000000000 --- a/packages/medusa/src/workflows/functions/attach-inventory-items.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - InventoryItemDTO, - MedusaContainer, - ProductTypes, -} from "@medusajs/types" -import { EntityManager } from "typeorm" -import { ProductVariantInventoryService } from "../../services" - -export async function attachInventoryItems({ - container, - manager, - data, -}: { - container: MedusaContainer - manager: EntityManager - data: { - variant: ProductTypes.ProductVariantDTO - inventoryItem: InventoryItemDTO - }[] -}) { - const productVariantInventoryService: ProductVariantInventoryService = - container.resolve("productVariantInventoryService").withTransaction(manager) - - const inventoryData = data.map(({ variant, inventoryItem }) => ({ - variantId: variant.id, - inventoryItemId: inventoryItem.id, - })) - - return await productVariantInventoryService.attachInventoryItem(inventoryData) -} diff --git a/packages/medusa/src/workflows/functions/create-inventory-items.ts b/packages/medusa/src/workflows/functions/create-inventory-items.ts deleted file mode 100644 index 9f7d666085..0000000000 --- a/packages/medusa/src/workflows/functions/create-inventory-items.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - IInventoryService, - MedusaContainer, - ProductTypes, -} from "@medusajs/types" -import { EntityManager } from "typeorm" - -export async function createInventoryItems({ - container, - manager, - data, -}: { - container: MedusaContainer - manager: EntityManager - data: ProductTypes.ProductDTO[] -}) { - const inventoryService: IInventoryService = - container.resolve("inventoryService") - const context = { transactionManager: manager } - - const variants = data.reduce( - ( - acc: ProductTypes.ProductVariantDTO[], - product: ProductTypes.ProductDTO - ) => { - return acc.concat(product.variants) - }, - [] - ) - - return await Promise.all( - variants.map(async (variant) => { - if (!variant.manage_inventory) { - return - } - - const inventoryItem = await inventoryService!.createInventoryItem( - { - sku: variant.sku!, - origin_country: variant.origin_country!, - hs_code: variant.hs_code!, - mid_code: variant.mid_code!, - material: variant.material!, - weight: variant.weight!, - length: variant.length!, - height: variant.height!, - width: variant.width!, - }, - context - ) - - return { variant, inventoryItem } - }) - ) -} diff --git a/packages/medusa/src/workflows/functions/create-prducts.ts b/packages/medusa/src/workflows/functions/create-prducts.ts deleted file mode 100644 index 171e9e3955..0000000000 --- a/packages/medusa/src/workflows/functions/create-prducts.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MedusaContainer, ProductTypes } from "@medusajs/types" - -export type CreateProductsData = ProductTypes.CreateProductDTO[] - -export async function createProducts({ - container, - data, -}: { - container: MedusaContainer - data: CreateProductsData -}): Promise { - const productModuleService: ProductTypes.IProductModuleService = - container.resolve("productModuleService") - - return await productModuleService.create(data) -} diff --git a/packages/medusa/src/workflows/functions/detach-inventory-items.ts b/packages/medusa/src/workflows/functions/detach-inventory-items.ts deleted file mode 100644 index f899e4dfa7..0000000000 --- a/packages/medusa/src/workflows/functions/detach-inventory-items.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - InventoryItemDTO, - MedusaContainer, - ProductTypes, -} from "@medusajs/types" -import { EntityManager } from "typeorm" -import { ProductVariantInventoryService } from "../../services" - -export async function detachInventoryItems({ - container, - manager, - data, -}: { - container: MedusaContainer - manager: EntityManager - data: { - variant: ProductTypes.ProductVariantDTO - inventoryItem: InventoryItemDTO - }[] -}) { - const productVariantInventoryService: ProductVariantInventoryService = - container.resolve("productVariantInventoryService").withTransaction(manager) - - return await Promise.all( - data.map(async ({ variant, inventoryItem }) => { - return await productVariantInventoryService.detachInventoryItem( - inventoryItem.id, - variant.id - ) - }) - ) -} diff --git a/packages/medusa/src/workflows/functions/prepare-create-products-data.ts b/packages/medusa/src/workflows/functions/prepare-create-products-data.ts deleted file mode 100644 index 69efd891de..0000000000 --- a/packages/medusa/src/workflows/functions/prepare-create-products-data.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { MedusaContainer, ProductTypes } from "@medusajs/types" -import { EntityManager } from "typeorm" -import { SalesChannelService, ShippingProfileService } from "../../services" -import { kebabCase } from "@medusajs/utils" -import { FlagRouter } from "../../utils/flag-router" -import SalesChannelFeatureFlag from "../../loaders/feature-flags/sales-channels" -import { SalesChannel, ShippingProfileType } from "../../models" -import { ProductVariantPricesCreateReq } from "../../types/product-variant" -import { AdminPostProductsReq } from "../../api" - -type CreateProductsInputData = AdminPostProductsReq[] - -type ShippingProfileId = string -type SalesChannelId = string -type ProductHandle = string -type VariantIndexAndPrices = { - index: number - prices: ProductVariantPricesCreateReq[] -} - -export type CreateProductsPreparedData = { - products: ProductTypes.CreateProductDTO[] - productsHandleShippingProfileIdMap: Map - productsHandleSalesChannelsMap: Map - productsHandleVariantsIndexPricesMap: Map< - ProductHandle, - VariantIndexAndPrices[] - > -} - -export async function prepareCreateProductsData({ - container, - manager, - data, -}: { - container: MedusaContainer - manager: EntityManager - data: CreateProductsInputData -}): Promise { - const shippingProfileService: ShippingProfileService = container - .resolve("shippingProfileService") - .withTransaction(manager) - const featureFlagRouter: FlagRouter = container.resolve("featureFlagRouter") - const salesChannelService: SalesChannelService = container - .resolve("salesChannelService") - .withTransaction(manager) - - const shippingProfileServiceTx = - shippingProfileService.withTransaction(manager) - - const shippingProfiles = await shippingProfileServiceTx.list({ - type: [ShippingProfileType.DEFAULT, ShippingProfileType.GIFT_CARD], - }) - const defaultShippingProfile = shippingProfiles.find( - (sp) => sp.type === ShippingProfileType.DEFAULT - ) - const gitCardShippingProfile = shippingProfiles.find( - (sp) => sp.type === ShippingProfileType.GIFT_CARD - ) - - let defaultSalesChannel: SalesChannel | undefined - if (featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key)) { - defaultSalesChannel = await salesChannelService - .withTransaction(manager) - .retrieveDefault() - } - - const productsHandleShippingProfileIdMap = new Map< - ProductHandle, - ShippingProfileId - >() - const productsHandleSalesChannelsMap = new Map< - ProductHandle, - SalesChannelId[] - >() - const productsHandleVariantsIndexPricesMap = new Map< - ProductHandle, - VariantIndexAndPrices[] - >() - - for (const product of data) { - product.handle ??= kebabCase(product.title) - - if (product.is_giftcard) { - productsHandleShippingProfileIdMap.set( - product.handle!, - gitCardShippingProfile!.id - ) - } else { - productsHandleShippingProfileIdMap.set( - product.handle!, - defaultShippingProfile!.id - ) - } - - if ( - featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key) && - !product.sales_channels?.length - ) { - productsHandleSalesChannelsMap.set(product.handle!, [ - defaultSalesChannel!.id, - ]) - } else { - productsHandleSalesChannelsMap.set( - product.handle!, - product.sales_channels!.map((s) => s.id) - ) - } - - if (product.variants) { - const hasPrices = product.variants.some((variant) => { - return variant.prices.length > 0 - }) - - if (hasPrices) { - const items = - productsHandleVariantsIndexPricesMap.get(product.handle!) ?? [] - - product.variants.forEach((variant, index) => { - items.push({ - index, - prices: variant.prices, - }) - }) - - productsHandleVariantsIndexPricesMap.set(product.handle!, items) - } - } - } - - data = data.map((productData) => { - delete productData.sales_channels - return productData - }) - - return { - products: data as ProductTypes.CreateProductDTO[], - productsHandleShippingProfileIdMap, - productsHandleSalesChannelsMap, - productsHandleVariantsIndexPricesMap, - } -} diff --git a/packages/medusa/src/workflows/functions/remove-inventory-items.ts b/packages/medusa/src/workflows/functions/remove-inventory-items.ts deleted file mode 100644 index a3bfe3d725..0000000000 --- a/packages/medusa/src/workflows/functions/remove-inventory-items.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { InventoryItemDTO, MedusaContainer } from "@medusajs/types" -import { EntityManager } from "typeorm" - -export async function removeInventoryItems({ - container, - manager, - data, -}: { - container: MedusaContainer - manager: EntityManager - data: { - inventoryItem: InventoryItemDTO - }[] -}) { - const inventoryService = container.resolve("inventoryService") - const context = { transactionManager: manager } - - return await inventoryService!.deleteInventoryItem( - data.map(({ inventoryItem }) => inventoryItem.id), - context - ) -} diff --git a/packages/medusa/src/workflows/functions/remove-products.ts b/packages/medusa/src/workflows/functions/remove-products.ts deleted file mode 100644 index 8a22e10d59..0000000000 --- a/packages/medusa/src/workflows/functions/remove-products.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { MedusaContainer, ProductTypes } from "@medusajs/types" - -export async function removeProducts({ - container, - data, -}: { - container: MedusaContainer - data: ProductTypes.ProductDTO[] -}) { - const productModuleService: ProductTypes.IProductModuleService = - container.resolve("productModuleService") - return await productModuleService.softDelete(data.map((p) => p.id)) -} diff --git a/packages/product/src/models/product.ts b/packages/product/src/models/product.ts index abad79a0cd..58e60bfba7 100644 --- a/packages/product/src/models/product.ts +++ b/packages/product/src/models/product.ts @@ -14,8 +14,12 @@ import { Unique, } from "@mikro-orm/core" -import { ProductTypes } from "@medusajs/types" -import { DALUtils, generateEntityId, kebabCase } from "@medusajs/utils" +import { + DALUtils, + generateEntityId, + kebabCase, + ProductUtils, +} from "@medusajs/utils" import ProductCategory from "./product-category" import ProductCollection from "./product-collection" import ProductOption from "./product-option" @@ -60,8 +64,8 @@ class Product { @Property({ columnType: "boolean", default: false }) is_giftcard!: boolean - @Enum(() => ProductTypes.ProductStatus) - status!: ProductTypes.ProductStatus + @Enum(() => ProductUtils.ProductStatus) + status!: ProductUtils.ProductStatus @Property({ columnType: "text", nullable: true }) thumbnail?: string | null diff --git a/packages/product/src/services/product.ts b/packages/product/src/services/product.ts index e835b1d311..e445ec54fb 100644 --- a/packages/product/src/services/product.ts +++ b/packages/product/src/services/product.ts @@ -3,7 +3,6 @@ import { Context, DAL, FindConfig, - ProductStatus, ProductTypes, WithRequiredProperty, } from "@medusajs/types" @@ -14,6 +13,7 @@ import { MedusaContext, MedusaError, ModulesSdkUtils, + ProductUtils, } from "@medusajs/utils" import { ProductRepository } from "@repositories" @@ -124,7 +124,7 @@ export default class ProductService { @MedusaContext() sharedContext: Context = {} ): Promise { data.forEach((product) => { - product.status ??= ProductStatus.DRAFT + product.status ??= ProductUtils.ProductStatus.DRAFT }) return (await (this.productRepository_ as ProductRepository).create( diff --git a/packages/product/src/types/services/product.ts b/packages/product/src/types/services/product.ts index 9a48927ac0..5f6637838c 100644 --- a/packages/product/src/types/services/product.ts +++ b/packages/product/src/types/services/product.ts @@ -1,4 +1,4 @@ -import { ProductStatus, ProductCategoryDTO } from "@medusajs/types" +import { ProductUtils } from "@medusajs/utils" export type ProductEventData = { id: string @@ -20,7 +20,7 @@ export interface UpdateProductDTO { images?: { id?: string; url: string }[] thumbnail?: string handle?: string - status?: ProductStatus + status?: ProductUtils.ProductStatus collection_id?: string width?: number height?: number diff --git a/packages/types/src/bundles.ts b/packages/types/src/bundles.ts index cc95cd0e83..4982b6d250 100644 --- a/packages/types/src/bundles.ts +++ b/packages/types/src/bundles.ts @@ -9,3 +9,6 @@ export * as StockLocationTypes from "./stock-location" export * as TransactionBaseTypes from "./transaction-base" export * as DAL from "./dal" export * as LoggerTypes from "./logger" +export * as FeatureFlagTypes from "./feature-flag" +export * as SalesChannelTypes from "./sales-channel" +export * as WorkflowTypes from "./workflow" diff --git a/packages/types/src/feature-flag/common.ts b/packages/types/src/feature-flag/common.ts new file mode 100644 index 0000000000..074acba22a --- /dev/null +++ b/packages/types/src/feature-flag/common.ts @@ -0,0 +1,32 @@ +export interface IFlagRouter { + isFeatureEnabled: (key: string) => boolean + listFlags: () => FeatureFlagsResponse +} + +/** + * @schema FeatureFlagsResponse + * type: array + * items: + * type: object + * required: + * - key + * - value + * properties: + * key: + * description: The key of the feature flag. + * type: string + * value: + * description: The value of the feature flag. + * type: boolean + */ +export type FeatureFlagsResponse = { + key: string + value: boolean +}[] + +export type FlagSettings = { + key: string + description: string + env_key: string + default_val: boolean +} diff --git a/packages/types/src/feature-flag/index.ts b/packages/types/src/feature-flag/index.ts new file mode 100644 index 0000000000..488a94fdff --- /dev/null +++ b/packages/types/src/feature-flag/index.ts @@ -0,0 +1 @@ +export * from "./common" diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 85be26902f..86d8dbe658 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -13,3 +13,6 @@ export * from "./stock-location" export * from "./transaction-base" export * from "./dal" export * from "./logger" +export * from "./feature-flag" +export * from "./sales-channel" +export * from "./workflow" diff --git a/packages/types/src/product/common.ts b/packages/types/src/product/common.ts index 43bd8c0ebd..56b4a12ff1 100644 --- a/packages/types/src/product/common.ts +++ b/packages/types/src/product/common.ts @@ -425,7 +425,7 @@ export interface CreateProductVariantOnlyDTO { } export interface UpdateProductVariantOnlyDTO { - id: string, + id: string title?: string sku?: string barcode?: string diff --git a/packages/types/src/sales-channel/common.ts b/packages/types/src/sales-channel/common.ts new file mode 100644 index 0000000000..728640ce24 --- /dev/null +++ b/packages/types/src/sales-channel/common.ts @@ -0,0 +1,13 @@ +export interface SalesChannelLocationDTO { + sales_channel_id: string + location_id: string + sales_channel: SalesChannelDTO +} + +export interface SalesChannelDTO { + id: string + description: string | null + is_disabled: boolean + metadata: Record | null + locations?: SalesChannelLocationDTO[] +} diff --git a/packages/types/src/sales-channel/index.ts b/packages/types/src/sales-channel/index.ts new file mode 100644 index 0000000000..488a94fdff --- /dev/null +++ b/packages/types/src/sales-channel/index.ts @@ -0,0 +1 @@ +export * from "./common" diff --git a/packages/types/src/workflow/common.ts b/packages/types/src/workflow/common.ts new file mode 100644 index 0000000000..0606bc35ec --- /dev/null +++ b/packages/types/src/workflow/common.ts @@ -0,0 +1,10 @@ +export interface WorkflowInputConfig { + listConfig?: { + select?: string[] + relations?: string[] + } + retrieveConfig?: { + select?: string[] + relations?: string[] + } +} diff --git a/packages/types/src/workflow/index.ts b/packages/types/src/workflow/index.ts new file mode 100644 index 0000000000..ee0394daae --- /dev/null +++ b/packages/types/src/workflow/index.ts @@ -0,0 +1,2 @@ +export * as ProductWorkflow from "./product" +export * as CommonWorkflow from "./common" diff --git a/packages/types/src/workflow/product/create-products.ts b/packages/types/src/workflow/product/create-products.ts new file mode 100644 index 0000000000..e587e4c36e --- /dev/null +++ b/packages/types/src/workflow/product/create-products.ts @@ -0,0 +1,100 @@ +import { ProductStatus } from "../../product" +import { WorkflowInputConfig } from "../common" + +export interface CreateProductTypeInputDTO { + id?: string + value: string +} + +export interface CreateProductTagInputDTO { + id?: string + value: string +} + +export interface CreateProductSalesChannelInputDTO { + id: string +} + +export interface CraeteProductProductCategoryInputDTO { + id: string +} + +export interface CreateProductOptionInputDTO { + title: string +} + +export interface CreateProductVariantPricesInputDTO { + id?: string + region_id?: string + currency_code?: string + amount: number + min_quantity?: number + max_quantity?: number +} + +export interface CraeteProductVariantOptionInputDTO { + value: string + option_id: string +} + +export interface CreateProductVariantInputDTO { + 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 + + prices?: CreateProductVariantPricesInputDTO[] + options?: CraeteProductVariantOptionInputDTO[] +} + +export interface CreateProductInputDTO { + title: string + subtitle?: string + description?: string + is_giftcard?: boolean + discountable?: boolean + images?: string[] + thumbnail?: string + handle?: string + status?: ProductStatus + type?: CreateProductTypeInputDTO + collection_id?: string + tags?: CreateProductTagInputDTO[] + categories?: CraeteProductProductCategoryInputDTO[] + options?: CreateProductOptionInputDTO[] + variants?: CreateProductVariantInputDTO[] + weight?: number + length?: number + height?: number + width?: number + hs_code?: string + origin_country?: string + mid_code?: string + material?: string + metadata?: Record + + sales_channels?: CreateProductSalesChannelInputDTO[] + listConfig: { + select: string[] + relations: string[] + } +} + +export interface CreateProductsWorkflowInputDTO { + products: CreateProductInputDTO[] + config?: WorkflowInputConfig +} diff --git a/packages/types/src/workflow/product/index.ts b/packages/types/src/workflow/product/index.ts new file mode 100644 index 0000000000..fa30720e57 --- /dev/null +++ b/packages/types/src/workflow/product/index.ts @@ -0,0 +1 @@ +export * from "./create-products" diff --git a/packages/utils/src/bundles.ts b/packages/utils/src/bundles.ts index d47397631c..d444b56ac9 100644 --- a/packages/utils/src/bundles.ts +++ b/packages/utils/src/bundles.ts @@ -3,3 +3,6 @@ export * as EventBusUtils from "./event-bus" export * as SearchUtils from "./search" export * as ModulesSdkUtils from "./modules-sdk" export * as DALUtils from "./dal" +export * as FeatureFlagUtils from "./feature-flags" +export * as ShippingProfileUtils from "./shipping" +export * as ProductUtils from "./product" diff --git a/packages/utils/src/feature-flags/analytics.ts b/packages/utils/src/feature-flags/analytics.ts new file mode 100644 index 0000000000..46923095fd --- /dev/null +++ b/packages/utils/src/feature-flags/analytics.ts @@ -0,0 +1,9 @@ +import { FeatureFlagTypes } from "@medusajs/types" + +export const AnalyticsFeatureFlag: FeatureFlagTypes.FlagSettings = { + key: "analytics", + default_val: true, + env_key: "MEDUSA_FF_ANALYTICS", + description: + "Enable Medusa to collect data on usage, errors and performance for the purpose of improving the product", +} diff --git a/packages/utils/src/feature-flags/index.ts b/packages/utils/src/feature-flags/index.ts new file mode 100644 index 0000000000..ac515f7beb --- /dev/null +++ b/packages/utils/src/feature-flags/index.ts @@ -0,0 +1,7 @@ +export * from "./analytics" +export * from "./order-editing" +export * from "./sales-channels" +export * from "./product-categories" +export * from "./publishable-api-keys" +export * from "./tax-inclusive-pricing" +export * from "./utils" diff --git a/packages/utils/src/feature-flags/order-editing.ts b/packages/utils/src/feature-flags/order-editing.ts new file mode 100644 index 0000000000..4c697e0f72 --- /dev/null +++ b/packages/utils/src/feature-flags/order-editing.ts @@ -0,0 +1,8 @@ +import { FeatureFlagTypes } from "@medusajs/types" + +export const OrderEditingFeatureFlag: FeatureFlagTypes.FlagSettings = { + key: "order_editing", + default_val: true, + env_key: "MEDUSA_FF_ORDER_EDITING", + description: "[WIP] Enable the order editing feature", +} diff --git a/packages/utils/src/feature-flags/product-categories.ts b/packages/utils/src/feature-flags/product-categories.ts new file mode 100644 index 0000000000..99222691c8 --- /dev/null +++ b/packages/utils/src/feature-flags/product-categories.ts @@ -0,0 +1,8 @@ +import { FeatureFlagTypes } from "@medusajs/types" + +export const ProductCategoryFeatureFlag: FeatureFlagTypes.FlagSettings = { + key: "product_categories", + default_val: false, + env_key: "MEDUSA_FF_PRODUCT_CATEGORIES", + description: "[WIP] Enable the product categories feature", +} diff --git a/packages/utils/src/feature-flags/publishable-api-keys.ts b/packages/utils/src/feature-flags/publishable-api-keys.ts new file mode 100644 index 0000000000..ef4135465f --- /dev/null +++ b/packages/utils/src/feature-flags/publishable-api-keys.ts @@ -0,0 +1,8 @@ +import { FeatureFlagTypes } from "@medusajs/types" + +export const PublishableAPIKeysFeatureFlag: FeatureFlagTypes.FlagSettings = { + key: "publishable_api_keys", + default_val: true, + env_key: "MEDUSA_FF_PUBLISHABLE_API_KEYS", + description: "[WIP] Enable the publishable API keys feature", +} diff --git a/packages/utils/src/feature-flags/sales-channels.ts b/packages/utils/src/feature-flags/sales-channels.ts new file mode 100644 index 0000000000..d4421d65c7 --- /dev/null +++ b/packages/utils/src/feature-flags/sales-channels.ts @@ -0,0 +1,8 @@ +import { FeatureFlagTypes } from "@medusajs/types" + +export const SalesChannelFeatureFlag: FeatureFlagTypes.FlagSettings = { + key: "sales_channels", + default_val: true, + env_key: "MEDUSA_FF_SALES_CHANNELS", + description: "[WIP] Enable the sales channels feature", +} diff --git a/packages/utils/src/feature-flags/tax-inclusive-pricing.ts b/packages/utils/src/feature-flags/tax-inclusive-pricing.ts new file mode 100644 index 0000000000..13e6a84baa --- /dev/null +++ b/packages/utils/src/feature-flags/tax-inclusive-pricing.ts @@ -0,0 +1,8 @@ +import { FeatureFlagTypes } from "@medusajs/types" + +export const TaxInclusivePricingFeatureFlag: FeatureFlagTypes.FlagSettings = { + key: "tax_inclusive_pricing", + default_val: false, + env_key: "MEDUSA_FF_TAX_INCLUSIVE_PRICING", + description: "[WIP] Enable tax inclusive pricing", +} diff --git a/packages/utils/src/feature-flags/utils/flag-router.ts b/packages/utils/src/feature-flags/utils/flag-router.ts new file mode 100644 index 0000000000..4ac7dc9277 --- /dev/null +++ b/packages/utils/src/feature-flags/utils/flag-router.ts @@ -0,0 +1,24 @@ +import { FeatureFlagTypes } from "@medusajs/types" + +export class FlagRouter implements FeatureFlagTypes.IFlagRouter { + private readonly flags: Record = {} + + constructor(flags: Record) { + this.flags = flags + } + + public isFeatureEnabled(key: string): boolean { + return !!this.flags[key] + } + + public setFlag(key: string, value = true): void { + this.flags[key] = value + } + + public listFlags(): FeatureFlagTypes.FeatureFlagsResponse { + return Object.entries(this.flags || {}).map(([key, value]) => ({ + key, + value, + })) + } +} diff --git a/packages/utils/src/feature-flags/utils/index.ts b/packages/utils/src/feature-flags/utils/index.ts new file mode 100644 index 0000000000..a24fa54af5 --- /dev/null +++ b/packages/utils/src/feature-flags/utils/index.ts @@ -0,0 +1 @@ +export * from "./flag-router" diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 79419c7344..17a3dbd961 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -6,3 +6,6 @@ export * from "./event-bus" export * from "./search" export * from "./modules-sdk" export * from "./dal" +export * from "./feature-flags" +export * from "./shipping" +export * from "./product" diff --git a/packages/utils/src/product/index.ts b/packages/utils/src/product/index.ts new file mode 100644 index 0000000000..a00fdcf0d0 --- /dev/null +++ b/packages/utils/src/product/index.ts @@ -0,0 +1,6 @@ +export enum ProductStatus { + DRAFT = "draft", + PROPOSED = "proposed", + PUBLISHED = "published", + REJECTED = "rejected", +} diff --git a/packages/utils/src/shipping/index.ts b/packages/utils/src/shipping/index.ts new file mode 100644 index 0000000000..c3580ec327 --- /dev/null +++ b/packages/utils/src/shipping/index.ts @@ -0,0 +1,5 @@ +export enum ShippingProfileType { + DEFAULT = "default", + GIFT_CARD = "gift_card", + CUSTOM = "custom", +} diff --git a/packages/workflows/src/definition/create-products.ts b/packages/workflows/src/definition/create-products.ts index b8a9b7663e..714b4a1555 100644 --- a/packages/workflows/src/definition/create-products.ts +++ b/packages/workflows/src/definition/create-products.ts @@ -3,46 +3,284 @@ import { TransactionStepsDefinition, WorkflowManager, } from "@medusajs/orchestration" -import { - createProducts as createProductsHandler, - removeProducts, -} from "../handlers" import { exportWorkflow, pipe } from "../helper" -import { ProductTypes } from "@medusajs/types" +import { ProductTypes, WorkflowTypes } from "@medusajs/types" +import { + InventoryHandlers, + MiddlewaresHandlers, + ProductHandlers, +} from "../handlers" +import { aggregateData } from "../helper/aggregate" -enum Actions { +export enum Actions { + prepare = "prepare", createProducts = "createProducts", + attachToSalesChannel = "attachToSalesChannel", + attachShippingProfile = "attachShippingProfile", + createPrices = "createPrices", + createInventoryItems = "createInventoryItems", + attachInventoryItems = "attachInventoryItems", + result = "result", } -const workflowSteps: TransactionStepsDefinition = { +export const workflowSteps: TransactionStepsDefinition = { next: { - action: Actions.createProducts, + action: Actions.prepare, + noCompensation: true, + next: { + action: Actions.createProducts, + next: [ + { + action: Actions.attachShippingProfile, + saveResponse: false, + }, + { + action: Actions.attachToSalesChannel, + saveResponse: false, + }, + { + action: Actions.createPrices, + next: { + action: Actions.createInventoryItems, + next: { + action: Actions.attachInventoryItems, + next: { + action: Actions.result, + noCompensation: true, + }, + }, + }, + }, + ], + }, }, } const handlers = new Map([ + [ + Actions.prepare, + { + invoke: pipe( + { + inputAlias: InputAlias.ProductsInputData, + invoke: { + from: InputAlias.ProductsInputData, + }, + }, + aggregateData(), + MiddlewaresHandlers.createProductsPrepareData + ), + }, + ], [ Actions.createProducts, { invoke: pipe( { - inputAlias: InputAlias.Products, invoke: { - from: InputAlias.Products, - alias: InputAlias.Products, + from: Actions.prepare, }, }, - createProductsHandler + aggregateData(), + ProductHandlers.createProducts ), compensate: pipe( { invoke: { from: Actions.createProducts, - alias: InputAlias.Products, + alias: ProductHandlers.removeProducts.aliases.products, }, }, - removeProducts + aggregateData(), + ProductHandlers.removeProducts + ), + }, + ], + [ + Actions.attachShippingProfile, + { + invoke: pipe( + { + invoke: [ + { + from: Actions.prepare, + }, + { + from: Actions.createProducts, + alias: + ProductHandlers.attachShippingProfileToProducts.aliases + .products, + }, + ], + }, + aggregateData(), + ProductHandlers.attachShippingProfileToProducts + ), + compensate: pipe( + { + invoke: [ + { + from: Actions.prepare, + }, + { + from: Actions.createProducts, + alias: + ProductHandlers.detachShippingProfileFromProducts.aliases + .products, + }, + ], + }, + aggregateData(), + ProductHandlers.detachShippingProfileFromProducts + ), + }, + ], + [ + Actions.attachToSalesChannel, + { + invoke: pipe( + { + invoke: [ + { + from: Actions.prepare, + }, + { + from: Actions.createProducts, + alias: + ProductHandlers.attachSalesChannelToProducts.aliases.products, + }, + ], + }, + aggregateData(), + ProductHandlers.attachSalesChannelToProducts + ), + compensate: pipe( + { + invoke: [ + { + from: Actions.prepare, + }, + { + from: Actions.createProducts, + alias: + ProductHandlers.detachSalesChannelFromProducts.aliases.products, + }, + ], + }, + aggregateData(), + ProductHandlers.detachSalesChannelFromProducts + ), + }, + ], + [ + Actions.createInventoryItems, + { + invoke: pipe( + { + invoke: { + from: Actions.createProducts, + alias: InventoryHandlers.createInventoryItems.aliases.products, + }, + }, + aggregateData(), + InventoryHandlers.createInventoryItems + ), + compensate: pipe( + { + invoke: { + from: Actions.createInventoryItems, + alias: + InventoryHandlers.removeInventoryItems.aliases.inventoryItems, + }, + }, + aggregateData(), + InventoryHandlers.removeInventoryItems + ), + }, + ], + [ + Actions.attachInventoryItems, + { + invoke: pipe( + { + invoke: { + from: Actions.createInventoryItems, + alias: + InventoryHandlers.attachInventoryItems.aliases.inventoryItems, + }, + }, + aggregateData(), + InventoryHandlers.attachInventoryItems + ), + compensate: pipe( + { + invoke: { + from: Actions.createInventoryItems, + alias: + InventoryHandlers.detachInventoryItems.aliases.inventoryItems, + }, + }, + aggregateData(), + InventoryHandlers.detachInventoryItems + ), + }, + ], + [ + Actions.createPrices, + { + invoke: pipe( + { + invoke: [ + { + from: Actions.prepare, + }, + { + from: Actions.createProducts, + alias: + ProductHandlers.updateProductsVariantsPrices.aliases.products, + }, + ], + }, + aggregateData(), + ProductHandlers.updateProductsVariantsPrices + ), + compensate: pipe( + { + invoke: [ + { + from: Actions.prepare, + }, + { + from: Actions.createProducts, + alias: + ProductHandlers.updateProductsVariantsPrices.aliases.products, + }, + ], + }, + aggregateData(), + MiddlewaresHandlers.createProductsPrepareCreatePricesCompensation, + ProductHandlers.updateProductsVariantsPrices + ), + }, + ], + [ + Actions.result, + { + invoke: pipe( + { + invoke: [ + { + from: Actions.prepare, + }, + { + from: Actions.createProducts, + alias: ProductHandlers.listProducts.aliases.products, + }, + ], + }, + aggregateData(), + ProductHandlers.listProducts ), }, ], @@ -51,6 +289,6 @@ const handlers = new Map([ WorkflowManager.register(Workflows.CreateProducts, workflowSteps, handlers) export const createProducts = exportWorkflow< - ProductTypes.CreateProductDTO[], + WorkflowTypes.ProductWorkflow.CreateProductsWorkflowInputDTO, ProductTypes.ProductDTO[] ->(Workflows.CreateProducts, Actions.createProducts) +>(Workflows.CreateProducts, Actions.result) diff --git a/packages/workflows/src/definitions.ts b/packages/workflows/src/definitions.ts index fb53ef7afe..08b3348a61 100644 --- a/packages/workflows/src/definitions.ts +++ b/packages/workflows/src/definitions.ts @@ -4,6 +4,7 @@ export enum Workflows { export enum InputAlias { Products = "products", + ProductsInputData = "productsInputData", RemovedProducts = "removedProducts", InventoryItems = "inventoryItems", diff --git a/packages/workflows/src/handlers/attach-inventory-items.ts b/packages/workflows/src/handlers/attach-inventory-items.ts deleted file mode 100644 index 732432eeb3..0000000000 --- a/packages/workflows/src/handlers/attach-inventory-items.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { InventoryItemDTO, ProductTypes } from "@medusajs/types" - -import { InputAlias } from "../definitions" -import { WorkflowArguments } from "../helper" -import { ProductVariantInventoryItem } from "@medusajs/medusa/src" - -export async function attachInventoryItems({ - container, - data, -}: WorkflowArguments & { - data: { - variant: ProductTypes.ProductVariantDTO - [InputAlias.InventoryItems]: InventoryItemDTO - }[] -}): Promise { - const manager = container.resolve("manager") - const productVariantInventoryService = container - .resolve("productVariantInventoryService") - .withTransaction(manager) - - return await Promise.all( - data - .filter((d) => d) - .map(async ({ variant, [InputAlias.InventoryItems]: inventoryItem }) => { - return await productVariantInventoryService.attachInventoryItem( - variant.id, - inventoryItem.id - ) - }) - ) -} diff --git a/packages/workflows/src/handlers/create-products.ts b/packages/workflows/src/handlers/create-products.ts deleted file mode 100644 index 79667249e5..0000000000 --- a/packages/workflows/src/handlers/create-products.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { InputAlias } from "../definitions" -import { ProductTypes } from "@medusajs/types" -import { WorkflowArguments } from "../helper" - -export async function createProducts({ - container, - context, - data, -}: WorkflowArguments & { - data: { [InputAlias.Products]: ProductTypes.CreateProductDTO[] } -}): Promise { - const productModuleService = container.resolve("productModuleService") - - return await productModuleService.create(data[InputAlias.Products], context) -} diff --git a/packages/workflows/src/handlers/index.ts b/packages/workflows/src/handlers/index.ts index 0c5bc125f6..4b2c9a34c4 100644 --- a/packages/workflows/src/handlers/index.ts +++ b/packages/workflows/src/handlers/index.ts @@ -1,5 +1,3 @@ -export * from "./remove-products" -export * from "./create-products" -export * from "./create-inventory-items" -export * from "./remove-inventory-items" -export * from "./attach-inventory-items" +export * as ProductHandlers from "./product" +export * as InventoryHandlers from "./inventory" +export * as MiddlewaresHandlers from "./middlewares" diff --git a/packages/workflows/src/handlers/inventory/attach-inventory-items.ts b/packages/workflows/src/handlers/inventory/attach-inventory-items.ts new file mode 100644 index 0000000000..b32ff30ef3 --- /dev/null +++ b/packages/workflows/src/handlers/inventory/attach-inventory-items.ts @@ -0,0 +1,36 @@ +import { InventoryItemDTO, ProductTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +export async function attachInventoryItems({ + container, + context, + data, +}: WorkflowArguments<{ + inventoryItems: { + variant: ProductTypes.ProductVariantDTO + inventoryItem: InventoryItemDTO + }[] +}>) { + const { manager } = context + + const productVariantInventoryService = container + .resolve("productVariantInventoryService") + .withTransaction(manager) + + if (!data?.inventoryItems.length) { + return + } + + const inventoryData = data.inventoryItems.map( + ({ variant, inventoryItem }) => ({ + variantId: variant.id, + inventoryItemId: inventoryItem.id, + }) + ) + + return await productVariantInventoryService.attachInventoryItem(inventoryData) +} + +attachInventoryItems.aliases = { + inventoryItems: "inventoryItems", +} diff --git a/packages/workflows/src/handlers/create-inventory-items.ts b/packages/workflows/src/handlers/inventory/create-inventory-items.ts similarity index 55% rename from packages/workflows/src/handlers/create-inventory-items.ts rename to packages/workflows/src/handlers/inventory/create-inventory-items.ts index 9336036db9..dffe7d7d3d 100644 --- a/packages/workflows/src/handlers/create-inventory-items.ts +++ b/packages/workflows/src/handlers/inventory/create-inventory-items.ts @@ -3,25 +3,32 @@ import { InventoryItemDTO, ProductTypes, } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" -import { InputAlias } from "../definitions" -import { WorkflowArguments } from "../helper" +type Result = { + variant: ProductTypes.ProductVariantDTO + inventoryItem: InventoryItemDTO +}[] export async function createInventoryItems({ container, + context, data, -}: WorkflowArguments & { - data: { - [InputAlias.Products]: ProductTypes.ProductDTO[] - } -}): Promise<{ variant: any; inventoryItem: InventoryItemDTO }[]> { - const manager = container.resolve("manager") +}: WorkflowArguments<{ + products: ProductTypes.ProductDTO[] +}>): Promise { const inventoryService: IInventoryService = container.resolve("inventoryService") - const context = { transactionManager: manager } - const products = data[InputAlias.Products] - const variants = products.reduce( + if (!inventoryService) { + const logger = container.resolve("logger") + logger.warn( + `Inventory service not found. You should install the @medusajs/inventory package to use inventory. The 'createInventoryItems' will be skipped.` + ) + return void 0 + } + + const variants = data.products.reduce( ( acc: ProductTypes.ProductVariantDTO[], product: ProductTypes.ProductDTO @@ -31,7 +38,7 @@ export async function createInventoryItems({ [] ) - return await Promise.all( + const result = await Promise.all( variants.map(async (variant) => { if (!variant.manage_inventory) { return @@ -49,10 +56,19 @@ export async function createInventoryItems({ height: variant.height!, width: variant.width!, }, - context + { + transactionManager: (context.transactionManager ?? + context.manager) as any, + } ) return { variant, inventoryItem } }) ) + + return result.filter(Boolean) as Result +} + +createInventoryItems.aliases = { + products: "products", } diff --git a/packages/workflows/src/handlers/inventory/detach-inventory-items.ts b/packages/workflows/src/handlers/inventory/detach-inventory-items.ts new file mode 100644 index 0000000000..6941630971 --- /dev/null +++ b/packages/workflows/src/handlers/inventory/detach-inventory-items.ts @@ -0,0 +1,36 @@ +import { InventoryItemDTO, ProductTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +export async function detachInventoryItems({ + container, + context, + data, +}: WorkflowArguments<{ + inventoryItems: { + variant: ProductTypes.ProductVariantDTO + inventoryItem: InventoryItemDTO + }[] +}>): Promise { + const { manager } = context + + const productVariantInventoryService = container + .resolve("productVariantInventoryService") + .withTransaction(manager) + + if (!data?.inventoryItems.length) { + return + } + + await Promise.all( + data.inventoryItems.map(async ({ variant, inventoryItem }) => { + return await productVariantInventoryService.detachInventoryItem( + inventoryItem.id, + variant.id + ) + }) + ) +} + +detachInventoryItems.aliases = { + inventoryItems: "inventoryItems", +} diff --git a/packages/workflows/src/handlers/inventory/index.ts b/packages/workflows/src/handlers/inventory/index.ts new file mode 100644 index 0000000000..7236452125 --- /dev/null +++ b/packages/workflows/src/handlers/inventory/index.ts @@ -0,0 +1,4 @@ +export * from "./detach-inventory-items" +export * from "./attach-inventory-items" +export * from "./create-inventory-items" +export * from "./remove-inventory-items" diff --git a/packages/workflows/src/handlers/inventory/remove-inventory-items.ts b/packages/workflows/src/handlers/inventory/remove-inventory-items.ts new file mode 100644 index 0000000000..649f148e6c --- /dev/null +++ b/packages/workflows/src/handlers/inventory/remove-inventory-items.ts @@ -0,0 +1,30 @@ +import { InventoryItemDTO } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +export async function removeInventoryItems({ + container, + context, + data, +}: WorkflowArguments<{ + inventoryItems: { inventoryItem: InventoryItemDTO }[] +}>) { + const { manager } = context + const inventoryService = 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!.deleteInventoryItem( + data.inventoryItems.map(({ inventoryItem }) => inventoryItem.id), + { transactionManager: manager } + ) +} + +removeInventoryItems.aliases = { + inventoryItems: "inventoryItems", +} diff --git a/packages/workflows/src/handlers/middlewares/create-products-prepare-create-prices-compensation.ts b/packages/workflows/src/handlers/middlewares/create-products-prepare-create-prices-compensation.ts new file mode 100644 index 0000000000..9387b9d29f --- /dev/null +++ b/packages/workflows/src/handlers/middlewares/create-products-prepare-create-prices-compensation.ts @@ -0,0 +1,50 @@ +import { ProductTypes, WorkflowTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +type ProductHandle = string +type VariantIndexAndPrices = { + index: number + prices: WorkflowTypes.ProductWorkflow.CreateProductVariantPricesInputDTO[] +} + +export async function createProductsPrepareCreatePricesCompensation({ + data, +}: WorkflowArguments<{ + productsHandleVariantsIndexPricesMap: Map< + ProductHandle, + VariantIndexAndPrices[] + > + products: ProductTypes.ProductDTO[] +}>) { + const productsHandleVariantsIndexPricesMap = + data.productsHandleVariantsIndexPricesMap + const products = data.products + + const updatedProductsHandleVariantsIndexPricesMap = new Map() + productsHandleVariantsIndexPricesMap.forEach((items, productHandle) => { + const existingItems = + updatedProductsHandleVariantsIndexPricesMap.get(productHandle) ?? [] + + items.forEach(({ index }) => { + existingItems.push({ + index, + prices: [], + }) + }) + + updatedProductsHandleVariantsIndexPricesMap.set(productHandle, items) + }) + + return { + alias: "", + value: { + productsHandleVariantsIndexPricesMap: + updatedProductsHandleVariantsIndexPricesMap, + products, + }, + } +} + +createProductsPrepareCreatePricesCompensation.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/middlewares/create-products-prepare-data.ts b/packages/workflows/src/handlers/middlewares/create-products-prepare-data.ts index 009378b01d..8e87b42426 100644 --- a/packages/workflows/src/handlers/middlewares/create-products-prepare-data.ts +++ b/packages/workflows/src/handlers/middlewares/create-products-prepare-data.ts @@ -1,8 +1,154 @@ -import { InputAlias } from "../../definitions" +import { ProductTypes, SalesChannelTypes, WorkflowTypes } from "@medusajs/types" +import { + FeatureFlagUtils, + kebabCase, + ShippingProfileUtils, +} from "@medusajs/utils" +import { WorkflowArguments } from "../../helper" + +type ShippingProfileId = string +type SalesChannelId = string +type ProductHandle = string +type VariantIndexAndPrices = { + index: number + prices: { + region_id?: string + currency_code?: string + amount: number + min_quantity?: number + max_quantity?: number + }[] +} + +export type CreateProductsPreparedData = { + products: ProductTypes.CreateProductDTO[] + productsHandleShippingProfileIdMap: Map + productsHandleSalesChannelsMap: Map + productsHandleVariantsIndexPricesMap: Map< + ProductHandle, + VariantIndexAndPrices[] + > + config?: WorkflowTypes.CommonWorkflow.WorkflowInputConfig +} + +export async function createProductsPrepareData({ + container, + context, + data, +}: WorkflowArguments): Promise { + const { manager } = context + let products = data.products + + const shippingProfileService = container + .resolve("shippingProfileService") + .withTransaction(manager) + const featureFlagRouter = container.resolve("featureFlagRouter") + const salesChannelService = container + .resolve("salesChannelService") + .withTransaction(manager) + const salesChannelFeatureFlagKey = + FeatureFlagUtils.SalesChannelFeatureFlag.key + + const shippingProfileServiceTx = + shippingProfileService.withTransaction(manager) + + const shippingProfiles = await shippingProfileServiceTx.list({ + type: [ + ShippingProfileUtils.ShippingProfileType.DEFAULT, + ShippingProfileUtils.ShippingProfileType.GIFT_CARD, + ], + }) + const defaultShippingProfile = shippingProfiles.find( + (sp) => sp.type === ShippingProfileUtils.ShippingProfileType.DEFAULT + ) + const gitCardShippingProfile = shippingProfiles.find( + (sp) => sp.type === ShippingProfileUtils.ShippingProfileType.GIFT_CARD + ) + + let defaultSalesChannel: SalesChannelTypes.SalesChannelDTO | undefined + if (featureFlagRouter.isFeatureEnabled(salesChannelFeatureFlagKey)) { + defaultSalesChannel = await salesChannelService + .withTransaction(manager) + .retrieveDefault() + } + + const productsHandleShippingProfileIdMap = new Map< + ProductHandle, + ShippingProfileId + >() + const productsHandleSalesChannelsMap = new Map< + ProductHandle, + SalesChannelId[] + >() + const productsHandleVariantsIndexPricesMap = new Map< + ProductHandle, + VariantIndexAndPrices[] + >() + + for (const product of products) { + product.handle ??= kebabCase(product.title) + + if (product.is_giftcard) { + productsHandleShippingProfileIdMap.set( + product.handle!, + gitCardShippingProfile!.id + ) + } else { + productsHandleShippingProfileIdMap.set( + product.handle!, + defaultShippingProfile!.id + ) + } + + if ( + featureFlagRouter.isFeatureEnabled(salesChannelFeatureFlagKey) && + !product.sales_channels?.length + ) { + productsHandleSalesChannelsMap.set(product.handle!, [ + defaultSalesChannel!.id, + ]) + } else { + productsHandleSalesChannelsMap.set( + product.handle!, + product.sales_channels!.map((s) => s.id) + ) + } + + if (product.variants) { + const hasPrices = product.variants.some((variant) => { + return (variant.prices?.length ?? 0) > 0 + }) + + if (hasPrices) { + const items = + productsHandleVariantsIndexPricesMap.get(product.handle!) ?? [] + + product.variants.forEach((variant, index) => { + items.push({ + index, + prices: variant.prices!, + }) + }) + + productsHandleVariantsIndexPricesMap.set(product.handle!, items) + } + } + } + + products = products.map((productData) => { + delete productData.sales_channels + return productData + }) -export function createProductsPrepareData({ data }) { return { - alias: InputAlias.Products as string, - value: [{}], + products: products as ProductTypes.CreateProductDTO[], + productsHandleShippingProfileIdMap, + productsHandleSalesChannelsMap, + productsHandleVariantsIndexPricesMap, + config: data.config, } } + +createProductsPrepareData.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/middlewares/index.ts b/packages/workflows/src/handlers/middlewares/index.ts new file mode 100644 index 0000000000..cdeb73da92 --- /dev/null +++ b/packages/workflows/src/handlers/middlewares/index.ts @@ -0,0 +1,2 @@ +export * from "./create-products-prepare-create-prices-compensation" +export * from "./create-products-prepare-data" diff --git a/packages/medusa/src/workflows/functions/attach-sales-channel-to-products.ts b/packages/workflows/src/handlers/product/attach-sales-channel-to-products.ts similarity index 56% rename from packages/medusa/src/workflows/functions/attach-sales-channel-to-products.ts rename to packages/workflows/src/handlers/product/attach-sales-channel-to-products.ts index 87fb325ce3..805fbaabf5 100644 --- a/packages/medusa/src/workflows/functions/attach-sales-channel-to-products.ts +++ b/packages/workflows/src/handlers/product/attach-sales-channel-to-products.ts @@ -1,32 +1,27 @@ -import { MedusaContainer, ProductTypes } from "@medusajs/types" -import { EntityManager } from "typeorm" -import { SalesChannelService } from "../../services" +import { ProductTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" type ProductHandle = string type SalesChannelId = string export async function attachSalesChannelToProducts({ container, - manager, + context, data, -}: { - container: MedusaContainer - manager: EntityManager - data: { - productsHandleSalesChannelsMap: Map - products: ProductTypes.ProductDTO[] - } -}): Promise { - const salesChannelService: SalesChannelService = container.resolve( - "salesChannelService" - ) +}: WorkflowArguments<{ + productsHandleSalesChannelsMap: Map + products: ProductTypes.ProductDTO[] +}>): Promise { + const { manager } = context + const productsHandleSalesChannelsMap = data.productsHandleSalesChannelsMap + const products = data.products + + const salesChannelService = container.resolve("salesChannelService") const salesChannelServiceTx = salesChannelService.withTransaction(manager) const salesChannelIdProductIdsMap = new Map() - data.products.forEach((product) => { - const salesChannelIds = data.productsHandleSalesChannelsMap.get( - product.handle! - ) + products.forEach((product) => { + const salesChannelIds = productsHandleSalesChannelsMap.get(product.handle!) if (salesChannelIds) { salesChannelIds.forEach((salesChannelId) => { const productIds = salesChannelIdProductIdsMap.get(salesChannelId) || [] @@ -47,3 +42,7 @@ export async function attachSalesChannelToProducts({ ) ) } + +attachSalesChannelToProducts.aliases = { + products: "products", +} diff --git a/packages/medusa/src/workflows/functions/attach-shipping-profile-to-products.ts b/packages/workflows/src/handlers/product/attach-shipping-profile-to-products.ts similarity index 51% rename from packages/medusa/src/workflows/functions/attach-shipping-profile-to-products.ts rename to packages/workflows/src/handlers/product/attach-shipping-profile-to-products.ts index 6952f3a541..df634412d6 100644 --- a/packages/medusa/src/workflows/functions/attach-shipping-profile-to-products.ts +++ b/packages/workflows/src/handlers/product/attach-shipping-profile-to-products.ts @@ -1,33 +1,30 @@ -import { MedusaContainer, ProductTypes } from "@medusajs/types" -import { EntityManager } from "typeorm" -import { ShippingProfileService } from "../../services" +import { ProductTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" type ProductHandle = string type ShippingProfileId = string export async function attachShippingProfileToProducts({ container, - manager, + context, data, -}: { - container: MedusaContainer - manager: EntityManager - data: { - productsHandleShippingProfileIdMap: Map - products: ProductTypes.ProductDTO[] - } -}): Promise { - const shippingProfileService: ShippingProfileService = container.resolve( - "shippingProfileService" - ) +}: WorkflowArguments<{ + productsHandleShippingProfileIdMap: Map + products: ProductTypes.ProductDTO[] +}>): Promise { + const { manager } = context + + const productsHandleShippingProfileIdMap = + data.productsHandleShippingProfileIdMap + const products = data.products + + const shippingProfileService = container.resolve("shippingProfileService") const shippingProfileServiceTx = shippingProfileService.withTransaction(manager) const profileIdProductIdsMap = new Map() - data.products.forEach((product) => { - const profileId = data.productsHandleShippingProfileIdMap.get( - product.handle! - ) + products.forEach((product) => { + const profileId = productsHandleShippingProfileIdMap.get(product.handle!) if (profileId) { const productIds = profileIdProductIdsMap.get(profileId) || [] productIds.push(product.id) @@ -43,3 +40,7 @@ export async function attachShippingProfileToProducts({ ) ) } + +attachShippingProfileToProducts.aliases = { + products: "products", +} diff --git a/packages/workflows/src/handlers/product/create-products.ts b/packages/workflows/src/handlers/product/create-products.ts new file mode 100644 index 0000000000..7c5cfb60fe --- /dev/null +++ b/packages/workflows/src/handlers/product/create-products.ts @@ -0,0 +1,21 @@ +import { ProductTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" +import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" + +export async function createProducts({ + container, + data, +}: WorkflowArguments<{ + products: ProductTypes.CreateProductDTO[] +}>): Promise { + const data_ = data.products + + const productModuleService: ProductTypes.IProductModuleService = + container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName) + + return await productModuleService.create(data_) +} + +createProducts.aliases = { + payload: "payload", +} diff --git a/packages/medusa/src/workflows/functions/detach-sales-channel-from-products.ts b/packages/workflows/src/handlers/product/detach-sales-channel-from-products.ts similarity index 56% rename from packages/medusa/src/workflows/functions/detach-sales-channel-from-products.ts rename to packages/workflows/src/handlers/product/detach-sales-channel-from-products.ts index 14c6130c90..526e32f617 100644 --- a/packages/medusa/src/workflows/functions/detach-sales-channel-from-products.ts +++ b/packages/workflows/src/handlers/product/detach-sales-channel-from-products.ts @@ -1,32 +1,27 @@ -import { MedusaContainer, ProductTypes } from "@medusajs/types" -import { EntityManager } from "typeorm" -import { SalesChannelService } from "../../services" +import { ProductTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" type ProductHandle = string type SalesChannelId = string export async function detachSalesChannelFromProducts({ container, - manager, + context, data, -}: { - container: MedusaContainer - manager: EntityManager - data: { - productsHandleSalesChannelsMap: Map - products: ProductTypes.ProductDTO[] - } -}): Promise { - const salesChannelService: SalesChannelService = container.resolve( - "salesChannelService" - ) +}: WorkflowArguments<{ + productsHandleSalesChannelsMap: Map + products: ProductTypes.ProductDTO[] +}>): Promise { + const { manager } = context + const productsHandleSalesChannelsMap = data.productsHandleSalesChannelsMap + const products = data.products + + const salesChannelService = container.resolve("salesChannelService") const salesChannelServiceTx = salesChannelService.withTransaction(manager) const salesChannelIdProductIdsMap = new Map() - data.products.forEach((product) => { - const salesChannelIds = data.productsHandleSalesChannelsMap.get( - product.handle! - ) + products.forEach((product) => { + const salesChannelIds = productsHandleSalesChannelsMap.get(product.handle!) if (salesChannelIds) { salesChannelIds.forEach((salesChannelId) => { const productIds = salesChannelIdProductIdsMap.get(salesChannelId) || [] @@ -47,3 +42,7 @@ export async function detachSalesChannelFromProducts({ ) ) } + +detachSalesChannelFromProducts.aliases = { + products: "products", +} diff --git a/packages/medusa/src/workflows/functions/detach-shipping-profile-from-products.ts b/packages/workflows/src/handlers/product/detach-shipping-profile-from-products.ts similarity index 52% rename from packages/medusa/src/workflows/functions/detach-shipping-profile-from-products.ts rename to packages/workflows/src/handlers/product/detach-shipping-profile-from-products.ts index 77755c349f..300b1bafb9 100644 --- a/packages/medusa/src/workflows/functions/detach-shipping-profile-from-products.ts +++ b/packages/workflows/src/handlers/product/detach-shipping-profile-from-products.ts @@ -1,33 +1,29 @@ -import { MedusaContainer, ProductTypes } from "@medusajs/types" -import { EntityManager } from "typeorm" -import { ShippingProfileService } from "../../services" +import { ProductTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" type ProductHandle = string type ShippingProfileId = string export async function detachShippingProfileFromProducts({ container, - manager, + context, data, -}: { - container: MedusaContainer - manager: EntityManager - data: { - productsHandleShippingProfileIdMap: Map - products: ProductTypes.ProductDTO[] - } -}): Promise { - const shippingProfileService: ShippingProfileService = container.resolve( - "shippingProfileService" - ) +}: WorkflowArguments<{ + productsHandleShippingProfileIdMap: Map + products: ProductTypes.ProductDTO[] +}>): Promise { + const { manager } = context + const productsHandleShippingProfileIdMap = + data.productsHandleShippingProfileIdMap + const products = data.products + + const shippingProfileService = container.resolve("shippingProfileService") const shippingProfileServiceTx = shippingProfileService.withTransaction(manager) const profileIdProductIdsMap = new Map() - data.products.forEach((product) => { - const profileId = data.productsHandleShippingProfileIdMap.get( - product.handle! - ) + products.forEach((product) => { + const profileId = productsHandleShippingProfileIdMap.get(product.handle!) if (profileId) { const productIds = profileIdProductIdsMap.get(profileId) || [] productIds.push(product.id) @@ -46,3 +42,7 @@ export async function detachShippingProfileFromProducts({ ) ) } + +detachShippingProfileFromProducts.aliases = { + products: "products", +} diff --git a/packages/medusa/src/workflows/functions/index.ts b/packages/workflows/src/handlers/product/index.ts similarity index 54% rename from packages/medusa/src/workflows/functions/index.ts rename to packages/workflows/src/handlers/product/index.ts index 7e118f4275..19109626b6 100644 --- a/packages/medusa/src/workflows/functions/index.ts +++ b/packages/workflows/src/handlers/product/index.ts @@ -1,12 +1,8 @@ -export * from "./create-prducts" -export * from "./remove-products" -export * from "./create-inventory-items" -export * from "./remove-inventory-items" -export * from "./attach-inventory-items" -export * from "./prepare-create-products-data" -export * from "./attach-shipping-profile-to-products" -export * from "./detach-shipping-profile-from-products" -export * from "./attach-sales-channel-to-products" +export * from "./create-products" export * from "./detach-sales-channel-from-products" +export * from "./attach-sales-channel-to-products" +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-variants-prices" -export * from "./detach-inventory-items" diff --git a/packages/workflows/src/handlers/product/list-products.ts b/packages/workflows/src/handlers/product/list-products.ts new file mode 100644 index 0000000000..9d0d47be76 --- /dev/null +++ b/packages/workflows/src/handlers/product/list-products.ts @@ -0,0 +1,44 @@ +import { ProductTypes, WorkflowTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +export async function listProducts({ + container, + context, + data, +}: WorkflowArguments<{ + products: ProductTypes.ProductDTO[] + config?: WorkflowTypes.CommonWorkflow.WorkflowInputConfig +}>): Promise { + const { manager } = context + + const products = data.products + const listConfig = data.config?.listConfig ?? {} + + const productService = container.resolve("productService") + const pricingService = container.resolve("pricingService") + + const config = {} + let shouldUseConfig = false + + if (listConfig.select) { + shouldUseConfig = !!listConfig.select.length + Object.assign(config, { select: listConfig.select }) + } + + if (listConfig.relations) { + shouldUseConfig = shouldUseConfig || !!listConfig.relations.length + Object.assign(config, { relations: listConfig.relations }) + } + + const rawProduct = await productService + .withTransaction(manager as any) + .retrieve(products[0].id, shouldUseConfig ? config : undefined) + + return await pricingService + .withTransaction(manager as any) + .setProductPrices([rawProduct]) +} + +listProducts.aliases = { + products: "products", +} diff --git a/packages/workflows/src/handlers/product/remove-products.ts b/packages/workflows/src/handlers/product/remove-products.ts new file mode 100644 index 0000000000..337dfa92b4 --- /dev/null +++ b/packages/workflows/src/handlers/product/remove-products.ts @@ -0,0 +1,21 @@ +import { ProductTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" +import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" + +export async function removeProducts({ + container, + data, +}: WorkflowArguments<{ products: ProductTypes.ProductDTO[] }>): Promise { + if (!data.products.length) { + return + } + + const productModuleService: ProductTypes.IProductModuleService = + container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName) + + await productModuleService.softDelete(data.products.map((p) => p.id)) +} + +removeProducts.aliases = { + products: "products", +} diff --git a/packages/medusa/src/workflows/functions/update-products-variants-prices.ts b/packages/workflows/src/handlers/product/update-products-variants-prices.ts similarity index 51% rename from packages/medusa/src/workflows/functions/update-products-variants-prices.ts rename to packages/workflows/src/handlers/product/update-products-variants-prices.ts index d0b1793244..e02248d6b1 100644 --- a/packages/medusa/src/workflows/functions/update-products-variants-prices.ts +++ b/packages/workflows/src/handlers/product/update-products-variants-prices.ts @@ -1,44 +1,38 @@ -import { MedusaContainer, ProductTypes } from "@medusajs/types" -import { EntityManager } from "typeorm" -import { ProductVariantService } from "../../services" -import { - ProductVariantPricesCreateReq, - UpdateVariantPricesData, -} from "../../types/product-variant" +import { ProductTypes, WorkflowTypes } from "@medusajs/types" import { MedusaError } from "@medusajs/utils" +import { WorkflowArguments } from "../../helper" type ProductHandle = string type VariantIndexAndPrices = { index: number - prices: ProductVariantPricesCreateReq[] + prices: WorkflowTypes.ProductWorkflow.CreateProductVariantPricesInputDTO[] } export async function updateProductsVariantsPrices({ container, - manager, + context, data, -}: { - container: MedusaContainer - manager: EntityManager - data: { - products: ProductTypes.ProductDTO[] - productsHandleVariantsIndexPricesMap: Map< - ProductHandle, - VariantIndexAndPrices[] - > - } -}) { - const productVariantService: ProductVariantService = container.resolve( - "productVariantService" - ) +}: WorkflowArguments<{ + productsHandleVariantsIndexPricesMap: Map< + ProductHandle, + VariantIndexAndPrices[] + > + products: ProductTypes.ProductDTO[] +}>) { + const { manager } = context + const products = data.products + const productsHandleVariantsIndexPricesMap = + data.productsHandleVariantsIndexPricesMap + + const productVariantService = container.resolve("productVariantService") const productVariantServiceTx = productVariantService.withTransaction(manager) - const variantIdsPricesData: UpdateVariantPricesData[] = [] + const variantIdsPricesData: any[] = [] const productsMap = new Map( - data.products.map((p) => [p.handle!, p]) + products.map((p) => [p.handle!, p]) ) - for (const mapData of data.productsHandleVariantsIndexPricesMap.entries()) { + for (const mapData of productsHandleVariantsIndexPricesMap.entries()) { const [handle, variantData] = mapData const product = productsMap.get(handle) @@ -60,3 +54,7 @@ export async function updateProductsVariantsPrices({ await productVariantServiceTx.updateVariantPrices(variantIdsPricesData) } + +updateProductsVariantsPrices.aliases = { + products: "products", +} diff --git a/packages/workflows/src/handlers/remove-inventory-items.ts b/packages/workflows/src/handlers/remove-inventory-items.ts deleted file mode 100644 index 530de4f2f4..0000000000 --- a/packages/workflows/src/handlers/remove-inventory-items.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { InventoryItemDTO } from "@medusajs/types" - -import { InputAlias } from "../definitions" -import { WorkflowArguments } from "../helper" - -export async function removeInventoryItems({ - container, - data, -}: WorkflowArguments & { - data: { - [InputAlias.InventoryItems]: InventoryItemDTO - }[] -}) { - const manager = container.resolve("manager") - const inventoryService = container.resolve("inventoryService") - const context = { transactionManager: manager } - - return await Promise.all( - data.map(async ({ [InputAlias.InventoryItems]: inventoryItem }) => { - return await inventoryService!.deleteInventoryItem( - inventoryItem.id, - context - ) - }) - ) -} diff --git a/packages/workflows/src/handlers/remove-products.ts b/packages/workflows/src/handlers/remove-products.ts deleted file mode 100644 index 274bd97e01..0000000000 --- a/packages/workflows/src/handlers/remove-products.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { InputAlias } from "../definitions" -import { ProductTypes } from "@medusajs/types" -import { WorkflowArguments } from "../helper" - -export async function removeProducts({ - container, - data, -}: WorkflowArguments & { - data: { - [InputAlias.Products]: ProductTypes.ProductDTO[] - } -}) { - const productModuleService = container.resolve("productModuleService") - return await productModuleService.softDelete( - data[InputAlias.Products].map((p) => p.id) - ) -} diff --git a/packages/workflows/src/helper/__tests__/aggregate.spec.ts b/packages/workflows/src/helper/__tests__/aggregate.spec.ts new file mode 100644 index 0000000000..febbf5e0f2 --- /dev/null +++ b/packages/workflows/src/helper/__tests__/aggregate.spec.ts @@ -0,0 +1,55 @@ +import { aggregateData } from "../aggregate" +import { WorkflowStepMiddlewareReturn } from "../pipe" + +describe("aggregate", function () { + it("should aggregate a new object from the source into a specify target", async function () { + const source = { + stringProp: "stringProp", + anArray: ["anArray"], + input: { + test: "test", + }, + another: { + anotherTest: "anotherTest", + }, + } + + const { value: result } = (await aggregateData( + ["input", "another", "stringProp", "anArray"], + "payload" + )({ data: source } as any)) as unknown as WorkflowStepMiddlewareReturn + + expect(result).toEqual({ + payload: { + ...source.input, + ...source.another, + anArray: source.anArray, + stringProp: source.stringProp, + }, + }) + }) + + it("should aggregate a new object from the entire source into the resul object", async function () { + const source = { + stringProp: "stringProp", + anArray: ["anArray"], + input: { + test: "test", + }, + another: { + anotherTest: "anotherTest", + }, + } + + const { value: result } = (await aggregateData()({ + data: source, + } as any)) as unknown as WorkflowStepMiddlewareReturn + + expect(result).toEqual({ + ...source.input, + ...source.another, + anArray: source.anArray, + stringProp: source.stringProp, + }) + }) +}) diff --git a/packages/workflows/src/helper/aggregate.ts b/packages/workflows/src/helper/aggregate.ts new file mode 100644 index 0000000000..fca711c2a3 --- /dev/null +++ b/packages/workflows/src/helper/aggregate.ts @@ -0,0 +1,49 @@ +import { PipelineHandler, WorkflowArguments } from "./pipe" +import { isObject } from "@medusajs/utils" + +/** + * Pipe utils that aggregates data from an object into a new object. + * The new object will have a target key with the aggregated data from the keys. + * @param keys + * @param target + */ +export function aggregateData< + T extends Record = Record, + TKeys extends keyof T = keyof T, + Target extends "payload" | string = string +>(keys: TKeys[] = [], target?: Target): PipelineHandler { + return async function ({ data }: WorkflowArguments) { + const workingKeys = (keys.length ? keys : Object.keys(data)) as TKeys[] + const value = workingKeys.reduce((acc, key) => { + let targetAcc = { ...(target ? acc[target as string] : acc) } + targetAcc ??= {} + + if (Array.isArray(data[key as string])) { + targetAcc[key as string] = data[key as string] + } else if (isObject(data[key as string])) { + targetAcc = { + ...targetAcc, + ...(data[key as string] as object), + } + } else { + targetAcc[key as string] = data[key as string] + } + + if (target) { + acc[target as string] = { + ...acc[target as string], + ...targetAcc, + } + } else { + acc = targetAcc + } + + return acc + }, {}) + + return { + alias: target, + value, + } + } +} diff --git a/packages/workflows/src/helper/pipe.ts b/packages/workflows/src/helper/pipe.ts index d471cd0584..c7cf22f3b4 100644 --- a/packages/workflows/src/helper/pipe.ts +++ b/packages/workflows/src/helper/pipe.ts @@ -6,14 +6,14 @@ import { import { InputAlias } from "../definitions" -type WorkflowStepMiddlewareReturn = { - alias: string +export type WorkflowStepMiddlewareReturn = { + alias?: string value: any } -type WorkflowStepMiddlewareInput = { +export type WorkflowStepMiddlewareInput = { from: string - alias: string + alias?: string } interface PipelineInput { @@ -32,11 +32,13 @@ export type WorkflowArguments = { export type PipelineHandler = ( args: WorkflowArguments -) => T extends undefined - ? Promise - : T +) => Promise< + T extends undefined + ? WorkflowStepMiddlewareReturn | WorkflowStepMiddlewareReturn[] + : T +> -export function pipe( +export function pipe( input: PipelineInput, ...functions: [...PipelineHandler[], PipelineHandler] ): WorkflowStepHandler { @@ -48,7 +50,7 @@ export function pipe( metadata, context, }) => { - const data = {} + let data = {} const original = { invoke: invoke ?? {}, @@ -60,7 +62,7 @@ export function pipe( } for (const key in input) { - if (!input[key]) { + if (!input[key] || key === "inputAlias") { continue } @@ -69,13 +71,16 @@ export function pipe( } for (const action of input[key]) { - if (action?.alias) { + if (action.alias) { data[action.alias] = original[key][action.from] + } else { + data[action.from] = original[key][action.from] } } } - return functions.reduce(async (_, fn) => { + let finalResult + for (const fn of functions) { let result = await fn({ container, payload, @@ -90,13 +95,22 @@ export function pipe( data[action.alias] = action.value } } - } else if ((result as WorkflowStepMiddlewareReturn)?.alias) { - data[(result as WorkflowStepMiddlewareReturn).alias] = ( - result as WorkflowStepMiddlewareReturn - ).value + } else if ( + result && + "alias" in (result as WorkflowStepMiddlewareReturn) + ) { + if ((result as WorkflowStepMiddlewareReturn).alias) { + data[(result as WorkflowStepMiddlewareReturn).alias!] = ( + result as WorkflowStepMiddlewareReturn + ).value + } else { + data = (result as WorkflowStepMiddlewareReturn).value + } } - return result - }, {}) + finalResult = result + } + + return finalResult } } diff --git a/yarn.lock b/yarn.lock index c14286be34..a05112c7a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6353,6 +6353,7 @@ __metadata: "@medusajs/orchestration": ^0.1.0 "@medusajs/types": ^1.9.0 "@medusajs/utils": ^1.9.3 + "@medusajs/workflows": "workspace:^" "@types/express": ^4.17.17 "@types/ioredis": ^4.28.10 "@types/jsonwebtoken": ^8.5.9 @@ -6577,7 +6578,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/workflows@workspace:packages/workflows": +"@medusajs/workflows@workspace:^, @medusajs/workflows@workspace:packages/workflows": version: 0.0.0-use.local resolution: "@medusajs/workflows@workspace:packages/workflows" dependencies: