@manablox/workflows 0.2.0 → 0.3.0
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/dist/index.d.ts +301 -0
- package/dist/index.js +1196 -0
- package/package.json +16 -9
- package/src/conditions.ts +0 -88
- package/src/cron.ts +0 -181
- package/src/engine.ts +0 -499
- package/src/index.ts +0 -9
- package/src/mail.ts +0 -38
- package/src/push.ts +0 -61
- package/src/service.ts +0 -167
- package/src/steps.ts +0 -256
- package/src/template.ts +0 -65
- package/src/validate.ts +0 -267
- package/test/conditions.test.ts +0 -80
- package/test/cron.test.ts +0 -79
- package/test/engine.test.ts +0 -555
- package/test/template.test.ts +0 -41
- package/test/validate.test.ts +0 -129
- package/tsconfig.json +0 -1
- package/vitest.config.ts +0 -9
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { ContentRecord, Logger, MailConfig, Manablox, PushConfig, WorkflowConditionRule, WorkflowConditionStep, WorkflowEvent, WorkflowEventTrigger, WorkflowRunContext, WorkflowStep, WorkflowTrigger } from "@manablox/core";
|
|
2
|
+
import { ContentRow, PushSubscriptionRow, Repositories, WorkflowRow, WorkflowRow as WorkflowRow$1, WorkflowRunRow, WorkflowRunRow as WorkflowRunRow$1 } from "@manablox/db";
|
|
3
|
+
//#region src/conditions.d.ts
|
|
4
|
+
export declare function evaluateRule(rule: WorkflowConditionRule, context: WorkflowRunContext): boolean;
|
|
5
|
+
/** Whether the run may continue past this step. */
|
|
6
|
+
export declare function evaluateCondition(step: Pick<WorkflowConditionStep, 'match' | 'rules'>, context: WorkflowRunContext): boolean;
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/cron.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* Five-field cron — `minute hour day-of-month month day-of-week` — matched against a
|
|
11
|
+
* wall-clock in a named timezone. Only matching is needed: the scheduler wakes every
|
|
12
|
+
* minute and asks "is now a match?", so there is no next-run arithmetic to get wrong.
|
|
13
|
+
*/
|
|
14
|
+
export interface CronSpec {
|
|
15
|
+
minute: Set<number>;
|
|
16
|
+
hour: Set<number>;
|
|
17
|
+
dayOfMonth: Set<number>;
|
|
18
|
+
month: Set<number>;
|
|
19
|
+
dayOfWeek: Set<number>;
|
|
20
|
+
/** `*` in the day-of-month field; decides how the two day fields combine. */
|
|
21
|
+
anyDayOfMonth: boolean;
|
|
22
|
+
anyDayOfWeek: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function parseCron(expression: string): CronSpec | null;
|
|
25
|
+
export declare function isValidCron(expression: string): boolean;
|
|
26
|
+
export declare function isValidTimezone(timezone: string): boolean;
|
|
27
|
+
export interface WallClock {
|
|
28
|
+
minute: number;
|
|
29
|
+
hour: number;
|
|
30
|
+
dayOfMonth: number;
|
|
31
|
+
month: number;
|
|
32
|
+
dayOfWeek: number;
|
|
33
|
+
}
|
|
34
|
+
/** The wall-clock reading of an instant in a timezone. */
|
|
35
|
+
export declare function wallClock(date: Date, timezone: string): WallClock;
|
|
36
|
+
/**
|
|
37
|
+
* Whether the minute containing `date` matches. As in Vixie cron, when both day fields
|
|
38
|
+
* are restricted a match on either is enough.
|
|
39
|
+
*/
|
|
40
|
+
export declare function cronMatches(spec: CronSpec, date: Date, timezone: string): boolean;
|
|
41
|
+
/** The start of the minute an instant falls in — the unit the scheduler claims. */
|
|
42
|
+
export declare function floorToMinute(date: Date): Date;
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/mail.d.ts
|
|
45
|
+
export interface MailMessage {
|
|
46
|
+
to: string[];
|
|
47
|
+
subject: string;
|
|
48
|
+
text: string;
|
|
49
|
+
html?: string | undefined;
|
|
50
|
+
}
|
|
51
|
+
/** What the email step needs from the outside world; the tests hand in a recorder. */
|
|
52
|
+
export interface Mailer {
|
|
53
|
+
send(message: MailMessage): Promise<{
|
|
54
|
+
id: string | null;
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A mailer over the configured SMTP URL, or `null` when there is none — the step then
|
|
59
|
+
* fails with `workflow.mail.notConfigured`, which the run log shows verbatim.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createMailer(config: MailConfig, logger: Logger): Mailer | null;
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/push.d.ts
|
|
64
|
+
export interface PushTarget {
|
|
65
|
+
endpoint: string;
|
|
66
|
+
keys: {
|
|
67
|
+
p256dh: string;
|
|
68
|
+
auth: string;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** What a notification carries; the admin's service worker reads exactly this. */
|
|
72
|
+
export interface PushPayload {
|
|
73
|
+
title: string;
|
|
74
|
+
body: string;
|
|
75
|
+
url: string | null;
|
|
76
|
+
}
|
|
77
|
+
/** What the push step needs from the outside world; the tests hand in a recorder. */
|
|
78
|
+
export interface Pusher {
|
|
79
|
+
/** `gone` means the push service no longer knows the subscription and the row should go. */
|
|
80
|
+
send(target: PushTarget, payload: PushPayload): Promise<'sent' | 'gone'>;
|
|
81
|
+
publicKey: string;
|
|
82
|
+
}
|
|
83
|
+
export declare function isPushConfigured(config: PushConfig): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* A Web Push sender over the configured VAPID keys, or `null` when there are none. The
|
|
86
|
+
* keys are made once per installation with `pnpm --filter @manablox/api push:keys`.
|
|
87
|
+
*/
|
|
88
|
+
export declare function createPusher(config: PushConfig, logger: Logger): Pusher | null;
|
|
89
|
+
/** A fresh VAPID key pair, for the CLI that prints them. */
|
|
90
|
+
export declare function generatePushKeys(): {
|
|
91
|
+
publicKey: string;
|
|
92
|
+
privateKey: string;
|
|
93
|
+
};
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/engine.d.ts
|
|
96
|
+
export interface WorkflowEngineOptions {
|
|
97
|
+
mailer?: Mailer | null | undefined;
|
|
98
|
+
pusher?: Pusher | null | undefined;
|
|
99
|
+
fetch?: typeof fetch | undefined;
|
|
100
|
+
/** Where the admin lives, for links in mails and notifications. */
|
|
101
|
+
adminUrl?: string | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* Hands a queued run to a worker. The default runs it in this process, off the
|
|
104
|
+
* request; a host with a queue passes `(runId) => jobs.enqueue('workflow:run', …)`.
|
|
105
|
+
*/
|
|
106
|
+
dispatch?: ((runId: string) => Promise<void>) | undefined;
|
|
107
|
+
/** How often the scheduler looks at the clock. Every 20 s catches each minute once. */
|
|
108
|
+
tickMs?: number | undefined;
|
|
109
|
+
now?: (() => Date) | undefined;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Runs workflows: listens for content events, keeps the clock for scheduled ones, and
|
|
113
|
+
* executes a run's steps one after another, pausing at a delay and picking it up again.
|
|
114
|
+
*
|
|
115
|
+
* A run is a row from the moment it is queued, so nothing is lost if the process goes
|
|
116
|
+
* away mid-way: the next tick finds `waiting` runs whose time has come, and a worker
|
|
117
|
+
* claims a run before touching it so two never execute the same one.
|
|
118
|
+
*/
|
|
119
|
+
export declare class WorkflowEngine {
|
|
120
|
+
private readonly manablox;
|
|
121
|
+
private readonly repos;
|
|
122
|
+
private readonly env;
|
|
123
|
+
private readonly dispatch;
|
|
124
|
+
private readonly now;
|
|
125
|
+
private readonly tickMs;
|
|
126
|
+
private readonly pending;
|
|
127
|
+
private timer;
|
|
128
|
+
private ticking;
|
|
129
|
+
constructor(manablox: Manablox, repos: Repositories, options?: WorkflowEngineOptions);
|
|
130
|
+
get adminUrl(): string;
|
|
131
|
+
/** Registers the content hooks. Call once, at boot. */
|
|
132
|
+
attach(): void;
|
|
133
|
+
/** Starts the clock for scheduled workflows and paused runs. */
|
|
134
|
+
start(): void;
|
|
135
|
+
stop(): Promise<void>;
|
|
136
|
+
/** Resolves once every run started in this process has finished. For tests and shutdown. */
|
|
137
|
+
idle(): Promise<void>;
|
|
138
|
+
private onContentEvent;
|
|
139
|
+
/**
|
|
140
|
+
* One look at the clock: start every scheduled workflow whose cron names this minute,
|
|
141
|
+
* and resume every paused run whose time has come. Safe to call from several
|
|
142
|
+
* processes — each claim is an atomic update.
|
|
143
|
+
*/
|
|
144
|
+
tick(now?: Date): Promise<void>;
|
|
145
|
+
private startScheduled;
|
|
146
|
+
/** A run started by hand from the editor, executed inline so the caller sees the log. */
|
|
147
|
+
runManually(workflow: WorkflowRow$1, document: ContentRow | null): Promise<WorkflowRunRow$1>;
|
|
148
|
+
private enqueue;
|
|
149
|
+
private background;
|
|
150
|
+
/** Executes a queued or paused run from where it stands. The worker entry point. */
|
|
151
|
+
run(runId: string): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Walks one chain — the top level, or a side of a fork — from `resume` when the run is
|
|
154
|
+
* being picked up inside it. A fork's chosen side is walked recursively; the position
|
|
155
|
+
* saved at a delay is the full path, so a resume finds its way back down.
|
|
156
|
+
*/
|
|
157
|
+
private runChain;
|
|
158
|
+
private finish;
|
|
159
|
+
private spaceInfo;
|
|
160
|
+
private actorInfo;
|
|
161
|
+
private documentUrl;
|
|
162
|
+
}
|
|
163
|
+
/** Whether an event trigger wants this event for this document. */
|
|
164
|
+
export declare function matchesEvent(trigger: WorkflowRow$1['trigger'], event: WorkflowEvent, row: Pick<ContentRecord, 'typeId' | 'locale'>): trigger is WorkflowEventTrigger;
|
|
165
|
+
/** A row as a template sees it: plain JSON, without the search machinery. */
|
|
166
|
+
export declare function serialise(row: ContentRecord | ContentRow): Record<string, unknown>;
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/validate.d.ts
|
|
169
|
+
/** What the editor hands over: a workflow as typed, before it is trusted. */
|
|
170
|
+
export interface WorkflowInput {
|
|
171
|
+
name: string;
|
|
172
|
+
description?: string | null | undefined;
|
|
173
|
+
enabled?: boolean | undefined;
|
|
174
|
+
trigger: WorkflowTrigger;
|
|
175
|
+
steps: WorkflowStep[];
|
|
176
|
+
}
|
|
177
|
+
export interface ValidatedWorkflow {
|
|
178
|
+
name: string;
|
|
179
|
+
description: string | null;
|
|
180
|
+
enabled: boolean;
|
|
181
|
+
trigger: WorkflowTrigger;
|
|
182
|
+
steps: WorkflowStep[];
|
|
183
|
+
}
|
|
184
|
+
export interface ValidationEnvironment {
|
|
185
|
+
/** Whether a content type id names a type of this space (or a code-defined one). */
|
|
186
|
+
typeExists: (typeId: string) => boolean;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Checks everything the shape alone cannot: an event that exists, a cron that parses, a
|
|
190
|
+
* step with somewhere to send to. Every problem is reported at once, each with the path
|
|
191
|
+
* of the field it concerns, so the editor can mark them all in one round.
|
|
192
|
+
*/
|
|
193
|
+
export declare function validateWorkflow(input: WorkflowInput, env: ValidationEnvironment): ValidatedWorkflow;
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/service.d.ts
|
|
196
|
+
/** What the editor needs to draw the palette, served rather than duplicated there. */
|
|
197
|
+
export interface WorkflowCatalog {
|
|
198
|
+
events: Array<{
|
|
199
|
+
id: string;
|
|
200
|
+
label: string;
|
|
201
|
+
description: string;
|
|
202
|
+
}>;
|
|
203
|
+
stepTypes: Array<{
|
|
204
|
+
id: string;
|
|
205
|
+
label: string;
|
|
206
|
+
description: string;
|
|
207
|
+
available: boolean;
|
|
208
|
+
}>;
|
|
209
|
+
operators: Array<{
|
|
210
|
+
id: string;
|
|
211
|
+
label: string;
|
|
212
|
+
}>;
|
|
213
|
+
/** Whether the instance can send mail / push at all; the editor warns when it cannot. */
|
|
214
|
+
mail: boolean;
|
|
215
|
+
push: boolean;
|
|
216
|
+
/** The VAPID public key a browser subscribes with, when push is configured. */
|
|
217
|
+
pushPublicKey: string | null;
|
|
218
|
+
adminUrl: string;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Workflows: the rules — a trigger that exists, steps with somewhere to go, a run that
|
|
222
|
+
* belongs to the space it is asked for in — with the engine doing the running.
|
|
223
|
+
*/
|
|
224
|
+
export declare class WorkflowService {
|
|
225
|
+
private readonly manablox;
|
|
226
|
+
private readonly repos;
|
|
227
|
+
private readonly engine;
|
|
228
|
+
constructor(manablox: Manablox, repos: Repositories, engine: WorkflowEngine);
|
|
229
|
+
catalog(): WorkflowCatalog;
|
|
230
|
+
list(spaceId: string): Promise<WorkflowRow[]>;
|
|
231
|
+
get(spaceId: string, id: string): Promise<WorkflowRow>;
|
|
232
|
+
create(spaceId: string, input: WorkflowInput): Promise<WorkflowRow>;
|
|
233
|
+
update(spaceId: string, id: string, input: WorkflowInput): Promise<WorkflowRow>;
|
|
234
|
+
/** The on/off switch, kept apart from a full save so the list can flip it. */
|
|
235
|
+
setEnabled(spaceId: string, id: string, enabled: boolean): Promise<WorkflowRow>;
|
|
236
|
+
delete(spaceId: string, id: string): Promise<void>;
|
|
237
|
+
runs(spaceId: string, id: string, limit?: number): Promise<WorkflowRunRow[]>;
|
|
238
|
+
run(spaceId: string, id: string): Promise<WorkflowRunRow>;
|
|
239
|
+
/**
|
|
240
|
+
* Runs a workflow now, against a document of the space when one is named. An
|
|
241
|
+
* event-triggered workflow needs one — its steps read `content` — while a scheduled
|
|
242
|
+
* workflow may run against its own selection.
|
|
243
|
+
*/
|
|
244
|
+
runNow(spaceId: string, id: string, contentId: string | null): Promise<WorkflowRunRow>;
|
|
245
|
+
subscriptions(userId: string): Promise<PushSubscriptionRow[]>;
|
|
246
|
+
subscribe(userId: string, subscription: {
|
|
247
|
+
endpoint: string;
|
|
248
|
+
keys: {
|
|
249
|
+
p256dh: string;
|
|
250
|
+
auth: string;
|
|
251
|
+
};
|
|
252
|
+
}, userAgent: string | null): Promise<PushSubscriptionRow>;
|
|
253
|
+
unsubscribe(userId: string, endpoint: string): Promise<void>;
|
|
254
|
+
private find;
|
|
255
|
+
private validationEnvironment;
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/steps.d.ts
|
|
259
|
+
/** The outside world as the steps see it; tests swap every part of it. */
|
|
260
|
+
export interface StepEnvironment {
|
|
261
|
+
repos: Repositories;
|
|
262
|
+
mailer: Mailer | null;
|
|
263
|
+
pusher: Pusher | null;
|
|
264
|
+
fetch: typeof fetch;
|
|
265
|
+
/** Where the admin lives, for links in mails and notifications. */
|
|
266
|
+
adminUrl: string;
|
|
267
|
+
}
|
|
268
|
+
export type StepOutcome = {
|
|
269
|
+
kind: 'ok';
|
|
270
|
+
message: string | null;
|
|
271
|
+
detail: Record<string, unknown> | null;
|
|
272
|
+
} | {
|
|
273
|
+
kind: 'stop';
|
|
274
|
+
message: string;
|
|
275
|
+
} | {
|
|
276
|
+
kind: 'wait';
|
|
277
|
+
minutes: number;
|
|
278
|
+
};
|
|
279
|
+
/** Runs one step against a context. Throws when the step fails; the runner logs it. */
|
|
280
|
+
export declare function executeStep(step: WorkflowStep, context: WorkflowRunContext, env: StepEnvironment): Promise<StepOutcome>;
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/template.d.ts
|
|
283
|
+
/**
|
|
284
|
+
* The placeholder syntax templates use: `{{ content.title }}`, `{{ content.fields.body }}`,
|
|
285
|
+
* `{{ actor.email }}`. A path resolves into the run context; an object or array comes out
|
|
286
|
+
* as JSON, anything missing as an empty string. Deliberately no logic — a workflow that
|
|
287
|
+
* needs a branch has a condition step for it.
|
|
288
|
+
*/
|
|
289
|
+
export declare function resolvePath(root: unknown, path: string): unknown;
|
|
290
|
+
export declare function stringify(value: unknown): string;
|
|
291
|
+
export declare function render(template: string, context: unknown): string;
|
|
292
|
+
/**
|
|
293
|
+
* Renders inside a JSON document: a placeholder that stands alone in a string is
|
|
294
|
+
* replaced by the value itself (`"count": "{{ documents.length }}"` becomes a number,
|
|
295
|
+
* `"doc": "{{ content }}"` an object); one embedded in text renders as text.
|
|
296
|
+
*/
|
|
297
|
+
export declare function renderJson(template: string, context: unknown): string;
|
|
298
|
+
/** Every placeholder a template names, for the editor's hints and for validation. */
|
|
299
|
+
export declare function placeholders(template: string): string[];
|
|
300
|
+
//#endregion
|
|
301
|
+
export type { WorkflowRow, WorkflowRunRow };
|