@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 +16 -11
- package/dist/index.d.mts +108 -109
- package/dist/index.mjs +158 -193
- 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,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/
|
|
38
|
+
//#region src/wf-io/use-wf-action-slot.d.ts
|
|
132
39
|
/**
|
|
133
|
-
*
|
|
40
|
+
* Low-level accessor for the workflow action slot in the current wf event context.
|
|
134
41
|
*
|
|
135
|
-
*
|
|
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 } =
|
|
55
|
+
* const { setAction } = useWfActionSlot()
|
|
138
56
|
* setAction(body.action)
|
|
139
57
|
* ```
|
|
140
58
|
*
|
|
141
|
-
* **In step handlers** (
|
|
59
|
+
* **In step handlers** (raw read — prefer `@WfAction()` / `useAtscriptWf().resolveAction()`):
|
|
142
60
|
* ```ts
|
|
143
|
-
* const { getAction } =
|
|
61
|
+
* const { getAction } = useWfActionSlot()
|
|
144
62
|
* const action = getAction()
|
|
145
63
|
* ```
|
|
146
64
|
*/
|
|
147
|
-
declare function
|
|
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:
|
|
190
|
+
action: WfActionRequest;
|
|
192
191
|
} | {
|
|
193
192
|
trigger: "auto";
|
|
194
193
|
timeoutMs: number;
|
|
195
|
-
action:
|
|
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:
|
|
206
|
+
action: WfActionRequest;
|
|
208
207
|
}
|
|
209
|
-
type
|
|
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 {
|
|
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 {
|
|
6
|
-
|
|
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/
|
|
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/
|
|
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
|
-
*
|
|
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
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
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
|
-
|
|
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/
|
|
97
|
+
//#region src/wf-io/use-wf-action-slot.ts
|
|
142
98
|
/**
|
|
143
|
-
*
|
|
99
|
+
* Low-level accessor for the workflow action slot in the current wf event context.
|
|
144
100
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
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
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
112
|
+
* **In a transport adapter** (to set the action from the request body):
|
|
161
113
|
* ```ts
|
|
162
|
-
* const { setAction } =
|
|
114
|
+
* const { setAction } = useWfActionSlot()
|
|
163
115
|
* setAction(body.action)
|
|
164
116
|
* ```
|
|
165
117
|
*
|
|
166
|
-
* **In step handlers** (
|
|
118
|
+
* **In step handlers** (raw read — prefer `@WfAction()` / `useAtscriptWf().resolveAction()`):
|
|
167
119
|
* ```ts
|
|
168
|
-
* const { getAction } =
|
|
120
|
+
* const { getAction } = useWfActionSlot()
|
|
169
121
|
* const action = getAction()
|
|
170
122
|
* ```
|
|
171
123
|
*/
|
|
172
|
-
function
|
|
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/
|
|
132
|
+
//#region src/wf-io/validator-cache.ts
|
|
133
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
181
134
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* (
|
|
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
|
-
*
|
|
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(@
|
|
193
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
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 (
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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/
|
|
264
|
+
//#region src/wf-io/wf-action.decorator.ts
|
|
283
265
|
/**
|
|
284
|
-
* Parameter decorator that resolves the action name
|
|
285
|
-
*
|
|
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
|
-
* @
|
|
292
|
-
* @
|
|
277
|
+
* @WfInput() input: PincodeForm,
|
|
278
|
+
* @WfAction() action: string | undefined,
|
|
293
279
|
* ) {
|
|
294
|
-
* if (action === 'resend')
|
|
295
|
-
*
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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 {
|
|
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.
|
|
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.
|
|
65
|
+
"@atscript/ui": "^0.1.70"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"@atscript/core": "^0.1.58",
|