Feat: @medusajs/workflows (#4553)

feat: medusa workflows
This commit is contained in:
Carlos R. L. Rodrigues
2023-07-25 10:13:14 -03:00
committed by GitHub
parent ae33f4825f
commit f12299deb1
52 changed files with 1358 additions and 331 deletions

View File

@@ -0,0 +1,56 @@
import { InputAlias, Workflows } from "../definitions"
import {
TransactionStepsDefinition,
WorkflowManager,
} from "@medusajs/orchestration"
import {
createProducts as createProductsHandler,
removeProducts,
} from "../handlers"
import { exportWorkflow, pipe } from "../helper"
import { ProductTypes } from "@medusajs/types"
enum Actions {
createProducts = "createProducts",
}
const workflowSteps: TransactionStepsDefinition = {
next: {
action: Actions.createProducts,
},
}
const handlers = new Map([
[
Actions.createProducts,
{
invoke: pipe(
{
inputAlias: InputAlias.Products,
invoke: {
from: InputAlias.Products,
alias: InputAlias.Products,
},
},
createProductsHandler
),
compensate: pipe(
{
invoke: {
from: Actions.createProducts,
alias: InputAlias.Products,
},
},
removeProducts
),
},
],
])
WorkflowManager.register(Workflows.CreateProducts, workflowSteps, handlers)
export const createProducts = exportWorkflow<
ProductTypes.CreateProductDTO[],
ProductTypes.ProductDTO[]
>(Workflows.CreateProducts, Actions.createProducts)

View File

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

View File

@@ -0,0 +1,14 @@
export enum Workflows {
CreateProducts = "create-products",
}
export enum InputAlias {
Products = "products",
RemovedProducts = "removedProducts",
InventoryItems = "inventoryItems",
RemovedInventoryItems = "removedInventoryItems",
AttachedInventoryItems = "attachedInventoryItems",
DetachedInventoryItems = "detachedInventoryItems",
}

View File

@@ -0,0 +1,35 @@
import { InventoryItemDTO, ProductTypes } from "@medusajs/types"
import { InputAlias } from "../definitions"
import { WorkflowArguments } from "../helper"
export async function attachInventoryItems({
container,
data,
}: WorkflowArguments & {
data: {
variant: ProductTypes.ProductVariantDTO
[InputAlias.InventoryItems]: InventoryItemDTO
}[]
}) {
const manager = container.resolve("manager")
const productVariantInventoryService = container
.resolve("productVariantInventoryService")
.withTransaction(manager)
const value = await Promise.all(
data
.filter((d) => d)
.map(async ({ variant, [InputAlias.InventoryItems]: inventoryItem }) => {
return await productVariantInventoryService.attachInventoryItem(
variant.id,
inventoryItem.id
)
})
)
return {
alias: InputAlias.AttachedInventoryItems,
value,
}
}

View File

@@ -0,0 +1,59 @@
import { IInventoryService, ProductTypes } from "@medusajs/types"
import { InputAlias } from "../definitions"
import { WorkflowArguments } from "../helper"
export async function createInventoryItems({
container,
data,
}: WorkflowArguments & {
data: {
[InputAlias.Products]: ProductTypes.ProductDTO[]
}
}) {
const manager = container.resolve("manager")
const inventoryService: IInventoryService =
container.resolve("inventoryService")
const context = { transactionManager: manager }
const products = data[InputAlias.Products]
const variants = products.reduce(
(
acc: ProductTypes.ProductVariantDTO[],
product: ProductTypes.ProductDTO
) => {
return acc.concat(product.variants)
},
[]
)
const value = 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 }
})
)
return {
alias: InputAlias.InventoryItems,
value,
}
}

View File

@@ -0,0 +1,23 @@
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[] }
}) {
const productModuleService = container.resolve("productModuleService")
const value = await productModuleService.create(
data[InputAlias.Products],
context
)
return {
alias: InputAlias.Products,
value,
}
}

View File

@@ -0,0 +1,5 @@
export * from "./remove-products"
export * from "./create-products"
export * from "./create-inventory-items"
export * from "./remove-inventory-items"
export * from "./attach-inventory-items"

View File

@@ -0,0 +1,31 @@
import { InventoryItemDTO, MedusaContainer } 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 }
const value = await Promise.all(
data.map(async ({ [InputAlias.InventoryItems]: inventoryItem }) => {
return await inventoryService!.deleteInventoryItem(
inventoryItem.id,
context
)
})
)
return {
alias: InputAlias.RemovedInventoryItems,
value,
}
}

View File

