@atscript/moost-wf 0.1.69 → 0.1.71

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,26 +35,89 @@ 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-atscript-wf.d.ts
39
+ /**
40
+ * Schema-driven workflow I/O primitives for atscript types. Returned helpers
41
+ * are pure and independent — composable consumers can interleave their own
42
+ * logic between checking the action and validating the input.
43
+ *
44
+ * - `resolveInput(opts?)` validates the current step input against the type
45
+ * schema and returns it typed; throws `StepRetriableError` when input is
46
+ * missing or invalid. Does NOT look at the wf action.
47
+ * - `resolveAction()` returns the current wf action name (or `undefined`),
48
+ * throwing `StepRetriableError` when the action is unknown to the schema.
49
+ * Does NOT look at the wf input.
50
+ * - `requireInput(opts?)` builds the `StepRetriableError` carrying the form
51
+ * schema + whitelisted context. Exposed so callers (composables, the
52
+ * `@WfInput` decorator) can throw their own custom failures.
53
+ *
54
+ * Validator instances are cached per `(type, opts)` pair.
55
+ */
56
+ declare function useAtscriptWf<T extends TAtscriptTypeDef>(type: TAtscriptAnnotatedType<T>): {
57
+ resolveInput(opts?: {
58
+ partial?: "deep";
59
+ }): InferDataType<T>;
60
+ resolveAction(): string | undefined;
61
+ requireInput(opts?: {
62
+ errors?: Record<string, string>;
63
+ formMessage?: string;
64
+ }): StepRetriableError<{
65
+ outlet: "http";
66
+ payload: unknown;
67
+ context: Record<string, unknown>;
68
+ }>;
69
+ };
70
+ //#endregion
71
+ //#region src/wf-io/wf-input.decorator.d.ts
132
72
  /**
133
- * Composable that reads and writes the workflow action from the event context.
73
+ * Parameter decorator that resolves to the validated typed input for the
74
+ * current workflow step. Owns the action-vs-input policy matrix on top of
75
+ * the pure `useAtscriptWf` primitives.
134
76
  *
135
- * **In the HTTP trigger** (to set the action from the request body):
77
+ * Policy:
78
+ * - No action fired → strict full validation.
79
+ * - With-data action → input required, partial-deep validation.
80
+ * - No-data action → input must be absent; returns `undefined` only when
81
+ * `pass: true` opts the step into ignoring the no-data action.
82
+ * - Unknown action → `StepRetriableError` (propagated from `resolveAction`).
83
+ *
84
+ * @example
136
85
  * ```ts
137
- * const { setAction } = useWfAction()
138
- * setAction(body.action)
86
+ * @Step('login')
87
+ * async login(@WfInput() input: LoginForm) {
88
+ * await this.auth.login(input.username, input.password)
89
+ * }
139
90
  * ```
91
+ */
92
+ declare function WfInput(opts?: {
93
+ pass?: boolean;
94
+ }): ParameterDecorator;
95
+ //#endregion
96
+ //#region src/wf-io/wf-action.decorator.d.ts
97
+ /**
98
+ * Parameter decorator — sugar for `useAtscriptWf(Type).resolveAction()`.
99
+ *
100
+ * Resolves to the current workflow action name from the input envelope, or
101
+ * `undefined` when no action was submitted.
140
102
  *
141
- * **In step handlers** (to read the action prefer `@AltAction()` decorator):
103
+ * The form type is **required**: the decorator validates the action against
104
+ * the form's declared `@ui.form.action` / `@wf.action.withData` whitelist and
105
+ * throws `StepRetriableError` for any unknown action — the step body never
106
+ * sees actions that aren't part of the form's contract.
107
+ *
108
+ * @example
142
109
  * ```ts
143
- * const { getAction } = useWfAction()
144
- * const action = getAction()
110
+ * @Step('mfa-verify')
111
+ * async mfaVerify(
112
+ * @WfInput() input: PincodeForm,
113
+ * @WfAction(PincodeForm) action: string | undefined,
114
+ * ) {
115
+ * if (action === 'resend') return this.sendOtp()
116
+ * await this.verifyCode(input.code)
117
+ * }
145
118
  * ```
146
119
  */
147
- declare function useWfAction(): {
148
- getAction: () => string | undefined;
149
- setAction: (action: string | undefined) => void;
150
- };
120
+ declare function WfAction<T extends TAtscriptTypeDef>(type: TAtscriptAnnotatedType<T>): ParameterDecorator;
151
121
  //#endregion
