@atscript/moost-wf 0.1.69 → 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.
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 (`finishWf(opts)`, `abortWf(reason, opts)`) 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[];
@@ -128,27 +35,119 @@ declare function extractPassContext(type: TAtscriptAnnotatedType, wfContext: Rec
128
35
  */
129
36
  declare function getFormActions(type: TAtscriptAnnotatedType): TFormActions;
130
37
  //#endregion
131
- //#region src/form-input/use-wf-action.d.ts
38
+ //#region src/wf-io/use-wf-action-slot.d.ts
132
39
  /**
133
- * 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.
134
41
  *
135
- * **In the HTTP trigger** (to set the action from the request body):
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.
52
+ *
53
+ * **In a transport adapter** (to set the action from the request body):
136
54
  * ```ts
137
- * const { setAction } = useWfAction()
55
+ * const { setAction } = useWfActionSlot()
138
56
  * setAction(body.action)
139
57
  * ```
140
58
  *
141
- * **In step handlers** (to read the action — prefer `@AltAction()` decorator):
59
+ * **In step handlers** (raw read — prefer `@WfAction()` / `useAtscriptWf().resolveAction()`):
142
60
  * ```ts
143
- * const { getAction } = useWfAction()
61
+ * const { getAction } = useWfActionSlot()
144
62
  * const action = getAction()
145
63
  * ```
146
64
  */
147
- declare function useWfAction(): {
65
+ declare function useWfActionSlot(): {
148
66
  getAction: () => string | undefined;
149
67
  setAction: (action: string | undefined) => void;
150
68
  };
151
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
152
151
  //#region src/outlet.d.ts
153
152
  /**
154
153
  * `createHttpOutlet` pre-configured for `<AsWfForm>` consumers.
@@ -188,11 +187,11 @@ interface WfMessage {
188
187
  }
189
188
  type WfNext = {
190
189
  trigger: "immediate";
191
- action: WfAction;
190
+ action: WfActionRequest;
192
191
  } | {
193
192
  trigger: "auto";
194
193
  timeoutMs: number;
195
- action: WfAction;
194
+ action: WfActionRequest;
196
195
  skipButton?: {
197
196
  label: string;
198
197
  behavior?: "now" | "cancel";
@@ -204,9 +203,9 @@ type WfNext = {
204
203
  };
205
204
  interface WfButton {
206
205
  label: string;
207
- action: WfAction;
206
+ action: WfActionRequest;
208
207
  }
209
- type WfAction = {
208
+ type WfActionRequest = {
210
209
  type: "redirect";
211
210
  target: string;
212
211
  reason?: string;
@@ -254,4 +253,4 @@ declare function finishWf<T = unknown>(opts?: FinishWfOpts<T>): void;
254
253
  */
255
254
  declare function abortWf(reason: string, opts?: FinishWfOpts): void;
256
255
  //#endregion
257
- export { AltAction, type FinishWfOpts, FormInput, FormInputRequired, type TFormInput, type WfAction, type WfButton, type WfFinished, type WfMessage, type WfNext, abortWf, createAsHttpOutlet, extractPassContext, finishWf, 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,9 +1,9 @@
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";
@@ -57,7 +57,7 @@ function getFormActions(type) {
57
57
  return result;
58
58
  }
59
59
  //#endregion
60
- //#region src/form-input/serialize.ts
60
+ //#region src/wf-io/serialize.ts
61
61
  const schemaCache = /* @__PURE__ */ new WeakMap();
62
62
  /**
63
63
  * Serialize an atscript annotated type to a JSON-transportable form schema.
@@ -84,92 +84,44 @@ function serializeFormSchema(type) {
84
84
  return schema;
85
85
  }
86
86
  //#endregion
87
- //#region src/form-input/required.ts
88
- /**
89
- * Thrown by @FormInput() to signal that the workflow should pause
90
- * and request form input from the client.
91
- *
92
- * Caught by {@link formInputInterceptor} and converted to an
93
- * `inputRequired` outlet response.
94
- */
95
- var FormInputRequired = class {
96
- constructor(schema, errors, context) {
97
- this.schema = schema;
98
- this.errors = errors;
99
- this.context = context;
100
- }
101
- };
102
- //#endregion
103
- //#region src/form-input/use.ts
87
+ //#region src/wf-io/wf-keys.ts
104
88
  /**
105
- * Composable that provides access to form data and the `requireInput()` helper
106
- * inside workflow step handlers.
107
- *
108
- * Called by the `@FormInput()` Resolve callback. Can also be used standalone
109
- * when you need to manually re-pause with errors:
89
+ * Internal event context key for the workflow action name.
110
90
  *
111
- * ```ts
112
- * const { requireInput } = useFormInput()
113
- * throw requireInput({ password: 'Invalid credentials' })
114
- * ```
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()`.
115
94
  */
116
- function useFormInput(type) {
117
- const wfState = useWfState();
118
- /**
119
- * Returns the current form input data from the workflow event.
120
- */
121
- function data() {
122
- return wfState.input();
123
- }
124
- /**
125
- * Creates a FormInputRequired signal that re-pauses the workflow
126
- * with the serialized form schema, whitelisted context, and optional errors.
127
- *
128
- * Usage: `throw requireInput({ fieldName: 'Error message' })`
129
- */
130
- function requireInput(errors) {
131
- if (!type || !isAnnotatedType(type)) throw new Error("useFormInput(): no atscript type available. Ensure @FormInput() is applied on the same method parameter.");
132
- const wfContext = wfState.ctx();
133
- return new FormInputRequired(serializeFormSchema(type), errors, extractPassContext(type, wfContext));
134
- }
135
- return {
136
- data,
137
- requireInput
138
- };
139
- }
95
+ const actionKey = key("wf.action");
140
96
  //#endregion
141
- //#region src/form-input/wf-keys.ts
97
+ //#region src/wf-io/use-wf-action-slot.ts
142
98
  /**
143
- * Event context key for the workflow action name.
99
+ * Low-level accessor for the workflow action slot in the current wf event context.
144
100
  *
145
- * The HTTP trigger should set this before calling `wf.resume()`:
146
- * ```ts
147
- * import { current } from '@wooksjs/event-core'
148
- * import { actionKey } from '@atscript/moost-wf'
149
- * current().set(actionKey, body.action)
150
- * ```
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).
151
106
  *
152
- * `@AltAction()` and `@FormInput()` read from this key.
153
- */
154
- const actionKey = key("wf.action");
155
- //#endregion
156
- //#region src/form-input/use-wf-action.ts
157
- /**
158
- * 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.
159
111
  *
160
- * **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):
161
113
  * ```ts
162
- * const { setAction } = useWfAction()
114
+ * const { setAction } = useWfActionSlot()
163
115
  * setAction(body.action)
164
116
  * ```
165
117
  *
166
- * **In step handlers** (to read the action — prefer `@AltAction()` decorator):
118
+ * **In step handlers** (raw read — prefer `@WfAction()` / `useAtscriptWf().resolveAction()`):
167
119
  * ```ts
168
- * const { getAction } = useWfAction()
120
+ * const { getAction } = useWfActionSlot()
169
121
  * const action = getAction()
170
122
  * ```
171
123
  */
172
- function useWfAction() {
124
+ function useWfActionSlot() {
173
125
  const ctx = current();
174
126
  return {
175
127
  getAction: () => ctx.has(actionKey) ? ctx.get(actionKey) : void 0,
@@ -177,154 +129,167 @@ function useWfAction() {
177
129
  };
178
130
  }
179
131
  //#endregion
180
- //#region src/form-input/decorator.ts
132
+ //#region src/wf-io/validator-cache.ts
133
+ const cache = /* @__PURE__ */ new WeakMap();
181
134
  /**
182
- * Parameter decorator for workflow steps that need form input.
183
- *
184
- * Combines parameter injection (via Resolve) with a method interceptor
185
- * (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.
186
218
  *
187
- * 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`).
188
225
  *
189
226
  * @example
190
227
  * ```ts
191
228
  * @Step('login')
192
- * async login(@FormInput() form: TFormInput<LoginForm>) {
193
- * const input = form.data()
194
- * try {
195
- * await this.auth.login(input.username, input.password)
196
- * } catch (e) {
197
- * throw form.requireInput({ password: 'Invalid credentials' })
198
- * }
229
+ * async login(@WfInput() input: LoginForm) {
230
+ * await this.auth.login(input.username, input.password)
199
231
  * }
200
232
  * ```
201
233
  */
202
- function FormInput() {
234
+ function WfInput(opts) {
203
235
  return (target, key, index) => {
204
236
  if (typeof index !== "number") return;
205
237
  Resolve((metas) => {
206
238
  const type = metas?.targetMeta?.type;
207
- return useFormInput(type);
208
- }, "FormInput")(target, key, index);
209
- Intercept(formInputCheckFn())(target, key, Object.getOwnPropertyDescriptor(target, key));
210
- };
211
- }
212
- function formInputCheckFn() {
213
- return {
214
- priority: TInterceptorPriority.INTERCEPTOR,
215
- async before(reply) {
216
- const wfState = useWfState();
217
- const input = wfState.input();
218
- const action = useWfAction().getAction();
219
- const { getMethodMeta } = useControllerContext();
220
- const paramMetas = getMethodMeta()?.params;
221
- let type;
222
- if (paramMetas) {
223
- for (const param of paramMetas) if (param?.targetMeta?.type && isAnnotatedType(param.targetMeta.type)) {
224
- type = param.targetMeta.type;
225
- break;
226
- }
227
- }
228
- if (!type) return;
229
- if (input === void 0 && !action) {
230
- reply(toInputRequired(type, wfState));
231
- return;
232
- }
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();
233
243
  if (action) {
244
+ const wfInput = useWfState().input();
234
245
  const { actions, actionsWithData } = getFormActions(type);
235
- if (actionsWithData.includes(action)) {
236
- validateOrReply(type, wfState, input ?? {}, {
237
- partial: "deep",
238
- unknownProps: "strip"
239
- }, 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` });
240
251
  return;
241
252
  }
242
- if (actions.includes(action)) return;
243
- reply(toInputRequired(type, wfState, { __form: `Action "${action}" is not supported` }));
244
- return;
253
+ if (isWithData) {
254
+ if (wfInput === void 0) throw wf.requireInput({ formMessage: `Action "${action}" expects input` });
255
+ return wf.resolveInput({ partial: "deep" });
256
+ }
245
257
  }
246
- validateOrReply(type, wfState, input, { unknownProps: "strip" }, reply);
247
- }
258
+ return wf.resolveInput();
259
+ }, "WfInput")(target, key, index);
260
+ if (opts?.pass === true) Optional()(target, key, index);
248
261
  };
249
262
  }
250
- function validateOrReply(type, wfState, input, validatorOpts, reply) {
251
- const validator = type.validator(validatorOpts);
252
- try {
253
- validator.validate(input);
254
- } catch (err) {
255
- if (isValidatorError(err)) {
256
- reply(toInputRequired(type, wfState, flattenErrors(err)));
257
- return;
258
- }
259
- throw err;
260
- }
261
- }
262
- function toInputRequired(type, wfState, errors) {
263
- const passedContext = extractPassContext(type, wfState.ctx());
264
- return { inputRequired: {
265
- payload: serializeFormSchema(type),
266
- transport: "http",
267
- context: errors ? {
268
- ...passedContext,
269
- errors
270
- } : { ...passedContext }
271
- } };
272
- }
273
- function flattenErrors(err) {
274
- const errors = {};
275
- for (const e of err.errors) errors[e.path] = e.message;
276
- return errors;
277
- }
278
- function isValidatorError(err) {
279
- return err !== null && typeof err === "object" && "errors" in err && Array.isArray(err.errors);
280
- }
281
263
  //#endregion
282
- //#region src/form-input/alt-action.decorator.ts
264
+ //#region src/wf-io/wf-action.decorator.ts
283
265
  /**
284
- * Parameter decorator that resolves the action name from the current
285
- * 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`).
286
272
  *
287
273
  * @example
288
274
  * ```ts
289
275
  * @Step('mfa-verify')
290
276
  * async mfaVerify(
291
- * @FormInput() form: TFormInput<PincodeForm>,
292
- * @AltAction() action: string | undefined,
277
+ * @WfInput() input: PincodeForm,
278
+ * @WfAction() action: string | undefined,
293
279
  * ) {
294
- * if (action === 'resend') {
295
- * await this.sendOtp(ctx.email)
296
- * return
297
- * }
298
- * await this.verifyCode(form.data().code)
280
+ * if (action === 'resend') return this.sendOtp()
281
+ * await this.verifyCode(input.code)
299
282
  * }
300
283
  * ```
301
284
  */
302
- const AltAction = () => Resolve(() => useWfAction().getAction(), "AltAction");
303
- //#endregion
304
- //#region src/form-input/interceptor.ts
305
- /**
306
- * Global interceptor that catches {@link FormInputRequired} signals
307
- * thrown by step handlers (via `form.requireInput()`) and converts them
308
- * to `inputRequired` outlet responses.
309
- *
310
- * Apply globally:
311
- * ```ts
312
- * app.applyGlobalInterceptors(formInputInterceptor())
313
- * ```
314
- */
315
- function formInputInterceptor() {
316
- return {
317
- priority: TInterceptorPriority.CATCH_ERROR,
318
- error(error, reply) {
319
- if (error instanceof FormInputRequired) reply({ inputRequired: {
320
- payload: error.schema,
321
- transport: "http",
322
- context: error.errors ? {
323
- ...error.context,
324
- errors: error.errors
325
- } : { ...error.context }
326
- } });
327
- }
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);
328
293
  };
329
294
  }
330
295
  //#endregion
@@ -455,4 +420,4 @@ function abortWf(reason, opts) {
455
420
  });
456
421
  }
457
422
  //#endregion
458
- export { AltAction, FormInput, FormInputRequired, abortWf, createAsHttpOutlet, extractPassContext, finishWf, 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.69",
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",
@@ -62,7 +62,7 @@
62
62
  "moost": "^0.6.14",
63
63
  "unplugin-atscript": "^0.1.58",
64
64
  "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.14",
65
- "@atscript/ui": "^0.1.69"
65
+ "@atscript/ui": "^0.1.70"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "@atscript/core": "^0.1.58",