@@ -0,0 +1,22 @@
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")
const value = await productModuleService.softDelete(
data[InputAlias.Products].map((p) => p.id)
)
return {
alias: InputAlias.RemovedProducts,
value,
}
}

View File

@@ -0,0 +1 @@
export const emptyHandler: any = () => {}

View File

@@ -0,0 +1,3 @@
export * from "./empty-handler"
export * from "./pipe"
export * from "./workflow-export"

View File

@@ -0,0 +1,98 @@
import { Context, MedusaContainer, SharedContext } from "@medusajs/types"
import {
TransactionMetadata,
WorkflowStepHandler,
} from "@medusajs/orchestration"
import { InputAlias } from "../definitions"
type WorkflowStepReturn = {
alias: string
value: any
}
type WorkflowStepInput = {
from: string
alias: string
}
interface PipelineInput {
inputAlias?: InputAlias | string
invoke?: WorkflowStepInput | WorkflowStepInput[]
compensate?: WorkflowStepInput | WorkflowStepInput[]
}
export type WorkflowArguments = {
container: MedusaContainer
payload: unknown
data: any
metadata: TransactionMetadata
context: Context | SharedContext
}
export type PipelineHandler = (
args: WorkflowArguments
) => Promise<WorkflowStepReturn | WorkflowStepReturn[]>
export function pipe(
input: PipelineInput,
...functions: PipelineHandler[]
): WorkflowStepHandler {
return async ({
container,
payload,
invoke,
compensate,
metadata,
context,
}) => {
const data = {}
const original = {
invoke: invoke ?? {},
compensate: compensate ?? {},
}
if (input.inputAlias) {
Object.assign(original.invoke, { [input.inputAlias]: payload })
}
for (const key in input) {
if (!input[key]) {
continue
}
if (!Array.isArray(input[key])) {
input[key] = [input[key]]
}
for (const action of input[key]) {
if (action?.alias) {
data[action.alias] = original[key][action.from]
}
}
}
return functions.reduce(async (_, fn) => {
let result = await fn({
container,
payload,
data,
metadata,
context: context as Context,
})
if (Array.isArray(result)) {
for (const action of result) {
if (action?.alias) {
data[action.alias] = action.value
}
}
} else if (result?.alias) {
data[result.alias] = result.value
}
return result
}, {})
}
}

View File

@@ -0,0 +1,107 @@
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
import {
DistributedTransaction,
LocalWorkflow,
TransactionState,
TransactionStepError,
} from "@medusajs/orchestration"
import { EOL } from "os"
import { MedusaModule } from "@medusajs/modules-sdk"
import { Workflows } from "../definitions"
import { ulid } from "ulid"
export type FlowRunOptions<TData = unknown> = {
input?: TData
context?: Context
resultFrom?: string | string[]
throwOnError?: boolean
}
export type WorkflowResult<TResult = unknown> = {
errors: TransactionStepError[]
transaction: DistributedTransaction
result: TResult
}
export const exportWorkflow = <TData = unknown, TResult = unknown>(
workflowId: Workflows,
defaultResult?: string
) => {
return function <TDataOverride = undefined, TResultOverride = undefined>(
container?: LoadedModule[] | MedusaContainer
): Omit<LocalWorkflow, "run"> & {
run: (
args?: FlowRunOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
) => Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
} {
if (!container) {
container = MedusaModule.getLoadedModules().map(
(mod) => Object.values(mod)[0]
)
}
const flow = new LocalWorkflow(workflowId, container)
const originalRun = flow.run.bind(flow)
const newRun = async (
{ input, context, throwOnError, resultFrom }: FlowRunOptions = {
throwOnError: true,
resultFrom: defaultResult,
}
) => {
const transaction = await originalRun(
context?.transactionId ?? ulid(),
input,
context
)
const errors = transaction.getErrors()
const failedStatus = [TransactionState.FAILED, TransactionState.REVERTED]
if (failedStatus.includes(transaction.getState()) && throwOnError) {
const errorMessage = errors
?.map((err) => `${err.error?.message}${EOL}${err.error?.stack}`)
?.join(`${EOL}`)
throw new Error(errorMessage)
}
let result: any = undefined
if (resultFrom) {
if (Array.isArray(resultFrom)) {
result = resultFrom.map(
(from) => transaction.getContext().invoke?.[from]
)
} else {
result = transaction.getContext().invoke?.[resultFrom]
}
}
return {
errors,
transaction,
result,
}
}
flow.run = newRun as any
return flow as unknown as LocalWorkflow & {
run: (
args?: FlowRunOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
) => Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
}
}
}

View File

@@ -0,0 +1,4 @@
export * from "./definition"
export * from "./definitions"
export * as Handlers from "./handlers"
export * from "./helper"