feat(medusa): workflow engine api (#6330)

What:

   Workflow Engine API.
   Endpoints for:
     - List workflow executions
     - Run a workflow
     - Set async steps as success or failure
     - Retrieve the details of a workflow run
This commit is contained in:
Carlos R. L. Rodrigues
2024-02-13 15:19:10 +00:00
committed by GitHub
parent 59ab66a773
commit 0c2a460751
44 changed files with 1425 additions and 80 deletions
+5 -4
View File
@@ -1,9 +1,9 @@
import { Router } from "express"
import errorHandler from "./middlewares/error-handler"
import compression from "compression"
import { Router } from "express"
import { compressionOptions, shouldCompressResponse } from "../utils/api"
import errorHandler from "./middlewares/error-handler"
import admin from "./routes/admin"
import store from "./routes/store"
import { shouldCompressResponse, compressionOptions } from "../utils/api"
// guaranteed to get dependencies
export default (container, config) => {
@@ -53,9 +53,9 @@ export * from "./routes/admin/product-types"
export * from "./routes/admin/products"
export * from "./routes/admin/publishable-api-keys"
export * from "./routes/admin/regions"
export * from "./routes/admin/reservations"
export * from "./routes/admin/return-reasons"
export * from "./routes/admin/returns"
export * from "./routes/admin/reservations"
export * from "./routes/admin/sales-channels"
export * from "./routes/admin/shipping-options"
export * from "./routes/admin/shipping-profiles"
@@ -66,6 +66,7 @@ export * from "./routes/admin/tax-rates"
export * from "./routes/admin/uploads"
export * from "./routes/admin/users"
export * from "./routes/admin/variants"
export * from "./routes/admin/workflows-executions"
// Store
export * from "./routes/store/auth"
export * from "./routes/store/carts"
@@ -41,6 +41,7 @@ import taxRateRoutes from "./tax-rates"
import uploadRoutes from "./uploads"
import userRoutes, { unauthenticatedUserRoutes } from "./users"
import variantRoutes from "./variants"
import workflowRoutes from "./workflows-executions"
const route = Router()
@@ -115,6 +116,7 @@ export default (app, container, config) => {
paymentCollectionRoutes(route)
paymentRoutes(route)
productCategoryRoutes(route)
workflowRoutes(route)
return app
}
@@ -0,0 +1,26 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
export default async (req: MedusaRequest, res: MedusaResponse) => {
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
ModuleRegistrationName.WORKFLOW_ENGINE
)
const { id, workflow_id, transaction_id } = req.params
const execution = await workflowEngineService.retrieveWorkflowExecution(
id ?? {
workflow_id,
transaction_id,
},
{
select: req.retrieveConfig.select,
relations: req.retrieveConfig.relations,
}
)
res.status(200).json({
workflow_execution: execution,
})
}
@@ -0,0 +1,83 @@
import middlewares, {
transformBody,
transformQuery,
} from "../../../middlewares"
import { Router } from "express"
import {
allowedAdminWorkflowExecutionsRelations,
defaultAdminWorkflowExecutionDetailFields,
defaultAdminWorkflowExecutionsFields,
defaultAdminWorkflowExecutionsRelations,
} from "./query-config"
import {
AdminGetWorkflowExecutionDetailsParams,
AdminGetWorkflowExecutionsParams,
AdminPostWorkflowsAsyncResponseReq,
AdminPostWorkflowsRunReq,
} from "./validators"
const route = Router()
const retrieveTransformQueryConfig = {
defaultFields: defaultAdminWorkflowExecutionDetailFields,
defaultRelations: defaultAdminWorkflowExecutionsRelations,
allowedRelations: allowedAdminWorkflowExecutionsRelations,
isList: false,
}
const listTransformQueryConfig = {
...retrieveTransformQueryConfig,
defaultFields: defaultAdminWorkflowExecutionsFields,
isList: true,
}
export default (app) => {
app.use("/workflows-executions", route)
route.get(
"/",
transformQuery(AdminGetWorkflowExecutionsParams, listTransformQueryConfig),
middlewares.wrap(require("./list-execution").default)
)
route.get(
"/:id",
transformQuery(
AdminGetWorkflowExecutionDetailsParams,
retrieveTransformQueryConfig
),
middlewares.wrap(require("./get-execution").default)
)
route.get(
"/:workflow_id/:transaction_id",
transformQuery(
AdminGetWorkflowExecutionDetailsParams,
retrieveTransformQueryConfig
),
middlewares.wrap(require("./get-execution").default)
)
route.post(
"/:id/steps/success",
transformBody(AdminPostWorkflowsAsyncResponseReq),
middlewares.wrap(require("./set-step-success").default)
)
route.post(
"/:id/steps/failure",
transformBody(AdminPostWorkflowsAsyncResponseReq),
middlewares.wrap(require("./set-step-failure").default)
)
route.post(
"/:id/run",
transformBody(AdminPostWorkflowsRunReq),
middlewares.wrap(require("./run-workflow").default)
)
return app
}
export * from "./query-config"
@@ -0,0 +1,29 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
export default async (req: MedusaRequest, res: MedusaResponse) => {
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
ModuleRegistrationName.WORKFLOW_ENGINE
)
const listConfig = req.listConfig
const [workflow_executions, count] =
await workflowEngineService.listAndCountWorkflowExecution(
req.filterableFields,
{
select: req.listConfig.select,
relations: req.listConfig.relations,
skip: listConfig.skip,
take: listConfig.take,
}
)
res.json({
workflow_executions,
count,
offset: listConfig.skip,
limit: listConfig.take,
})
}
@@ -0,0 +1,23 @@
export const defaultAdminWorkflowExecutionsRelations = []
export const allowedAdminWorkflowExecutionsRelations = []
export const defaultAdminWorkflowExecutionsFields = [
"id",
"workflow_id",
"transaction_id",
"state",
"created_at",
"updated_at",
"deleted_at",
]
export const defaultAdminWorkflowExecutionDetailFields = [
"id",
"workflow_id",
"transaction_id",
"context",
"execution",
"state",
"created_at",
"updated_at",
"deleted_at",
]
@@ -0,0 +1,35 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
IWorkflowEngineService,
WorkflowOrchestratorTypes,
} from "@medusajs/workflows-sdk"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
import { AdminPostWorkflowsRunReq } from "./validators"
export default async (req: MedusaRequest, res: MedusaResponse) => {
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
ModuleRegistrationName.WORKFLOW_ENGINE
)
const { id: workflow_id } = req.params
const { transaction_id, input } =
req.validatedBody as AdminPostWorkflowsRunReq
const options = {
transactionId: transaction_id,
input,
context: {
requestId: req.requestId,
},
throwOnError: false,
} as WorkflowOrchestratorTypes.WorkflowOrchestratorRunDTO
const { acknowledgement } = await workflowEngineService.run(
workflow_id,
options
)
return res.status(200).json({ acknowledgement })
}
@@ -0,0 +1,41 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { TransactionHandlerType, isDefined } from "@medusajs/utils"
import { IWorkflowEngineService, StepResponse } from "@medusajs/workflows-sdk"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
import { AdminPostWorkflowsAsyncResponseReq } from "./validators"
export default async (req: MedusaRequest, res: MedusaResponse) => {
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
ModuleRegistrationName.WORKFLOW_ENGINE
)
const { id: workflow_id } = req.params
const body = req.validatedBody as AdminPostWorkflowsAsyncResponseReq
const { transaction_id, step_id } = body
const compensateInput = body.compensate_input
const stepResponse = isDefined(body.response)
? new StepResponse(body.response, compensateInput)
: undefined
const stepAction = body.action || TransactionHandlerType.INVOKE
await workflowEngineService.setStepFailure({
idempotencyKey: {
action: stepAction,
transactionId: transaction_id,
stepId: step_id,
workflowId: workflow_id,
},
stepResponse,
options: {
container: req.scope,
context: {
requestId: req.requestId,
},
},
})
return res.status(200).json({ success: true })
}
@@ -0,0 +1,41 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { TransactionHandlerType, isDefined } from "@medusajs/utils"
import { IWorkflowEngineService, StepResponse } from "@medusajs/workflows-sdk"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
import { AdminPostWorkflowsAsyncResponseReq } from "./validators"
export default async (req: MedusaRequest, res: MedusaResponse) => {
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
ModuleRegistrationName.WORKFLOW_ENGINE
)
const { id: workflow_id } = req.params
const body = req.validatedBody as AdminPostWorkflowsAsyncResponseReq
const { transaction_id, step_id } = body
const compensateInput = body.compensate_input
const stepResponse = isDefined(body.response)
? new StepResponse(body.response, compensateInput)
: undefined
const stepAction = body.action || TransactionHandlerType.INVOKE
await workflowEngineService.setStepSuccess({
idempotencyKey: {
action: stepAction,
transactionId: transaction_id,
stepId: step_id,
workflowId: workflow_id,
},
stepResponse,
options: {
container: req.scope,
context: {
requestId: req.requestId,
},
},
})
return res.status(200).json({ success: true })
}
@@ -0,0 +1,62 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
export default async (req: MedusaRequest, res: MedusaResponse) => {
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
ModuleRegistrationName.WORKFLOW_ENGINE
)
const { id: workflow_id, transaction_id } = req.query as any
const subscriberId = "__sub__" + Math.random().toString(36).substring(2, 9)
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
})
req.on("close", () => {
res.end()
void workflowEngineService.unsubscribe({
workflowId: workflow_id,
transactionId: transaction_id,
subscriberOrId: subscriberId,
})
})
req.on("error", (err: any) => {
if (err.code === "ECONNRESET") {
res.end()
}
})
void workflowEngineService.subscribe({
workflowId: workflow_id,
transactionId: transaction_id,
subscriber: async (args) => {
const {
eventType,
workflowId,
transactionId,
step,
response,
result,
errors,
} = args
const data = {
event_type: eventType,
workflow_id: workflowId,
transaction_id: transactionId,
step,
response,
result,
errors,
}
res.write(`event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`)
},
subscriberId,
})
}
@@ -0,0 +1,54 @@
import { TransactionHandlerType } from "@medusajs/utils"
import { Transform } from "class-transformer"
import { IsEnum, IsOptional, IsString } from "class-validator"
import { FindParams, extendedFindParamsMixin } from "../../../../types/common"
import { IsType } from "../../../../utils"
export class AdminGetWorkflowExecutionDetailsParams extends FindParams {}
export class AdminGetWorkflowExecutionsParams extends extendedFindParamsMixin({
limit: 100,
offset: 0,
}) {
/**
* transaction id(s) to filter workflow executions by transaction_id.
*/
@IsOptional()
@IsType([String, [String]])
transaction_id?: string | string[]
/**
* workflow id(s) to filter workflow executions by workflow_id
*/
@IsOptional()
@IsType([String, [String]])
workflow_id?: string | string[]
}
export class AdminPostWorkflowsRunReq {
@IsOptional()
input?: unknown
@IsOptional()
@IsString()
transaction_id?: string
}
export class AdminPostWorkflowsAsyncResponseReq {
@IsString()
transaction_id: string
@IsString()
step_id: string
@IsOptional()
response?: unknown
@IsOptional()
compensate_input?: unknown
@IsOptional()
@Transform(({ value }) => (value + "").toLowerCase())
@IsEnum(TransactionHandlerType)
action?: TransactionHandlerType
}