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 authored and GitHub committed 2024-02-13 15:19:10 +00:00
1 parent 59ab66a773
commit 0c2a460751
44 files changed
+1425 -80

No files matched your search

@@ -0,0 +1,20 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
ModuleRegistrationName.WORKFLOW_ENGINE
)
const { id } = req.params
const execution = await workflowEngineService.retrieveWorkflowExecution(id, {
select: req.retrieveConfig.select,
relations: req.retrieveConfig.relations,
})
res.status(200).json({
workflow_execution: execution,
})
}
@@ -0,0 +1,65 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk"
import {
MedusaRequest,
MedusaResponse,
} from "../../../../../../../types/routing"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
ModuleRegistrationName.WORKFLOW_ENGINE
)
const { 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,26 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk"
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
ModuleRegistrationName.WORKFLOW_ENGINE
)
const { workflow_id, transaction_id } = req.params
const execution = await workflowEngineService.retrieveWorkflowExecution(
{
workflow_id,
transaction_id,
},
{
select: req.retrieveConfig.select,
relations: req.retrieveConfig.relations,
}
)
res.status(200).json({
workflow_execution: execution,
})
}
@@ -0,0 +1,34 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
IWorkflowEngineService,
WorkflowOrchestratorTypes,
} from "@medusajs/workflows-sdk"
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
import { AdminPostWorkflowsRunReq } from "../../validators"
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
ModuleRegistrationName.WORKFLOW_ENGINE
)
const { 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 const POST = async (req: MedusaRequest, res: MedusaResponse) => {
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
ModuleRegistrationName.WORKFLOW_ENGINE
)
const { 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 const POST = async (req: MedusaRequest, res: MedusaResponse) => {
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
ModuleRegistrationName.WORKFLOW_ENGINE
)
const { 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,60 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk"
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
ModuleRegistrationName.WORKFLOW_ENGINE
)
const { workflow_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,
subscriberOrId: subscriberId,
})
})
req.on("error", (err: any) => {
if (err.code === "ECONNRESET") {
res.end()
}
})
void workflowEngineService.subscribe({
workflowId: workflow_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,58 @@
import { transformBody, transformQuery } from "../../../api/middlewares"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import * as QueryConfig from "./query-config"
import {
AdminGetWorkflowExecutionDetailsParams,
AdminGetWorkflowExecutionsParams,
AdminPostWorkflowsAsyncResponseReq,
AdminPostWorkflowsRunReq,
} from "./validators"
export const adminWorkflowsExecutionsMiddlewares: MiddlewareRoute[] = [
{
method: ["GET"],
matcher: "/admin/workflows-executions",
middlewares: [
transformQuery(
AdminGetWorkflowExecutionsParams,
QueryConfig.listTransformQueryConfig
),
],
},
{
method: ["GET"],
matcher: "/admin/workflows-executions/:id",
middlewares: [
transformQuery(
AdminGetWorkflowExecutionDetailsParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["GET"],
matcher: "/admin/workflows-executions/:workflow_id/:transaction_id",
middlewares: [
transformQuery(
AdminGetWorkflowExecutionDetailsParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/workflows-executions/:id/run",
middlewares: [transformBody(AdminPostWorkflowsRunReq)],
},
{
method: ["POST"],
matcher: "/admin/workflows-executions/:id/steps/success",
middlewares: [transformBody(AdminPostWorkflowsAsyncResponseReq)],
},
{
method: ["POST"],
matcher: "/admin/workflows-executions/:id/steps/failure",
middlewares: [transformBody(AdminPostWorkflowsAsyncResponseReq)],
},
]
@@ -0,0 +1,36 @@
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",
]
export const retrieveTransformQueryConfig = {
defaultFields: defaultAdminWorkflowExecutionDetailFields,
defaultRelations: defaultAdminWorkflowExecutionsRelations,
allowedRelations: allowedAdminWorkflowExecutionsRelations,
isList: false,
}
export const listTransformQueryConfig = {
...retrieveTransformQueryConfig,
defaultFields: defaultAdminWorkflowExecutionsFields,
isList: true,
}
@@ -0,0 +1,29 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk"
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
export const GET = 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,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
}
@@ -4,6 +4,7 @@ import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/mid
import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares"
import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares"
import { adminRegionRoutesMiddlewares } from "./admin/regions/middlewares"
import { adminWorkflowsExecutionsMiddlewares } from "./admin/workflows-executions/middlewares"
import { authRoutesMiddlewares } from "./auth/middlewares"
import { storeCartRoutesMiddlewares } from "./store/carts/middlewares"
import { storeCustomerRoutesMiddlewares } from "./store/customers/middlewares"
@@ -18,6 +19,7 @@ export const config: MiddlewaresConfig = {
...storeCustomerRoutesMiddlewares,
...storeCartRoutesMiddlewares,
...authRoutesMiddlewares,
...adminWorkflowsExecutionsMiddlewares,
...storeRegionRoutesMiddlewares,
...adminRegionRoutesMiddlewares,
],
+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
}
@@ -1,7 +1,7 @@
import { MedusaApp } from "@medusajs/modules-sdk"
import { RemoteJoinerQuery } from "@medusajs/types"
import { TransactionHandlerType } from "@medusajs/utils"
import { IWorkflowsModuleService } from "@medusajs/workflows-sdk"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk"
import { knex } from "knex"
import { setTimeout } from "timers/promises"
import "../__fixtures__"
@@ -22,7 +22,7 @@ const afterEach_ = async () => {
describe("Workflow Orchestrator module", function () {
describe("Testing basic workflow", function () {
let workflowOrcModule: IWorkflowsModuleService
let workflowOrcModule: IWorkflowEngineService
let query: (
query: string | RemoteJoinerQuery | object,
variables?: Record<string, unknown>
@@ -52,8 +52,7 @@ describe("Workflow Orchestrator module", function () {
await runMigrations()
workflowOrcModule =
modules.workflows as unknown as IWorkflowsModuleService
workflowOrcModule = modules.workflows as unknown as IWorkflowEngineService
})
afterEach(afterEach_)
@@ -1,12 +1,12 @@
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MedusaModule,
MODULE_PACKAGE_NAMES,
MedusaModule,
Modules,
} from "@medusajs/modules-sdk"
import { ModulesSdkTypes } from "@medusajs/types"
import { WorkflowOrchestratorTypes } from "@medusajs/workflows-sdk"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk"
import { moduleDefinition } from "../module-definition"
import { InitializeModuleInjectableDependencies } from "../types"
@@ -17,20 +17,18 @@ export const initialize = async (
| ExternalModuleDeclaration
| InternalModuleDeclaration,
injectedDependencies?: InitializeModuleInjectableDependencies
): Promise<WorkflowOrchestratorTypes.IWorkflowsModuleService> => {
): Promise<IWorkflowEngineService> => {
const loaded =
// eslint-disable-next-line max-len
await MedusaModule.bootstrap<WorkflowOrchestratorTypes.IWorkflowsModuleService>(
{
moduleKey: Modules.WORKFLOW_ENGINE,
defaultPath: MODULE_PACKAGE_NAMES[Modules.WORKFLOW_ENGINE],
declaration: options as
| InternalModuleDeclaration
| ExternalModuleDeclaration,
injectedDependencies,
moduleExports: moduleDefinition,
}
)
await MedusaModule.bootstrap<IWorkflowEngineService>({
moduleKey: Modules.WORKFLOW_ENGINE,
defaultPath: MODULE_PACKAGE_NAMES[Modules.WORKFLOW_ENGINE],
declaration: options as
| InternalModuleDeclaration
| ExternalModuleDeclaration,
injectedDependencies,
moduleExports: moduleDefinition,
})
return loaded[Modules.WORKFLOW_ENGINE]
}
@@ -10,8 +10,11 @@ import {
InjectManager,
InjectSharedContext,
MedusaContext,
MedusaError,
isString,
} from "@medusajs/utils"
import type {
IWorkflowEngineService,
ReturnWorkflow,
UnwrapWorkflowInputDataType,
WorkflowOrchestratorTypes,
@@ -25,9 +28,7 @@ type InjectedDependencies = {
workflowOrchestratorService: WorkflowOrchestratorService
}
export class WorkflowsModuleService
implements WorkflowOrchestratorTypes.IWorkflowsModuleService
{
export class WorkflowsModuleService implements IWorkflowEngineService {
protected baseRepository_: DAL.RepositoryService
protected workflowExecutionService_: ModulesSdkTypes.InternalModuleService<any>
protected workflowOrchestratorService_: WorkflowOrchestratorService
@@ -49,12 +50,70 @@ export class WorkflowsModuleService
return joinerConfig
}
@InjectManager("baseRepository_")
async retrieveWorkflowExecution(
idOrObject:
| string
| {
workflow_id: string
transaction_id: string
},
config: FindConfig<WorkflowOrchestratorTypes.WorkflowExecutionDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<WorkflowOrchestratorTypes.WorkflowExecutionDTO> {
const objValue = isString(idOrObject)
? { id: idOrObject }
: {
workflow_id: idOrObject.workflow_id,
transaction_id: idOrObject.transaction_id,
}
const wfExecution = await this.workflowExecutionService_.list(
objValue,
config,
sharedContext
)
if (wfExecution.length === 0) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`WorkflowExecution with ${Object.keys(objValue).join(
", "
)}: ${Object.values(objValue).join(", ")} was not found`
)
}
// eslint-disable-next-line max-len
return await this.baseRepository_.serialize<WorkflowOrchestratorTypes.WorkflowExecutionDTO>(
wfExecution[0],
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async listWorkflowExecution(
filters: WorkflowOrchestratorTypes.FilterableWorkflowExecutionProps = {},
config: FindConfig<WorkflowOrchestratorTypes.WorkflowExecutionDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<WorkflowOrchestratorTypes.WorkflowExecutionDTO[]> {
if (filters.transaction_id) {
if (Array.isArray(filters.transaction_id)) {
filters.transaction_id = {
$in: filters.transaction_id,
}
}
}
if (filters.workflow_id) {
if (Array.isArray(filters.workflow_id)) {
filters.workflow_id = {
$in: filters.workflow_id,
}
}
}
const wfExecutions = await this.workflowExecutionService_.list(
filters,
config,
@@ -74,6 +133,22 @@ export class WorkflowsModuleService
config: FindConfig<WorkflowOrchestratorTypes.WorkflowExecutionDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[WorkflowOrchestratorTypes.WorkflowExecutionDTO[], number]> {
if (filters.transaction_id) {
if (Array.isArray(filters.transaction_id)) {
filters.transaction_id = {
$in: filters.transaction_id,
}
}
}
if (filters.workflow_id) {
if (Array.isArray(filters.workflow_id)) {
filters.workflow_id = {
$in: filters.workflow_id,
}
}
}
const [wfExecutions, count] =
await this.workflowExecutionService_.listAndCount(
filters,
@@ -4,9 +4,9 @@ import {
TransactionCheckpoint,
TransactionStep,
} from "@medusajs/orchestration"
import { ModulesSdkTypes } from "@medusajs/types"
import { TransactionState } from "@medusajs/utils"
import { WorkflowOrchestratorService } from "@services"
import { ModulesSdkTypes } from "@medusajs/types"
// eslint-disable-next-line max-len
export class InMemoryDistributedTransactionStorage extends DistributedTransactionStorage {
@@ -55,6 +55,22 @@ export class InMemoryDistributedTransactionStorage extends DistributedTransactio
])
}
private stringifyWithSymbol(key, value) {
if (key === "__type" && typeof value === "symbol") {
return Symbol.keyFor(value)
}
return value
}
private jsonWithSymbol(key, value) {
if (key === "__type" && typeof value === "string") {
return Symbol.for(value)
}
return value
}
async get(key: string): Promise<TransactionCheckpoint | undefined> {
return this.storage.get(key)
}
@@ -89,10 +105,13 @@ export class InMemoryDistributedTransactionStorage extends DistributedTransactio
})
}
const stringifiedData = JSON.stringify(data, this.stringifyWithSymbol)
const parsedData = JSON.parse(stringifiedData)
if (hasFinished && !retentionTime) {
await this.deleteFromDb(data)
await this.deleteFromDb(parsedData)
} else {
await this.saveToDb(data)
await this.saveToDb(parsedData)
}
if (hasFinished) {
@@ -5,7 +5,7 @@ import {
} from "@medusajs/orchestration"
import { RemoteJoinerQuery } from "@medusajs/types"
import { TransactionHandlerType } from "@medusajs/utils"
import { IWorkflowsModuleService } from "@medusajs/workflows-sdk"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk"
import { knex } from "knex"
import { setTimeout } from "timers/promises"
import "../__fixtures__"
@@ -26,7 +26,7 @@ const afterEach_ = async () => {
describe("Workflow Orchestrator module", function () {
describe("Testing basic workflow", function () {
let workflowOrcModule: IWorkflowsModuleService
let workflowOrcModule: IWorkflowEngineService
let query: (
query: string | RemoteJoinerQuery | object,
variables?: Record<string, unknown>
@@ -61,8 +61,7 @@ describe("Workflow Orchestrator module", function () {
await runMigrations()
workflowOrcModule =
modules.workflows as unknown as IWorkflowsModuleService
workflowOrcModule = modules.workflows as unknown as IWorkflowEngineService
})
afterEach(afterEach_)
@@ -1,12 +1,13 @@
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MedusaModule,
MODULE_PACKAGE_NAMES,
MedusaModule,
Modules,
} from "@medusajs/modules-sdk"
import { ModulesSdkTypes } from "@medusajs/types"
import { WorkflowOrchestratorTypes } from "@medusajs/workflows-sdk"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk"
import { moduleDefinition } from "../module-definition"
import { InitializeModuleInjectableDependencies } from "../types"
@@ -17,20 +18,18 @@ export const initialize = async (
| ExternalModuleDeclaration
| InternalModuleDeclaration,
injectedDependencies?: InitializeModuleInjectableDependencies
): Promise<WorkflowOrchestratorTypes.IWorkflowsModuleService> => {
): Promise<IWorkflowEngineService> => {
const loaded =
// eslint-disable-next-line max-len
await MedusaModule.bootstrap<WorkflowOrchestratorTypes.IWorkflowsModuleService>(
{
moduleKey: Modules.WORKFLOW_ENGINE,
defaultPath: MODULE_PACKAGE_NAMES[Modules.WORKFLOW_ENGINE],
declaration: options as
| InternalModuleDeclaration
| ExternalModuleDeclaration,
injectedDependencies,
moduleExports: moduleDefinition,
}
)
await MedusaModule.bootstrap<IWorkflowEngineService>({
moduleKey: Modules.WORKFLOW_ENGINE,
defaultPath: MODULE_PACKAGE_NAMES[Modules.WORKFLOW_ENGINE],
declaration: options as
| InternalModuleDeclaration
| ExternalModuleDeclaration,
injectedDependencies,
moduleExports: moduleDefinition,
})
return loaded[Modules.WORKFLOW_ENGINE]
}
@@ -10,14 +10,17 @@ import {
InjectManager,
InjectSharedContext,
MedusaContext,
MedusaError,
isString,
} from "@medusajs/utils"
import type {
IWorkflowEngineService,
ReturnWorkflow,
UnwrapWorkflowInputDataType,
WorkflowOrchestratorTypes,
} from "@medusajs/workflows-sdk"
import {WorkflowOrchestratorService} from "@services"
import {joinerConfig} from "../joiner-config"
import { WorkflowOrchestratorService } from "@services"
import { joinerConfig } from "../joiner-config"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
@@ -25,9 +28,7 @@ type InjectedDependencies = {
workflowOrchestratorService: WorkflowOrchestratorService
}
export class WorkflowsModuleService
implements WorkflowOrchestratorTypes.IWorkflowsModuleService
{
export class WorkflowsModuleService implements IWorkflowEngineService {
protected baseRepository_: DAL.RepositoryService
protected workflowExecutionService_: ModulesSdkTypes.InternalModuleService<any>
protected workflowOrchestratorService_: WorkflowOrchestratorService
@@ -49,12 +50,70 @@ export class WorkflowsModuleService
return joinerConfig
}
@InjectManager("baseRepository_")
async retrieveWorkflowExecution(
idOrObject:
| string
| {
workflow_id: string
transaction_id: string
},
config: FindConfig<WorkflowOrchestratorTypes.WorkflowExecutionDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<WorkflowOrchestratorTypes.WorkflowExecutionDTO> {
const objValue = isString(idOrObject)
? { id: idOrObject }
: {
workflow_id: idOrObject.workflow_id,
transaction_id: idOrObject.transaction_id,
}
const wfExecution = await this.workflowExecutionService_.list(
objValue,
config,
sharedContext
)
if (wfExecution.length === 0) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`WorkflowExecution with ${Object.keys(objValue).join(
", "
)}: ${Object.values(objValue).join(", ")} was not found`
)
}
// eslint-disable-next-line max-len
return await this.baseRepository_.serialize<WorkflowOrchestratorTypes.WorkflowExecutionDTO>(
wfExecution[0],
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async listWorkflowExecution(
filters: WorkflowOrchestratorTypes.FilterableWorkflowExecutionProps = {},
config: FindConfig<WorkflowOrchestratorTypes.WorkflowExecutionDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<WorkflowOrchestratorTypes.WorkflowExecutionDTO[]> {
if (filters.transaction_id) {
if (Array.isArray(filters.transaction_id)) {
filters.transaction_id = {
$in: filters.transaction_id,
}
}
}
if (filters.workflow_id) {
if (Array.isArray(filters.workflow_id)) {
filters.workflow_id = {
$in: filters.workflow_id,
}
}
}
const wfExecutions = await this.workflowExecutionService_.list(
filters,
config,
@@ -74,6 +133,22 @@ export class WorkflowsModuleService
config: FindConfig<WorkflowOrchestratorTypes.WorkflowExecutionDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[WorkflowOrchestratorTypes.WorkflowExecutionDTO[], number]> {
if (filters.transaction_id) {
if (Array.isArray(filters.transaction_id)) {
filters.transaction_id = {
$in: filters.transaction_id,
}
}
}
if (filters.workflow_id) {
if (Array.isArray(filters.workflow_id)) {
filters.workflow_id = {
$in: filters.workflow_id,
}
}
}
const [wfExecutions, count] =
await this.workflowExecutionService_.listAndCount(
filters,
@@ -4,11 +4,9 @@ import {
TransactionCheckpoint,
TransactionStep,
} from "@medusajs/orchestration"
import { TransactionState } from "@medusajs/utils"
import { ModulesSdkTypes } from "@medusajs/types"
import {
WorkflowOrchestratorService,
} from "@services"
import { TransactionState } from "@medusajs/utils"
import { WorkflowOrchestratorService } from "@services"
import { Queue, Worker } from "bullmq"
import Redis from "ioredis"
@@ -21,7 +19,7 @@ enum JobType {
// eslint-disable-next-line max-len
export class RedisDistributedTransactionStorage extends DistributedTransactionStorage {
private static TTL_AFTER_COMPLETED = 60 * 15 // 15 minutes
private workflowExecutionService_: ModulesSdkTypes.InternalModuleService<any>
private workflowExecutionService_: ModulesSdkTypes.InternalModuleService<any>
private workflowOrchestratorService_: WorkflowOrchestratorService
private redisClient: Redis
@@ -34,7 +32,7 @@ export class RedisDistributedTransactionStorage extends DistributedTransactionSt
redisWorkerConnection,
redisQueueName,
}: {
workflowExecutionService: ModulesSdkTypes.InternalModuleService<any>,
workflowExecutionService: ModulesSdkTypes.InternalModuleService<any>
redisConnection: Redis
redisWorkerConnection: Redis
redisQueueName: string
@@ -161,33 +159,28 @@ export class RedisDistributedTransactionStorage extends DistributedTransactionSt
})
}
const stringifiedData = JSON.stringify(data, this.stringifyWithSymbol)
const parsedData = JSON.parse(stringifiedData)
if (!hasFinished) {
if (ttl) {
await this.redisClient.set(
key,
JSON.stringify(data, this.stringifyWithSymbol),
"EX",
ttl
)
await this.redisClient.set(key, stringifiedData, "EX", ttl)
} else {
await this.redisClient.set(
key,
JSON.stringify(data, this.stringifyWithSymbol)
)
await this.redisClient.set(key, stringifiedData)
}
}
if (hasFinished && !retentionTime) {
await this.deleteFromDb(data)
await this.deleteFromDb(parsedData)
} else {
await this.saveToDb(data)
await this.saveToDb(parsedData)
}
if (hasFinished) {
// await this.redisClient.del(key)
await this.redisClient.set(
key,
JSON.stringify(data, this.stringifyWithSymbol),
stringifiedData,
"EX",
RedisDistributedTransactionStorage.TTL_AFTER_COMPLETED
)
+1
View File
@@ -1,5 +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"
+5 -5
View File
@@ -1,4 +1,4 @@
import { BaseFilterable } from "@medusajs/types"
import { BaseFilterable, OperatorMap } from "@medusajs/types"
export interface WorkflowExecutionDTO {
id: string
@@ -14,8 +14,8 @@ export interface WorkflowExecutionDTO {
export interface FilterableWorkflowExecutionProps
extends BaseFilterable<FilterableWorkflowExecutionProps> {
id?: string[]
workflow_id?: string[]
transaction_id?: string[]
state?: any[]
id?: string | string[] | OperatorMap<string>
workflow_id?: string | string[] | OperatorMap<string>
transaction_id?: string | string[] | OperatorMap<string>
state?: string | string[] | OperatorMap<string>
}
+13 -2
View File
@@ -31,7 +31,18 @@ export type IdempotencyKeyParts = {
action: "invoke" | "compensate"
}
export interface IWorkflowsModuleService extends IModuleService {
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>,
@@ -88,7 +99,7 @@ export interface IWorkflowsModuleService extends IModuleService {
stepResponse,
options,
}: {
idempotencyKey: string | object
idempotencyKey: string | IdempotencyKeyParts
stepResponse: unknown
options?: Record<string, any>
},