@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 +16 -11
- package/dist/index.d.mts +84 -114
- package/dist/index.mjs +122 -205
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,18 +6,23 @@
|
|
|
6
6
|
|
|
7
7
|
# @atscript/moost-wf
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Documentation: [ui.atscript.dev](https://ui.atscript.dev)
|
|
10
10
|
|
|
11
|
-
Server-side workflow integration for [Moost](https://github.com/moostjs/moost) — decorators,
|
|
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
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
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,
|
|
35
|
-
| `@atscript/moost-wf/plugin` | atscript build-time plugin
|
|
36
|
-
| `@atscript/moost-wf/store` |
|
|
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
|
|
46
|
+
MIT © 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 {
|
|
3
|
-
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
3
|
+
import { StepRetriableError } from "@wooksjs/event-wf";
|
|
4
4
|
|
|
5
|
-
//#region src/
|
|
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/
|
|
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/
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
138
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
144
|
-
*
|
|
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
|
|
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:
|
|
161
|
+
action: WfActionRequest;
|
|
192
162
|
} | {
|
|
193
163
|
trigger: "auto";
|
|
194
164
|
timeoutMs: number;
|
|
195
|
-
action:
|
|
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:
|
|
177
|
+
action: WfActionRequest;
|
|
208
178
|
}
|
|
209
|
-
type
|
|
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 {
|
|
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 {
|
|
5
|
-
import { useWfFinished } from "@wooksjs/event-wf";
|
|
6
|
-
|
|
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/
|
|
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/
|
|
86
|
+
//#region src/wf-io/validator-cache.ts
|
|
87
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
160
|
+
resolveInput,
|
|
161
|
+
resolveAction,
|
|
137
162
|
requireInput
|
|
138
163
|
};
|
|
139
164
|
}
|
|
140
165
|
//#endregion
|
|
141
|
-
//#region src/
|
|
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
|
|
183
|
-
*
|
|
184
|
-
*
|
|
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
|
-
*
|
|
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(@
|
|
193
|
-
*
|
|
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
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
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 (
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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/
|
|
217
|
+
//#region src/wf-io/wf-action.decorator.ts
|
|
283
218
|
/**
|
|
284
|
-
* Parameter decorator
|
|
285
|
-
*
|
|
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
|
-
* @
|
|
292
|
-
* @
|
|
233
|
+
* @WfInput() input: PincodeForm,
|
|
234
|
+
* @WfAction(PincodeForm) action: string | undefined,
|
|
293
235
|
* ) {
|
|
294
|
-
* if (action === 'resend')
|
|
295
|
-
*
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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 {
|
|
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.
|
|
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.
|
|
65
|
+
"@atscript/ui": "^0.1.71"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"@atscript/core": "^0.1.58",
|