chore(): start moving some packages to the core directory (#7215)

This commit is contained in:
Adrien de Peretti
2024-05-03 13:37:41 +02:00
committed by GitHub
parent fdee748eed
commit bbccd6481d
1436 changed files with 275 additions and 756 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,56 @@
import { mergeData } from "../merge-data"
import { WorkflowStepMiddlewareReturn } from "../pipe"
describe("merge", function () {
it("should merge 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 result = (await mergeData(
["input", "another", "stringProp", "anArray"],
"payload"
)({ data: source } as any)) as unknown as WorkflowStepMiddlewareReturn
expect(result).toEqual({
alias: "payload",
value: {
...source.input,
...source.another,
anArray: source.anArray,
stringProp: source.stringProp,
},
})
})
it("should merge 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 mergeData()({
data: source,
} as any)) as unknown as WorkflowStepMiddlewareReturn
expect(result).toEqual({
...source.input,
...source.another,
anArray: source.anArray,
stringProp: source.stringProp,
})
})
})
@@ -0,0 +1,234 @@
import { pipe } from "../pipe"
describe("Pipe", function () {
it("should evaluate the input object and append the values to the data object and return the result from the handler", async function () {
const payload = { input: "input" }
const output = { test: "test" }
const invoke = {
input: payload,
step1: { ...payload, step1Data: { test: "test" } },
step2: { ...payload, step2Data: { test: "test" } },
}
const handler = jest.fn().mockImplementation(async () => output)
const input = {
inputAlias: "payload",
invoke: [
{
from: "payload",
alias: "input",
},
{
from: "step1",
alias: "previousDataStep1",
},
{
from: "step2",
alias: "previousDataStep2",
},
],
}
const result = await pipe(input, handler)({ invoke, payload } as any)
expect(handler).toHaveBeenCalled()
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
data: {
input: payload,
previousDataStep1: invoke.step1,
previousDataStep2: invoke.step2,
},
})
)
expect(result).toBeDefined()
expect(result).toEqual(output)
})
it("should evaluate the input object and append the values to the data object using the merge and return the result from the handler", async function () {
const payload = { input: "input" }
const output = { test: "test" }
const invoke = {
input: payload,
step1: { step1Data: { test: "test" } },
step2: [{ test: "test" }],
}
const handler = jest.fn().mockImplementation(async () => output)
const input = {
inputAlias: "payload",
merge: true,
invoke: [
{
from: "payload",
},
{
from: "step1",
},
{
from: "step2",
alias: "step2Data",
},
],
}
const result = await pipe(input, handler)({ invoke, payload } as any)
expect(handler).toHaveBeenCalled()
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
data: {
...payload,
...invoke.step1,
step2Data: invoke.step2,
},
})
)
expect(result).toBeDefined()
expect(result).toEqual(output)
})
it("should evaluate the input object and append the values to the data object using the merge to store on the merge alias and return the result from the handler", async function () {
const payload = { input: "input" }
const output = { test: "test" }
const invoke = {
input: payload,
step1: { step1Data: { test: "test" } },
step2: { step2Data: { test: "test" } },
}
const handler = jest.fn().mockImplementation(async () => output)
const input = {
inputAlias: "payload",
mergeAlias: "mergedData",
invoke: [
{
from: "payload",
alias: "input",
},
{
from: "step1",
alias: "step1Data",
},
{
from: "step2",
alias: "step2Data",
},
],
}
const result = await pipe(input, handler)({ invoke, payload } as any)
expect(handler).toHaveBeenCalled()
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
data: {
input: payload,
step1Data: invoke.step1,
step2Data: invoke.step2,
mergedData: {
...payload,
...invoke.step1,
...invoke.step2,
},
},
})
)
expect(result).toBeDefined()
expect(result).toEqual(output)
})
it("should evaluate the input object and append the values to the data object using the merge to store on the merge alias from the merge from values and return the result from the handler", async function () {
const payload = { input: "input" }
const output = { test: "test" }
const invoke = {
input: payload,
step1: { step1Data: { test: "test" } },
step2: { step2Data: { test: "test" } },
}
const handler = jest.fn().mockImplementation(async () => output)
const input = {
inputAlias: "payload",
mergeAlias: "mergedData",
mergeFrom: ["input", "step1Data"],
invoke: [
{
from: "payload",
alias: "input",
},
{
from: "step1",
alias: "step1Data",
},
{
from: "step2",
alias: "step2Data",
},
],
}
const result = await pipe(input, handler)({ invoke, payload } as any)
expect(handler).toHaveBeenCalled()
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
data: {
input: payload,
step1Data: invoke.step1,
step2Data: invoke.step2,
mergedData: {
...payload,
...invoke.step1,
},
},
})
)
expect(result).toBeDefined()
expect(result).toEqual(output)
})
it("should execute onComplete function if available but the output result shouldn't change", async function () {
const payload = { input: "input" }
const output = { test: "test" }
const invoke = {
input: payload,
}
const onComplete = jest.fn(async ({ data }) => {
data.__changed = true
return
})
const handler = jest.fn().mockImplementation(async () => output)
const input = {
inputAlias: "payload",
invoke: [
{
from: "payload",
alias: "input",
},
],
onComplete,
}
const result = await pipe(input, handler)({ invoke, payload } as any)
expect(handler).toHaveBeenCalled()
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
data: {
input: payload,
},
})
)
expect(onComplete).toHaveBeenCalled()
expect(result).toEqual(output)
})
})
@@ -0,0 +1,144 @@
import { createMedusaContainer } from "@medusajs/utils"
import { MedusaWorkflow } from "../../medusa-workflow"
import { exportWorkflow } from "../workflow-export"
jest.mock("@medusajs/orchestration", () => {
return {
TransactionHandlerType: {
INVOKE: "invoke",
COMPENSATE: "compensate",
},
TransactionState: {
FAILED: "failed",
REVERTED: "reverted",
},
LocalWorkflow: jest.fn(() => {
return {
run: jest.fn(() => {
return {
getErrors: jest.fn(),
getState: jest.fn(() => "done"),
getContext: jest.fn(() => {
return {
invoke: { result_step: "invoke_test" },
}
}),
}
}),
registerStepSuccess: jest.fn(() => {
return {
getErrors: jest.fn(),
getState: jest.fn(() => "done"),
getContext: jest.fn(() => {
return {
invoke: { result_step: "invoke_test" },
}
}),
}
}),
registerStepFailure: jest.fn(() => {
return {
getErrors: jest.fn(),
getState: jest.fn(() => "done"),
getContext: jest.fn(() => {
return {
invoke: { result_step: "invoke_test" },
}
}),
}
}),
cancel: jest.fn(() => {
return {
getErrors: jest.fn(),
getState: jest.fn(() => "reverted"),
getContext: jest.fn(() => {
return {
invoke: { result_step: "invoke_test" },
}
}),
}
}),
}
}),
}
})
describe("Export Workflow", function () {
afterEach(() => {
MedusaWorkflow.workflows = {}
})
it("should prepare the input data before initializing the transaction", async function () {
let transformedInput
const prepare = jest.fn().mockImplementation(async (data) => {
data.__transformed = true
transformedInput = data
return data
})
const work = exportWorkflow("id" as any, "result_step", prepare)
const container = createMedusaContainer()
const wfHandler = work(container)
const input = {
test: "payload",
}
const { result } = await wfHandler.run({
input,
})
expect(input).toEqual({
test: "payload",
})
expect(transformedInput).toEqual({
test: "payload",
__transformed: true,
})
expect(result).toEqual("invoke_test")
})
describe("Using the exported workflow run", function () {
afterEach(() => {
MedusaWorkflow.workflows = {}
})
it("should prepare the input data before initializing the transaction", async function () {
let transformedInput
const prepare = jest.fn().mockImplementation(async (data) => {
data.__transformed = true
transformedInput = data
return data
})
const work = exportWorkflow("id" as any, "result_step", prepare)
const input = {
test: "payload",
}
const container = createMedusaContainer()
const { result } = await work.run({
input,
container,
})
expect(input).toEqual({
test: "payload",
})
expect(transformedInput).toEqual({
test: "payload",
__transformed: true,
})
expect(result).toEqual("invoke_test")
})
})
})
@@ -0,0 +1 @@
export const emptyHandler: any = () => {}
@@ -0,0 +1,5 @@
export * from "./merge-data"
export * from "./empty-handler"
export * from "./pipe"
export * from "./workflow-export"
export * from "./type"
@@ -0,0 +1,49 @@
import { PipelineHandler, WorkflowArguments } from "./pipe"
import { isObject } from "@medusajs/utils"
/**
* Pipe utils that merges data from an object into a new object.
* The new object will have a target key with the merged data from the keys if specified.
* @param keys
* @param target
*/
export function mergeData<
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: target ? value[target as string] : value,
}
}
}
@@ -0,0 +1,167 @@
import {
DistributedTransaction,
TransactionMetadata,
WorkflowStepHandler,
} from "@medusajs/orchestration"
import { Context, MedusaContainer, SharedContext } from "@medusajs/types"
import { mergeData } from "./merge-data"
export type WorkflowStepMiddlewareReturn = {
alias?: string
value: any
}
export type WorkflowStepMiddlewareInput = {
from: string
alias?: string
}
interface PipelineInput {
/**
* The alias of the input data to store in
*/
inputAlias?: string
/**
* Descriptors to get the data from
*/
invoke?: WorkflowStepMiddlewareInput | WorkflowStepMiddlewareInput[]
compensate?: WorkflowStepMiddlewareInput | WorkflowStepMiddlewareInput[]
onComplete?: (args: WorkflowOnCompleteArguments) => Promise<void>
/**
* Apply the data merging
*/
merge?: boolean
/**
* Store the merged data in a new key, if this is present no need to set merge: true
*/
mergeAlias?: string
/**
* Store the merged data from the chosen aliases, if this is present no need to set merge: true
*/
mergeFrom?: string[]
}
export type WorkflowArguments<T = any> = {
container: MedusaContainer
payload: unknown
data: T
metadata: TransactionMetadata
context: Context | SharedContext
}
export type WorkflowOnCompleteArguments<T = any> = {
container: MedusaContainer
payload: unknown
data: T
metadata: TransactionMetadata
transaction: DistributedTransaction
context: Context | SharedContext
}
export type PipelineHandler<T extends any = undefined> = (
args: WorkflowArguments
) => Promise<
T extends undefined
? WorkflowStepMiddlewareReturn | WorkflowStepMiddlewareReturn[]
: T
>
export function pipe<T>(
input: PipelineInput,
...functions: [...PipelineHandler[], PipelineHandler<T>]
): WorkflowStepHandler {
// Apply the aggregator just before the last handler
if (
(input.merge || input.mergeAlias || input.mergeFrom) &&
functions.length
) {
const handler = functions.pop()!
functions.push(mergeData(input.mergeFrom, input.mergeAlias), handler)
}
return async ({
container,
payload,
invoke,
compensate,
metadata,
transaction,
context,
}) => {
let data = {}
const original = {
invoke: invoke ?? {},
compensate: compensate ?? {},
}
if (input.inputAlias) {
Object.assign(original.invoke, { [input.inputAlias]: payload })
}
const dataKeys = ["invoke", "compensate"]
for (const key of dataKeys) {
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]
} else {
data[action.from] = original[key][action.from]
}
}
}
let finalResult
for (const fn of functions) {
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" in (result as WorkflowStepMiddlewareReturn)
) {
if ((result as WorkflowStepMiddlewareReturn).alias) {
data[(result as WorkflowStepMiddlewareReturn).alias!] = (
result as WorkflowStepMiddlewareReturn
).value
} else {
data = (result as WorkflowStepMiddlewareReturn).value
}
}
finalResult = result
}
if (typeof input.onComplete === "function") {
const dataCopy = JSON.parse(JSON.stringify(data))
await input.onComplete({
container,
payload,
data: dataCopy,
metadata,
transaction,
context: context as Context,
})
}
return finalResult
}
}
@@ -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>
}
@@ -0,0 +1,436 @@
import {
LocalWorkflow,
TransactionHandlerType,
TransactionState,
} from "@medusajs/orchestration"
import { LoadedModule, MedusaContainer } from "@medusajs/types"
import { MedusaModule } from "@medusajs/modules-sdk"
import { MedusaContextType } from "@medusajs/utils"
import { EOL } from "os"
import { ulid } from "ulid"
import { MedusaWorkflow } from "../medusa-workflow"
import { resolveValue } from "../utils/composer"
import {
ExportedWorkflow,
FlowCancelOptions,
FlowRegisterStepFailureOptions,
FlowRegisterStepSuccessOptions,
FlowRunOptions,
MainExportedWorkflow,
WorkflowResult,
} from "./type"
function createContextualWorkflowRunner<
TData = unknown,
TResult = unknown,
TDataOverride = undefined,
TResultOverride = undefined
>({
workflowId,
defaultResult,
dataPreparation,
options,
container,
}: {
workflowId: string
defaultResult?: string | Symbol
dataPreparation?: (data: TData) => Promise<unknown>
options?: {
wrappedInput?: boolean
}
container?: LoadedModule[] | MedusaContainer
}): Omit<
LocalWorkflow,
"run" | "registerStepSuccess" | "registerStepFailure" | "cancel"
> &
ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride> {
const flow = new LocalWorkflow(workflowId, container!)
const originalRun = flow.run.bind(flow)
const originalRegisterStepSuccess = flow.registerStepSuccess.bind(flow)
const originalRegisterStepFailure = flow.registerStepFailure.bind(flow)
const originalCancel = flow.cancel.bind(flow)
const originalExecution = async (
method,
{
throwOnError,
resultFrom,
isCancel = false,
container: executionContainer,
},
...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)
let errors = transaction.getErrors(TransactionHandlerType.INVOKE)
const failedStatus = [TransactionState.FAILED, TransactionState.REVERTED]
const isCancelled =
isCancel && transaction.getState() === TransactionState.REVERTED
if (
!isCancelled &&
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
if (options?.wrappedInput) {
result = resolveValue(resultFrom, transaction.getContext())
if (result instanceof Promise) {
result = await result.catch((e) => {
errors ??= []
errors.push(e)
})
}
} else {
result = transaction.getContext().invoke?.[resultFrom]
}
return {
errors,
transaction,
result,
}
}
const newRun = async (
{
input,
context: outerContext,
throwOnError,
resultFrom,
events,
container,
}: FlowRunOptions = {
throwOnError: true,
resultFrom: defaultResult,
}
) => {
resultFrom ??= defaultResult
throwOnError ??= true
const context = {
...outerContext,
__type: MedusaContextType,
}
context.transactionId ??= ulid()
if (typeof dataPreparation === "function") {
try {
const copyInput = input ? JSON.parse(JSON.stringify(input)) : input
input = await dataPreparation(copyInput as TData)
} catch (err) {
if (throwOnError) {
throw new Error(
`Data preparation failed: ${err.message}${EOL}${err.stack}`
)
}
return {
errors: [err],
}
}
}
return await originalExecution(
originalRun,
{ throwOnError, resultFrom, container },
context.transactionId,
input,
context,
events
)
}
flow.run = newRun as any
const newRegisterStepSuccess = async (
{
response,
idempotencyKey,
context: outerContext,
throwOnError,
resultFrom,
events,
container,
}: FlowRegisterStepSuccessOptions = {
idempotencyKey: "",
throwOnError: true,
resultFrom: defaultResult,
}
) => {
resultFrom ??= defaultResult
throwOnError ??= true
const [, transactionId] = idempotencyKey.split(":")
const context = {
...outerContext,
transactionId,
__type: MedusaContextType,
}
return await originalExecution(
originalRegisterStepSuccess,
{ throwOnError, resultFrom, container },
idempotencyKey,
response,
context,
events
)
}
flow.registerStepSuccess = newRegisterStepSuccess as any
const newRegisterStepFailure = async (
{
response,
idempotencyKey,
context: outerContext,
throwOnError,
resultFrom,
events,
container,
}: FlowRegisterStepFailureOptions = {
idempotencyKey: "",
throwOnError: true,
resultFrom: defaultResult,
}
) => {
resultFrom ??= defaultResult
throwOnError ??= true
const [, transactionId] = idempotencyKey.split(":")
const context = {
...outerContext,
transactionId,
__type: MedusaContextType,
}
return await originalExecution(
originalRegisterStepFailure,
{ throwOnError, resultFrom, container },
idempotencyKey,
response,
context,
events
)
}
flow.registerStepFailure = newRegisterStepFailure as any
const newCancel = async (
{
transaction,
transactionId,
context: outerContext,
throwOnError,
events,
container,
}: FlowCancelOptions = {
throwOnError: true,
}
) => {
throwOnError ??= true
const context = {
...outerContext,
transactionId,
__type: MedusaContextType,
}
return await originalExecution(
originalCancel,
{
throwOnError,
resultFrom: undefined,
isCancel: true,
container,
},
transaction ?? transactionId,
context,
events
)
}
flow.cancel = newCancel as any
return flow as unknown as LocalWorkflow &
ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride>
}
export const exportWorkflow = <TData = unknown, TResult = unknown>(
workflowId: string,
defaultResult?: string | Symbol,
dataPreparation?: (data: TData) => Promise<unknown>,
options?: {
wrappedInput?: boolean
}
): MainExportedWorkflow<TData, TResult> => {
function exportedWorkflow<
TDataOverride = undefined,
TResultOverride = undefined
>(
// TODO: rm when all usage have been migrated
container?: LoadedModule[] | MedusaContainer
): Omit<
LocalWorkflow,
"run" | "registerStepSuccess" | "registerStepFailure" | "cancel"
> &
ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride> {
return createContextualWorkflowRunner<
TData,
TResult,
TDataOverride,
TResultOverride
>({
workflowId,
defaultResult,
dataPreparation,
options,
container,
})
}
const buildRunnerFn = <
TAction extends
| "run"
| "registerStepSuccess"
| "registerStepFailure"
| "cancel",
TDataOverride,
TResultOverride
>(
action: "run" | "registerStepSuccess" | "registerStepFailure" | "cancel",
container?: LoadedModule[] | MedusaContainer
) => {
const contextualRunner = createContextualWorkflowRunner<
TData,
TResult,
TDataOverride,
TResultOverride
>({
workflowId,
defaultResult,
dataPreparation,
options,
container,
})
return contextualRunner[action] as ExportedWorkflow<
TData,
TResult,
TDataOverride,
TResultOverride
>[TAction]
}
exportedWorkflow.run = async <
TDataOverride = undefined,
TResultOverride = undefined
>(
args?: FlowRunOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
): Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
> => {
const container = args?.container
delete args?.container
const inputArgs = { ...args } as FlowRunOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
return await buildRunnerFn<"run", TDataOverride, TResultOverride>(
"run",
container
)(inputArgs)
}
exportedWorkflow.registerStepSuccess = async <
TDataOverride = undefined,
TResultOverride = undefined
>(
args?: FlowRegisterStepSuccessOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
): Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
> => {
const container = args?.container
delete args?.container
const inputArgs = { ...args } as FlowRegisterStepSuccessOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
return await buildRunnerFn<
"registerStepSuccess",
TDataOverride,
TResultOverride
>(
"registerStepSuccess",
container
)(inputArgs)
}
exportedWorkflow.registerStepFailure = async <
TDataOverride = undefined,
TResultOverride = undefined
>(
args?: FlowRegisterStepFailureOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
): Promise<
WorkflowResult<
TResultOverride extends undefined ? TResult : TResultOverride
>
> => {
const container = args?.container
delete args?.container
const inputArgs = { ...args } as FlowRegisterStepFailureOptions<
TDataOverride extends undefined ? TData : TDataOverride
>
return await buildRunnerFn<
"registerStepFailure",
TDataOverride,
TResultOverride
>(
"registerStepFailure",
container
)(inputArgs)
}
exportedWorkflow.cancel = async (
args?: FlowCancelOptions
): Promise<WorkflowResult> => {
const container = args?.container
delete args?.container
const inputArgs = { ...args } as FlowCancelOptions
return await buildRunnerFn<"cancel", unknown, unknown>(
"cancel",
container
)(inputArgs)
}
MedusaWorkflow.registerWorkflow(workflowId, exportedWorkflow)
return exportedWorkflow as MainExportedWorkflow<TData, TResult>
}
+6
View File
@@ -0,0 +1,6 @@
export * from "./helper"
export * from "./medusa-workflow"
export * as WorkflowOrchestratorTypes from "./types"
export { IWorkflowEngineService } from "./types/service"
export * from "./utils/composer"
export * as Composer from "./utils/composer"
@@ -0,0 +1,31 @@
import { LocalWorkflow } from "@medusajs/orchestration"
import { LoadedModule, MedusaContainer } from "@medusajs/types"
import { ExportedWorkflow } from "./helper"
export class MedusaWorkflow {
static workflows: Record<
string,
(
container?: LoadedModule[] | MedusaContainer
) => Omit<
LocalWorkflow,
"run" | "registerStepSuccess" | "registerStepFailure" | "cancel"
> &
ExportedWorkflow
> = {}
static registerWorkflow(workflowId, exportedWorkflow) {
if (workflowId in MedusaWorkflow.workflows) {
throw new Error(`Workflow with id ${workflowId} already registered.`)
}
MedusaWorkflow.workflows[workflowId] = exportedWorkflow
}
static getWorkflow(workflowId) {
return MedusaWorkflow.workflows[workflowId]
}
}
global.MedusaWorkflow ??= MedusaWorkflow
exports.MedusaWorkflow = global.MedusaWorkflow
@@ -0,0 +1,21 @@
import { BaseFilterable, OperatorMap } from "@medusajs/types"
export interface WorkflowExecutionDTO {
id: string
workflow_id: string
transaction_id: string
execution: string
context: string
state: any
created_at: Date
updated_at: Date
deleted_at: Date
}
export interface FilterableWorkflowExecutionProps
extends BaseFilterable<FilterableWorkflowExecutionProps> {
id?: string | string[] | OperatorMap<string>
workflow_id?: string | string[] | OperatorMap<string>
transaction_id?: string | string[] | OperatorMap<string>
state?: string | string[] | OperatorMap<string>
}
@@ -0,0 +1,3 @@
export * from "./common"
export * from "./mutations"
export * from "./service"
@@ -0,0 +1,7 @@
export interface UpsertWorkflowExecutionDTO {
workflow_id: string
transaction_id: string
execution: Record<string, unknown>
context: Record<string, unknown>
state: any
}
@@ -0,0 +1,127 @@
import {
ContainerLike,
Context,
FindConfig,
IModuleService,
} from "@medusajs/types"
import { ReturnWorkflow, UnwrapWorkflowInputDataType } from "../utils/composer"
import {
FilterableWorkflowExecutionProps,
WorkflowExecutionDTO,
} from "./common"
type FlowRunOptions<TData = unknown> = {
input?: TData
context?: Context
resultFrom?: string | string[] | Symbol
throwOnError?: boolean
events?: Record<string, Function>
}
export interface WorkflowOrchestratorRunDTO<T = unknown>
extends FlowRunOptions<T> {
transactionId?: string
container?: ContainerLike
}
export type IdempotencyKeyParts = {
workflowId: string
transactionId: string
stepId: string
action: "invoke" | "compensate"
}
export interface IWorkflowEngineService extends IModuleService {
retrieveWorkflowExecution(
idOrObject:
| string
| {
workflow_id: string
transaction_id: string
},
config?: FindConfig<WorkflowExecutionDTO>,
sharedContext?: Context
): Promise<WorkflowExecutionDTO>
listWorkflowExecution(
filters?: FilterableWorkflowExecutionProps,
config?: FindConfig<WorkflowExecutionDTO>,
sharedContext?: Context
): Promise<WorkflowExecutionDTO[]>
listAndCountWorkflowExecution(
filters?: FilterableWorkflowExecutionProps,
config?: FindConfig<WorkflowExecutionDTO>,
sharedContext?: Context
): Promise<[WorkflowExecutionDTO[], number]>
run<
TWorkflow extends ReturnWorkflow<any, any, any> = ReturnWorkflow<
any,
any,
any
>,
TData = UnwrapWorkflowInputDataType<TWorkflow>
>(
workflowId: string,
options?: WorkflowOrchestratorRunDTO,
sharedContext?: Context
): Promise<{
errors: Error[]
transaction: object
result: any
acknowledgement: object
}>
getRunningTransaction(
workflowId: string,
transactionId: string,
options?: Record<string, any>,
sharedContext?: Context
): Promise<unknown>
setStepSuccess(
{
idempotencyKey,
stepResponse,
options,
}: {
idempotencyKey: string | IdempotencyKeyParts
stepResponse: unknown
options?: Record<string, any>
},
sharedContext?: Context
)
setStepFailure(
{
idempotencyKey,
stepResponse,
options,
}: {
idempotencyKey: string | IdempotencyKeyParts
stepResponse: unknown
options?: Record<string, any>
},
sharedContext?: Context
)
subscribe(
args: {
workflowId: string
transactionId?: string
subscriber: Function
subscriberId?: string
},
sharedContext?: Context
): Promise<void>
unsubscribe(
args: {
workflowId: string
transactionId?: string
subscriberOrId: string | Function
},
sharedContext?: Context
)
}
@@ -0,0 +1,67 @@
import {
createStep,
createWorkflow,
StepResponse,
WorkflowData,
} from "./composer"
const step1 = createStep("step1", async (input: {}, context) => {
return new StepResponse({ step1: "step1" })
})
type Step2Input = { filters: { id: string[] } } | { filters: { id: string } }
const step2 = createStep("step2", async (input: Step2Input, context) => {
return new StepResponse({ step2: input })
})
const step3 = createStep("step3", async () => {
return new StepResponse({ step3: "step3" })
})
const workflow = createWorkflow(
"sub-workflow",
function (input: WorkflowData<{ outsideWorkflowData: string }>) {
step1()
step3()
return step2({ filters: { id: input.outsideWorkflowData } })
}
)
const workflow2 = createWorkflow("workflow", function () {
const step1Res = step1()
step3()
const workflowRes = workflow.asStep({ outsideWorkflowData: step1Res.step1 })
return workflowRes
})
workflow2()
.run({})
.then((res) => {
console.log(res.result)
})
/*const step1 = createStep("step1", async (input: {}, context) => {
return new StepResponse({ step1: ["step1"] })
})
const step2 = createStep("step2", async (input: string[], context) => {
return new StepResponse({ step2: input })
})
const step3 = createStep("step3", async () => {
return new StepResponse({ step3: "step3" })
})
const workflow = createWorkflow("workflow", function () {
const step1Res = step1()
step3()
return step2(step1Res.step1)
})
workflow()
.run({})
.then((res) => {
console.log(res.result)
})*/
@@ -0,0 +1,144 @@
import { createStep } from "../create-step"
import { createWorkflow } from "../create-workflow"
import { StepResponse } from "../helpers"
import { transform } from "../transform"
import { WorkflowData } from "../type"
let count = 1
const getNewWorkflowId = () => `workflow-${count++}`
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 () {
const step1 = createStep("step1", async () => {
return new StepResponse({ result: "step1" })
})
const step2 = createStep("step2", async () => {
throw new Error("step2 failed")
})
const work = createWorkflow("id" as any, () => {
const resStep1 = step1()
const resStep2 = step2()
const transformedData = transform({ data: resStep2 }, (data) => {
return { result: data.data.result }
})
return transform({ data: transformedData, resStep2 }, (data) => {
return { result: data.data }
})
})
const { errors } = await work.run({ input: {}, throwOnError: false })
expect(errors).toEqual([
{
action: "step2",
handlerType: "invoke",
error: expect.objectContaining({
message: "step2 failed",
}),
},
expect.objectContaining({
message: "Cannot read properties of undefined (reading 'result')",
}),
])
})
})
@@ -0,0 +1,412 @@
import {
TransactionStepsDefinition,
WorkflowManager,
WorkflowStepHandler,
WorkflowStepHandlerArguments,
} from "@medusajs/orchestration"
import { OrchestrationUtils, deepCopy, isString } from "@medusajs/utils"
import { ulid } from "ulid"
import { StepResponse, resolveValue } from "./helpers"
import { proxify } from "./helpers/proxy"
import {
CreateWorkflowComposerContext,
StepExecutionContext,
StepFunction,
StepFunctionResult,
WorkflowData,
} from "./type"
/**
* The type of invocation function passed to a step.
*
* @typeParam TInput - The type of the input that the function expects.
* @typeParam TOutput - The type of the output that the function returns.
* @typeParam TCompensateInput - The type of the input that the compensation function expects.
*
* @returns The expected output based on the type parameter `TOutput`.
*/
type InvokeFn<TInput, TOutput, TCompensateInput> = (
/**
* The input of the step.
*/
input: TInput,
/**
* The step's context.
*/
context: StepExecutionContext
) =>
| void
| StepResponse<
TOutput,
TCompensateInput extends undefined ? TOutput : TCompensateInput
>
| Promise<void | StepResponse<
TOutput,
TCompensateInput extends undefined ? TOutput : TCompensateInput
>>
/**
* The type of compensation function passed to a step.
*
* @typeParam T -
* The type of the argument passed to the compensation function. If not specified, then it will be the same type as the invocation function's output.
*
* @returns There's no expected type to be returned by the compensation function.
*/
type CompensateFn<T> = (
/**
* The argument passed to the compensation function.
*/
input: T | undefined,
/**
* The step's context.
*/
context: StepExecutionContext
) => unknown | Promise<unknown>
interface ApplyStepOptions<
TStepInputs extends {
[K in keyof TInvokeInput]: WorkflowData<TInvokeInput[K]>
},
TInvokeInput,
TInvokeResultOutput,
TInvokeResultCompensateInput
> {
stepName: string
stepConfig?: TransactionStepsDefinition
input?: TStepInputs
invokeFn: InvokeFn<
TInvokeInput,
TInvokeResultOutput,
TInvokeResultCompensateInput
>
compensateFn?: CompensateFn<TInvokeResultCompensateInput>
}
/**
* @internal
*
* Internal function to create the invoke and compensate handler for a step.
* This is where the inputs and context are passed to the underlying invoke and compensate function.
*
* @param stepName
* @param stepConfig
* @param input
* @param invokeFn
* @param compensateFn
*/
function applyStep<
TInvokeInput,
TStepInput extends {
[K in keyof TInvokeInput]: WorkflowData<TInvokeInput[K]>
},
TInvokeResultOutput,
TInvokeResultCompensateInput
>({
stepName,
stepConfig = {},
input,
invokeFn,
compensateFn,
}: ApplyStepOptions<
TStepInput,
TInvokeInput,
TInvokeResultOutput,
TInvokeResultCompensateInput
>): StepFunctionResult<TInvokeResultOutput> {
return function (this: CreateWorkflowComposerContext) {
if (!this.workflowId) {
throw new Error(
"createStep must be used inside a createWorkflow definition"
)
}
const handler = {
invoke: async (stepArguments: WorkflowStepHandlerArguments) => {
const metadata = stepArguments.metadata
const idempotencyKey = metadata.idempotency_key
stepArguments.context!.idempotencyKey = idempotencyKey
const executionContext: StepExecutionContext = {
workflowId: metadata.model_id,
stepName: metadata.action,
action: "invoke",
idempotencyKey,
attempt: metadata.attempt,
container: stepArguments.container,
metadata,
context: stepArguments.context!,
}
const argInput = input ? await resolveValue(input, stepArguments) : {}
const stepResponse: StepResponse<any, any> = await invokeFn.apply(
this,
[argInput, executionContext]
)
const stepResponseJSON =
stepResponse?.__type === OrchestrationUtils.SymbolWorkflowStepResponse
? stepResponse.toJSON()
: stepResponse
return {
__type: OrchestrationUtils.SymbolWorkflowWorkflowData,
output: stepResponseJSON,
}
},
compensate: compensateFn
? async (stepArguments: WorkflowStepHandlerArguments) => {
const metadata = stepArguments.metadata
const idempotencyKey = metadata.idempotency_key
stepArguments.context!.idempotencyKey = idempotencyKey
const executionContext: StepExecutionContext = {
workflowId: metadata.model_id,
stepName: metadata.action,
action: "compensate",
idempotencyKey,
attempt: metadata.attempt,
container: stepArguments.container,
metadata,
context: stepArguments.context!,
}
const stepOutput = (stepArguments.invoke[stepName] as any)?.output
const invokeResult =
stepOutput?.__type ===
OrchestrationUtils.SymbolWorkflowStepResponse
? stepOutput.compensateInput &&
deepCopy(stepOutput.compensateInput)
: stepOutput && deepCopy(stepOutput)
const args = [invokeResult, executionContext]
const output = await compensateFn.apply(this, args)
return {
output,
}
}
: undefined,
}
wrapAsyncHandler(stepConfig, handler)
stepConfig.uuid = ulid()
stepConfig.noCompensation = !compensateFn
this.flow.addAction(stepName, stepConfig)
if (!this.handlers.has(stepName)) {
this.handlers.set(stepName, handler)
}
const ret = {
__type: OrchestrationUtils.SymbolWorkflowStep,
__step__: stepName,
config: (
localConfig: { name?: string } & Omit<
TransactionStepsDefinition,
"next" | "uuid" | "action"
>
) => {
const newStepName = localConfig.name ?? stepName
delete localConfig.name
this.handlers.set(newStepName, handler)
this.flow.replaceAction(stepConfig.uuid!, newStepName, {
...stepConfig,
...localConfig,
})
ret.__step__ = newStepName
WorkflowManager.update(this.workflowId, this.flow, this.handlers)
return proxify(ret)
},
}
return proxify(ret)
}
}
/**
* @internal
*
* Internal function to handle async steps to be automatically marked as completed after they are executed.
*
* @param stepConfig
* @param handle
*/
function wrapAsyncHandler(
stepConfig: TransactionStepsDefinition,
handle: {
invoke: WorkflowStepHandler
compensate?: WorkflowStepHandler
}
) {
if (stepConfig.async) {
if (typeof handle.invoke === "function") {
const originalInvoke = handle.invoke
handle.invoke = async (stepArguments: WorkflowStepHandlerArguments) => {
const response = (await originalInvoke(stepArguments)) as any
if (
response?.output?.__type !==
OrchestrationUtils.SymbolWorkflowStepResponse
) {
return
}
stepArguments.step.definition.backgroundExecution = true
return response
}
}
}
if (stepConfig.compensateAsync) {
if (typeof handle.compensate === "function") {
const originalCompensate = handle.compensate!
handle.compensate = async (
stepArguments: WorkflowStepHandlerArguments
) => {
const response = (await originalCompensate(stepArguments)) as any
if (
response?.output?.__type !==
OrchestrationUtils.SymbolWorkflowStepResponse
) {
return
}
stepArguments.step.definition.backgroundExecution = true
return response
}
}
}
}
/**
* This function creates a {@link StepFunction} that can be used as a step in a workflow constructed by the {@link createWorkflow} function.
*
* @typeParam TInvokeInput - The type of the expected input parameter to the invocation function.
* @typeParam TInvokeResultOutput - The type of the expected output parameter of the invocation function.
* @typeParam TInvokeResultCompensateInput - The type of the expected input parameter to the compensation function.
*
* @returns A step function to be used in a workflow.
*
* @example
* import {
* createStep,
* StepResponse,
* StepExecutionContext,
* WorkflowData
* } from "@medusajs/workflows-sdk"
*
* interface CreateProductInput {
* title: string
* }
*
* export const createProductStep = createStep(
* "createProductStep",
* async function (
* input: CreateProductInput,
* context
* ) {
* const productService = context.container.resolve(
* "productService"
* )
* const product = await productService.create(input)
* return new StepResponse({
* product
* }, {
* product_id: product.id
* })
* },
* async function (
* input,
* context
* ) {
* const productService = context.container.resolve(
* "productService"
* )
* await productService.delete(input.product_id)
* }
* )
*/
export function createStep<
TInvokeInput,
TInvokeResultOutput,
TInvokeResultCompensateInput
>(
/**
* The name of the step or its configuration.
*/
nameOrConfig:
| string
| ({ name: string } & Omit<
TransactionStepsDefinition,
"next" | "uuid" | "action"
>),
/**
* An invocation function that will be executed when the workflow is executed. The function must return an instance of {@link StepResponse}. The constructor of {@link StepResponse}
* accepts the output of the step as a first argument, and optionally as a second argument the data to be passed to the compensation function as a parameter.
*/
invokeFn: InvokeFn<
TInvokeInput,
TInvokeResultOutput,
TInvokeResultCompensateInput
>,
/**
* A compensation function that's executed if an error occurs in the workflow. It's used to roll-back actions when errors occur.
* It accepts as a parameter the second argument passed to the constructor of the {@link StepResponse} instance returned by the invocation function. If the
* invocation function doesn't pass the second argument to `StepResponse` constructor, the compensation function receives the first argument
* passed to the `StepResponse` constructor instead.
*/
compensateFn?: CompensateFn<TInvokeResultCompensateInput>
): StepFunction<TInvokeInput, TInvokeResultOutput> {
const stepName =
(isString(nameOrConfig) ? nameOrConfig : nameOrConfig.name) ?? invokeFn.name
const config = isString(nameOrConfig) ? {} : nameOrConfig
const returnFn = function (
input:
| {
[K in keyof TInvokeInput]: WorkflowData<TInvokeInput[K]>
}
| undefined
): WorkflowData<TInvokeResultOutput> {
if (!global[OrchestrationUtils.SymbolMedusaWorkflowComposerContext]) {
throw new Error(
"createStep must be used inside a createWorkflow definition"
)
}
const stepBinder = (
global[
OrchestrationUtils.SymbolMedusaWorkflowComposerContext
] as CreateWorkflowComposerContext
).stepBinder
return stepBinder<TInvokeResultOutput>(
applyStep<
TInvokeInput,
{ [K in keyof TInvokeInput]: WorkflowData<TInvokeInput[K]> },
TInvokeResultOutput,
TInvokeResultCompensateInput
>({
stepName,
stepConfig: config,
input,
invokeFn,
compensateFn,
})
)
} as StepFunction<TInvokeInput, TInvokeResultOutput>
returnFn.__type = OrchestrationUtils.SymbolWorkflowStepBind
returnFn.__step__ = stepName
return returnFn
}
@@ -0,0 +1,221 @@
import {
TransactionModelOptions,
WorkflowHandler,
WorkflowManager,
} from "@medusajs/orchestration"
import { LoadedModule, MedusaContainer } from "@medusajs/types"
import { isString, OrchestrationUtils } from "@medusajs/utils"
import { exportWorkflow } from "../../helper"
import { proxify } from "./helpers/proxy"
import {
CreateWorkflowComposerContext,
ReturnWorkflow,
StepFunction,
WorkflowData,
WorkflowDataProperties,
} from "./type"
import { createStep } from "./create-step"
import { StepResponse } from "./helpers"
global[OrchestrationUtils.SymbolMedusaWorkflowComposerContext] = null
/**
* 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 returned workflow is an exported workflow of type {@link ReturnWorkflow}, meaning it's not executed right away. To execute it,
* invoke the exported workflow, then run its `run` method.
*
* @typeParam TData - The type of the input passed to the composer function.
* @typeParam TResult - The type of the output returned by the composer function.
* @typeParam THooks - The type of hooks defined in the workflow.
*
* @returns The created workflow. You can later execute the workflow by invoking it, then using its `run` method.
*
* @example
* import { createWorkflow } from "@medusajs/workflows-sdk"
* import { MedusaRequest, MedusaResponse, Product } from "@medusajs/medusa"
* import {
* createProductStep,
* getProductStep,
* createPricesStep
* } from "./steps"
*
* interface WorkflowInput {
* title: string
* }
*
* const myWorkflow = createWorkflow<
* WorkflowInput,
* Product
* >("my-workflow", (input) => {
* // Everything here will be executed and resolved later
* // during the execution. Including the data access.
*
* const product = createProductStep(input)
* const prices = createPricesStep(product)
* return getProductStep(product.id)
* }
* )
*
* export async function GET(
* req: MedusaRequest,
* res: MedusaResponse
* ) {
* const { result: product } = await myWorkflow(req.scope)
* .run({
* input: {
* title: "Shirt"
* }
* })
*
* res.json({
* product
* })
* }
*/
export function createWorkflow<
TData,
TResult,
THooks extends Record<string, Function> = Record<string, Function>
>(
/**
* The name of the workflow or its configuration.
*/
nameOrConfig: string | ({ name: string } & TransactionModelOptions),
/**
* The constructor function that is executed when the `run` method in {@link ReturnWorkflow} is used.
* The function can't be an arrow function or an asynchronus function. It also can't directly manipulate data.
* You'll have to use the {@link transform} function if you need to directly manipulate data.
*/
composer: (input: WorkflowData<TData>) =>
| void
| WorkflowData<TResult>
| {
[K in keyof TResult]:
| WorkflowData<TResult[K]>
| WorkflowDataProperties<TResult[K]>
}
): ReturnWorkflow<TData, TResult, THooks> {
const name = isString(nameOrConfig) ? nameOrConfig : nameOrConfig.name
const options = isString(nameOrConfig) ? {} : nameOrConfig
const handlers: WorkflowHandler = new Map()
if (WorkflowManager.getWorkflow(name)) {
WorkflowManager.unregister(name)
}
WorkflowManager.register(name, undefined, handlers, options)
const context: CreateWorkflowComposerContext = {
workflowId: name,
flow: WorkflowManager.getTransactionDefinition(name),
handlers,
hooks_: [],
hooksCallback_: {},
hookBinder: (name, fn) => {
context.hooks_.push(name)
return fn(context)
},
stepBinder: (fn) => {
return fn.bind(context)()
},
parallelizeBinder: (fn) => {
return fn.bind(context)()
},
}
global[OrchestrationUtils.SymbolMedusaWorkflowComposerContext] = context
const inputPlaceHolder = proxify<WorkflowData>({
__type: OrchestrationUtils.SymbolInputReference,
__step__: "",
config: () => {
// TODO: config default value?
throw new Error("Config is not available for the input object.")
},
})
const returnedStep = composer.apply(context, [inputPlaceHolder])
delete global[OrchestrationUtils.SymbolMedusaWorkflowComposerContext]
WorkflowManager.update(name, context.flow, handlers, options)
const workflow = exportWorkflow<TData, TResult>(
name,
returnedStep,
undefined,
{
wrappedInput: true,
}
)
const mainFlow = <TDataOverride = undefined, TResultOverride = undefined>(
container?: LoadedModule[] | MedusaContainer
) => {
const workflow_ = workflow<TDataOverride, TResultOverride>(container)
const expandedFlow: any = workflow_
expandedFlow.config = (config) => {
workflow_.setOptions(config)
}
return expandedFlow
}
let shouldRegisterHookHandler = true
for (const hook of context.hooks_) {
mainFlow[hook] = (fn) => {
context.hooksCallback_[hook] ??= []
if (!shouldRegisterHookHandler) {
console.warn(
`A hook handler has already been registered for the ${hook} hook. The current handler registration will be skipped.`
)
return
}
context.hooksCallback_[hook].push(fn)
shouldRegisterHookHandler = false
}
}
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>
}
@@ -0,0 +1,2 @@
export * from "./step-response"
export * from "./resolve-value"
@@ -0,0 +1,28 @@
import { transform } from "../transform"
import { WorkflowData, WorkflowTransactionContext } from "../type"
import { OrchestrationUtils } from "@medusajs/utils"
import { resolveValue } from "./resolve-value"
export function proxify<T>(obj: WorkflowData<any>): T {
return new Proxy(obj, {
get(target: any, prop: string | symbol): any {
if (prop in target) {
return target[prop]
}
return transform(target[prop], async function (input, context) {
const { invoke } = context as WorkflowTransactionContext
let output =
target.__type === OrchestrationUtils.SymbolInputReference ||
target.__type === OrchestrationUtils.SymbolWorkflowStepTransformer
? target
: invoke?.[obj.__step__]?.output
output = await resolveValue(output, context)
output = output?.[prop]
return output && JSON.parse(JSON.stringify(output))
})
},
}) as unknown as T
}
@@ -0,0 +1,77 @@
import { deepCopy, OrchestrationUtils, promiseAll } from "@medusajs/utils"
async function resolveProperty(property, transactionContext) {
const { invoke: invokeRes } = transactionContext
if (property?.__type === OrchestrationUtils.SymbolInputReference) {
return transactionContext.payload
} else if (
property?.__type === OrchestrationUtils.SymbolWorkflowStepTransformer
) {
return await property.__resolver(transactionContext)
} else if (property?.__type === OrchestrationUtils.SymbolWorkflowHook) {
return await property.__value(transactionContext)
} else if (property?.__type === OrchestrationUtils.SymbolWorkflowStep) {
const output =
invokeRes[property.__step__]?.output ?? invokeRes[property.__step__]
if (output?.__type === OrchestrationUtils.SymbolWorkflowStepResponse) {
return output.output
}
return output
} else if (
property?.__type === OrchestrationUtils.SymbolWorkflowStepResponse
) {
return property.output
} else {
return property
}
}
/**
* @internal
*/
export async function resolveValue(input, transactionContext) {
const unwrapInput = async (
inputTOUnwrap: Record<string, unknown>,
parentRef: any
) => {
if (inputTOUnwrap == null) {
return inputTOUnwrap
}
if (Array.isArray(inputTOUnwrap)) {
return await promiseAll(
inputTOUnwrap.map((i) => resolveValue(i, transactionContext))
)
}
if (typeof inputTOUnwrap !== "object") {
return inputTOUnwrap
}
for (const key of Object.keys(inputTOUnwrap)) {
parentRef[key] = await resolveProperty(
inputTOUnwrap[key],
transactionContext
)
if (typeof parentRef[key] === "object") {
await unwrapInput(parentRef[key], parentRef[key])
}
}
return parentRef
}
const copiedInput =
input?.__type === OrchestrationUtils.SymbolWorkflowWorkflowData
? deepCopy(input.output)
: deepCopy(input)
const result = copiedInput?.__type
? await resolveProperty(copiedInput, transactionContext)
: await unwrapInput(copiedInput, {})
return result && JSON.parse(JSON.stringify(result))
}
@@ -0,0 +1,150 @@
import { PermanentStepFailureError } from "@medusajs/orchestration"
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`.
*
* @typeParam TOutput - The type of the output of the step.
* @typeParam TCompensateInput -
* The type of the compensation input. If the step doesn't specify any compensation input, then the type of `TCompensateInput` is the same
* as that of `TOutput`.
*/
export class StepResponse<TOutput, TCompensateInput = TOutput> {
readonly #__type = OrchestrationUtils.SymbolWorkflowStepResponse
readonly #output: TOutput
readonly #compensateInput?: TCompensateInput
/**
* The constructor of the StepResponse
*
* @typeParam TOutput - The type of the output of the step.
* @typeParam TCompensateInput -
* The type of the compensation input. If the step doesn't specify any compensation input, then the type of `TCompensateInput` is the same
* as that of `TOutput`.
*/
constructor(
/**
* The output of the step.
*/
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.
*/
compensateInput?: TCompensateInput
) {
if (isDefined(output)) {
this.#output = output
}
this.#compensateInput = (compensateInput ?? output) as TCompensateInput
}
/**
* Creates a StepResponse that indicates that the step has failed and the retry mechanism should not kick in anymore.
*
* @param message - An optional message to be logged.
*
* @example
* import { Product } from "@medusajs/medusa"
* import {
* createStep,
* StepResponse,
* createWorkflow
* } from "@medusajs/workflows-sdk"
*
* interface CreateProductInput {
* title: string
* }
*
* export const createProductStep = createStep(
* "createProductStep",
* async function (
* input: CreateProductInput,
* context
* ) {
* const productService = context.container.resolve(
* "productService"
* )
*
* try {
* const product = await productService.create(input)
* return new StepResponse({
* product
* }, {
* product_id: product.id
* })
* } catch (e) {
* return StepResponse.permanentFailure(`Couldn't create the product: ${e}`)
* }
* }
* )
*
* interface WorkflowInput {
* title: string
* }
*
* const myWorkflow = createWorkflow<
* WorkflowInput,
* Product
* >("my-workflow", (input) => {
* // Everything here will be executed and resolved later
* // during the execution. Including the data access.
*
* const product = createProductStep(input)
* }
* )
*
* myWorkflow()
* .run({
* input: {
* title: "Shirt"
* }
* })
* .then(({ errors, result }) => {
* if (errors.length) {
* errors.forEach((err) => {
* if (typeof err.error === "object" && "message" in err.error) {
* console.error(err.error.message)
* } else {
* console.error(err.error)
* }
* })
* }
* console.log(result)
* })
*/
static permanentFailure(message = "Permanent failure"): never {
throw new PermanentStepFailureError(message)
}
/**
* @internal
*/
get __type() {
return this.#__type
}
/**
* @internal
*/
get output(): TOutput {
return this.#output
}
/**
* @internal
*/
get compensateInput(): TCompensateInput {
return this.#compensateInput as TCompensateInput
}
/**
* @internal
*/
toJSON() {
return {
__type: this.#__type,
output: this.#output,
compensateInput: this.#compensateInput,
}
}
}
@@ -0,0 +1,146 @@
import { WorkflowStepHandlerArguments } from "@medusajs/orchestration"
import { OrchestrationUtils, deepCopy } from "@medusajs/utils"
import { resolveValue } from "./helpers"
import {
CreateWorkflowComposerContext,
StepExecutionContext,
WorkflowData,
} from "./type"
/**
*
* @ignore
*
* This function allows you to add hooks in your workflow that provide access to some data. Then, consumers of that workflow can add a handler function that performs
* an action with the provided data or modify it.
*
* For example, in a "create product" workflow, you may add a hook after the product is created, providing access to the created product.
* Then, developers using that workflow can hook into that point to access the product, modify its attributes, then return the updated product.
*
* @typeParam TOutput - The expected output of the hook's handler function.
* @returns The output of handler functions of this hook. If there are no handler functions, the output is `undefined`.
*
* @example
* import {
* createWorkflow,
* StepExecutionContext,
* hook,
* transform
* } from "@medusajs/workflows-sdk"
* import {
* createProductStep,
* getProductStep,
* createPricesStep
* } from "./steps"
* import {
* MedusaRequest,
* MedusaResponse,
* Product, ProductService
* } from "@medusajs/medusa"
*
* interface WorkflowInput {
* title: string
* }
*
* const myWorkflow = createWorkflow<
* WorkflowInput,
* Product
* >("my-workflow",
* function (input) {
* const product = createProductStep(input)
*
* const hookProduct = hook<Product>("createdProductHook", product)
*
* const newProduct = transform({
* product,
* hookProduct
* }, (input) => {
* return input.hookProduct || input.product
* })
*
* const prices = createPricesStep(newProduct)
*
* return getProductStep(product.id)
* }
* )
*
* myWorkflow.createdProductHook(
* async (product, context: StepExecutionContext) => {
* const productService: ProductService = context.container.resolve("productService")
*
* const updatedProduct = await productService.update(product.id, {
* description: "a cool shirt"
* })
*
* return updatedProduct
* })
*
* export async function POST(
* req: MedusaRequest,
* res: MedusaResponse
* ) {
* const { result: product } = await myWorkflow(req.scope)
* .run({
* input: {
* title: req.body.title
* }
* })
*
* res.json({
* product
* })
* }
*/
export function hook<TOutput>(
/**
* The name of the hook. This will be used by the consumer to add a handler method for the hook.
*/
name: string,
/**
* The data that a handler function receives as a parameter.
*/
value: any
): WorkflowData<TOutput> {
const hookBinder = (
global[
OrchestrationUtils.SymbolMedusaWorkflowComposerContext
] as CreateWorkflowComposerContext
).hookBinder
return hookBinder(name, function (context) {
return {
__value: async function (
transactionContext: WorkflowStepHandlerArguments
) {
const metadata = transactionContext.metadata
const idempotencyKey = metadata.idempotency_key
transactionContext.context!.idempotencyKey = idempotencyKey
const executionContext: StepExecutionContext = {
workflowId: metadata.model_id,
stepName: metadata.action,
action: metadata.action_type,
idempotencyKey,
attempt: metadata.attempt,
container: transactionContext.container,
metadata,
context: transactionContext.context!,
}
const allValues = await resolveValue(value, transactionContext)
const stepValue = allValues ? deepCopy(allValues) : allValues
let finalResult
const functions = context.hooksCallback_[name]
for (let i = 0; i < functions.length; i++) {
const fn = functions[i]
const arg = i === 0 ? stepValue : finalResult
finalResult = await fn.apply(fn, [arg, executionContext])
}
return finalResult
},
__type: OrchestrationUtils.SymbolWorkflowHook,
}
})
}
@@ -0,0 +1,8 @@
export * from "./create-step"
export * from "./create-workflow"
export * from "./hook"
export * from "./parallelize"
export * from "./helpers/resolve-value"
export * from "./helpers/step-response"
export * from "./transform"
export * from "./type"
@@ -0,0 +1,71 @@
import { CreateWorkflowComposerContext, WorkflowData } from "./type"
import { OrchestrationUtils } from "@medusajs/utils"
/**
* This function is used to run multiple steps in parallel. The result of each step will be returned as part of the result array.
*
* @typeParam TResult - The type of the expected result.
*
* @returns The step results. The results are ordered in the array by the order they're passed in the function's parameter.
*
* @example
* import {
* createWorkflow,
* parallelize
* } from "@medusajs/workflows-sdk"
* import {
* createProductStep,
* getProductStep,
* createPricesStep,
* attachProductToSalesChannelStep
* } from "./steps"
*
* interface WorkflowInput {
* title: string
* }
*
* const myWorkflow = createWorkflow<
* WorkflowInput,
* Product
* >("my-workflow", (input) => {
* const product = createProductStep(input)
*
* const [prices, productSalesChannel] = parallelize(
* createPricesStep(product),
* attachProductToSalesChannelStep(product)
* )
*
* const id = product.id
* return getProductStep(product.id)
* }
* )
*/
export function parallelize<TResult extends WorkflowData[]>(
...steps: TResult
): TResult {
if (!global[OrchestrationUtils.SymbolMedusaWorkflowComposerContext]) {
throw new Error(
"parallelize must be used inside a createWorkflow definition"
)
}
const parallelizeBinder = (
global[
OrchestrationUtils.SymbolMedusaWorkflowComposerContext
] as CreateWorkflowComposerContext
).parallelizeBinder
const resultSteps = steps.map((step) => step)
return parallelizeBinder<TResult>(function (
this: CreateWorkflowComposerContext
) {
const stepOntoMerge = steps.shift()!
this.flow.mergeActions(
stepOntoMerge.__step__,
...steps.map((step) => step.__step__)
)
return resultSteps as unknown as TResult
})
}
@@ -0,0 +1,194 @@
import { resolveValue } from "./helpers"
import { StepExecutionContext, WorkflowData } from "./type"
import { proxify } from "./helpers/proxy"
import { OrchestrationUtils } from "@medusajs/utils"
type Func1<T extends object | WorkflowData, U> = (
input: T extends WorkflowData<infer U>
? U
: T extends object
? { [K in keyof T]: T[K] extends WorkflowData<infer U> ? U : T[K] }
: {},
context: StepExecutionContext
) => U | Promise<U>
type Func<T, U> = (input: T, context: StepExecutionContext) => U | Promise<U>
/**
*
* This function transforms the output of other utility functions.
*
* For example, if you're using the value(s) of some step(s) as an input to a later step. As you can't directly manipulate data in the workflow constructor function passed to {@link createWorkflow},
* the `transform` function provides access to the runtime value of the step(s) output so that you can manipulate them.
*
* Another example is if you're using the runtime value of some step(s) as the output of a workflow.
*
* If you're also retrieving the output of a hook and want to check if its value is set, you must use a workflow to get the runtime value of that hook.
*
* @returns There's no expected value to be returned by the `transform` function.
*
* @example
* import {
* createWorkflow,
* transform
* } from "@medusajs/workflows-sdk"
* import { step1, step2 } from "./steps"
*
* type WorkflowInput = {
* name: string
* }
*
* type WorkflowOutput = {
* message: string
* }
*
* const myWorkflow = createWorkflow<
* WorkflowInput,
* WorkflowOutput
* >
* ("hello-world", (input) => {
* const str1 = step1(input)
* const str2 = step2(input)
*
* return transform({
* str1,
* str2
* }, (input) => ({
* message: `${input.str1}${input.str2}`
* }))
* })
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RFinal>(
/**
* The output(s) of other step functions.
*/
values: T,
/**
* The transform function used to perform action on the runtime values of the provided `values`.
*/
...func:
| [Func1<T, RFinal>]
): WorkflowData<RFinal>
/**
* @internal
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RA, RFinal>(
values: T,
...func:
| [Func1<T, RFinal>]
| [Func1<T, RA>, Func<RA, RFinal>]
): WorkflowData<RFinal>
/**
* @internal
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RA, RB, RFinal>(
values: T,
...func:
| [Func1<T, RFinal>]
| [Func1<T, RA>, Func<RA, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RFinal>]
): WorkflowData<RFinal>
/**
* @internal
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RA, RB, RC, RFinal>(
values: T,
...func:
| [Func1<T, RFinal>]
| [Func1<T, RA>, Func<RA, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RFinal>]
): WorkflowData<RFinal>
/**
* @internal
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RA, RB, RC, RD, RFinal>(
values: T,
...func:
| [Func1<T, RFinal>]
| [Func1<T, RA>, Func<RA, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RD>, Func<RD, RFinal>]
): WorkflowData<RFinal>
/**
* @internal
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RA, RB, RC, RD, RE, RFinal>(
values: T,
...func:
| [Func1<T, RFinal>]
| [Func1<T, RA>, Func<RA, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RD>, Func<RD, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RD>, Func<RD, RE>, Func<RE, RFinal>]
): WorkflowData<RFinal>
/**
* @internal
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RA, RB, RC, RD, RE, RF, RFinal>(
values: T,
...func:
| [Func1<T, RFinal>]
| [Func1<T, RA>, Func<RA, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RD>, Func<RD, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RD>, Func<RD, RE>, Func<RE, RFinal>]
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RD>, Func<RD, RE>, Func<RE, RF>, Func<RF, RFinal>]
): WorkflowData<RFinal>
export function transform(
values: any | any[],
...functions: Function[]
): unknown {
const ret = {
__type: OrchestrationUtils.SymbolWorkflowStepTransformer,
__resolver: undefined,
}
const returnFn = async function (transactionContext): Promise<any> {
const allValues = await resolveValue(values, transactionContext)
const stepValue = allValues
? JSON.parse(JSON.stringify(allValues))
: allValues
let finalResult
for (let i = 0; i < functions.length; i++) {
const fn = functions[i]
const arg = i === 0 ? stepValue : finalResult
finalResult = await fn.apply(fn, [arg, transactionContext])
}
return finalResult
}
const proxyfiedRet = proxify<WorkflowData & { __resolver: any }>(
ret as unknown as WorkflowData
)
proxyfiedRet.__resolver = returnFn as any
return proxyfiedRet
}
@@ -0,0 +1,225 @@
import {
LocalWorkflow,
OrchestratorBuilder,
TransactionContext as OriginalWorkflowTransactionContext,
TransactionModelOptions,
TransactionPayload,
TransactionStepsDefinition,
WorkflowHandler,
} from "@medusajs/orchestration"
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
import { ExportedWorkflow } from "../../helper"
export type StepFunctionResult<TOutput extends unknown | unknown[] = unknown> =
(this: CreateWorkflowComposerContext) => WorkflowData<TOutput>
type StepFunctionReturnConfig<TOutput> = {
config(
config: { name?: string } & Omit<
TransactionStepsDefinition,
"next" | "uuid" | "action"
>
): WorkflowData<TOutput>
}
type KeysOfUnion<T> = T extends T ? keyof T : never
/**
* A step function to be used in a workflow.
*
* @typeParam TInput - The type of the input of the step.
* @typeParam TOutput - The type of the output of the step.
*/
export type StepFunction<
TInput,
TOutput = unknown
> = (KeysOfUnion<TInput> extends []
? // Function that doesn't expect any input
{
(): WorkflowData<TOutput> & StepFunctionReturnConfig<TOutput>
}
: // function that expects an input object
{
(input: WorkflowData<TInput> | TInput): WorkflowData<TOutput> &
StepFunctionReturnConfig<TOutput>
}) &
WorkflowDataProperties<TOutput>
export type WorkflowDataProperties<T = unknown> = {
__type: string
__step__: string
}
/**
* This type is used to encapsulate the input or output type of all utils.
*
* @typeParam T - The type of a step's input or result.
*/
export type WorkflowData<T = unknown> = (T extends Array<infer Item>
? Array<Item | WorkflowData<Item>>
: T extends object
? {
[Key in keyof T]: T[Key] | WorkflowData<T[Key]>
}
: T & WorkflowDataProperties<T>) &
T &
WorkflowDataProperties<T> & {
config(
config: { name?: string } & Omit<
TransactionStepsDefinition,
"next" | "uuid" | "action"
>
): WorkflowData<T>
}
export type CreateWorkflowComposerContext = {
hooks_: string[]
hooksCallback_: Record<string, Function[]>
workflowId: string
flow: OrchestratorBuilder
handlers: WorkflowHandler
stepBinder: <TOutput = unknown>(
fn: StepFunctionResult
) => WorkflowData<TOutput>
hookBinder: <TOutput = unknown>(
name: string,
fn: Function
) => WorkflowData<TOutput>
parallelizeBinder: <TOutput extends WorkflowData[] = WorkflowData[]>(
fn: (this: CreateWorkflowComposerContext) => TOutput
) => TOutput
}
/**
* The step's context.
*/
export interface StepExecutionContext {
/**
* The ID of the workflow.
*/
workflowId: string
/**
* The attempt number of the step.
*/
attempt: number
/**
* The idempoency key of the step.
*/
idempotencyKey: string
/**
* The name of the step.
*/
stepName: string
/**
* The action of the step.
*/
action: "invoke" | "compensate"
/**
* The container used to access resources, such as services, in the step.
*/
container: MedusaContainer
/**
* Metadata passed in the input.
*/
metadata: TransactionPayload["metadata"]
/**
* {@inheritDoc types!Context}
*/
context: Context
}
export type WorkflowTransactionContext = StepExecutionContext &
OriginalWorkflowTransactionContext & {
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