feat(medusa, types, utils, workflow): Migrate medusa workflow to the workflow package (#4682)

This commit is contained in:
Adrien de Peretti
2023-08-05 16:03:45 +02:00
committed by GitHub
parent 433914ae01
commit dc46927bc6
68 changed files with 1224 additions and 1064 deletions
@@ -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: {
+1
View File
@@ -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",
@@ -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] })
}
@@ -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<ProductTypes.ProductDTO[]> => {
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)
}
@@ -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)[]
}
@@ -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[]
@@ -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)
}
@@ -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 }
})
)
}
@@ -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<ProductTypes.ProductDTO[]> {
const productModuleService: ProductTypes.IProductModuleService =
container.resolve("productModuleService")
return await productModuleService.create(data)
}
@@ -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
)
})
)
}
@@ -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<ProductHandle, ShippingProfileId>
productsHandleSalesChannelsMap: Map<ProductHandle, SalesChannelId[]>
productsHandleVariantsIndexPricesMap: Map<
ProductHandle,
VariantIndexAndPrices[]
>
}
export async function prepareCreateProductsData({
container,
manager,
data,
}: {
container: MedusaContainer
manager: EntityManager
data: CreateProductsInputData
}): Promise<CreateProductsPreparedData> {
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,
}
}
@@ -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
)
}
@@ -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))
}
+8 -4
View File
@@ -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
+2 -2
View File
@@ -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<TEntity extends Product = Product> {
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
data.forEach((product) => {
product.status ??= ProductStatus.DRAFT
product.status ??= ProductUtils.ProductStatus.DRAFT
})
return (await (this.productRepository_ as ProductRepository).create(
@@ -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
+3
View File
@@ -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"
+32
View File
@@ -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
}
+1
View File
@@ -0,0 +1 @@
export * from "./common"
+3
View File
@@ -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"
+1 -1
View File
@@ -425,7 +425,7 @@ export interface CreateProductVariantOnlyDTO {
}
export interface UpdateProductVariantOnlyDTO {
id: string,
id: string
title?: string
sku?: string
barcode?: string
@@ -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<string, unknown> | null
locations?: SalesChannelLocationDTO[]
}
@@ -0,0 +1 @@
export * from "./common"
+10
View File
@@ -0,0 +1,10 @@
export interface WorkflowInputConfig {
listConfig?: {
select?: string[]
relations?: string[]
}
retrieveConfig?: {
select?: string[]
relations?: string[]
}
}
+2
View File
@@ -0,0 +1,2 @@
export * as ProductWorkflow from "./product"
export * as CommonWorkflow from "./common"
@@ -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<string, unknown>
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<string, unknown>
sales_channels?: CreateProductSalesChannelInputDTO[]
listConfig: {
select: string[]
relations: string[]
}
}
export interface CreateProductsWorkflowInputDTO {
products: CreateProductInputDTO[]
config?: WorkflowInputConfig
}
@@ -0,0 +1 @@
export * from "./create-products"
+3
View File
@@ -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"
@@ -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",
}
@@ -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"
@@ -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",
}
@@ -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",
}
@@ -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",
}
@@ -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",
}
@@ -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",
}
@@ -0,0 +1,24 @@
import { FeatureFlagTypes } from "@medusajs/types"
export class FlagRouter implements FeatureFlagTypes.IFlagRouter {
private readonly flags: Record<string, boolean> = {}
constructor(flags: Record<string, boolean>) {
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,
}))
}
}
@@ -0,0 +1 @@
export * from "./flag-router"
+3
View File
@@ -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"
+6
View File
@@ -0,0 +1,6 @@
export enum ProductStatus {
DRAFT = "draft",
PROPOSED = "proposed",
PUBLISHED = "published",
REJECTED = "rejected",
}
+5
View File
@@ -0,0 +1,5 @@
export enum ShippingProfileType {
DEFAULT = "default",
GIFT_CARD = "gift_card",
CUSTOM = "custom",
}
@@ -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)
+1
View File
@@ -4,6 +4,7 @@ export enum Workflows {
export enum InputAlias {
Products = "products",
ProductsInputData = "productsInputData",
RemovedProducts = "removedProducts",
InventoryItems = "inventoryItems",
@@ -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<ProductVariantInventoryItem[]> {
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
)
})
)
}
@@ -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<ProductTypes.ProductDTO[]> {
const productModuleService = container.resolve("productModuleService")
return await productModuleService.create(data[InputAlias.Products], context)
}
+3 -5
View File
@@ -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"
@@ -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",
}
@@ -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<Result | void> {
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",
}
@@ -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<void> {
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",
}
@@ -0,0 +1,4 @@
export * from "./detach-inventory-items"
export * from "./attach-inventory-items"
export * from "./create-inventory-items"
export * from "./remove-inventory-items"
@@ -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",
}
@@ -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",
}
@@ -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<ProductHandle, ShippingProfileId>
productsHandleSalesChannelsMap: Map<ProductHandle, SalesChannelId[]>
productsHandleVariantsIndexPricesMap: Map<
ProductHandle,
VariantIndexAndPrices[]
>
config?: WorkflowTypes.CommonWorkflow.WorkflowInputConfig
}
export async function createProductsPrepareData({
container,
context,
data,
}: WorkflowArguments<WorkflowTypes.ProductWorkflow.CreateProductsWorkflowInputDTO>): Promise<CreateProductsPreparedData> {
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",
}
@@ -0,0 +1,2 @@
export * from "./create-products-prepare-create-prices-compensation"
export * from "./create-products-prepare-data"
@@ -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<ProductHandle, SalesChannelId[]>
products: ProductTypes.ProductDTO[]
}
}): Promise<void> {
const salesChannelService: SalesChannelService = container.resolve(
"salesChannelService"
)
}: WorkflowArguments<{
productsHandleSalesChannelsMap: Map<ProductHandle, SalesChannelId[]>
products: ProductTypes.ProductDTO[]
}>): Promise<void> {
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<ProductHandle, SalesChannelId[]>()
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",
}
@@ -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<ProductHandle, ShippingProfileId>
products: ProductTypes.ProductDTO[]
}
}): Promise<void> {
const shippingProfileService: ShippingProfileService = container.resolve(
"shippingProfileService"
)
}: WorkflowArguments<{
productsHandleShippingProfileIdMap: Map<ProductHandle, ShippingProfileId>
products: ProductTypes.ProductDTO[]
}>): Promise<void> {
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<ShippingProfileId, ProductHandle[]>()
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",
}
@@ -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<ProductTypes.ProductDTO[]> {
const data_ = data.products
const productModuleService: ProductTypes.IProductModuleService =
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
return await productModuleService.create(data_)
}
createProducts.aliases = {
payload: "payload",
}
@@ -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<ProductHandle, SalesChannelId[]>
products: ProductTypes.ProductDTO[]
}
}): Promise<void> {
const salesChannelService: SalesChannelService = container.resolve(
"salesChannelService"
)
}: WorkflowArguments<{
productsHandleSalesChannelsMap: Map<ProductHandle, SalesChannelId[]>
products: ProductTypes.ProductDTO[]
}>): Promise<void> {
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<ProductHandle, SalesChannelId[]>()
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",
}
@@ -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<ProductHandle, ShippingProfileId>
products: ProductTypes.ProductDTO[]
}
}): Promise<void> {
const shippingProfileService: ShippingProfileService = container.resolve(
"shippingProfileService"
)
}: WorkflowArguments<{
productsHandleShippingProfileIdMap: Map<ProductHandle, ShippingProfileId>
products: ProductTypes.ProductDTO[]
}>): Promise<void> {
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<ShippingProfileId, ProductHandle[]>()
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",
}
@@ -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"
@@ -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<ProductTypes.ProductDTO[]> {
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",
}
@@ -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<void> {
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",
}
@@ -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<string, ProductTypes.ProductDTO>(
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",
}
@@ -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
)
})
)
}
@@ -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)
)
}
@@ -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,
})
})
})
@@ -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<string, unknown> = Record<string, unknown>,
TKeys extends keyof T = keyof T,
Target extends "payload" | string = string
>(keys: TKeys[] = [], target?: Target): PipelineHandler {
return async function ({ data }: WorkflowArguments<T>) {
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,
}
}
}
+32 -18
View File
@@ -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<T = any> = {
export type PipelineHandler<T extends any = undefined> = (
args: WorkflowArguments
) => T extends undefined
? Promise<WorkflowStepMiddlewareReturn | WorkflowStepMiddlewareReturn[]>
: T
) => Promise<
T extends undefined
? WorkflowStepMiddlewareReturn | WorkflowStepMiddlewareReturn[]
: T
>
export function pipe<T = undefined>(
export function pipe<T>(
input: PipelineInput,
...functions: [...PipelineHandler[], PipelineHandler<T>]
): WorkflowStepHandler {
@@ -48,7 +50,7 @@ export function pipe<T = undefined>(
metadata,
context,
}) => {
const data = {}
let data = {}
const original = {
invoke: invoke ?? {},
@@ -60,7 +62,7 @@ export function pipe<T = undefined>(
}
for (const key in input) {
if (!input[key]) {
if (!input[key] || key === "inputAlias") {
continue
}
@@ -69,13 +71,16 @@ export function pipe<T = undefined>(
}
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<T = undefined>(
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
}
}
+2 -1
View File
@@ -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: