@moostjs/event-wf
Version:
280 lines (276 loc) • 10.2 kB
JavaScript
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(`[36m(${handler.type})[32m${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(`[36m(${handler.type})[32m${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 };