feat(orchestration,workflows): pipe oncomplete and workflow preparation (#4697)
* chore: pipe onComplete and workflow preparation step * changeset * fix: tests --------- Co-authored-by: Adrien de Peretti <adrien.deperetti@gmail.com> Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
co-authored by
Adrien de Peretti
Oli Juhl
parent
d1e298f5dc
commit
c0ca002901
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/orchestration": minor
|
||||||
|
"@medusajs/workflows": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add pipe onComplete callback and preparation function to exportsWorkflow
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
DistributedTransaction,
|
||||||
TransactionHandlerType,
|
TransactionHandlerType,
|
||||||
TransactionOrchestrator,
|
TransactionOrchestrator,
|
||||||
TransactionPayload,
|
TransactionPayload,
|
||||||
@@ -858,4 +859,32 @@ describe("Transaction Orchestrator", () => {
|
|||||||
expect(mocks.oneCompensate).toBeCalledTimes(1)
|
expect(mocks.oneCompensate).toBeCalledTimes(1)
|
||||||
expect(mocks.twoCompensate).toBeCalledTimes(1)
|
expect(mocks.twoCompensate).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("Should receive the current transaction as reference in the handler", async () => {
|
||||||
|
let transactionInHandler
|
||||||
|
|
||||||
|
async function handler(
|
||||||
|
actionId: string,
|
||||||
|
functionHandlerType: TransactionHandlerType,
|
||||||
|
payload: TransactionPayload,
|
||||||
|
transaction?: DistributedTransaction
|
||||||
|
) {
|
||||||
|
transactionInHandler = transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
const strategy = new TransactionOrchestrator("transaction-name", {
|
||||||
|
next: {
|
||||||
|
action: "firstMethod",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const transaction = await strategy.beginTransaction(
|
||||||
|
"transaction_id_123",
|
||||||
|
handler
|
||||||
|
)
|
||||||
|
|
||||||
|
await strategy.resume(transaction)
|
||||||
|
|
||||||
|
expect(transaction).toBe(transactionInHandler)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { isDefined } from "@medusajs/utils"
|
import { isDefined } from "@medusajs/utils"
|
||||||
import { TransactionFlow } from "./transaction-orchestrator"
|
import { TransactionFlow } from "./transaction-orchestrator"
|
||||||
|
import { TransactionStepHandler } from "./transaction-step"
|
||||||
import { TransactionHandlerType, TransactionState } from "./types"
|
import { TransactionHandlerType, TransactionState } from "./types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,11 +80,7 @@ export class DistributedTransaction {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private flow: TransactionFlow,
|
private flow: TransactionFlow,
|
||||||
public handler: (
|
public handler: TransactionStepHandler,
|
||||||
actionId: string,
|
|
||||||
handlerType: TransactionHandlerType,
|
|
||||||
payload: TransactionPayload
|
|
||||||
) => Promise<unknown>,
|
|
||||||
public payload?: any,
|
public payload?: any,
|
||||||
errors?: TransactionStepError[],
|
errors?: TransactionStepError[],
|
||||||
context?: TransactionContext
|
context?: TransactionContext
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import {
|
|||||||
TransactionCheckpoint,
|
TransactionCheckpoint,
|
||||||
TransactionPayload,
|
TransactionPayload,
|
||||||
} from "./distributed-transaction"
|
} from "./distributed-transaction"
|
||||||
|
import { TransactionStep, TransactionStepHandler } from "./transaction-step"
|
||||||
import {
|
import {
|
||||||
TransactionHandlerType,
|
TransactionHandlerType,
|
||||||
TransactionModel,
|
|
||||||
TransactionState,
|
TransactionState,
|
||||||
TransactionStepStatus,
|
TransactionStepStatus,
|
||||||
TransactionStepsDefinition,
|
TransactionStepsDefinition,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { TransactionStep, TransactionStepHandler } from "./transaction-step"
|
|
||||||
|
|
||||||
import { EventEmitter } from "events"
|
import { EventEmitter } from "events"
|
||||||
|
|
||||||
@@ -366,7 +365,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
if (!step.definition.async) {
|
if (!step.definition.async) {
|
||||||
execution.push(
|
execution.push(
|
||||||
transaction
|
transaction
|
||||||
.handler(step.definition.action + "", type, payload)
|
.handler(step.definition.action + "", type, payload, transaction)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
await TransactionOrchestrator.setStepSuccess(
|
await TransactionOrchestrator.setStepSuccess(
|
||||||
transaction,
|
transaction,
|
||||||
@@ -387,7 +386,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
execution.push(
|
execution.push(
|
||||||
transaction.saveCheckpoint().then(async () =>
|
transaction.saveCheckpoint().then(async () =>
|
||||||
transaction
|
transaction
|
||||||
.handler(step.definition.action + "", type, payload)
|
.handler(step.definition.action + "", type, payload, transaction)
|
||||||
.catch(async (error) => {
|
.catch(async (error) => {
|
||||||
await TransactionOrchestrator.setStepFailure(
|
await TransactionOrchestrator.setStepFailure(
|
||||||
transaction,
|
transaction,
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { TransactionPayload } from "./distributed-transaction"
|
|
||||||
import {
|
import {
|
||||||
TransactionStepsDefinition,
|
DistributedTransaction,
|
||||||
TransactionStepStatus,
|
TransactionPayload,
|
||||||
TransactionState,
|
} from "./distributed-transaction"
|
||||||
|
import {
|
||||||
TransactionHandlerType,
|
TransactionHandlerType,
|
||||||
|
TransactionState,
|
||||||
|
TransactionStepStatus,
|
||||||
|
TransactionStepsDefinition,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
export type TransactionStepHandler = (
|
export type TransactionStepHandler = (
|
||||||
actionId: string,
|
actionId: string,
|
||||||
handlerType: TransactionHandlerType,
|
handlerType: TransactionHandlerType,
|
||||||
payload: TransactionPayload
|
payload: TransactionPayload,
|
||||||
|
transaction?: DistributedTransaction
|
||||||
) => Promise<unknown>
|
) => Promise<unknown>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Context, MedusaContainer } from "@medusajs/types"
|
import { Context, MedusaContainer } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
|
DistributedTransaction,
|
||||||
OrchestratorBuilder,
|
OrchestratorBuilder,
|
||||||
TransactionHandlerType,
|
TransactionHandlerType,
|
||||||
TransactionMetadata,
|
TransactionMetadata,
|
||||||
@@ -35,6 +36,7 @@ export type WorkflowStepHandler = (args: {
|
|||||||
invoke: { [actions: string]: unknown }
|
invoke: { [actions: string]: unknown }
|
||||||
compensate: { [actions: string]: unknown }
|
compensate: { [actions: string]: unknown }
|
||||||
metadata: TransactionMetadata
|
metadata: TransactionMetadata
|
||||||
|
transaction: DistributedTransaction
|
||||||
context?: Context
|
context?: Context
|
||||||
}) => unknown
|
}) => unknown
|
||||||
|
|
||||||
@@ -136,7 +138,8 @@ export class WorkflowManager {
|
|||||||
return async (
|
return async (
|
||||||
actionId: string,
|
actionId: string,
|
||||||
handlerType: TransactionHandlerType,
|
handlerType: TransactionHandlerType,
|
||||||
payload?: any
|
payload?: any,
|
||||||
|
transaction?: DistributedTransaction
|
||||||
) => {
|
) => {
|
||||||
const command = handlers.get(actionId)
|
const command = handlers.get(actionId)
|
||||||
|
|
||||||
@@ -157,6 +160,7 @@ export class WorkflowManager {
|
|||||||
invoke,
|
invoke,
|
||||||
compensate,
|
compensate,
|
||||||
metadata,
|
metadata,
|
||||||
|
transaction: transaction as DistributedTransaction,
|
||||||
context,
|
context,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,6 @@
|
|||||||
"prepare": "cross-env NODE_ENV=production yarn run build",
|
"prepare": "cross-env NODE_ENV=production yarn run build",
|
||||||
"build": "rimraf dist && tsc --build",
|
"build": "rimraf dist && tsc --build",
|
||||||
"watch": "tsc --build --watch",
|
"watch": "tsc --build --watch",
|
||||||
"test": "jest --passWithNoTests"
|
"test": "jest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,4 +45,44 @@ describe("Pipe", function () {
|
|||||||
expect(result).toBeDefined()
|
expect(result).toBeDefined()
|
||||||
expect(result).toEqual(output)
|
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,64 @@
|
|||||||
|
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" },
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Export Workflow", function () {
|
||||||
|
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 wfHandler = work()
|
||||||
|
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Context, MedusaContainer, SharedContext } from "@medusajs/types"
|
|
||||||
import {
|
import {
|
||||||
TransactionMetadata,
|
TransactionMetadata,
|
||||||
WorkflowStepHandler,
|
WorkflowStepHandler,
|
||||||
} from "@medusajs/orchestration"
|
} from "@medusajs/orchestration"
|
||||||
|
import { Context, MedusaContainer, SharedContext } from "@medusajs/types"
|
||||||
|
|
||||||
|
import { DistributedTransaction } from "@medusajs/orchestration"
|
||||||
import { InputAlias } from "../definitions"
|
import { InputAlias } from "../definitions"
|
||||||
|
|
||||||
export type WorkflowStepMiddlewareReturn = {
|
export type WorkflowStepMiddlewareReturn = {
|
||||||
@@ -20,6 +21,7 @@ interface PipelineInput {
|
|||||||
inputAlias?: InputAlias | string
|
inputAlias?: InputAlias | string
|
||||||
invoke?: WorkflowStepMiddlewareInput | WorkflowStepMiddlewareInput[]
|
invoke?: WorkflowStepMiddlewareInput | WorkflowStepMiddlewareInput[]
|
||||||
compensate?: WorkflowStepMiddlewareInput | WorkflowStepMiddlewareInput[]
|
compensate?: WorkflowStepMiddlewareInput | WorkflowStepMiddlewareInput[]
|
||||||
|
onComplete?: (args: WorkflowOnCompleteArguments) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkflowArguments<T = any> = {
|
export type WorkflowArguments<T = any> = {
|
||||||
@@ -30,6 +32,15 @@ export type WorkflowArguments<T = any> = {
|
|||||||
context: Context | SharedContext
|
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> = (
|
export type PipelineHandler<T extends any = undefined> = (
|
||||||
args: WorkflowArguments
|
args: WorkflowArguments
|
||||||
) => Promise<
|
) => Promise<
|
||||||
@@ -48,6 +59,7 @@ export function pipe<T>(
|
|||||||
invoke,
|
invoke,
|
||||||
compensate,
|
compensate,
|
||||||
metadata,
|
metadata,
|
||||||
|
transaction,
|
||||||
context,
|
context,
|
||||||
}) => {
|
}) => {
|
||||||
let data = {}
|
let data = {}
|
||||||
@@ -61,8 +73,9 @@ export function pipe<T>(
|
|||||||
Object.assign(original.invoke, { [input.inputAlias]: payload })
|
Object.assign(original.invoke, { [input.inputAlias]: payload })
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in input) {
|
const dataKeys = ["invoke", "compensate"]
|
||||||
if (!input[key] || key === "inputAlias") {
|
for (const key of dataKeys) {
|
||||||
|
if (!input[key]) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +124,18 @@ export function pipe<T>(
|
|||||||
finalResult = result
|
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
|
return finalResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export type WorkflowResult<TResult = unknown> = {
|
|||||||
|
|
||||||
export const exportWorkflow = <TData = unknown, TResult = unknown>(
|
export const exportWorkflow = <TData = unknown, TResult = unknown>(
|
||||||
workflowId: Workflows,
|
workflowId: Workflows,
|
||||||
defaultResult?: string
|
defaultResult?: string,
|
||||||
|
dataPreparation?: (data: TData) => Promise<unknown>
|
||||||
) => {
|
) => {
|
||||||
return function <TDataOverride = undefined, TResultOverride = undefined>(
|
return function <TDataOverride = undefined, TResultOverride = undefined>(
|
||||||
container?: LoadedModule[] | MedusaContainer
|
container?: LoadedModule[] | MedusaContainer
|
||||||
@@ -60,6 +61,22 @@ export const exportWorkflow = <TData = unknown, TResult = unknown>(
|
|||||||
resultFrom ??= defaultResult
|
resultFrom ??= defaultResult
|
||||||
throwOnError ??= true
|
throwOnError ??= true
|
||||||
|
|
||||||
|
if (typeof dataPreparation === "function") {
|
||||||
|
try {
|
||||||
|
const copyInput = JSON.parse(JSON.stringify(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],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const transaction = await originalRun(
|
const transaction = await originalRun(
|
||||||
context?.transactionId ?? ulid(),
|
context?.transactionId ?? ulid(),
|
||||||
input,
|
input,
|
||||||
|
|||||||
Reference in New Issue
Block a user