@atscript/moost-wf 0.1.68 → 0.1.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +16 -11
  2. package/dist/index.d.mts +147 -136
  3. package/dist/index.mjs +189 -251
  4. package/package.json +18 -18
package/README.md CHANGED
@@ -6,18 +6,23 @@
6
6
 
7
7
  # @atscript/moost-wf
8
8
 
9
- 📚 **Documentation:** [ui.atscript.dev](https://ui.atscript.dev)
9
+ Documentation: [ui.atscript.dev](https://ui.atscript.dev)
10
10
 
11
- Server-side workflow integration for [Moost](https://github.com/moostjs/moost) — decorators, interceptors, and serialization that pair with [`@atscript/vue-wf`](../vue-wf) to drive multi-step forms from atscript-annotated `.as` types.
11
+ Server-side workflow integration for [Moost](https://github.com/moostjs/moost) — decorators, composables, and serialization that pair with [`@atscript/vue-wf`](../vue-wf) to drive multi-step forms from atscript-annotated `.as` types.
12
12
 
13
13
  Part of the [atscript-ui](https://github.com/moostjs/atscript-ui) monorepo.
14
14
 
15
15
  ## What it provides
16
16
 
17
- - Workflow decorators that wrap Moost handlers and expose them as `@atscript/vue-wf`-compatible endpoints
18
- - Interceptors that serialize / deserialize workflow state across HTTP requests
19
- - `@AsWfState` storage abstraction with a default `@atscript/db`-backed implementation
20
- - Unified `WfFinished` envelope + helpers (`finishWfWithData`, `finishWfWithRedirect`, `finishWfWithChoice`, …) for terminal-screen UX. See [Finish Screens](https://ui.atscript.dev/workflows/finish-screens).
17
+ - `useAtscriptWf(Type)` composable schema-aware `resolveInput()` / `resolveAction()` / `requireInput()` for step handlers.
18
+ - `@WfInput()` / `@WfAction()` parameter decorators sugar over `useAtscriptWf()` with the full action-vs-input policy matrix.
19
+ - `useWfActionSlot()` for low-level action context access (transport adapters writing the action; raw read+clear flows). Step handlers should prefer `useAtscriptWf(Type).resolveAction()`.
20
+ - Schema helpers: `serializeFormSchema`, `extractPassContext`, `getFormActions`.
21
+ - HTTP outlet integration: `createAsHttpOutlet`, `handleAsOutletRequest`.
22
+ - Finish-screen envelope helpers: `finishWf`, `abortWf`, `isWfFinished`, and the `WfFinished` / `WfNext` / `WfMessage` / `WfButton` / `WfActionRequest` types rendered by `<AsWfFinish>`. See [Finish Screens](https://ui.atscript.dev/workflows/finish-screens).
23
+ - Opt-in persistent state store `AsWfStore` (subpath `/store`) backed by `@atscript/db`.
24
+
25
+ The workflow engine catches `StepRetriableError` (thrown by `requireInput()`) natively — no global interceptor wiring required.
21
26
 
22
27
  ## Install
23
28
 
@@ -25,17 +30,17 @@ Part of the [atscript-ui](https://github.com/moostjs/atscript-ui) monorepo.
25
30
  pnpm add @atscript/moost-wf
26
31
  ```
27
32
 
28
- Peer requirements: `moost`, `@moostjs/event-wf`, `@atscript/core`, `@atscript/typescript`.
33
+ Peer requirements: `moost`, `@moostjs/event-wf`, `@wooksjs/event-core`, `@wooksjs/event-wf`, `@atscript/core`, `@atscript/typescript`.
29
34
 
30
35
  ## Entry points
31
36
 
32
37
  | Subpath | What it exports |
33
38
  | ----------------------------- | ---------------------------------------------------------------------------------- |
34
- | `@atscript/moost-wf` | Workflow decorators, interceptors, runtime |
35
- | `@atscript/moost-wf/plugin` | atscript build-time plugin |
36
- | `@atscript/moost-wf/store` | Generated runtime for `AsWfStateRecord` |
39
+ | `@atscript/moost-wf` | Workflow decorators, composables, outlet helpers, finish-screen envelope helpers |
40
+ | `@atscript/moost-wf/plugin` | atscript build-time plugin (registers `@wf.*` annotations) |
41
+ | `@atscript/moost-wf/store` | `AsWfStore` runtime + `AsWfStateRecord` model |
37
42
  | `@atscript/moost-wf/store.as` | Raw `.as` source for the workflow-state record — re-import if you customize fields |
38
43
 
39
44
  ## License
40
45
 
41
- MIT © Artem Maltsev
46
+ MIT &copy; Artem Maltsev
package/dist/index.d.mts CHANGED
@@ -1,101 +1,8 @@
1
+ import { InferDataType, TAtscriptAnnotatedType, TAtscriptTypeDef } from "@atscript/typescript/utils";
1
2
  import { WfOutlet, WfOutletTriggerConfig, WfOutletTriggerDeps } from "@moostjs/event-wf";
2
- import { TInterceptorDef } from "moost";
3
- import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
3
+ import { StepRetriableError } from "@wooksjs/event-wf";
4
4
 
5
- //#region src/form-input/required.d.ts
6
- /**
7
- * Thrown by @FormInput() to signal that the workflow should pause
8
- * and request form input from the client.
9
- *
10
- * Caught by {@link formInputInterceptor} and converted to an
11
- * `inputRequired` outlet response.
12
- */
13
- declare class FormInputRequired {
14
- readonly schema: unknown;
15
- readonly errors?: Record<string, string> | undefined;
16
- readonly context?: Record<string, unknown> | undefined;
17
- constructor(schema: unknown, errors?: Record<string, string> | undefined, context?: Record<string, unknown> | undefined);
18
- }
19
- //#endregion
20
- //#region src/form-input/use.d.ts
21
- /**
22
- * Composable that provides access to form data and the `requireInput()` helper
23
- * inside workflow step handlers.
24
- *
25
- * Called by the `@FormInput()` Resolve callback. Can also be used standalone
26
- * when you need to manually re-pause with errors:
27
- *
28
- * ```ts
29
- * const { requireInput } = useFormInput()
30
- * throw requireInput({ password: 'Invalid credentials' })
31
- * ```
32
- */
33
- declare function useFormInput(type?: TAtscriptAnnotatedType): {
34
- data: <T = unknown>() => T | undefined;
35
- requireInput: (errors?: Record<string, string>) => FormInputRequired;
36
- };
37
- //#endregion
38
- //#region src/form-input/decorator.d.ts
39
- /**
40
- * Parameter decorator for workflow steps that need form input.
41
- *
42
- * Combines parameter injection (via Resolve) with a method interceptor
43
- * (via Intercept) that validates input before the step handler executes.
44
- *
45
- * The injected value is `{ data(), requireInput(errors?) }`.
46
- *
47
- * @example
48
- * ```ts
49
- * @Step('login')
50
- * async login(@FormInput() form: TFormInput<LoginForm>) {
51
- * const input = form.data()
52
- * try {
53
- * await this.auth.login(input.username, input.password)
54
- * } catch (e) {
55
- * throw form.requireInput({ password: 'Invalid credentials' })
56
- * }
57
- * }
58
- * ```
59
- */
60
- declare function FormInput(): ParameterDecorator;
61
- type TFormInput<_T = unknown> = ReturnType<typeof useFormInput>;
62
- //#endregion
63
- //#region src/form-input/alt-action.decorator.d.ts
64
- /**
65
- * Parameter decorator that resolves the action name from the current
66
- * workflow event context. Returns `undefined` for normal form submits.
67
- *
68
- * @example
69
- * ```ts
70
- * @Step('mfa-verify')
71
- * async mfaVerify(
72
- * @FormInput() form: TFormInput<PincodeForm>,
73
- * @AltAction() action: string | undefined,
74
- * ) {
75
- * if (action === 'resend') {
76
- * await this.sendOtp(ctx.email)
77
- * return
78
- * }
79
- * await this.verifyCode(form.data().code)
80
- * }
81
- * ```
82
- */
83
- declare const AltAction: () => ParameterDecorator & PropertyDecorator;
84
- //#endregion
85
- //#region src/form-input/interceptor.d.ts
86
- /**
87
- * Global interceptor that catches {@link FormInputRequired} signals
88
- * thrown by step handlers (via `form.requireInput()`) and converts them
89
- * to `inputRequired` outlet responses.
90
- *
91
- * Apply globally:
92
- * ```ts
93
- * app.applyGlobalInterceptors(formInputInterceptor())
94
- * ```
95
- */
96
- declare function formInputInterceptor(): TInterceptorDef;
97
- //#endregion
98
- //#region src/form-input/serialize.d.ts
5
+ //#region src/wf-io/serialize.d.ts
99
6
  /**
100
7
  * Serialize an atscript annotated type to a JSON-transportable form schema.
101
8
  *
@@ -112,7 +19,7 @@ declare function formInputInterceptor(): TInterceptorDef;
112
19
  */
113
20
  declare function serializeFormSchema(type: TAtscriptAnnotatedType): unknown;
114
21
  //#endregion
115
- //#region src/form-input/context.d.ts
22
+ //#region src/wf-io/context.d.ts
116
23
  interface TFormActions {
117
24
  actions: string[];
118
25
  actionsWithData: string[];
@@ -124,32 +31,123 @@ interface TFormActions {
124
31
  declare function extractPassContext(type: TAtscriptAnnotatedType, wfContext: Record<string, unknown>): Record<string, unknown>;
125
32
  /**
126
33
  * Read declared action names from `@ui.form.action` and `@wf.action.withData` annotations.
127
- * Also reads legacy `@ui.altAction` as a stateless action fallback.
128
34
  * Results are cached per type identity.
129
35
  */
130
36
  declare function getFormActions(type: TAtscriptAnnotatedType): TFormActions;
131
37
  //#endregion
132
- //#region src/form-input/use-wf-action.d.ts
38
+ //#region src/wf-io/use-wf-action-slot.d.ts
133
39
  /**
134
- * Composable that reads and writes the workflow action from the event context.
40
+ * Low-level accessor for the workflow action slot in the current wf event context.
41
+ *
42
+ * Used by:
43
+ * - Transport adapters (HTTP / CLI / WS controllers) to **write** the action
44
+ * from the incoming request (`useWfActionSlot().setAction(body.action)`).
45
+ * - Composable helpers that need to **read + clear** the slot atomically
46
+ * (e.g. one-shot action consumption patterns).
47
+ *
48
+ * In step handlers, prefer `useAtscriptWf(Type).resolveAction()` — it reads
49
+ * the same slot but validates the value against the schema's
50
+ * `@ui.form.action` / `@wf.action.withData` declarations and throws
51
+ * `StepRetriableError` on unknown actions.
135
52
  *
136
- * **In the HTTP trigger** (to set the action from the request body):
53
+ * **In a transport adapter** (to set the action from the request body):
137
54
  * ```ts
138
- * const { setAction } = useWfAction()
55
+ * const { setAction } = useWfActionSlot()
139
56
  * setAction(body.action)
140
57
  * ```
141
58
  *
142
- * **In step handlers** (to read the action — prefer `@AltAction()` decorator):
59
+ * **In step handlers** (raw read — prefer `@WfAction()` / `useAtscriptWf().resolveAction()`):
143
60
  * ```ts
144
- * const { getAction } = useWfAction()
61
+ * const { getAction } = useWfActionSlot()
145
62
  * const action = getAction()
146
63
  * ```
147
64
  */
148
- declare function useWfAction(): {
65
+ declare function useWfActionSlot(): {
149
66
  getAction: () => string | undefined;
150
67
  setAction: (action: string | undefined) => void;
151
68
  };
152
69
  //#endregion
70
+ //#region src/wf-io/use-atscript-wf.d.ts
71
+ /**
72
+ * Schema-driven workflow I/O primitives for atscript types. Returned helpers
73
+ * are pure and independent — composable consumers can interleave their own
74
+ * logic between checking the action and validating the input.
75
+ *
76
+ * - `resolveInput(opts?)` validates the current step input against the type
77
+ * schema and returns it typed; throws `StepRetriableError` when input is
78
+ * missing or invalid. Does NOT look at the wf action.
79
+ * - `resolveAction()` returns the current wf action name (or `undefined`),
80
+ * throwing `StepRetriableError` when the action is unknown to the schema.
81
+ * Does NOT look at the wf input.
82
+ * - `requireInput(opts?)` builds the `StepRetriableError` carrying the form
83
+ * schema + whitelisted context. Exposed so callers (composables, the
84
+ * `@WfInput` decorator) can throw their own custom failures.
85
+ *
86
+ * Validator instances are cached per `(type, opts)` pair.
87
+ */
88
+ declare function useAtscriptWf<T extends TAtscriptTypeDef>(type: TAtscriptAnnotatedType<T>): {
89
+ resolveInput(opts?: {
90
+ partial?: "deep";
91
+ }): InferDataType<T>;
92
+ resolveAction(): string | undefined;
93
+ requireInput(opts?: {
94
+ errors?: Record<string, string>;
95
+ formMessage?: string;
96
+ }): StepRetriableError<{
97
+ outlet: "http";
98
+ payload: unknown;
99
+ context: Record<string, unknown>;
100
+ }>;
101
+ };
102
+ //#endregion
103
+ //#region src/wf-io/wf-input.decorator.d.ts
104
+ /**
105
+ * Parameter decorator that resolves to the validated typed input for the
106
+ * current workflow step. Owns the action-vs-input policy matrix on top of
107
+ * the pure `useAtscriptWf` primitives.
108
+ *
109
+ * Policy:
110
+ * - No action fired → strict full validation.
111
+ * - With-data action → input required, partial-deep validation.
112
+ * - No-data action → input must be absent; returns `undefined` only when
113
+ * `pass: true` opts the step into ignoring the no-data action.
114
+ * - Unknown action → `StepRetriableError` (propagated from `resolveAction`).
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * @Step('login')
119
+ * async login(@WfInput() input: LoginForm) {
120
+ * await this.auth.login(input.username, input.password)
121
+ * }
122
+ * ```
123
+ */
124
+ declare function WfInput(opts?: {
125
+ pass?: boolean;
126
+ }): ParameterDecorator;
127
+ //#endregion
128
+ //#region src/wf-io/wf-action.decorator.d.ts
129
+ /**
130
+ * Parameter decorator that resolves to the current workflow action name.
131
+ *
132
+ * If the parameter is annotated with an atscript type, the action is
133
+ * validated against the type's `@ui.form.action` / `@wf.action.withData`
134
+ * declarations — unknown actions throw `StepRetriableError`. When no
135
+ * annotated type is available the action is returned raw (or `undefined`).
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * @Step('mfa-verify')
140
+ * async mfaVerify(
141
+ * @WfInput() input: PincodeForm,
142
+ * @WfAction() action: string | undefined,
143
+ * ) {
144
+ * if (action === 'resend') return this.sendOtp()
145
+ * await this.verifyCode(input.code)
146
+ * }
147
+ * ```
148
+ */
149
+ declare function WfAction(): ParameterDecorator;
150
+ //#endregion
153
151
  //#region src/outlet.d.ts
154
152
  /**
155
153
  * `createHttpOutlet` pre-configured for `<AsWfForm>` consumers.
@@ -179,7 +177,7 @@ interface WfFinished<TData = unknown> {
179
177
  finished: true;
180
178
  data?: TData;
181
179
  message?: WfMessage;
182
- end?: WfFinishedEnd;
180
+ next?: WfNext;
183
181
  aborted?: boolean;
184
182
  reason?: string;
185
183
  }
@@ -187,27 +185,27 @@ interface WfMessage {
187
185
  level: "info" | "success" | "warn" | "error";
188
186
  text: string;
189
187
  }
190
- type WfFinishedEnd = {
191
- mode: "immediate";
192
- action: WfAction;
188
+ type WfNext = {
189
+ trigger: "immediate";
190
+ action: WfActionRequest;
193
191
  } | {
194
- mode: "auto";
192
+ trigger: "auto";
195
193
  timeoutMs: number;
196
- action: WfAction;
194
+ action: WfActionRequest;
197
195
  skipButton?: {
198
196
  label: string;
199
197
  behavior?: "now" | "cancel";
200
198
  };
201
199
  } | {
202
- mode: "manual";
200
+ trigger: "manual";
203
201
  primary?: WfButton;
204
202
  options?: WfButton[];
205
203
  };
206
204
  interface WfButton {
207
205
  label: string;
208
- action: WfAction;
206
+ action: WfActionRequest;
209
207
  }
210
- type WfAction = {
208
+ type WfActionRequest = {
211
209
  type: "redirect";
212
210
  target: string;
213
211
  reason?: string;
@@ -218,28 +216,41 @@ type WfAction = {
218
216
  };
219
217
  /** Type-guard for the unified envelope. */
220
218
  declare function isWfFinished(v: unknown): v is WfFinished;
221
- declare function finishWf<T>(payload: WfFinished<T>): void;
222
- declare function finishWfWithData<T>(data: T, message?: WfMessage): void;
223
- declare function finishWfWithMessage(level: WfMessage["level"], text: string): void;
224
- interface RedirectOpts {
225
- reason?: string;
226
- message?: WfMessage;
227
- /** Present → `mode: 'auto'` with countdown; absent → `mode: 'immediate'`. */
228
- autoMs?: number;
229
- /** Only honored when `autoMs` is set — adds a "skip / cancel" button. */
230
- skipLabel?: string;
231
- }
232
- declare function finishWfWithRedirect(target: string, opts?: RedirectOpts): void;
233
- interface ChoiceOpts {
234
- data?: unknown;
219
+ /**
220
+ * Options bag shared by `finishWf` and `abortWf`. Every field is optional
221
+ * pick whichever envelope properties the terminal screen needs.
222
+ */
223
+ interface FinishWfOpts<T = unknown> {
224
+ data?: T;
235
225
  message?: WfMessage;
236
- primary?: WfButton;
237
- options?: WfButton[];
226
+ next?: WfNext;
238
227
  }
239
- declare function finishWfWithChoice(opts: ChoiceOpts): void;
240
- declare function finishWfAborted(reason: string, opts?: {
241
- message?: WfMessage;
242
- end?: WfFinishedEnd;
243
- }): void;
228
+ /**
229
+ * Build a `WfFinished` envelope and hand it to wooks. All envelope
230
+ * properties are optional; pass `data`, `message`, and/or `next`:
231
+ *
232
+ * finishWf({ data: { id: 42 } });
233
+ * finishWf({ message: { level: "success", text: "Saved." } });
234
+ * finishWf({
235
+ * next: {
236
+ * trigger: "auto",
237
+ * timeoutMs: 3000,
238
+ * action: { type: "redirect", target: "/home" },
239
+ * },
240
+ * });
241
+ */
242
+ declare function finishWf<T = unknown>(opts?: FinishWfOpts<T>): void;
243
+ /**
244
+ * Build an aborted `WfFinished` envelope (`aborted: true` + `reason`) and
245
+ * hand it to wooks. The same options as `finishWf` are accepted — an
246
+ * aborted flow may still carry partial `data`, a `message`, or a `next`
247
+ * action that lets the user navigate away.
248
+ *
249
+ * abortWf("user-cancelled");
250
+ * abortWf("rate-limited", {
251
+ * message: { level: "warn", text: "Try again later." },
252
+ * });
253
+ */
254
+ declare function abortWf(reason: string, opts?: FinishWfOpts): void;
244
255
  //#endregion
245
- export { AltAction, type ChoiceOpts, FormInput, FormInputRequired, type RedirectOpts, type TFormInput, type WfAction, type WfButton, type WfFinished, type WfFinishedEnd, type WfMessage, createAsHttpOutlet, extractPassContext, finishWf, finishWfAborted, finishWfWithChoice, finishWfWithData, finishWfWithMessage, finishWfWithRedirect, formInputInterceptor, getFormActions, handleAsOutletRequest, isWfFinished, serializeFormSchema, useFormInput, useWfAction };
256
+ export { type FinishWfOpts, WfAction, type WfActionRequest, type WfButton, type WfFinished, WfInput, type WfMessage, type WfNext, abortWf, createAsHttpOutlet, extractPassContext, finishWf, getFormActions, handleAsOutletRequest, isWfFinished, serializeFormSchema, useAtscriptWf, useWfActionSlot };
package/dist/index.mjs CHANGED
@@ -1,13 +1,12 @@
1
- import { createHttpOutlet, handleWfOutletRequest, useWfState } from "@moostjs/event-wf";
2
- import { Intercept, Resolve, TInterceptorPriority, useControllerContext } from "moost";
3
1
  import { isAnnotatedType, serializeAnnotatedType } from "@atscript/typescript/utils";
4
2
  import { current, key } from "@wooksjs/event-core";
5
- import { useWfFinished } from "@wooksjs/event-wf";
6
- //#region src/form-input/context.ts
3
+ import { createHttpOutlet, handleWfOutletRequest, useWfState } from "@moostjs/event-wf";
4
+ import { StepRetriableError, useWfFinished } from "@wooksjs/event-wf";
5
+ import { Optional, Resolve } from "moost";
6
+ //#region src/wf-io/context.ts
7
7
  const WF_CONTEXT_PASS = "wf.context.pass";
8
8
  const UI_FORM_ACTION = "ui.form.action";
9
9
  const WF_ACTION_WITH_DATA = "wf.action.withData";
10
- const UI_ALT_ACTION = "ui.altAction";
11
10
  const formActionsCache = /* @__PURE__ */ new WeakMap();
12
11
  /**
13
12
  * Extract whitelisted context keys from workflow state.
@@ -22,7 +21,6 @@ function extractPassContext(type, wfContext) {
22
21
  }
23
22
  /**
24
23
  * Read declared action names from `@ui.form.action` and `@wf.action.withData` annotations.
25
- * Also reads legacy `@ui.altAction` as a stateless action fallback.
26
24
  * Results are cached per type identity.
27
25
  */
28
26
  function getFormActions(type) {
@@ -50,8 +48,6 @@ function getFormActions(type) {
50
48
  actionsWithData.push(wfAction);
51
49
  continue;
52
50
  }
53
- const altAction = fieldType.metadata.get(UI_ALT_ACTION);
54
- if (altAction) actions.push(typeof altAction === "string" ? altAction : altAction.id);
55
51
  }
56
52
  const result = {
57
53
  actions,
@@ -61,7 +57,7 @@ function getFormActions(type) {
61
57
  return result;
62
58
  }
63
59
  //#endregion
64
- //#region src/form-input/serialize.ts
60
+ //#region src/wf-io/serialize.ts
65
61
  const schemaCache = /* @__PURE__ */ new WeakMap();
66
62
  /**
67
63
  * Serialize an atscript annotated type to a JSON-transportable form schema.
@@ -88,92 +84,44 @@ function serializeFormSchema(type) {
88
84
  return schema;
89
85
  }
90
86
  //#endregion
91
- //#region src/form-input/required.ts
92
- /**
93
- * Thrown by @FormInput() to signal that the workflow should pause
94
- * and request form input from the client.
95
- *
96
- * Caught by {@link formInputInterceptor} and converted to an
97
- * `inputRequired` outlet response.
98
- */
99
- var FormInputRequired = class {
100
- constructor(schema, errors, context) {
101
- this.schema = schema;
102
- this.errors = errors;
103
- this.context = context;
104
- }
105
- };
106
- //#endregion
107
- //#region src/form-input/use.ts
87
+ //#region src/wf-io/wf-keys.ts
108
88
  /**
109
- * Composable that provides access to form data and the `requireInput()` helper
110
- * inside workflow step handlers.
111
- *
112
- * Called by the `@FormInput()` Resolve callback. Can also be used standalone
113
- * when you need to manually re-pause with errors:
89
+ * Internal event context key for the workflow action name.
114
90
  *
115
- * ```ts
116
- * const { requireInput } = useFormInput()
117
- * throw requireInput({ password: 'Invalid credentials' })
118
- * ```
91
+ * Not exported from the package barrel — HTTP triggers should call
92
+ * `useWfActionSlot().setAction(body.action)` before `wf.resume()`, and step
93
+ * handlers should read via `@WfAction()` or `useAtscriptWf()`.
119
94
  */
120
- function useFormInput(type) {
121
- const wfState = useWfState();
122
- /**
123
- * Returns the current form input data from the workflow event.
124
- */
125
- function data() {
126
- return wfState.input();
127
- }
128
- /**
129
- * Creates a FormInputRequired signal that re-pauses the workflow
130
- * with the serialized form schema, whitelisted context, and optional errors.
131
- *
132
- * Usage: `throw requireInput({ fieldName: 'Error message' })`
133
- */
134
- function requireInput(errors) {
135
- if (!type || !isAnnotatedType(type)) throw new Error("useFormInput(): no atscript type available. Ensure @FormInput() is applied on the same method parameter.");
136
- const wfContext = wfState.ctx();
137
- return new FormInputRequired(serializeFormSchema(type), errors, extractPassContext(type, wfContext));
138
- }
139
- return {
140
- data,
141
- requireInput
142
- };
143
- }
95
+ const actionKey = key("wf.action");
144
96
  //#endregion
145
- //#region src/form-input/wf-keys.ts
97
+ //#region src/wf-io/use-wf-action-slot.ts
146
98
  /**
147
- * Event context key for the workflow action name.
99
+ * Low-level accessor for the workflow action slot in the current wf event context.
148
100
  *
149
- * The HTTP trigger should set this before calling `wf.resume()`:
150
- * ```ts
151
- * import { current } from '@wooksjs/event-core'
152
- * import { actionKey } from '@atscript/moost-wf'
153
- * current().set(actionKey, body.action)
154
- * ```
101
+ * Used by:
102
+ * - Transport adapters (HTTP / CLI / WS controllers) to **write** the action
103
+ * from the incoming request (`useWfActionSlot().setAction(body.action)`).
104
+ * - Composable helpers that need to **read + clear** the slot atomically
105
+ * (e.g. one-shot action consumption patterns).
155
106
  *
156
- * `@AltAction()` and `@FormInput()` read from this key.
157
- */
158
- const actionKey = key("wf.action");
159
- //#endregion
160
- //#region src/form-input/use-wf-action.ts
161
- /**
162
- * Composable that reads and writes the workflow action from the event context.
107
+ * In step handlers, prefer `useAtscriptWf(Type).resolveAction()` it reads
108
+ * the same slot but validates the value against the schema's
109
+ * `@ui.form.action` / `@wf.action.withData` declarations and throws
110
+ * `StepRetriableError` on unknown actions.
163
111
  *
164
- * **In the HTTP trigger** (to set the action from the request body):
112
+ * **In a transport adapter** (to set the action from the request body):
165
113
  * ```ts
166
- * const { setAction } = useWfAction()
114
+ * const { setAction } = useWfActionSlot()
167
115
  * setAction(body.action)
168
116
  * ```
169
117
  *
170
- * **In step handlers** (to read the action — prefer `@AltAction()` decorator):
118
+ * **In step handlers** (raw read — prefer `@WfAction()` / `useAtscriptWf().resolveAction()`):
171
119
  * ```ts
172
- * const { getAction } = useWfAction()
120
+ * const { getAction } = useWfActionSlot()
173
121
  * const action = getAction()
174
122
  * ```
175
123
  */
176
- function useWfAction() {
124
+ function useWfActionSlot() {
177
125
  const ctx = current();
178
126
  return {
179
127
  getAction: () => ctx.has(actionKey) ? ctx.get(actionKey) : void 0,
@@ -181,154 +129,167 @@ function useWfAction() {
181
129
  };
182
130
  }
183
131
  //#endregion
184
- //#region src/form-input/decorator.ts
132
+ //#region src/wf-io/validator-cache.ts
133
+ const cache = /* @__PURE__ */ new WeakMap();
185
134
  /**
186
- * Parameter decorator for workflow steps that need form input.
187
- *
188
- * Combines parameter injection (via Resolve) with a method interceptor
189
- * (via Intercept) that validates input before the step handler executes.
135
+ * Memoize `type.validator(opts)` by `(type, opts)`. Outer WeakMap keyed by the
136
+ * atscript type identity; inner Map keyed by the two opts we care about
137
+ * (`partial`, `unknownProps`). Returns the same validator instance for the
138
+ * same `(type, opts)` pair.
139
+ */
140
+ function getCachedValidator(type, opts) {
141
+ const key = `${String(opts?.partial ?? "-")}|${String(opts?.unknownProps ?? "-")}`;
142
+ let perType = cache.get(type);
143
+ if (!perType) {
144
+ perType = /* @__PURE__ */ new Map();
145
+ cache.set(type, perType);
146
+ }
147
+ let validator = perType.get(key);
148
+ if (!validator) {
149
+ validator = type.validator(opts);
150
+ perType.set(key, validator);
151
+ }
152
+ return validator;
153
+ }
154
+ //#endregion
155
+ //#region src/wf-io/use-atscript-wf.ts
156
+ function flattenValidatorErrors(err) {
157
+ const out = {};
158
+ for (const e of err.errors) out[e.path] = e.message;
159
+ return out;
160
+ }
161
+ function isValidatorError(err) {
162
+ return err !== null && typeof err === "object" && "errors" in err && Array.isArray(err.errors);
163
+ }
164
+ function useAtscriptWf(type) {
165
+ const wfState = useWfState();
166
+ const wfAction = useWfActionSlot();
167
+ function requireInput({ errors, formMessage } = {}) {
168
+ const passContext = extractPassContext(type, wfState.ctx() ?? {});
169
+ const mergedErrors = errors ? { ...errors } : formMessage ? {} : void 0;
170
+ if (formMessage && mergedErrors) mergedErrors.__form = formMessage;
171
+ const context = mergedErrors ? {
172
+ ...passContext,
173
+ errors: mergedErrors
174
+ } : { ...passContext };
175
+ return new StepRetriableError(new Error(formMessage ?? "Input required"), void 0, {
176
+ outlet: "http",
177
+ payload: serializeFormSchema(type),
178
+ context
179
+ });
180
+ }
181
+ function validateOrThrow(input, opts) {
182
+ const validator = getCachedValidator(type, opts);
183
+ try {
184
+ validator.validate(input);
185
+ } catch (err) {
186
+ if (isValidatorError(err)) throw requireInput({ errors: flattenValidatorErrors(err) });
187
+ throw err;
188
+ }
189
+ }
190
+ function resolveInput(opts) {
191
+ const input = wfState.input();
192
+ if (input === void 0) throw requireInput();
193
+ validateOrThrow(input, opts?.partial === "deep" ? {
194
+ partial: "deep",
195
+ unknownProps: "strip"
196
+ } : { unknownProps: "strip" });
197
+ return input;
198
+ }
199
+ function resolveAction() {
200
+ const action = wfAction.getAction();
201
+ if (action === void 0) return void 0;
202
+ const { actions, actionsWithData } = getFormActions(type);
203
+ if (!actions.includes(action) && !actionsWithData.includes(action)) throw requireInput({ formMessage: `Action "${action}" is not supported` });
204
+ return action;
205
+ }
206
+ return {
207
+ resolveInput,
208
+ resolveAction,
209
+ requireInput
210
+ };
211
+ }
212
+ //#endregion
213
+ //#region src/wf-io/wf-input.decorator.ts
214
+ /**
215
+ * Parameter decorator that resolves to the validated typed input for the
216
+ * current workflow step. Owns the action-vs-input policy matrix on top of
217
+ * the pure `useAtscriptWf` primitives.
190
218
  *
191
- * The injected value is `{ data(), requireInput(errors?) }`.
219
+ * Policy:
220
+ * - No action fired → strict full validation.
221
+ * - With-data action → input required, partial-deep validation.
222
+ * - No-data action → input must be absent; returns `undefined` only when
223
+ * `pass: true` opts the step into ignoring the no-data action.
224
+ * - Unknown action → `StepRetriableError` (propagated from `resolveAction`).
192
225
  *
193
226
  * @example
194
227
  * ```ts
195
228
  * @Step('login')
196
- * async login(@FormInput() form: TFormInput<LoginForm>) {
197
- * const input = form.data()
198
- * try {
199
- * await this.auth.login(input.username, input.password)
200
- * } catch (e) {
201
- * throw form.requireInput({ password: 'Invalid credentials' })
202
- * }
229
+ * async login(@WfInput() input: LoginForm) {
230
+ * await this.auth.login(input.username, input.password)
203
231
  * }
204
232
  * ```
205
233
  */
206
- function FormInput() {
234
+ function WfInput(opts) {
207
235
  return (target, key, index) => {
208
236
  if (typeof index !== "number") return;
209
237
  Resolve((metas) => {
210
238
  const type = metas?.targetMeta?.type;
211
- return useFormInput(type);
212
- }, "FormInput")(target, key, index);
213
- Intercept(formInputCheckFn())(target, key, Object.getOwnPropertyDescriptor(target, key));
214
- };
215
- }
216
- function formInputCheckFn() {
217
- return {
218
- priority: TInterceptorPriority.INTERCEPTOR,
219
- async before(reply) {
220
- const wfState = useWfState();
221
- const input = wfState.input();
222
- const action = useWfAction().getAction();
223
- const { getMethodMeta } = useControllerContext();
224
- const paramMetas = getMethodMeta()?.params;
225
- let type;
226
- if (paramMetas) {
227
- for (const param of paramMetas) if (param?.targetMeta?.type && isAnnotatedType(param.targetMeta.type)) {
228
- type = param.targetMeta.type;
229
- break;
230
- }
231
- }
232
- if (!type) return;
233
- if (input === void 0 && !action) {
234
- reply(toInputRequired(type, wfState));
235
- return;
236
- }
239
+ if (!type || !isAnnotatedType(type)) throw new Error("@WfInput(): no atscript type available on the parameter. Annotate the parameter with an atscript-derived type.");
240
+ const wf = useAtscriptWf(type);
241
+ const pass = opts?.pass === true;
242
+ const action = wf.resolveAction();
237
243
  if (action) {
244
+ const wfInput = useWfState().input();
238
245
  const { actions, actionsWithData } = getFormActions(type);
239
- if (actionsWithData.includes(action)) {
240
- validateOrReply(type, wfState, input ?? {}, {
241
- partial: "deep",
242
- unknownProps: "strip"
243
- }, reply);
246
+ const isNoData = actions.includes(action);
247
+ const isWithData = actionsWithData.includes(action);
248
+ if (isNoData) {
249
+ if (!pass) throw wf.requireInput({ formMessage: wfInput === void 0 ? `Action "${action}" requires no data but this step expects input` : `Action "${action}" requires no data; input not allowed here` });
250
+ if (wfInput !== void 0) throw wf.requireInput({ formMessage: `Action "${action}" requires no data; input not allowed here` });
244
251
  return;
245
252
  }
246
- if (actions.includes(action)) return;
247
- reply(toInputRequired(type, wfState, { __form: `Action "${action}" is not supported` }));
248
- return;
253
+ if (isWithData) {
254
+ if (wfInput === void 0) throw wf.requireInput({ formMessage: `Action "${action}" expects input` });
255
+ return wf.resolveInput({ partial: "deep" });
256
+ }
249
257
  }
250
- validateOrReply(type, wfState, input, { unknownProps: "strip" }, reply);
251
- }
258
+ return wf.resolveInput();
259
+ }, "WfInput")(target, key, index);
260
+ if (opts?.pass === true) Optional()(target, key, index);
252
261
  };
253
262
  }
254
- function validateOrReply(type, wfState, input, validatorOpts, reply) {
255
- const validator = type.validator(validatorOpts);
256
- try {
257
- validator.validate(input);
258
- } catch (err) {
259
- if (isValidatorError(err)) {
260
- reply(toInputRequired(type, wfState, flattenErrors(err)));
261
- return;
262
- }
263
- throw err;
264
- }
265
- }
266
- function toInputRequired(type, wfState, errors) {
267
- const passedContext = extractPassContext(type, wfState.ctx());
268
- return { inputRequired: {
269
- payload: serializeFormSchema(type),
270
- transport: "http",
271
- context: errors ? {
272
- ...passedContext,
273
- errors
274
- } : { ...passedContext }
275
- } };
276
- }
277
- function flattenErrors(err) {
278
- const errors = {};
279
- for (const e of err.errors) errors[e.path] = e.message;
280
- return errors;
281
- }
282
- function isValidatorError(err) {
283
- return err !== null && typeof err === "object" && "errors" in err && Array.isArray(err.errors);
284
- }
285
263
  //#endregion
286
- //#region src/form-input/alt-action.decorator.ts
264
+ //#region src/wf-io/wf-action.decorator.ts
287
265
  /**
288
- * Parameter decorator that resolves the action name from the current
289
- * workflow event context. Returns `undefined` for normal form submits.
266
+ * Parameter decorator that resolves to the current workflow action name.
267
+ *
268
+ * If the parameter is annotated with an atscript type, the action is
269
+ * validated against the type's `@ui.form.action` / `@wf.action.withData`
270
+ * declarations — unknown actions throw `StepRetriableError`. When no
271
+ * annotated type is available the action is returned raw (or `undefined`).
290
272
  *
291
273
  * @example
292
274
  * ```ts
293
275
  * @Step('mfa-verify')
294
276
  * async mfaVerify(
295
- * @FormInput() form: TFormInput<PincodeForm>,
296
- * @AltAction() action: string | undefined,
277
+ * @WfInput() input: PincodeForm,
278
+ * @WfAction() action: string | undefined,
297
279
  * ) {
298
- * if (action === 'resend') {
299
- * await this.sendOtp(ctx.email)
300
- * return
301
- * }
302
- * await this.verifyCode(form.data().code)
280
+ * if (action === 'resend') return this.sendOtp()
281
+ * await this.verifyCode(input.code)
303
282
  * }
304
283
  * ```
305
284
  */
306
- const AltAction = () => Resolve(() => useWfAction().getAction(), "AltAction");
307
- //#endregion
308
- //#region src/form-input/interceptor.ts
309
- /**
310
- * Global interceptor that catches {@link FormInputRequired} signals
311
- * thrown by step handlers (via `form.requireInput()`) and converts them
312
- * to `inputRequired` outlet responses.
313
- *
314
- * Apply globally:
315
- * ```ts
316
- * app.applyGlobalInterceptors(formInputInterceptor())
317
- * ```
318
- */
319
- function formInputInterceptor() {
320
- return {
321
- priority: TInterceptorPriority.CATCH_ERROR,
322
- error(error, reply) {
323
- if (error instanceof FormInputRequired) reply({ inputRequired: {
324
- payload: error.schema,
325
- transport: "http",
326
- context: error.errors ? {
327
- ...error.context,
328
- errors: error.errors
329
- } : { ...error.context }
330
- } });
331
- }
285
+ function WfAction() {
286
+ return (target, key, index) => {
287
+ if (typeof index !== "number") return;
288
+ Resolve((metas) => {
289
+ const type = metas?.targetMeta?.type;
290
+ if (type && isAnnotatedType(type)) return useAtscriptWf(type).resolveAction();
291
+ return useWfActionSlot().getAction();
292
+ }, "WfAction")(target, key, index);
332
293
  };
333
294
  }
334
295
  //#endregion
@@ -419,67 +380,44 @@ function setEnvelope(envelope) {
419
380
  value: envelope
420
381
  });
421
382
  }
422
- function finishWf(payload) {
423
- setEnvelope(payload);
424
- }
425
- function finishWfWithData(data, message) {
426
- finishWf({
427
- finished: true,
428
- data,
429
- message
430
- });
431
- }
432
- function finishWfWithMessage(level, text) {
433
- finishWf({
434
- finished: true,
435
- message: {
436
- level,
437
- text
438
- }
439
- });
440
- }
441
- function finishWfWithRedirect(target, opts = {}) {
442
- const action = {
443
- type: "redirect",
444
- target,
445
- reason: opts.reason
446
- };
447
- const end = opts.autoMs ? {
448
- mode: "auto",
449
- timeoutMs: opts.autoMs,
450
- action,
451
- skipButton: opts.skipLabel ? { label: opts.skipLabel } : void 0
452
- } : {
453
- mode: "immediate",
454
- action
455
- };
456
- finishWf({
457
- finished: true,
458
- message: opts.message,
459
- end
460
- });
461
- }
462
- function finishWfWithChoice(opts) {
463
- if (!opts.primary && (!opts.options || opts.options.length === 0)) throw new Error("finishWfWithChoice() requires at least a primary button or one option.");
464
- finishWf({
383
+ /**
384
+ * Build a `WfFinished` envelope and hand it to wooks. All envelope
385
+ * properties are optional; pass `data`, `message`, and/or `next`:
386
+ *
387
+ * finishWf({ data: { id: 42 } });
388
+ * finishWf({ message: { level: "success", text: "Saved." } });
389
+ * finishWf({
390
+ * next: {
391
+ * trigger: "auto",
392
+ * timeoutMs: 3000,
393
+ * action: { type: "redirect", target: "/home" },
394
+ * },
395
+ * });
396
+ */
397
+ function finishWf(opts) {
398
+ setEnvelope({
465
399
  finished: true,
466
- data: opts.data,
467
- message: opts.message,
468
- end: {
469
- mode: "manual",
470
- primary: opts.primary,
471
- options: opts.options
472
- }
400
+ ...opts
473
401
  });
474
402
  }
475
- function finishWfAborted(reason, opts = {}) {
476
- finishWf({
403
+ /**
404
+ * Build an aborted `WfFinished` envelope (`aborted: true` + `reason`) and
405
+ * hand it to wooks. The same options as `finishWf` are accepted — an
406
+ * aborted flow may still carry partial `data`, a `message`, or a `next`
407
+ * action that lets the user navigate away.
408
+ *
409
+ * abortWf("user-cancelled");
410
+ * abortWf("rate-limited", {
411
+ * message: { level: "warn", text: "Try again later." },
412
+ * });
413
+ */
414
+ function abortWf(reason, opts) {
415
+ setEnvelope({
477
416
  finished: true,
478
417
  aborted: true,
479
418
  reason,
480
- message: opts.message,
481
- end: opts.end
419
+ ...opts
482
420
  });
483
421
  }
484
422
  //#endregion
485
- export { AltAction, FormInput, FormInputRequired, createAsHttpOutlet, extractPassContext, finishWf, finishWfAborted, finishWfWithChoice, finishWfWithData, finishWfWithMessage, finishWfWithRedirect, formInputInterceptor, getFormActions, handleAsOutletRequest, isWfFinished, serializeFormSchema, useFormInput, useWfAction };
423
+ export { WfAction, WfInput, abortWf, createAsHttpOutlet, extractPassContext, finishWf, getFormActions, handleAsOutletRequest, isWfFinished, serializeFormSchema, useAtscriptWf, useWfActionSlot };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/moost-wf",
3
- "version": "0.1.68",
3
+ "version": "0.1.70",
4
4
  "description": "Workflow form integration for moost — decorators, interceptors, and serialization driven by atscript type metadata",
5
5
  "keywords": [
6
6
  "atscript",
@@ -51,26 +51,26 @@
51
51
  "access": "public"
52
52
  },
53
53
  "devDependencies": {
54
- "@atscript/core": "^0.1.56",
55
- "@atscript/db": "^0.1.80",
56
- "@atscript/db-sqlite": "^0.1.80",
57
- "@atscript/typescript": "^0.1.56",
58
- "@moostjs/event-wf": "^0.6.10",
54
+ "@atscript/core": "^0.1.58",
55
+ "@atscript/db": "^0.1.82",
56
+ "@atscript/db-sqlite": "^0.1.82",
57
+ "@atscript/typescript": "^0.1.58",
58
+ "@moostjs/event-wf": "^0.6.14",
59
59
  "@prostojs/wf": "^0.1.1",
60
- "@wooksjs/event-core": "^0.7.12",
61
- "@wooksjs/event-wf": "^0.7.12",
62
- "moost": "^0.6.10",
63
- "unplugin-atscript": "^0.1.56",
64
- "vitest": "npm:@voidzero-dev/vite-plus-test@latest",
65
- "@atscript/ui": "^0.1.68"
60
+ "@wooksjs/event-core": "^0.7.13",
61
+ "@wooksjs/event-wf": "^0.7.13",
62
+ "moost": "^0.6.14",
63
+ "unplugin-atscript": "^0.1.58",
64
+ "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.14",
65
+ "@atscript/ui": "^0.1.70"
66
66
  },
67
67
  "peerDependencies": {
68
- "@atscript/core": "^0.1.56",
69
- "@atscript/typescript": "^0.1.56",
70
- "@moostjs/event-wf": "^0.6.10",
71
- "@wooksjs/event-core": "^0.7.12",
72
- "@wooksjs/event-wf": "^0.7.12",
73
- "moost": "^0.6.10"
68
+ "@atscript/core": "^0.1.58",
69
+ "@atscript/typescript": "^0.1.58",
70
+ "@moostjs/event-wf": "^0.6.14",
71
+ "@wooksjs/event-core": "^0.7.13",
72
+ "@wooksjs/event-wf": "^0.7.13",
73
+ "moost": "^0.6.14"
74
74
  },
75
75
  "scripts": {
76
76
  "build": "vp pack",