152
122
  //#region src/outlet.d.ts
153
123
  /**
@@ -188,11 +158,11 @@ interface WfMessage {
188
158
  }
189
159
  type WfNext = {
190
160
  trigger: "immediate";
191
- action: WfAction;
161
+ action: WfActionRequest;
192
162
  } | {
193
163
  trigger: "auto";
194
164
  timeoutMs: number;
195
- action: WfAction;
165
+ action: WfActionRequest;
196
166
  skipButton?: {
197
167
  label: string;
198
168
  behavior?: "now" | "cancel";
@@ -204,9 +174,9 @@ type WfNext = {
204
174
  };
205
175
  interface WfButton {
206
176
  label: string;
207
- action: WfAction;
177
+ action: WfActionRequest;
208
178
  }
209
- type WfAction = {
179
+ type WfActionRequest = {
210
180
  type: "redirect";
211
181
  target: string;
212
182
  reason?: string;
@@ -254,4 +224,4 @@ declare function finishWf<T = unknown>(opts?: FinishWfOpts<T>): void;
254
224
  */
255
225
  declare function abortWf(reason: string, opts?: FinishWfOpts): void;
256
226
  //#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 };
227
+ export { type FinishWfOpts, WfAction, type WfActionRequest, type WfButton, type WfFinished, WfInput, type WfMessage, type WfNext, abortWf, createAsHttpOutlet, extractPassContext, finishWf, getFormActions, handleAsOutletRequest, isWfFinished, serializeFormSchema, useAtscriptWf };
package/dist/index.mjs CHANGED
@@ -1,9 +1,8 @@
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
- import { current, key } from "@wooksjs/event-core";
5
- import { useWfFinished } from "@wooksjs/event-wf";
6
- //#region src/form-input/context.ts
2
+ import { createHttpOutlet, handleWfOutletRequest, useWfState } from "@moostjs/event-wf";
3
+ import { StepRetriableError, useWfFinished } from "@wooksjs/event-wf";
4
+ import { Optional, Resolve } from "moost";
5
+ //#region src/wf-io/context.ts
7
6
  const WF_CONTEXT_PASS = "wf.context.pass";
8
7
  const UI_FORM_ACTION = "ui.form.action";
9
8
  const WF_ACTION_WITH_DATA = "wf.action.withData";
@@ -57,7 +56,7 @@ function getFormActions(type) {
57
56
  return result;
58
57
  }
59
58
  //#endregion
60
- //#region src/form-input/serialize.ts
59
+ //#region src/wf-io/serialize.ts
61
60
  const schemaCache = /* @__PURE__ */ new WeakMap();
62
61
  /**
63
62
  * Serialize an atscript annotated type to a JSON-transportable form schema.
@@ -84,247 +83,165 @@ function serializeFormSchema(type) {
84
83
  return schema;
85
84
  }
86
85
  //#endregion
87
- //#region src/form-input/required.ts
86
+ //#region src/wf-io/validator-cache.ts
87
+ const cache = /* @__PURE__ */ new WeakMap();
88
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.
89
+ * Memoize `type.validator(opts)` by `(type, opts)`. Outer WeakMap keyed by the
90
+ * atscript type identity; inner Map keyed by the two opts we care about
91
+ * (`partial`, `unknownProps`). Returns the same validator instance for the
92
+ * same `(type, opts)` pair.
94
93
  */
95
- var FormInputRequired = class {
96
- constructor(schema, errors, context) {
97
- this.schema = schema;
98
- this.errors = errors;
99
- this.context = context;
94
+ function getCachedValidator(type, opts) {
95
+ const key = `${String(opts?.partial ?? "-")}|${String(opts?.unknownProps ?? "-")}`;
96
+ let perType = cache.get(type);
97
+ if (!perType) {
98
+ perType = /* @__PURE__ */ new Map();
99
+ cache.set(type, perType);
100
100
  }
101
- };
101
+ let validator = perType.get(key);
102
+ if (!validator) {
103
+ validator = type.validator(opts);
104
+ perType.set(key, validator);
105
+ }
106
+ return validator;
107
+ }
102
108
  //#endregion
103
- //#region src/form-input/use.ts
104
- /**
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:
110
- *
111
- * ```ts
112
- * const { requireInput } = useFormInput()
113
- * throw requireInput({ password: 'Invalid credentials' })
114
- * ```
115
- */
116
- function useFormInput(type) {
109
+ //#region src/wf-io/use-atscript-wf.ts
110
+ function flattenValidatorErrors(err) {
111
+ const out = {};
112
+ for (const e of err.errors) out[e.path] = e.message;
113
+ return out;
114
+ }
115
+ function isValidatorError(err) {
116
+ return err !== null && typeof err === "object" && "errors" in err && Array.isArray(err.errors);
117
+ }
118
+ function useAtscriptWf(type) {
117
119
  const wfState = useWfState();
118
- /**
119
- * Returns the current form input data from the workflow event.
120
- */
121
- function data() {
122
- return wfState.input();
120
+ function requireInput({ errors, formMessage } = {}) {
121
+ const passContext = extractPassContext(type, wfState.ctx() ?? {});
122
+ const mergedErrors = errors ? { ...errors } : formMessage ? {} : void 0;
123
+ if (formMessage && mergedErrors) mergedErrors.__form = formMessage;
124
+ const context = mergedErrors ? {
125
+ ...passContext,
126
+ errors: mergedErrors
127
+ } : { ...passContext };
128
+ return new StepRetriableError(new Error(formMessage ?? "Input required"), void 0, {
129
+ outlet: "http",
130
+ payload: serializeFormSchema(type),
131
+ context
132
+ });
133
+ }
134
+ function validateOrThrow(input, opts) {
135
+ const validator = getCachedValidator(type, opts);
136
+ try {
137
+ validator.validate(input);
138
+ } catch (err) {
139
+ if (isValidatorError(err)) throw requireInput({ errors: flattenValidatorErrors(err) });
140
+ throw err;
141
+ }
123
142
  }
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));
143
+ function resolveInput(opts) {
144
+ const input = wfState.input()?.formData;
145
+ if (input === void 0) throw requireInput();
146
+ validateOrThrow(input, opts?.partial === "deep" ? {
147
+ partial: "deep",
148
+ unknownProps: "strip"
149
+ } : { unknownProps: "strip" });
150
+ return input;
151
+ }
152
+ function resolveAction() {
153
+ const action = wfState.input()?.action;
154
+ if (action === void 0) return void 0;
155
+ const { actions, actionsWithData } = getFormActions(type);
156
+ if (!actions.includes(action) && !actionsWithData.includes(action)) throw requireInput({ formMessage: `Action "${action}" is not supported` });
157
+ return action;
134
158
  }
135
159
  return {
136
- data,
160
+ resolveInput,
161
+ resolveAction,
137
162
  requireInput
138
163
  };
139
164
  }
140
165
  //#endregion
141
- //#region src/form-input/wf-keys.ts
142
- /**
143
- * Event context key for the workflow action name.
144
- *
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
- * ```
151
- *
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.
159
- *
160
- * **In the HTTP trigger** (to set the action from the request body):
161
- * ```ts
162
- * const { setAction } = useWfAction()
163
- * setAction(body.action)
164
- * ```
165
- *
166
- * **In step handlers** (to read the action — prefer `@AltAction()` decorator):
167
- * ```ts
168
- * const { getAction } = useWfAction()
169
- * const action = getAction()
170
- * ```
171
- */
172
- function useWfAction() {
173
- const ctx = current();
174
- return {
175
- getAction: () => ctx.has(actionKey) ? ctx.get(actionKey) : void 0,
176
- setAction: (action) => ctx.set(actionKey, action)
177
- };
178
- }
179
- //#endregion
180
- //#region src/form-input/decorator.ts
166
+ //#region src/wf-io/wf-input.decorator.ts
181
167
  /**
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.
168
+ * Parameter decorator that resolves to the validated typed input for the
169
+ * current workflow step. Owns the action-vs-input policy matrix on top of
170
+ * the pure `useAtscriptWf` primitives.
186
171
  *
187
- * The injected value is `{ data(), requireInput(errors?) }`.
172
+ * Policy:
173
+ * - No action fired → strict full validation.
174
+ * - With-data action → input required, partial-deep validation.
175
+ * - No-data action → input must be absent; returns `undefined` only when
176
+ * `pass: true` opts the step into ignoring the no-data action.
177
+ * - Unknown action → `StepRetriableError` (propagated from `resolveAction`).
188
178
  *
189
179
  * @example
190
180
  * ```ts
191
181
  * @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
- * }
182
+ * async login(@WfInput() input: LoginForm) {
183
+ * await this.auth.login(input.username, input.password)
199
184
  * }
200
185
  * ```
201
186
  */
202
- function FormInput() {
187
+ function WfInput(opts) {
203
188
  return (target, key, index) => {
204
189
  if (typeof index !== "number") return;
205
190
  Resolve((metas) => {
206
191
  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
- }
192
+ if (!type || !isAnnotatedType(type)) throw new Error("@WfInput(): no atscript type available on the parameter. Annotate the parameter with an atscript-derived type.");
193
+ const wf = useAtscriptWf(type);
194
+ const pass = opts?.pass === true;
195
+ const action = wf.resolveAction();
233
196
  if (action) {
197
+ const wfInput = useWfState().input()?.formData;
234
198
  const { actions, actionsWithData } = getFormActions(type);
235
- if (actionsWithData.includes(action)) {
236
- validateOrReply(type, wfState, input ?? {}, {
237
- partial: "deep",
238
- unknownProps: "strip"
239
- }, reply);
199
+ const isNoData = actions.includes(action);
200
+ const isWithData = actionsWithData.includes(action);
201
+ if (isNoData) {
202
+ 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` });
203
+ if (wfInput !== void 0) throw wf.requireInput({ formMessage: `Action "${action}" requires no data; input not allowed here` });
240
204
  return;
241
205
  }
242
- if (actions.includes(action)) return;
243
- reply(toInputRequired(type, wfState, { __form: `Action "${action}" is not supported` }));
244
- return;
206
+ if (isWithData) {
207
+ if (wfInput === void 0) throw wf.requireInput({ formMessage: `Action "${action}" expects input` });
208
+ return wf.resolveInput({ partial: "deep" });
209
+ }
245
210
  }
246
- validateOrReply(type, wfState, input, { unknownProps: "strip" }, reply);
247
- }
211
+ return wf.resolveInput();
212
+ }, "WfInput")(target, key, index);
213
+ if (opts?.pass === true) Optional()(target, key, index);
248
214
  };
249
215
  }
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
216
  //#endregion
282
- //#region src/form-input/alt-action.decorator.ts
217
+ //#region src/wf-io/wf-action.decorator.ts
283
218
  /**
284
- * Parameter decorator that resolves the action name from the current
285
- * workflow event context. Returns `undefined` for normal form submits.
219
+ * Parameter decorator sugar for `useAtscriptWf(Type).resolveAction()`.
220
+ *
221
+ * Resolves to the current workflow action name from the input envelope, or
222
+ * `undefined` when no action was submitted.
223
+ *
224
+ * The form type is **required**: the decorator validates the action against
225
+ * the form's declared `@ui.form.action` / `@wf.action.withData` whitelist and
226
+ * throws `StepRetriableError` for any unknown action — the step body never
227
+ * sees actions that aren't part of the form's contract.
286
228
  *
287
229
  * @example
288
230
  * ```ts
289
231
  * @Step('mfa-verify')
290
232
  * async mfaVerify(
291
- * @FormInput() form: TFormInput<PincodeForm>,
292
- * @AltAction() action: string | undefined,
233
+ * @WfInput() input: PincodeForm,
234
+ * @WfAction(PincodeForm) action: string | undefined,
293
235
  * ) {
294
- * if (action === 'resend') {
295
- * await this.sendOtp(ctx.email)
296
- * return
297
- * }
298
- * await this.verifyCode(form.data().code)
236
+ * if (action === 'resend') return this.sendOtp()
237
+ * await this.verifyCode(input.code)
299
238
  * }
300
239
  * ```
301
240
  */
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
- }
241
+ function WfAction(type) {
242
+ return (target, key, index) => {
243
+ if (typeof index !== "number") return;
244
+ Resolve(() => useAtscriptWf(type).resolveAction(), "WfAction")(target, key, index);
328
245
  };
329
246
  }
330
247
  //#endregion
@@ -455,4 +372,4 @@ function abortWf(reason, opts) {
455
372
  });
456
373
  }
457
374
  //#endregion
458
- export { AltAction, FormInput, FormInputRequired, abortWf, createAsHttpOutlet, extractPassContext, finishWf, formInputInterceptor, getFormActions, handleAsOutletRequest, isWfFinished, serializeFormSchema, useFormInput, useWfAction };
375
+ export { WfAction, WfInput, abortWf, createAsHttpOutlet, extractPassContext, finishWf, getFormActions, handleAsOutletRequest, isWfFinished, serializeFormSchema, useAtscriptWf };
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.71",
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.71"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "@atscript/core": "^0.1.58",