UNPKG

@moostjs/event-wf

Version:
280 lines (276 loc) 10.2 kB
import { WooksWf, createEmailOutlet, createHttpOutlet, createOutletHandler, createWfApp, handleWfOutletRequest, handleWfOutletRequest as handleWfOutletRequest$1, swapStrategy, useWfFinished, useWfOutlet, useWfState, useWfState as useWfState$1, useWfStrategy, wfKind } from "@wooksjs/event-wf"; import { Resolve, defineMoostEventHandler, getMoostInfact, getMoostMate, setControllerContext, useScopeId } from "moost"; import { StepRetriableError } from "@prostojs/wf"; import { EncapsulatedStateStrategy, HandleStateStrategy, WfStateStoreMemory, outlet, outletEmail, outletHttp } from "@prostojs/wf/outlets"; //#region packages/event-wf/src/meta-types.ts function getWfMate() { return getMoostMate(); } //#endregion //#region packages/event-wf/src/decorators/wf.decorator.ts /** * Registers a method as a workflow step handler. * * @param path - Step identifier used in workflow schemas. Defaults to the method name. * * @example * ```ts * @Step('validate') * validateInput(@WorkflowParam('input') data: unknown) { * return schema.parse(data) * } * ``` */ function Step(path) { return getWfMate().decorate("handlers", { path, type: "WF_STEP" }, true); } /** * Registers a method as a workflow flow entry point. * Use with `@WorkflowSchema` to define the step sequence. * * @param path - Workflow identifier. Defaults to the method name. * * @example * ```ts * @Workflow('onboarding') * @WorkflowSchema([{ step: 'validate' }, { step: 'save' }]) * onboarding() {} * ``` */ function Workflow(path) { return getWfMate().decorate("handlers", { path, type: "WF_FLOW" }, true); } /** * Attaches a workflow schema (step sequence) to a `@Workflow` method. * * @param schema - Array of step definitions composing the workflow. */ function WorkflowSchema(schema) { return getWfMate().decorate("wfSchema", schema); } /** * Sets TTL (in ms) for the workflow state when this step pauses. * The adapter wraps the step handler to set `expires` on the outlet signal, * which is then passed to `strategy.persist(state, { ttl })`. * * @param ttlMs - Time-to-live in milliseconds for the paused state token. * * @example * ```ts * @Step('send-invite') * @StepTTL(60 * 60 * 1000) // 1 hour * async sendInvite(@WorkflowParam('context') ctx: any) { * return outletEmail(ctx.email, 'invite') * } * ``` */ function StepTTL(ttlMs) { return getWfMate().decorate("wfStepTTL", ttlMs); } /** * Parameter decorator that resolves a workflow context value into a step handler argument. * * @param name - The workflow value to resolve: * - `'state'` — Full workflow state object * - `'resume'` — Whether the workflow is being resumed * - `'indexes'` — Current step indexes * - `'schemaId'` — Active workflow schema identifier * - `'stepId'` — Current step identifier * - `'context'` — Workflow context data * - `'input'` — Input passed to `start()` or `resume()` * * @example * ```ts * @Step('process') * handle(@WorkflowParam('input') data: string, @WorkflowParam('context') ctx: MyCtx) { } * ``` */ const WorkflowParam = (name) => { switch (name) { case "state": return Resolve(() => useWfState$1(), "Workflow-State"); case "resume": return Resolve(() => useWfState$1().resume, "Workflow-Resume"); case "indexes": return Resolve(() => useWfState$1().indexes(), "Workflow-Indexes"); case "schemaId": return Resolve(() => useWfState$1().schemaId, "Workflow-SchemaId"); case "stepId": return Resolve(() => useWfState$1().stepId(), "Workflow-StepId"); case "context": return Resolve(() => useWfState$1().ctx(), "Workflow-Context"); case "input": return Resolve(() => useWfState$1().input(), "Workflow-Input"); default: throw new Error(`Unknown WorkflowParam: ${name}`); } }; //#endregion //#region packages/event-wf/src/event-wf.ts function _define_property(obj, key, value) { if (key in obj) Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true }); else obj[key] = value; return obj; } const LOGGER_TITLE = "moost-wf"; /** * Moost adapter for workflow events. Wraps `@wooksjs/event-wf` to register * `@Step` and `@Workflow` handlers with full Moost DI and interceptor support. * * @template T - Workflow context type. * @template IR - Intermediate result type. * * @example * ```ts * const wf = new MoostWf() * app.adapter(wf).controllers(MyWorkflows).init() * const result = await wf.start('my-flow', {}, { input }) * ``` */ var MoostWf = class { async onNotFound() { return defineMoostEventHandler({ loggerTitle: LOGGER_TITLE, getIterceptorHandler: () => this.moost?.getGlobalInterceptorHandler(), getControllerInstance: () => this.moost, callControllerMethod: () => /* @__PURE__ */ new Error("WF Handler not found"), logErrors: this.debug, targetPath: "", handlerType: "__SYSTEM__" })(); } onInit(moost) { this.moost = moost; this.toInit.forEach((fn) => { fn(); }); } /** Returns the underlying `WooksWf` application instance. */ getWfApp() { return this.wfApp; } /** Attaches a spy function that observes workflow step executions. */ attachSpy(fn) { return this.wfApp.attachSpy(fn); } /** Detaches a previously attached workflow spy. */ detachSpy(fn) { this.wfApp.detachSpy(fn); } /** * Handles an outlet trigger request within an HTTP handler. * * Reads `wfid` (workflow ID) and `wfs` (state token) from the HTTP request, * starts or resumes the workflow, and dispatches pauses to registered outlets. * * Must be called from within an HTTP event context (e.g. a `@Post` handler) * so that wooks HTTP composables are available. * * @param config - Outlet trigger configuration (allowed workflows, state strategy, outlets, etc.) * @returns The outlet result (form payload + token), finished response, or error. */ handleOutlet(config) { return handleWfOutletRequest$1(config, { start: (schemaId, context, opts) => this.start(schemaId, context, opts), resume: (state, opts) => this.resume(state, opts) }); } /** * Starts a new workflow execution. * * @param schemaId - Identifier of the registered workflow schema. * @param initialContext - Initial context data for the workflow. * @param opts - Optional run options: `input` for the first step, `eventContext` * to link this run to a parent event scope (e.g. an HTTP request), and * `strategy.name` to pick the initial state strategy. Passing a bare * `input` value (legacy positional form) is no longer supported — wrap it * in `{ input }`. */ start(schemaId, initialContext, opts) { return this.wfApp.start(schemaId, initialContext, { ...opts, cleanup: () => { getMoostInfact().unregisterScope(useScopeId()); } }); } /** * Resumes a previously paused workflow from a saved state. * * @param state - Saved workflow state containing schema, context, and step indexes. * @param opts - Optional run options: `input` for the resumed step, `eventContext` * for parent-chain composable access, and `strategy.name` for the strategy * that loaded this state. Legacy positional `input` is no longer supported. */ resume(state, opts) { return this.wfApp.resume(state, { ...opts, cleanup: () => { getMoostInfact().unregisterScope(useScopeId()); } }); } bindHandler(opts) { let fn; for (const handler of opts.handlers) { if (!["WF_STEP", "WF_FLOW"].includes(handler.type)) continue; const schemaId = handler.path; const path = typeof schemaId === "string" ? schemaId : typeof opts.method === "string" ? opts.method : ""; const targetPath = `${`${opts.prefix || ""}/${path}`.replaceAll(/\/\/+/g, "/")}${path.endsWith("//") ? "/" : ""}`; fn = defineMoostEventHandler({ loggerTitle: LOGGER_TITLE, getIterceptorHandler: opts.getIterceptorHandler, getControllerInstance: opts.getInstance, controllerMethod: opts.method, controllerName: opts.controllerName, resolveArgs: opts.resolveArgs, manualUnscope: true, targetPath, controllerPrefix: opts.prefix, handlerType: handler.type }); if (handler.type === "WF_STEP") { const stepTTL = getWfMate().read(opts.fakeInstance, opts.method)?.wfStepTTL; let stepHandler = fn; if (stepTTL !== void 0) { const wrapped = stepHandler; stepHandler = async () => { try { const result = await wrapped(); if (result && typeof result === "object" && "inputRequired" in result) return { ...result, expires: Date.now() + stepTTL }; return result; } catch (error) { if (error && typeof error === "object" && "inputRequired" in error && error.expires === void 0) error.expires = Date.now() + stepTTL; throw error; } }; } this.wfApp.step(targetPath, { handler: stepHandler }); opts.logHandler(`(${handler.type})${targetPath}`); } else { const mate = getWfMate(); let wfSchema = mate.read(opts.fakeInstance, opts.method)?.wfSchema; if (!wfSchema) wfSchema = mate.read(opts.fakeInstance)?.wfSchema; const _fn = async () => { setControllerContext(this.moost, "bindHandler", targetPath, { prefix: opts.prefix }); return fn(); }; this.toInit.push(() => { this.wfApp.flow(targetPath, wfSchema || [], opts.prefix === "/" ? "" : opts.prefix, _fn); opts.logHandler(`(${handler.type})${targetPath}`); }); } } } constructor(opts, debug) { _define_property(this, "opts", void 0); _define_property(this, "debug", void 0); _define_property(this, "name", void 0); _define_property(this, "wfApp", void 0); _define_property(this, "moost", void 0); _define_property(this, "toInit", void 0); this.opts = opts; this.debug = debug; this.name = "workflow"; this.toInit = []; if (opts && opts instanceof WooksWf) this.wfApp = opts; else if (opts) this.wfApp = createWfApp(opts); else this.wfApp = createWfApp(); } }; //#endregion export { EncapsulatedStateStrategy, HandleStateStrategy, MoostWf, Step, StepRetriableError, StepTTL, WfStateStoreMemory, Workflow, WorkflowParam, WorkflowSchema, createEmailOutlet, createHttpOutlet, createOutletHandler, handleWfOutletRequest, outlet, outletEmail, outletHttp, swapStrategy, useWfFinished, useWfOutlet, useWfState, useWfStrategy, wfKind };