feat(workflows-sdk): Execute workflows as step in other workflows (#7183)

This commit is contained in:
Adrien de Peretti authored and GitHub committed 2024-05-02 11:29:28 -03:00
1 parent b8a0a459a8
commit 7a351eef09
13 files changed
+474 -273

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/workflows-sdk": patch
---
feat(workflows-sdk): Execute workflows as step in other workflows
@@ -1,11 +1,11 @@
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types" import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
import { import {
MedusaContext,
MedusaContextType,
MedusaModuleType,
createMedusaContainer, createMedusaContainer,
isDefined, isDefined,
isString, isString,
MedusaContext,
MedusaContextType,
MedusaModuleType,
} from "@medusajs/utils" } from "@medusajs/utils"
import { asValue } from "awilix" import { asValue } from "awilix"
import { import {
@@ -29,7 +29,7 @@ type StepHandler = {
} }
export class LocalWorkflow { export class LocalWorkflow {
protected container: MedusaContainer protected container_: MedusaContainer
protected workflowId: string protected workflowId: string
protected flow: OrchestratorBuilder protected flow: OrchestratorBuilder
protected customOptions: Partial<TransactionModelOptions> = {} protected customOptions: Partial<TransactionModelOptions> = {}
@@ -37,9 +37,17 @@ export class LocalWorkflow {
protected handlers: Map<string, StepHandler> protected handlers: Map<string, StepHandler>
protected medusaContext?: Context protected medusaContext?: Context
get container() {
return this.container_
}
set container(modulesLoaded: LoadedModule[] | MedusaContainer) {
this.resolveContainer(modulesLoaded)
}
constructor( constructor(
workflowId: string, workflowId: string,
modulesLoaded: LoadedModule[] | MedusaContainer modulesLoaded?: LoadedModule[] | MedusaContainer
) { ) {
const globalWorkflow = WorkflowManager.getWorkflow(workflowId) const globalWorkflow = WorkflowManager.getWorkflow(workflowId)
if (!globalWorkflow) { if (!globalWorkflow) {
@@ -51,6 +59,10 @@ export class LocalWorkflow {
this.workflow = globalWorkflow this.workflow = globalWorkflow
this.handlers = new Map(globalWorkflow.handlers_) this.handlers = new Map(globalWorkflow.handlers_)
this.resolveContainer(modulesLoaded)
}
private resolveContainer(modulesLoaded?: LoadedModule[] | MedusaContainer) {
let container let container
if (!Array.isArray(modulesLoaded) && modulesLoaded) { if (!Array.isArray(modulesLoaded) && modulesLoaded) {
@@ -68,7 +80,7 @@ export class LocalWorkflow {
} }
} }
this.container = this.contextualizedMedusaModules(container) this.container_ = this.contextualizedMedusaModules(container)
} }
private contextualizedMedusaModules(container) { private contextualizedMedusaModules(container) {
@@ -326,7 +338,7 @@ export class LocalWorkflow {
const transaction = await orchestrator.beginTransaction( const transaction = await orchestrator.beginTransaction(
uniqueTransactionId, uniqueTransactionId,
handler(this.container, context), handler(this.container_, context),
input input
) )
@@ -349,7 +361,7 @@ export class LocalWorkflow {
const transaction = await orchestrator.retrieveExistingTransaction( const transaction = await orchestrator.retrieveExistingTransaction(
uniqueTransactionId, uniqueTransactionId,
handler(this.container, context) handler(this.container_, context)
) )
return transaction return transaction
@@ -397,7 +409,7 @@ export class LocalWorkflow {
const transaction = await orchestrator.registerStepSuccess( const transaction = await orchestrator.registerStepSuccess(
idempotencyKey, idempotencyKey,
handler(this.container, context), handler(this.container_, context),
undefined, undefined,
response response
) )
@@ -425,7 +437,7 @@ export class LocalWorkflow {
const transaction = await orchestrator.registerStepFailure( const transaction = await orchestrator.registerStepFailure(
idempotencyKey, idempotencyKey,
error, error,
handler(this.container, context) handler(this.container_, context)
) )
cleanUpEventListeners() cleanUpEventListeners()
@@ -5,17 +5,20 @@ import {
TransactionStep, TransactionStep,
} from "@medusajs/orchestration" } from "@medusajs/orchestration"
import { ContainerLike, Context, MedusaContainer } from "@medusajs/types" import { ContainerLike, Context, MedusaContainer } from "@medusajs/types"
import { InjectSharedContext, MedusaContext, isString } from "@medusajs/utils" import { InjectSharedContext, isString, MedusaContext } from "@medusajs/utils"
import { import {
MedusaWorkflow,
ReturnWorkflow,
resolveValue,
type FlowRunOptions, type FlowRunOptions,
MedusaWorkflow,
resolveValue,
ReturnWorkflow,
} from "@medusajs/workflows-sdk" } from "@medusajs/workflows-sdk"
import { ulid } from "ulid" import { ulid } from "ulid"
import { InMemoryDistributedTransactionStorage } from "../utils" import { InMemoryDistributedTransactionStorage } from "../utils"
export type WorkflowOrchestratorRunOptions<T> = FlowRunOptions<T> & { export type WorkflowOrchestratorRunOptions<T> = Omit<
FlowRunOptions<T>,
"container"
> & {
transactionId?: string transactionId?: string
container?: ContainerLike container?: ContainerLike
} }
@@ -9,9 +9,9 @@ import {
import { import {
InjectManager, InjectManager,
InjectSharedContext, InjectSharedContext,
isString,
MedusaContext, MedusaContext,
MedusaError, MedusaError,
isString,
} from "@medusajs/utils" } from "@medusajs/utils"
import type { import type {
IWorkflowEngineService, IWorkflowEngineService,
@@ -191,7 +191,7 @@ export class WorkflowsModuleService implements IWorkflowEngineService {
transactionId: string, transactionId: string,
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
) { ) {
return this.workflowOrchestratorService_.getRunningTransaction( return await this.workflowOrchestratorService_.getRunningTransaction(
workflowId, workflowId,
transactionId, transactionId,
context context
@@ -211,7 +211,7 @@ export class WorkflowsModuleService implements IWorkflowEngineService {
}, },
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
) { ) {
return this.workflowOrchestratorService_.setStepSuccess( return await this.workflowOrchestratorService_.setStepSuccess(
{ {
idempotencyKey, idempotencyKey,
stepResponse, stepResponse,
@@ -234,7 +234,7 @@ export class WorkflowsModuleService implements IWorkflowEngineService {
}, },
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
) { ) {
return this.workflowOrchestratorService_.setStepFailure( return await this.workflowOrchestratorService_.setStepFailure(
{ {
idempotencyKey, idempotencyKey,
stepResponse, stepResponse,
@@ -10,18 +10,21 @@ import {
Logger, Logger,
MedusaContainer, MedusaContainer,
} from "@medusajs/types" } from "@medusajs/types"
import { InjectSharedContext, MedusaContext, isString } from "@medusajs/utils" import { InjectSharedContext, isString, MedusaContext } from "@medusajs/utils"
import { import {
FlowRunOptions, FlowRunOptions,
MedusaWorkflow, MedusaWorkflow,
ReturnWorkflow,
resolveValue, resolveValue,
ReturnWorkflow,
} from "@medusajs/workflows-sdk" } from "@medusajs/workflows-sdk"
import Redis from "ioredis" import Redis from "ioredis"
import { ulid } from "ulid" import { ulid } from "ulid"
import type { RedisDistributedTransactionStorage } from "../utils" import type { RedisDistributedTransactionStorage } from "../utils"
export type WorkflowOrchestratorRunOptions<T> = FlowRunOptions<T> & { export type WorkflowOrchestratorRunOptions<T> = Omit<
FlowRunOptions<T>,
"container"
> & {
transactionId?: string transactionId?: string
container?: ContainerLike container?: ContainerLike
} }
@@ -2,3 +2,4 @@ export * from "./merge-data"
export * from "./empty-handler" export * from "./empty-handler"
export * from "./pipe" export * from "./pipe"
export * from "./workflow-export" export * from "./workflow-export"
export * from "./type"
+134
View File
@@ -0,0 +1,134 @@
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
import {
DistributedTransaction,
DistributedTransactionEvents,
LocalWorkflow,
TransactionStepError,
} from "@medusajs/orchestration"
export type FlowRunOptions<TData = unknown> = {
input?: TData
context?: Context
resultFrom?: string | string[] | Symbol
throwOnError?: boolean
events?: DistributedTransactionEvents
container?: LoadedModule[] | MedusaContainer
}
export type FlowRegisterStepSuccessOptions<TData = unknown> = {
idempotencyKey: string
response?: TData
context?: Context
resultFrom?: string | string[] | Symbol
throwOnError?: boolean
events?: DistributedTransactionEvents
container?: LoadedModule[] | MedusaContainer
}
export type FlowRegisterStepFailureOptions<TData = unknown> = {
idempotencyKey: string
response?: TData
context?: Context
resultFrom?: string | string[] | Symbol
throwOnError?: boolean
events?: DistributedTransactionEvents
container?: LoadedModule[] | MedusaContainer
}
export type FlowCancelOptions = {
transaction?: DistributedTransaction
transactionId?: string
context?: Context
throwOnError?: boolean
events?: DistributedTransactionEvents
container?: LoadedModule[] | MedusaContainer
}
export type WorkflowResult<TResult = unknown> = {
errors: TransactionStepError[]
transaction: DistributedTransaction
result: TResult
}
export type ExportedWorkflow<
TData = unknown,
TResult = unknown,
TDataOverride = undefined,
TResultOverride = undefined
> = {
run: (
args?: FlowRunOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
) => Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
registerStepSuccess: (
args?: FlowRegisterStepSuccessOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
) => Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
registerStepFailure: (
args?: FlowRegisterStepFailureOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
) => Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
cancel: (args?: FlowCancelOptions) => Promise<WorkflowResult>
}
export type MainExportedWorkflow<TData = unknown, TResult = unknown> = {
// Main function on the exported workflow
<TDataOverride = undefined, TResultOverride = undefined>(
container?: LoadedModule[] | MedusaContainer
): Omit<
LocalWorkflow,
"run" | "registerStepSuccess" | "registerStepFailure" | "cancel"
> &
ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride>
} & {
/**
* You can also directly call run, registerStepSuccess, registerStepFailure and cancel on the exported workflow
*/
run<TDataOverride = undefined, TResultOverride = undefined>(
args?: FlowRunOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
): Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
registerStepSuccess<TDataOverride = undefined, TResultOverride = undefined>(
args?: FlowRegisterStepSuccessOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
): Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
registerStepFailure<TDataOverride = undefined, TResultOverride = undefined>(
args?: FlowRegisterStepFailureOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
): Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
cancel(args?: FlowCancelOptions): Promise<WorkflowResult>
}
@@ -1,12 +1,9 @@
import { import {
DistributedTransaction,
DistributedTransactionEvents,
LocalWorkflow, LocalWorkflow,
TransactionHandlerType, TransactionHandlerType,
TransactionState, TransactionState,
TransactionStepError,
} from "@medusajs/orchestration" } from "@medusajs/orchestration"
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types" import { LoadedModule, MedusaContainer } from "@medusajs/types"
import { MedusaModule } from "@medusajs/modules-sdk" import { MedusaModule } from "@medusajs/modules-sdk"
import { MedusaContextType } from "@medusajs/utils" import { MedusaContextType } from "@medusajs/utils"
@@ -14,139 +11,15 @@ import { EOL } from "os"
import { ulid } from "ulid" import { ulid } from "ulid"
import { MedusaWorkflow } from "../medusa-workflow" import { MedusaWorkflow } from "../medusa-workflow"
import { resolveValue } from "../utils/composer" import { resolveValue } from "../utils/composer"
import {
export type FlowRunOptions<TData = unknown> = { ExportedWorkflow,
input?: TData FlowCancelOptions,
context?: Context FlowRegisterStepFailureOptions,
resultFrom?: string | string[] | Symbol FlowRegisterStepSuccessOptions,
throwOnError?: boolean FlowRunOptions,
events?: DistributedTransactionEvents MainExportedWorkflow,
} WorkflowResult,
} from "./type"
export type FlowRegisterStepSuccessOptions<TData = unknown> = {
idempotencyKey: string
response?: TData
context?: Context
resultFrom?: string | string[] | Symbol
throwOnError?: boolean
events?: DistributedTransactionEvents
}
export type FlowRegisterStepFailureOptions<TData = unknown> = {
idempotencyKey: string
response?: TData
context?: Context
resultFrom?: string | string[] | Symbol
throwOnError?: boolean
events?: DistributedTransactionEvents
}
export type FlowCancelOptions = {
transaction?: DistributedTransaction
transactionId?: string
context?: Context
throwOnError?: boolean
events?: DistributedTransactionEvents
}
export type WorkflowResult<TResult = unknown> = {
errors: TransactionStepError[]
transaction: DistributedTransaction
result: TResult
}
export type ExportedWorkflow<
TData = unknown,
TResult = unknown,
TDataOverride = undefined,
TResultOverride = undefined
> = {
run: (
args?: FlowRunOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
) => Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
registerStepSuccess: (
args?: FlowRegisterStepSuccessOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
) => Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
registerStepFailure: (
args?: FlowRegisterStepFailureOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
) => Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
cancel: (args?: FlowCancelOptions) => Promise<WorkflowResult>
}
export type MainExportedWorkflow<TData = unknown, TResult = unknown> = {
// Main function on the exported workflow
<TDataOverride = undefined, TResultOverride = undefined>(
container?: LoadedModule[] | MedusaContainer
): Omit<
LocalWorkflow,
"run" | "registerStepSuccess" | "registerStepFailure" | "cancel"
> &
ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride>
/**
* You can also directly call run, registerStepSuccess, registerStepFailure and cancel on the exported workflow
*/
run<TDataOverride = undefined, TResultOverride = undefined>(
args?: FlowRunOptions<
TDataOverride extends undefined ? TData : TDataOverride
> & {
container?: LoadedModule[] | MedusaContainer
}
): Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
registerStepSuccess<TDataOverride = undefined, TResultOverride = undefined>(
args?: FlowRegisterStepSuccessOptions<
TDataOverride extends undefined ? TData : TDataOverride
> & {
container?: LoadedModule[] | MedusaContainer
}
): Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
registerStepFailure<TDataOverride = undefined, TResultOverride = undefined>(
args?: FlowRegisterStepFailureOptions<
TDataOverride extends undefined ? TData : TDataOverride
> & {
container?: LoadedModule[] | MedusaContainer
}
): Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
>
cancel(
args?: FlowCancelOptions & {
container?: LoadedModule[] | MedusaContainer
}
): Promise<WorkflowResult>
}
function createContextualWorkflowRunner< function createContextualWorkflowRunner<
TData = unknown, TData = unknown,
@@ -172,12 +45,6 @@ function createContextualWorkflowRunner<
"run" | "registerStepSuccess" | "registerStepFailure" | "cancel" "run" | "registerStepSuccess" | "registerStepFailure" | "cancel"
> & > &
ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride> { ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride> {
if (!container) {
container = MedusaModule.getLoadedModules().map(
(mod) => Object.values(mod)[0]
)
}
const flow = new LocalWorkflow(workflowId, container!) const flow = new LocalWorkflow(workflowId, container!)
const originalRun = flow.run.bind(flow) const originalRun = flow.run.bind(flow)
@@ -187,9 +54,24 @@ function createContextualWorkflowRunner<
const originalExecution = async ( const originalExecution = async (
method, method,
{ throwOnError, resultFrom, isCancel = false }, {
throwOnError,
resultFrom,
isCancel = false,
container: executionContainer,
},
...args ...args
) => { ) => {
if (!executionContainer && !flow.container) {
executionContainer = MedusaModule.getLoadedModules().map(
(mod) => Object.values(mod)[0]
)
}
if (!flow.container) {
flow.container = executionContainer
}
const transaction = await method.apply(method, args) const transaction = await method.apply(method, args)
let errors = transaction.getErrors(TransactionHandlerType.INVOKE) let errors = transaction.getErrors(TransactionHandlerType.INVOKE)
@@ -236,6 +118,7 @@ function createContextualWorkflowRunner<
throwOnError, throwOnError,
resultFrom, resultFrom,
events, events,
container,
}: FlowRunOptions = { }: FlowRunOptions = {
throwOnError: true, throwOnError: true,
resultFrom: defaultResult, resultFrom: defaultResult,
@@ -269,7 +152,7 @@ function createContextualWorkflowRunner<
return await originalExecution( return await originalExecution(
originalRun, originalRun,
{ throwOnError, resultFrom }, { throwOnError, resultFrom, container },
context.transactionId, context.transactionId,
input, input,
context, context,
@@ -286,6 +169,7 @@ function createContextualWorkflowRunner<
throwOnError, throwOnError,
resultFrom, resultFrom,
events, events,
container,
}: FlowRegisterStepSuccessOptions = { }: FlowRegisterStepSuccessOptions = {
idempotencyKey: "", idempotencyKey: "",
throwOnError: true, throwOnError: true,
@@ -304,7 +188,7 @@ function createContextualWorkflowRunner<
return await originalExecution( return await originalExecution(
originalRegisterStepSuccess, originalRegisterStepSuccess,
{ throwOnError, resultFrom }, { throwOnError, resultFrom, container },
idempotencyKey, idempotencyKey,
response, response,
context, context,
@@ -321,6 +205,7 @@ function createContextualWorkflowRunner<
throwOnError, throwOnError,
resultFrom, resultFrom,
events, events,
container,
}: FlowRegisterStepFailureOptions = { }: FlowRegisterStepFailureOptions = {
idempotencyKey: "", idempotencyKey: "",
throwOnError: true, throwOnError: true,
@@ -339,7 +224,7 @@ function createContextualWorkflowRunner<
return await originalExecution( return await originalExecution(
originalRegisterStepFailure, originalRegisterStepFailure,
{ throwOnError, resultFrom }, { throwOnError, resultFrom, container },
idempotencyKey, idempotencyKey,
response, response,
context, context,
@@ -355,6 +240,7 @@ function createContextualWorkflowRunner<
context: outerContext, context: outerContext,
throwOnError, throwOnError,
events, events,
container,
}: FlowCancelOptions = { }: FlowCancelOptions = {
throwOnError: true, throwOnError: true,
} }
@@ -373,6 +259,7 @@ function createContextualWorkflowRunner<
throwOnError, throwOnError,
resultFrom: undefined, resultFrom: undefined,
isCancel: true, isCancel: true,
container,
}, },
transaction ?? transactionId, transaction ?? transactionId,
context, context,
@@ -397,6 +284,7 @@ export const exportWorkflow = <TData = unknown, TResult = unknown>(
TDataOverride = undefined, TDataOverride = undefined,
TResultOverride = undefined TResultOverride = undefined
>( >(
// TODO: rm when all usage have been migrated
container?: LoadedModule[] | MedusaContainer container?: LoadedModule[] | MedusaContainer
): Omit< ): Omit<
LocalWorkflow, LocalWorkflow,
@@ -456,9 +344,7 @@ export const exportWorkflow = <TData = unknown, TResult = unknown>(
>( >(
args?: FlowRunOptions< args?: FlowRunOptions<
TDataOverride extends undefined ? TData : TDataOverride TDataOverride extends undefined ? TData : TDataOverride
> & { >
container?: LoadedModule[] | MedusaContainer
}
): Promise< ): Promise<
WorkflowResult< WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride TResultOverride extends undefined ? TResult : TResultOverride
@@ -482,9 +368,7 @@ export const exportWorkflow = <TData = unknown, TResult = unknown>(
>( >(
args?: FlowRegisterStepSuccessOptions< args?: FlowRegisterStepSuccessOptions<
TDataOverride extends undefined ? TData : TDataOverride TDataOverride extends undefined ? TData : TDataOverride
> & { >
container?: LoadedModule[] | MedusaContainer
}
): Promise< ): Promise<
WorkflowResult< WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride TResultOverride extends undefined ? TResult : TResultOverride
@@ -512,9 +396,7 @@ export const exportWorkflow = <TData = unknown, TResult = unknown>(
>( >(
args?: FlowRegisterStepFailureOptions< args?: FlowRegisterStepFailureOptions<
TDataOverride extends undefined ? TData : TDataOverride TDataOverride extends undefined ? TData : TDataOverride
> & { >
container?: LoadedModule[] | MedusaContainer
}
): Promise< ): Promise<
WorkflowResult< WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride TResultOverride extends undefined ? TResult : TResultOverride
@@ -537,9 +419,7 @@ export const exportWorkflow = <TData = unknown, TResult = unknown>(
} }
exportedWorkflow.cancel = async ( exportedWorkflow.cancel = async (
args?: FlowCancelOptions & { args?: FlowCancelOptions
container?: LoadedModule[] | MedusaContainer
}
): Promise<WorkflowResult> => { ): Promise<WorkflowResult> => {
const container = args?.container const container = args?.container
delete args?.container delete args?.container
@@ -552,5 +432,5 @@ export const exportWorkflow = <TData = unknown, TResult = unknown>(
} }
MedusaWorkflow.registerWorkflow(workflowId, exportedWorkflow) MedusaWorkflow.registerWorkflow(workflowId, exportedWorkflow)
return exportedWorkflow return exportedWorkflow as MainExportedWorkflow<TData, TResult>
} }
@@ -1,7 +1,12 @@
import { createStep, createWorkflow, StepResponse } from "./composer" import {
createStep,
createWorkflow,
StepResponse,
WorkflowData,
} from "./composer"
const step1 = createStep("step1", async (input: {}, context) => { const step1 = createStep("step1", async (input: {}, context) => {
return new StepResponse({ step1: ["step1"] }) return new StepResponse({ step1: "step1" })
}) })
type Step2Input = { filters: { id: string[] } } | { filters: { id: string } } type Step2Input = { filters: { id: string[] } } | { filters: { id: string } }
@@ -13,17 +18,22 @@ const step3 = createStep("step3", async () => {
return new StepResponse({ step3: "step3" }) return new StepResponse({ step3: "step3" })
}) })
const workflow = createWorkflow("workflow", function () { const workflow = createWorkflow(
const step1Res = step1() "sub-workflow",
step3() function (input: WorkflowData<{ outsideWorkflowData: string }>) {
return step2({ filters: { id: step1Res.step1 } }) step1()
}) step3()
return step2({ filters: { id: input.outsideWorkflowData } })
}
)
const workflow2 = createWorkflow("workflow", function () { const workflow2 = createWorkflow("workflow", function () {
const step1Res = step1() const step1Res = step1()
step3() step3()
workflow()
return step2({ filters: { id: step1Res.step1 } }) const workflowRes = workflow.asStep({ outsideWorkflowData: step1Res.step1 })
return workflowRes
}) })
workflow2() workflow2()
@@ -2,8 +2,109 @@ import { createStep } from "../create-step"
import { createWorkflow } from "../create-workflow" import { createWorkflow } from "../create-workflow"
import { StepResponse } from "../helpers" import { StepResponse } from "../helpers"
import { transform } from "../transform" import { transform } from "../transform"
import { WorkflowData } from "../type"
let count = 1
const getNewWorkflowId = () => `workflow-${count++}`
describe("Workflow composer", () => { describe("Workflow composer", () => {
describe("running sub workflows", () => {
it("should succeed", async function () {
const step1 = createStep("step1", async () => {
return new StepResponse({ result: "step1" })
})
const step2 = createStep("step2", async (input: string) => {
return new StepResponse({ result: input })
})
const step3 = createStep("step3", async (input: string) => {
return new StepResponse({ result: input })
})
const subWorkflow = createWorkflow(
getNewWorkflowId(),
function (input: WorkflowData<string>) {
step1()
return step2(input)
}
)
const workflow = createWorkflow(getNewWorkflowId(), function () {
const subWorkflowRes = subWorkflow.runAsStep({
input: "hi from outside",
})
return step3(subWorkflowRes.result)
})
const { result } = await workflow.run({ input: {} })
expect(result).toEqual({ result: "hi from outside" })
})
it("should revert the workflow and sub workflow on failure", async function () {
const step1Mock = jest.fn()
const step1 = createStep(
"step1",
async () => {
return new StepResponse({ result: "step1" })
},
step1Mock
)
const step2Mock = jest.fn()
const step2 = createStep(
"step2",
async (input: string) => {
return new StepResponse({ result: input })
},
step2Mock
)
const step3Mock = jest.fn()
const step3 = createStep(
"step3",
async () => {
return new StepResponse()
},
step3Mock
)
const step4WithError = createStep("step4", async () => {
throw new Error("Step4 failed")
})
const subWorkflow = createWorkflow(
getNewWorkflowId(),
function (input: WorkflowData<string>) {
step1()
return step2(input)
}
)
const workflow = createWorkflow(getNewWorkflowId(), function () {
step3()
const subWorkflowRes = subWorkflow.runAsStep({
input: "hi from outside",
})
step4WithError()
return subWorkflowRes
})
const { errors } = await workflow.run({ throwOnError: false })
expect(errors).toEqual([
expect.objectContaining({
error: expect.objectContaining({
message: "Step4 failed",
}),
}),
])
expect(step1Mock).toHaveBeenCalledTimes(1)
expect(step2Mock).toHaveBeenCalledTimes(1)
expect(step3Mock).toHaveBeenCalledTimes(1)
})
})
it("should not throw an unhandled error on failed transformer resolution after a step fail, but should rather push the errors in the errors result", async function () { it("should not throw an unhandled error on failed transformer resolution after a step fail, but should rather push the errors in the errors result", async function () {
const step1 = createStep("step1", async () => { const step1 = createStep("step1", async () => {
return new StepResponse({ result: "step1" }) return new StepResponse({ result: "step1" })
@@ -25,7 +126,7 @@ describe("Workflow composer", () => {
}) })
}) })
const { errors } = await work().run({ input: {}, throwOnError: false }) const { errors } = await work.run({ input: {}, throwOnError: false })
expect(errors).toEqual([ expect(errors).toEqual([
{ {
@@ -1,96 +1,24 @@
import { import {
LocalWorkflow,
TransactionModelOptions, TransactionModelOptions,
WorkflowHandler, WorkflowHandler,
WorkflowManager, WorkflowManager,
} from "@medusajs/orchestration" } from "@medusajs/orchestration"
import { LoadedModule, MedusaContainer } from "@medusajs/types" import { LoadedModule, MedusaContainer } from "@medusajs/types"
import { isString, OrchestrationUtils } from "@medusajs/utils" import { isString, OrchestrationUtils } from "@medusajs/utils"
import { ExportedWorkflow, exportWorkflow } from "../../helper" import { exportWorkflow } from "../../helper"
import { proxify } from "./helpers/proxy" import { proxify } from "./helpers/proxy"
import { import {
CreateWorkflowComposerContext, CreateWorkflowComposerContext,
ReturnWorkflow,
StepFunction,
WorkflowData, WorkflowData,
WorkflowDataProperties, WorkflowDataProperties,
} from "./type" } from "./type"
import { createStep } from "./create-step"
import { StepResponse } from "./helpers"
global[OrchestrationUtils.SymbolMedusaWorkflowComposerContext] = null global[OrchestrationUtils.SymbolMedusaWorkflowComposerContext] = null
/**
* An exported workflow, which is the type of a workflow constructed by the {@link createWorkflow} function. The exported workflow can be invoked to create
* an executable workflow, optionally within a specified container. So, to execute the workflow, you must invoke the exported workflow, then run the
* `run` method of the exported workflow.
*
* @example
* To execute a workflow:
*
* ```ts
* myWorkflow()
* .run({
* input: {
* name: "John"
* }
* })
* .then(({ result }) => {
* console.log(result)
* })
* ```
*
* To specify the container of the workflow, you can pass it as an argument to the call of the exported workflow. This is necessary when executing the workflow
* within a Medusa resource such as an API Route or a Subscriber.
*
* For example:
*
* ```ts
* import type {
* MedusaRequest,
* MedusaResponse
* } from "@medusajs/medusa";
* import myWorkflow from "../../../workflows/hello-world";
*
* export async function GET(
* req: MedusaRequest,
* res: MedusaResponse
* ) {
* const { result } = await myWorkflow(req.scope)
* .run({
* input: {
* name: req.query.name as string
* }
* })
*
* res.send(result)
* }
* ```
*/
export type ReturnWorkflow<
TData,
TResult,
THooks extends Record<string, Function>
> = {
<TDataOverride = undefined, TResultOverride = undefined>(
container?: LoadedModule[] | MedusaContainer
): Omit<
LocalWorkflow,
"run" | "registerStepSuccess" | "registerStepFailure" | "cancel"
> &
ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride>
} & THooks & {
getName: () => string
} & {
config: (config: TransactionModelOptions) => void
}
/**
* Extract the raw type of the expected input data of a workflow.
*
* @example
* type WorkflowInputData = UnwrapWorkflowInputDataType<typeof myWorkflow>
*/
export type UnwrapWorkflowInputDataType<
T extends ReturnWorkflow<any, any, any>
> = T extends ReturnWorkflow<infer TData, infer R, infer THooks> ? TData : never
/** /**
* This function creates a workflow with the provided name and a constructor function. * This function creates a workflow with the provided name and a constructor function.
* The constructor function builds the workflow from steps created by the {@link createStep} function. * The constructor function builds the workflow from steps created by the {@link createStep} function.
@@ -256,5 +184,38 @@ export function createWorkflow<
mainFlow.getName = () => name mainFlow.getName = () => name
mainFlow.run = mainFlow().run
mainFlow.runAsStep = ({
input,
}: {
input: TData
}): ReturnType<StepFunction<TData, TResult>> => {
// TODO: Async sub workflow is not supported yet
// Info: Once the export workflow can fire the execution through the engine if loaded, the async workflow can be executed,
// the step would inherit the async configuration and subscribe to the onFinish event of the sub worklow and mark itself as success or failure
return createStep(
`${name}-as-step`,
async (stepInput: TData, stepContext) => {
const { container, ...sharedContext } = stepContext
const transaction = await workflow.run({
input: stepInput as any,
container,
context: sharedContext,
})
return new StepResponse(transaction.result, transaction)
},
async (transaction, { container }) => {
if (!transaction) {
return
}
await workflow(container).cancel(transaction)
}
)(input) as ReturnType<StepFunction<TData, TResult>>
}
return mainFlow as ReturnWorkflow<TData, TResult, THooks> return mainFlow as ReturnWorkflow<TData, TResult, THooks>
} }
@@ -1,5 +1,5 @@
import { PermanentStepFailureError } from "@medusajs/orchestration" import { PermanentStepFailureError } from "@medusajs/orchestration"
import { OrchestrationUtils } from "@medusajs/utils" import { isDefined, OrchestrationUtils } from "@medusajs/utils"
/** /**
* This class is used to create the response returned by a step. A step return its data by returning an instance of `StepResponse`. * This class is used to create the response returned by a step. A step return its data by returning an instance of `StepResponse`.
@@ -26,13 +26,15 @@ export class StepResponse<TOutput, TCompensateInput = TOutput> {
/** /**
* The output of the step. * The output of the step.
*/ */
output: TOutput, output?: TOutput,
/** /**
* The input to be passed as a parameter to the step's compensation function. If not provided, the `output` will be provided instead. * The input to be passed as a parameter to the step's compensation function. If not provided, the `output` will be provided instead.
*/ */
compensateInput?: TCompensateInput compensateInput?: TCompensateInput
) { ) {
this.#output = output if (isDefined(output)) {
this.#output = output
}
this.#compensateInput = (compensateInput ?? output) as TCompensateInput this.#compensateInput = (compensateInput ?? output) as TCompensateInput
} }
@@ -1,11 +1,14 @@
import { import {
LocalWorkflow,
OrchestratorBuilder, OrchestratorBuilder,
TransactionContext as OriginalWorkflowTransactionContext, TransactionContext as OriginalWorkflowTransactionContext,
TransactionModelOptions,
TransactionPayload, TransactionPayload,
TransactionStepsDefinition, TransactionStepsDefinition,
WorkflowHandler, WorkflowHandler,
} from "@medusajs/orchestration" } from "@medusajs/orchestration"
import { Context, MedusaContainer } from "@medusajs/types" import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
import { ExportedWorkflow } from "../../helper"
export type StepFunctionResult<TOutput extends unknown | unknown[] = unknown> = export type StepFunctionResult<TOutput extends unknown | unknown[] = unknown> =
(this: CreateWorkflowComposerContext) => WorkflowData<TOutput> (this: CreateWorkflowComposerContext) => WorkflowData<TOutput>
@@ -134,3 +137,89 @@ export type WorkflowTransactionContext = StepExecutionContext &
OriginalWorkflowTransactionContext & { OriginalWorkflowTransactionContext & {
invoke: { [key: string]: { output: any } } invoke: { [key: string]: { output: any } }
} }
/**
* An exported workflow, which is the type of a workflow constructed by the {@link createWorkflow} function. The exported workflow can be invoked to create
* an executable workflow, optionally within a specified container. So, to execute the workflow, you must invoke the exported workflow, then run the
* `run` method of the exported workflow.
*
* @example
* To execute a workflow:
*
* ```ts
* myWorkflow()
* .run({
* input: {
* name: "John"
* }
* })
* .then(({ result }) => {
* console.log(result)
* })
* ```
*
* To specify the container of the workflow, you can pass it as an argument to the call of the exported workflow. This is necessary when executing the workflow
* within a Medusa resource such as an API Route or a Subscriber.
*
* For example:
*
* ```ts
* import type {
* MedusaRequest,
* MedusaResponse
* } from "@medusajs/medusa";
* import myWorkflow from "../../../workflows/hello-world";
*
* export async function GET(
* req: MedusaRequest,
* res: MedusaResponse
* ) {
* const { result } = await myWorkflow(req.scope)
* .run({
* input: {
* name: req.query.name as string
* }
* })
*
* res.send(result)
* }
* ```
*/
export type ReturnWorkflow<
TData,
TResult,
THooks extends Record<string, Function>
> = {
<TDataOverride = undefined, TResultOverride = undefined>(
container?: LoadedModule[] | MedusaContainer
): Omit<
LocalWorkflow,
"run" | "registerStepSuccess" | "registerStepFailure" | "cancel"
> &
ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride>
} & THooks & {
runAsStep: ({
input,
}: {
input: TData
}) => ReturnType<StepFunction<TData, TResult>>
run: <TDataOverride = undefined, TResultOverride = undefined>(
...args: Parameters<
ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride>["run"]
>
) => ReturnType<
ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride>["run"]
>
getName: () => string
config: (config: TransactionModelOptions) => void
}
/**
* Extract the raw type of the expected input data of a workflow.
*
* @example
* type WorkflowInputData = UnwrapWorkflowInputDataType<typeof myWorkflow>
*/
export type UnwrapWorkflowInputDataType<
T extends ReturnWorkflow<any, any, any>
> = T extends ReturnWorkflow<infer TData, infer R, infer THooks> ? TData : never