@sanoma-ai/workflows 0.7.0 → 0.8.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/README.md +43 -12
- package/dist/apps-list.d.ts +90 -0
- package/dist/apps-list.js +109 -0
- package/dist/apps-list.js.map +1 -0
- package/dist/apps.d.ts +42 -0
- package/dist/apps.js +137 -0
- package/dist/apps.js.map +1 -0
- package/dist/call-workflow.js +5 -6
- package/dist/call-workflow.js.map +1 -1
- package/dist/call.js +6 -3
- package/dist/call.js.map +1 -1
- package/dist/cedar.d.ts +1 -1
- package/dist/cedar.js +6 -4
- package/dist/cedar.js.map +1 -1
- package/dist/change-steps.d.ts +10 -0
- package/dist/change-steps.js +204 -0
- package/dist/change-steps.js.map +1 -0
- package/dist/change-workflow.d.ts +54 -0
- package/dist/change-workflow.js +161 -0
- package/dist/change-workflow.js.map +1 -0
- package/dist/change.d.ts +121 -24
- package/dist/change.js +196 -17
- package/dist/change.js.map +1 -1
- package/dist/claims.d.ts +58 -0
- package/dist/claims.js +27 -0
- package/dist/claims.js.map +1 -0
- package/dist/client.d.ts +92 -0
- package/dist/client.js +268 -6
- package/dist/client.js.map +1 -1
- package/dist/collection.d.ts +6 -1
- package/dist/collection.js +11 -3
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +56 -8
- package/dist/config.js +71 -29
- package/dist/config.js.map +1 -1
- package/dist/constructors.d.ts +37 -1
- package/dist/constructors.js +6 -2
- package/dist/constructors.js.map +1 -1
- package/dist/db.d.ts +21 -21
- package/dist/db.js +31 -54
- package/dist/db.js.map +1 -1
- package/dist/deadlines.d.ts +18 -12
- package/dist/deadlines.js +18 -24
- package/dist/deadlines.js.map +1 -1
- package/dist/define.d.ts +30 -14
- package/dist/define.js +9 -1
- package/dist/define.js.map +1 -1
- package/dist/demo.d.ts +15 -4
- package/dist/demo.js +27 -12
- package/dist/demo.js.map +1 -1
- package/dist/describe.d.ts +16 -2
- package/dist/describe.js +39 -4
- package/dist/describe.js.map +1 -1
- package/dist/direct.d.ts +4 -3
- package/dist/direct.js +7 -4
- package/dist/direct.js.map +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -1
- package/dist/fake.d.ts +26 -0
- package/dist/fake.js +123 -0
- package/dist/fake.js.map +1 -1
- package/dist/host.d.ts +2 -3
- package/dist/host.js +1 -1
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/lint.js +20 -6
- package/dist/lint.js.map +1 -1
- package/dist/op.d.ts +8 -1
- package/dist/op.js.map +1 -1
- package/dist/outline.js +13 -6
- package/dist/outline.js.map +1 -1
- package/dist/paths.d.ts +16 -0
- package/dist/paths.js +54 -2
- package/dist/paths.js.map +1 -1
- package/dist/person.d.ts +5 -0
- package/dist/person.js +3 -1
- package/dist/person.js.map +1 -1
- package/dist/resources.d.ts +7 -2
- package/dist/resources.js +27 -10
- package/dist/resources.js.map +1 -1
- package/dist/run.d.ts +7 -0
- package/dist/run.js.map +1 -1
- package/dist/scenario.d.ts +2 -2
- package/dist/scenario.js +10 -4
- package/dist/scenario.js.map +1 -1
- package/dist/scenarios.d.ts +1 -1
- package/dist/scenarios.js.map +1 -1
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +19 -5
- package/dist/sdk.js.map +1 -1
- package/dist/shared.d.ts +43 -2
- package/dist/shared.js +46 -4
- package/dist/shared.js.map +1 -1
- package/dist/sites.d.ts +26 -5
- package/dist/sites.js +56 -20
- package/dist/sites.js.map +1 -1
- package/dist/source.d.ts +179 -5
- package/dist/source.js +13 -0
- package/dist/source.js.map +1 -1
- package/dist/supervisor.d.ts +5 -4
- package/dist/supervisor.js +2 -11
- package/dist/supervisor.js.map +1 -1
- package/dist/version.d.ts +8 -0
- package/dist/version.js +21 -13
- package/dist/version.js.map +1 -1
- package/dist/wakeup-driver.d.ts +128 -0
- package/dist/wakeup-driver.js +216 -0
- package/dist/wakeup-driver.js.map +1 -0
- package/dist/wakeup-service.d.ts +4 -4
- package/dist/wakeup-service.js +19 -25
- package/dist/wakeup-service.js.map +1 -1
- package/dist/wakeup-type.d.ts +43 -0
- package/dist/wakeup-type.js +80 -0
- package/dist/wakeup-type.js.map +1 -0
- package/dist/wakeup.d.ts +1 -8
- package/dist/wakeup.js +2 -11
- package/dist/wakeup.js.map +1 -1
- package/dist/worker.d.ts +2 -2
- package/dist/worker.js +50 -7
- package/dist/worker.js.map +1 -1
- package/oxlint.json +3 -2
- package/package.json +3 -1
- package/src/apps-list.ts +119 -0
- package/src/apps.ts +162 -0
- package/src/call-workflow.ts +10 -2
- package/src/call.ts +6 -3
- package/src/cedar.ts +9 -7
- package/src/change-steps.ts +274 -0
- package/src/change-workflow.ts +198 -0
- package/src/change.ts +257 -27
- package/src/claims.ts +79 -0
- package/src/client.ts +320 -4
- package/src/collection.ts +22 -3
- package/src/config.ts +104 -35
- package/src/constructors.ts +8 -2
- package/src/db.ts +39 -66
- package/src/deadlines.ts +27 -30
- package/src/define.ts +31 -12
- package/src/demo.ts +55 -24
- package/src/describe.ts +67 -4
- package/src/direct.ts +9 -5
- package/src/errors.ts +3 -0
- package/src/fake.ts +137 -0
- package/src/host.ts +3 -4
- package/src/index.ts +22 -3
- package/src/lint.ts +21 -6
- package/src/op.ts +8 -1
- package/src/outline.ts +15 -6
- package/src/paths.ts +57 -2
- package/src/person.ts +4 -1
- package/src/resources.ts +31 -11
- package/src/run.ts +5 -0
- package/src/scenario.ts +14 -4
- package/src/scenarios.ts +1 -1
- package/src/sdk.ts +21 -7
- package/src/shared.ts +60 -5
- package/src/sites.ts +70 -24
- package/src/source.ts +168 -5
- package/src/supervisor.ts +7 -12
- package/src/version.ts +25 -16
- package/src/wakeup-driver.ts +270 -0
- package/src/wakeup-service.ts +21 -28
- package/src/wakeup-type.ts +90 -0
- package/src/wakeup.ts +6 -17
- package/src/worker.ts +48 -9
package/src/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
@@ -6,6 +6,8 @@ import { type ApplyDeclared, applyWorkflow } from "./apply.ts";
|
|
|
6
6
|
import { applySteps } from "./apply-steps.ts";
|
|
7
7
|
import { callWorkflow } from "./call-workflow.ts";
|
|
8
8
|
import { changeDriver, changeOps, reservedFor } from "./change.ts";
|
|
9
|
+
import { changeSteps } from "./change-steps.ts";
|
|
10
|
+
import { changeWorkflow } from "./change-workflow.ts";
|
|
9
11
|
import {
|
|
10
12
|
type Collection,
|
|
11
13
|
collectionsConnector,
|
|
@@ -14,10 +16,11 @@ import {
|
|
|
14
16
|
isCollection,
|
|
15
17
|
} from "./collection.ts";
|
|
16
18
|
import { lazyPool, type LazyPool } from "./db.ts";
|
|
19
|
+
import { isDirectory, kindDirs, SCENARIOS_DIR } from "./paths.ts";
|
|
17
20
|
import { callerFile, type Use, type WorkflowDefinition } from "./define.ts";
|
|
18
21
|
import { DRIFT_WORKFLOW, driftWorkflow } from "./drift.ts";
|
|
19
22
|
import { statusesOf, unconfigured } from "./env.ts";
|
|
20
|
-
import type
|
|
23
|
+
import { type Fake, fakeSanomaWakeup } from "./fake.ts";
|
|
21
24
|
import { peopleConnector } from "./person.ts";
|
|
22
25
|
import type { LedgerStore } from "./ledger.ts";
|
|
23
26
|
import {
|
|
@@ -32,17 +35,21 @@ import {
|
|
|
32
35
|
checkConnector,
|
|
33
36
|
setupOf,
|
|
34
37
|
VENDOR,
|
|
38
|
+
withSetup,
|
|
35
39
|
} from "./op.ts";
|
|
36
40
|
import { type CedarPolicySource, policyAt } from "./cedar.ts";
|
|
37
41
|
import type { Policy } from "./policy.ts";
|
|
38
42
|
import { type Resource, resourceTypesOf, withoutWriteOnly } from "./resource.ts";
|
|
39
|
-
import { APPLY_WORKFLOW, BUILT_IN_WORKFLOWS, CALL_WORKFLOW, DATA_VENDOR } from "./shared.ts";
|
|
40
|
-
import { sourceDriver, sourceOps } from "./source.ts";
|
|
43
|
+
import { APPLY_WORKFLOW, BUILT_IN_WORKFLOWS, CALL_WORKFLOW, CHANGE_WORKFLOW, DATA_VENDOR } from "./shared.ts";
|
|
44
|
+
import { type RemoteHalf, sourceDriver, sourceOps } from "./source.ts";
|
|
45
|
+
import { appsListDriver, appsListOps } from "./apps-list.ts";
|
|
41
46
|
import type { Steps } from "./steps.ts";
|
|
42
47
|
import { checkSchedule } from "./schedules.ts";
|
|
43
|
-
import { computeVersion } from "./version.ts";
|
|
48
|
+
import { changeCodeOf, computeVersion } from "./version.ts";
|
|
44
49
|
import type { DeclaredResource } from "./resources.ts";
|
|
45
50
|
import { checkWakeupOps, type DeclaredWakeup } from "./wakeup.ts";
|
|
51
|
+
import { sanomaWakeupDriver, wakeupSteps } from "./wakeup-driver.ts";
|
|
52
|
+
import { wakeupOps } from "./wakeup-type.ts";
|
|
46
53
|
import { checkDatabaseOps } from "./database.ts";
|
|
47
54
|
|
|
48
55
|
/**
|
|
@@ -53,7 +60,11 @@ import { checkDatabaseOps } from "./database.ts";
|
|
|
53
60
|
* policies are `scenarios/`, `resources/` and `policies/` in `root`.
|
|
54
61
|
*/
|
|
55
62
|
export interface SanomaConfig {
|
|
56
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Workflows besides those the launcher finds (`sanoma` imports every file of `workflows/` and
|
|
65
|
+
* each app's `apps/<app>/workflows/`): listing one it finds as well is no second one.
|
|
66
|
+
*/
|
|
67
|
+
workflows?: WorkflowDefinition<any, any>[];
|
|
57
68
|
/**
|
|
58
69
|
* The connectors (from `defineConnector`, as their packages export them). The worker takes
|
|
59
70
|
* each operation's effect, schemas and retry setting from here, not from the workflow, and
|
|
@@ -103,6 +114,12 @@ export interface SanomaConfig {
|
|
|
103
114
|
* that sets it and declares a wakeup of its own.
|
|
104
115
|
*/
|
|
105
116
|
wakeup?: true;
|
|
117
|
+
/**
|
|
118
|
+
* What the app says when it runs this repo as a demo (`sanoma demo`): `start`, the URL its
|
|
119
|
+
* banner's "Start with these apps" opens, such as the hosted wizard with the demo's apps
|
|
120
|
+
* chosen; without one, the banner has no such link.
|
|
121
|
+
*/
|
|
122
|
+
demo?: { start?: string };
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
/** A config, checked, with everything a worker, client or app derives from it. */
|
|
@@ -115,12 +132,14 @@ export interface ResolvedConfig {
|
|
|
115
132
|
databaseUrl: string;
|
|
116
133
|
/** The DBOS application version: `<appName>@<DBOS__APPVERSION, or a hash of the workflows>`. */
|
|
117
134
|
version: string;
|
|
135
|
+
/** The hash of the built-in change workflow's own code (`changeCodeOf`), which each change run carries. */
|
|
136
|
+
changeCode: string;
|
|
118
137
|
/** The DBOS queue runs are started on: `sanoma:<appName>`. */
|
|
119
138
|
queueName: string;
|
|
120
139
|
/**
|
|
121
140
|
* The operations the connectors declare, by id, the built-in `sanoma.change.*` (`changeOps`, in
|
|
122
|
-
* every config), `sanoma.source.resolve` when one declares a host type,
|
|
123
|
-
* (`data.<name>.<op>`).
|
|
141
|
+
* every config), `sanoma.source.resolve` and `sanoma.wakeup`'s when one declares a host type,
|
|
142
|
+
* and the collections' (`data.<name>.<op>`).
|
|
124
143
|
*/
|
|
125
144
|
ops: Map<string, Op>;
|
|
126
145
|
/** The drivers' functions, by operation id, each guarded by its driver's `env` (`indexDrivers`). */
|
|
@@ -146,9 +165,20 @@ export interface ResolvedConfig {
|
|
|
146
165
|
logins: ReadonlyMap<string, NonNullable<ConnectorSetup["login"]>>;
|
|
147
166
|
/** The makers of the model halves the connectors attach, by vendor id in `connectors` order: the app makes each once for its model registry. */
|
|
148
167
|
models: ReadonlyMap<string, NonNullable<ConnectorSetup["model"]>>;
|
|
149
|
-
/**
|
|
168
|
+
/** The remote halves the connectors attach, made once for this resolution, by vendor id in `connectors` order: `remoteFor` finds a URL's. */
|
|
169
|
+
remotes: ReadonlyMap<string, RemoteHalf>;
|
|
170
|
+
/**
|
|
171
|
+
* The resources the data files under `root` declare whose type is a host (`<file>#<export>`),
|
|
172
|
+
* read at each call: what a change's deploy and a push to `main` apply. None when no connector
|
|
173
|
+
* declares a host type.
|
|
174
|
+
*/
|
|
175
|
+
hosts(): Promise<string[]>;
|
|
176
|
+
/**
|
|
177
|
+
* `root`, as a `file:` URL, when `scenarios/` or an app's `apps/<app>/scenarios/` in it is a
|
|
178
|
+
* directory: the feature files under those, each named by its path from the root.
|
|
179
|
+
*/
|
|
150
180
|
scenarios?: URL;
|
|
151
|
-
/** The resource types the connectors declare, by `<vendor>.<type>`: what the drift workflow checks and scenarios seed. */
|
|
181
|
+
/** The resource types the connectors declare (and `sanoma.wakeup`, beside a host type), by `<vendor>.<type>`: what the drift workflow checks and scenarios seed. */
|
|
152
182
|
types: ReadonlyMap<string, Resource>;
|
|
153
183
|
/** The steps scenarios can use besides the generic ones: each connector's (`withSetup`), once each, then the config's own. */
|
|
154
184
|
steps: readonly Steps[];
|
|
@@ -253,8 +283,8 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
253
283
|
throw new Error("The config needs `connectors`: the connectors its workflows call, as their packages export them");
|
|
254
284
|
}
|
|
255
285
|
for (const connector of config.connectors as unknown[]) checkConnector(connector);
|
|
256
|
-
if (!Array.isArray(config.workflows)) {
|
|
257
|
-
throw new Error("The config
|
|
286
|
+
if (config.workflows !== undefined && !Array.isArray(config.workflows)) {
|
|
287
|
+
throw new Error("The config's `workflows`, when given, is an array of defineWorkflow definitions");
|
|
258
288
|
}
|
|
259
289
|
// An untyped config naming fake drivers would otherwise run live. Delete once sanoma-ops has migrated.
|
|
260
290
|
if (config.drivers !== undefined || config.fakes !== undefined)
|
|
@@ -270,39 +300,45 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
270
300
|
const root = rootOf(config);
|
|
271
301
|
const appName = appNameAt(root);
|
|
272
302
|
const scenarios = scenariosAt(root);
|
|
273
|
-
|
|
303
|
+
// With the built-in `sanoma.wakeup` when they declare a host type.
|
|
304
|
+
const running = withBuiltIns(config.connectors);
|
|
305
|
+
const types = resourceTypesOf(running);
|
|
274
306
|
// Built-in: `sanoma.source.resolve`, which apply and drift call for a host's revision.
|
|
275
|
-
const
|
|
307
|
+
const hostTyped = [...types.values()].some((type) => type.host);
|
|
276
308
|
// Built-in: the collections' operations, `data.<name>.<op>`, on the config's database.
|
|
277
309
|
const databaseUrl = resolveDatabaseUrl(config);
|
|
278
310
|
const declared = [...collections.values()];
|
|
279
311
|
const dataStore = declared.length ? lazyPool(databaseUrl, "data") : undefined;
|
|
280
|
-
// Built-in, in every config: a change's operations, which the template's policy names
|
|
312
|
+
// Built-in, in every config: a change's operations, which the template's policy names
|
|
313
|
+
// (`sanoma.change.merge`), and the listing of apps a page may call (`sanoma.apps.list`).
|
|
281
314
|
const ops = indexConnectors([
|
|
282
|
-
...
|
|
315
|
+
...running,
|
|
283
316
|
changeOps,
|
|
284
|
-
|
|
317
|
+
appsListOps,
|
|
318
|
+
...(hostTyped ? [sourceOps] : []),
|
|
285
319
|
...(dataStore ? [collectionsConnector(declared)] : []),
|
|
286
320
|
]);
|
|
287
321
|
checkWakeupOps(types, ops);
|
|
288
322
|
checkDatabaseOps(types, ops);
|
|
289
|
-
const fakes = makersOf(
|
|
290
|
-
const instanceFakes = options.fakes === "instance" ? instanceFakesOf(
|
|
323
|
+
const fakes = makersOf(running, "fake");
|
|
324
|
+
const instanceFakes = options.fakes === "instance" ? instanceFakesOf(running, fakes) : undefined;
|
|
291
325
|
const driversByVendor = instanceFakes
|
|
292
326
|
? new Map([...instanceFakes].map(([vendor, fake]) => [vendor, fake.driver]))
|
|
293
|
-
: new Map([...makersOf(
|
|
327
|
+
: new Map([...makersOf(running, "driver")].map(([vendor, make]) => [vendor, make()]));
|
|
328
|
+
const remotes = new Map([...makersOf(config.connectors, "remote")].map(([vendor, make]) => [vendor, make()]));
|
|
294
329
|
const drivers = indexDrivers(
|
|
295
330
|
[
|
|
296
331
|
...driversByVendor.values(),
|
|
297
|
-
changeDriver(),
|
|
298
|
-
|
|
332
|
+
changeDriver(remotes),
|
|
333
|
+
appsListDriver,
|
|
334
|
+
...(hostTyped ? [sourceDriver] : []),
|
|
299
335
|
...(dataStore ? [collectionsDriver(declared, dataStore)] : []),
|
|
300
336
|
],
|
|
301
337
|
ops,
|
|
302
338
|
);
|
|
303
339
|
if (dataStore) fakes.set(DATA_VENDOR, () => collectionsFake(declared));
|
|
304
340
|
const names = new Map<string, WorkflowDefinition<any, any>>();
|
|
305
|
-
for (const wf of config.workflows) {
|
|
341
|
+
for (const wf of config.workflows ?? []) {
|
|
306
342
|
if (BUILT_IN_WORKFLOWS.has(wf.name)) {
|
|
307
343
|
throw new Error(`A workflow is named "${wf.name}", the name of a built-in workflow; name it otherwise`);
|
|
308
344
|
}
|
|
@@ -317,17 +353,21 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
317
353
|
names.set(wf.name, wf);
|
|
318
354
|
}
|
|
319
355
|
// Built-in: checking the resources the data files declare against their vendors, and applying them.
|
|
356
|
+
const connectors = connectorsOf(config);
|
|
357
|
+
// The types the data files may declare: the vendors' and the people's, whose resources are left out.
|
|
358
|
+
const declarable = resourceTypesOf(connectors);
|
|
359
|
+
const read = () => readDeclared(root, connectors, declarable, types);
|
|
320
360
|
if (types.size) {
|
|
321
|
-
const connectors = connectorsOf(config);
|
|
322
|
-
// The types the data files may declare: the vendors' and the people's, whose resources are left out.
|
|
323
|
-
const declarable = resourceTypesOf(connectors);
|
|
324
|
-
const read = () => readDeclared(root, connectors, declarable, types);
|
|
325
361
|
names.set(
|
|
326
362
|
DRIFT_WORKFLOW,
|
|
327
363
|
driftWorkflow(types, ops, drivers, () => DBOS.runStep(read, { name: "drift:data-files" })),
|
|
328
364
|
);
|
|
329
365
|
names.set(APPLY_WORKFLOW, applyWorkflow(types, ops, drivers, applySteps(appName, root, read)));
|
|
330
366
|
}
|
|
367
|
+
// Built-in, in every config: the life of a change, which deploys to the declared hosts after its merge.
|
|
368
|
+
const hosts = async () =>
|
|
369
|
+
(await read()).resources.filter((r) => types.get(`${r.vendor}.${r.type}`)?.host).map((r) => r.id);
|
|
370
|
+
names.set(CHANGE_WORKFLOW, changeWorkflow(changeOps, changeSteps({ remotes, hosts })));
|
|
331
371
|
// Built-in: a direct call the policy holds for an approval, made a run, when any operation may be called directly.
|
|
332
372
|
const call = callWorkflow(ops, drivers, types);
|
|
333
373
|
if (call.uses.length) names.set(CALL_WORKFLOW, call);
|
|
@@ -337,6 +377,7 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
337
377
|
databaseUrl,
|
|
338
378
|
// The built-in's code and operations are part of the app's version, like the config's own.
|
|
339
379
|
version: computeVersion({ appName, workflows: [...names.values()] }),
|
|
380
|
+
changeCode: changeCodeOf(names.get(CHANGE_WORKFLOW)!),
|
|
340
381
|
queueName: `sanoma:${appName}`,
|
|
341
382
|
ops,
|
|
342
383
|
drivers,
|
|
@@ -345,9 +386,11 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
345
386
|
...(instanceFakes && { instanceFakes }),
|
|
346
387
|
logins: makersOf(config.connectors, "login"),
|
|
347
388
|
models: makersOf(config.connectors, "model"),
|
|
389
|
+
remotes,
|
|
390
|
+
hosts,
|
|
348
391
|
...(scenarios && { scenarios }),
|
|
349
392
|
types,
|
|
350
|
-
steps: [...new Set([...
|
|
393
|
+
steps: [...new Set([...running.flatMap((c) => setupOf(c).steps ?? []), ...(config.steps ?? [])])],
|
|
351
394
|
collections,
|
|
352
395
|
...(dataStore && { dataStore }),
|
|
353
396
|
workflows: names,
|
|
@@ -383,9 +426,32 @@ function collectionsOf(config: SanomaConfig): Map<string, Collection> {
|
|
|
383
426
|
return collections;
|
|
384
427
|
}
|
|
385
428
|
|
|
386
|
-
/**
|
|
429
|
+
/**
|
|
430
|
+
* `sanoma.wakeup`, the runtime's own: the type's operations and `sanoma.wakeup.register` (effect
|
|
431
|
+
* `write`), which the worker registers each deadline through before it stops, with its live
|
|
432
|
+
* driver (`sanomaWakeupDriver`), its fake (`fakeSanomaWakeup`) and its step. `withBuiltIns`
|
|
433
|
+
* merges it into every config with a host type.
|
|
434
|
+
*/
|
|
435
|
+
export const sanomaWakeup = withSetup(wakeupOps, {
|
|
436
|
+
driver: sanomaWakeupDriver,
|
|
437
|
+
fake: fakeSanomaWakeup,
|
|
438
|
+
steps: wakeupSteps,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The connectors and, when they declare a host type, the runtime's own wakeup (`sanoma.wakeup`),
|
|
443
|
+
* the fallback for a host with no clock of its own: what a config runs on. The same array when
|
|
444
|
+
* they declare none.
|
|
445
|
+
*/
|
|
446
|
+
const withBuiltIns = (connectors: Connector<any, any>[]): Connector<any, any>[] =>
|
|
447
|
+
[...resourceTypesOf(connectors).values()].some((type) => type.host) ? [...connectors, sanomaWakeup] : connectors;
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* The config's connectors (with `sanoma.wakeup` when they declare a host type) and the runtime's
|
|
451
|
+
* people types' (`peopleConnector`): what its data files are read against.
|
|
452
|
+
*/
|
|
387
453
|
export const connectorsOf = (config: Pick<SanomaConfig, "connectors">): Connector<any, any>[] => [
|
|
388
|
-
...config.connectors,
|
|
454
|
+
...withBuiltIns(config.connectors),
|
|
389
455
|
peopleConnector,
|
|
390
456
|
];
|
|
391
457
|
|
|
@@ -448,18 +514,21 @@ function appNameAt(root: string | undefined): string {
|
|
|
448
514
|
return typeof named === "string" && named.trim() ? named.trim() : "sanoma";
|
|
449
515
|
}
|
|
450
516
|
|
|
451
|
-
/**
|
|
517
|
+
/**
|
|
518
|
+
* The root, as a `file:` URL to the directory (with its trailing slash), when `<root>/scenarios/`
|
|
519
|
+
* or an app's `apps/<app>/scenarios/` is a directory: the scenarios are the feature files there,
|
|
520
|
+
* each named by its path from the root (`scenarioFiles`).
|
|
521
|
+
*/
|
|
452
522
|
function scenariosAt(root: string | undefined): URL | undefined {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
return pathToFileURL(`${dir}/`);
|
|
523
|
+
if (root === undefined || !kindDirs(root, SCENARIOS_DIR).some(isDirectory)) return undefined;
|
|
524
|
+
return pathToFileURL(`${root}/`);
|
|
456
525
|
}
|
|
457
526
|
|
|
458
527
|
/**
|
|
459
528
|
* The one wakeup the config's data files declare, or undefined: with none, and with one the reader
|
|
460
529
|
* leaves out for a problem (its declaration's or its host's; several wakeups are each one).
|
|
461
530
|
* Undefined means the instance has no wakeup, so it must not stop. Server-side: it reads the data
|
|
462
|
-
* files and the lockfile, only when
|
|
531
|
+
* files and the lockfile, only when the config has a wakeup type (`sanoma.wakeup` beside any host type).
|
|
463
532
|
*/
|
|
464
533
|
export async function declaredWakeup(config: SanomaConfig): Promise<DeclaredWakeup | undefined> {
|
|
465
534
|
const connectors = connectorsOf(config);
|
package/src/constructors.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { sanoma as people } from "./person.ts";
|
|
2
|
+
import { wakeup } from "./wakeup-type.ts";
|
|
3
|
+
|
|
1
4
|
// `@sanoma-ai/workflows/resources`: what a data file imports from the runtime: `secret`, which
|
|
2
|
-
// names a stored secret, and `sanoma`, the constructors of its people types (`sanoma.person`, …)
|
|
5
|
+
// names a stored secret, and `sanoma`, the constructors of its people types (`sanoma.person`, …)
|
|
6
|
+
// and of its wakeup, `sanoma.wakeup({ host })`.
|
|
3
7
|
export { secret, type Secret } from "./secret.ts";
|
|
4
|
-
|
|
8
|
+
|
|
9
|
+
/** The runtime's own constructors: the people types' and the wakeup's. */
|
|
10
|
+
export const sanoma = { ...people, wakeup };
|
package/src/db.ts
CHANGED
|
@@ -19,10 +19,9 @@ export const LEDGER_TABLE = "sanoma_ledger";
|
|
|
19
19
|
/** Where the collections keep their records (`collection.ts`): one row per collection and key, the record as JSON. */
|
|
20
20
|
export const DATA_TABLE = "sanoma_data";
|
|
21
21
|
/**
|
|
22
|
-
* The instance's side of its wakeup: one row, made with the table. `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* the last registration failed.
|
|
22
|
+
* The instance's side of its wakeup: one row, made with the table. `at` is the deadline last
|
|
23
|
+
* registered, `stopped_at` when the worker last stopped itself (the schedules backfill from it),
|
|
24
|
+
* `last_error` why the last registration failed.
|
|
26
25
|
*/
|
|
27
26
|
export const WAKEUP_TABLE = "sanoma_wakeup";
|
|
28
27
|
/** The operator agent's chats (`chats.ts`): who each is for, its engine and its draft. */
|
|
@@ -52,9 +51,8 @@ const TABLES: readonly [what: string, ddl: string][] = [
|
|
|
52
51
|
],
|
|
53
52
|
[
|
|
54
53
|
"wakeup",
|
|
55
|
-
`CREATE TABLE IF NOT EXISTS ${WAKEUP_TABLE} (id integer PRIMARY KEY CHECK (id = 1),
|
|
56
|
-
|
|
57
|
-
`last_error text); INSERT INTO ${WAKEUP_TABLE} (id) VALUES (1) ON CONFLICT (id) DO NOTHING`,
|
|
54
|
+
`CREATE TABLE IF NOT EXISTS ${WAKEUP_TABLE} (id integer PRIMARY KEY CHECK (id = 1), at timestamptz, ` +
|
|
55
|
+
`stopped_at timestamptz, last_error text); INSERT INTO ${WAKEUP_TABLE} (id) VALUES (1) ON CONFLICT (id) DO NOTHING`,
|
|
58
56
|
],
|
|
59
57
|
[
|
|
60
58
|
"chats",
|
|
@@ -103,6 +101,29 @@ export function openPool(databaseUrl: string, what: string): Pool {
|
|
|
103
101
|
*/
|
|
104
102
|
export const DBOS_DATABASE_OPTIONS = { systemDatabaseIdleTransactionTimeoutMs: 0 } as const;
|
|
105
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Stamps the change runs `ids` still unfinished on another version with the application version
|
|
106
|
+
* `version`, so DBOS gives them to a worker of that version: those it stamped, each with its status
|
|
107
|
+
* (a PENDING one is the caller's to queue again). DBOS has no call for it (a run's version is set at
|
|
108
|
+
* its start, `forkWorkflow` makes a new id, `rewindWorkflow` takes only an ended run), so this is the
|
|
109
|
+
* runtime's one write to DBOS's own tables, in its schema `dbos` (dbos-sdk 5.2.11). One statement:
|
|
110
|
+
* of two workers stamping a run, one does.
|
|
111
|
+
*/
|
|
112
|
+
export async function stampVersion(
|
|
113
|
+
db: Db,
|
|
114
|
+
ids: readonly string[],
|
|
115
|
+
version: string,
|
|
116
|
+
): Promise<{ id: string; pending: boolean }[]> {
|
|
117
|
+
const { rows } = await db.query<{ workflow_uuid: string; status: string }>(
|
|
118
|
+
`UPDATE dbos.workflow_status SET application_version = $1
|
|
119
|
+
WHERE workflow_uuid = ANY($2) AND name = 'change' AND status IN ('PENDING', 'ENQUEUED')
|
|
120
|
+
AND application_version <> $1
|
|
121
|
+
RETURNING workflow_uuid, status`,
|
|
122
|
+
[version, ids],
|
|
123
|
+
);
|
|
124
|
+
return rows.map((r) => ({ id: r.workflow_uuid, pending: r.status === "PENDING" }));
|
|
125
|
+
}
|
|
126
|
+
|
|
106
127
|
/**
|
|
107
128
|
* A DBOS client of the app `applicationName` on the database at `databaseUrl`, as every client of
|
|
108
129
|
* the runtime, its tests and `@sanoma-ai/testing` make one (`DBOS_DATABASE_OPTIONS`).
|
|
@@ -297,68 +318,20 @@ export async function createTable(db: Db, ddl: string, what: string): Promise<vo
|
|
|
297
318
|
}
|
|
298
319
|
}
|
|
299
320
|
|
|
300
|
-
// The wakeup row's
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
// `at` is the deadline registered for the row's generation, null until one is: a new generation
|
|
304
|
-
// (each start and each stop attempt) clears it, so a wake registered earlier is stale.
|
|
305
|
-
|
|
306
|
-
/** Makes `owner` the worker that registers the instance's wakeups; a running worker needs no wake registered before. */
|
|
307
|
-
export async function claimWakeup(db: Db, owner: string): Promise<void> {
|
|
308
|
-
await db.query(`UPDATE ${WAKEUP_TABLE} SET owner = $1, generation = generation + 1, at = NULL WHERE id = 1`, [owner]);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/** Counts the row's generation up while `owner` still owns it: the new generation, or undefined when another worker owns the row now. */
|
|
312
|
-
export async function nextWakeupGeneration(db: Db, owner: string): Promise<number | undefined> {
|
|
313
|
-
const { rows } = await db.query<{ generation: string }>(
|
|
314
|
-
`UPDATE ${WAKEUP_TABLE} SET generation = generation + 1, at = NULL WHERE id = 1 AND owner = $1 RETURNING generation`,
|
|
315
|
-
[owner],
|
|
316
|
-
);
|
|
317
|
-
return rows[0] && Number(rows[0].generation);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/** Records that the wakeup took `at` for `generation`. */
|
|
321
|
-
export async function recordWakeupRegistration(db: Db, owner: string, generation: number, at: string): Promise<void> {
|
|
322
|
-
await db.query(
|
|
323
|
-
`UPDATE ${WAKEUP_TABLE} SET at = $3, registered_at = now(), last_error = NULL WHERE id = 1 AND owner = $1 AND generation = $2`,
|
|
324
|
-
[owner, generation, at],
|
|
325
|
-
);
|
|
326
|
-
}
|
|
321
|
+
// The wakeup row's writes: one worker registers per instance (the supervisor runs one at a time,
|
|
322
|
+
// and a deploy stops the old container before the new one starts), so none names a worker.
|
|
327
323
|
|
|
328
|
-
/** Records
|
|
329
|
-
export async function
|
|
330
|
-
await db.query(`UPDATE ${WAKEUP_TABLE} SET
|
|
331
|
-
owner,
|
|
332
|
-
generation,
|
|
333
|
-
error,
|
|
334
|
-
]);
|
|
324
|
+
/** Records that the wakeup took `at`. */
|
|
325
|
+
export async function recordWakeupRegistration(db: Db, at: string): Promise<void> {
|
|
326
|
+
await db.query(`UPDATE ${WAKEUP_TABLE} SET at = $1, last_error = NULL WHERE id = 1`, [at]);
|
|
335
327
|
}
|
|
336
328
|
|
|
337
|
-
/** Records
|
|
338
|
-
export async function
|
|
339
|
-
await db.query(`UPDATE ${WAKEUP_TABLE} SET
|
|
340
|
-
owner,
|
|
341
|
-
generation,
|
|
342
|
-
]);
|
|
329
|
+
/** Records why the last registration failed. */
|
|
330
|
+
export async function recordWakeupError(db: Db, error: string): Promise<void> {
|
|
331
|
+
await db.query(`UPDATE ${WAKEUP_TABLE} SET last_error = $1 WHERE id = 1`, [error]);
|
|
343
332
|
}
|
|
344
333
|
|
|
345
|
-
/**
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
* one a later stop replaced, nor one registered by a worker that then kept running. For the
|
|
349
|
-
* route's supervisor, which decides on its own what a wake means when no wakeup is declared.
|
|
350
|
-
*/
|
|
351
|
-
export async function shouldWake(databaseUrl: string, generation: number): Promise<boolean> {
|
|
352
|
-
await prepareDatabase(databaseUrl);
|
|
353
|
-
const conn = await connect(databaseUrl);
|
|
354
|
-
try {
|
|
355
|
-
const { rows } = await conn.query<{ current: boolean }>(
|
|
356
|
-
`SELECT generation = $1 AND at IS NOT NULL AND stopped_at >= registered_at ` +
|
|
357
|
-
`AS current FROM ${WAKEUP_TABLE} WHERE id = 1`,
|
|
358
|
-
[generation],
|
|
359
|
-
);
|
|
360
|
-
return rows[0]?.current === true;
|
|
361
|
-
} finally {
|
|
362
|
-
await conn.end().catch(() => undefined);
|
|
363
|
-
}
|
|
334
|
+
/** Records that the worker stops itself now. */
|
|
335
|
+
export async function recordWakeupStop(db: Db): Promise<void> {
|
|
336
|
+
await db.query(`UPDATE ${WAKEUP_TABLE} SET stopped_at = now() WHERE id = 1`);
|
|
364
337
|
}
|
package/src/deadlines.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
2
3
|
import { dbosStatusesOf } from "./client.ts";
|
|
3
4
|
import type { Db } from "./db.ts";
|
|
4
|
-
import {
|
|
5
|
+
import { recordWakeupError, recordWakeupRegistration, recordWakeupStop } from "./db.ts";
|
|
5
6
|
import type { ResolvedConfig } from "./config.ts";
|
|
6
7
|
import { warn } from "./log.ts";
|
|
7
8
|
import type { CallContext, DriverFn, Op } from "./op.ts";
|
|
@@ -13,8 +14,11 @@ import type { WakeupRegistration } from "./wakeup.ts";
|
|
|
13
14
|
// blocked in a `ctx` wait, and until when. DBOS sees both as PENDING. A worker that registers a
|
|
14
15
|
// wakeup stops itself once nothing can run before the next deadline (`stopWhenIdle`).
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* What a run blocked in a wait waits for: a sleep, until its deadline; or, with no deadline, an
|
|
19
|
+
* approval's decision, a message (a change run's `ChangeEvent`), or another run it started.
|
|
20
|
+
*/
|
|
21
|
+
export type Wait = { kind: "sleep"; until: number } | { kind: "approval" | "message" | "run" };
|
|
18
22
|
|
|
19
23
|
/** A deadline besides the sleeping runs', such as a schedule's next fire; undefined for none. */
|
|
20
24
|
export type DeadlineSource = () => Date | undefined;
|
|
@@ -124,18 +128,15 @@ export async function idle(deadlines: Deadlines, scope: IdleScope, horizonMs: nu
|
|
|
124
128
|
return (await DBOS.getLatestApplicationVersion()).versionName !== scope.version;
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
/** What `onStopped` hears when the worker stopped itself: the deadline it registered
|
|
131
|
+
/** What `onStopped` hears when the worker stopped itself: the deadline it registered, null for none. */
|
|
128
132
|
export interface SelfStop {
|
|
129
133
|
at: string | null;
|
|
130
|
-
generation: number;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
/** What the stop rule acts on: the wakeup row, the wakeup's register operation, and the worker. */
|
|
134
137
|
export interface StopRule {
|
|
135
138
|
/** The wakeup row's database. */
|
|
136
139
|
db: () => Promise<Db>;
|
|
137
|
-
/** This worker's id on the row (`owner`), fresh for each worker. */
|
|
138
|
-
owner: string;
|
|
139
140
|
/** The declared wakeup's binding, else its declaration's id: what scopes a registration's idempotency key to this instance. */
|
|
140
141
|
wakeup: string;
|
|
141
142
|
/** The wakeup type's register operation, and its driver (none when the connector attaches none). */
|
|
@@ -150,12 +151,12 @@ export interface StopRule {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
/**
|
|
153
|
-
* Checks every `everyMs` whether the worker is `idle`; when it is,
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
154
|
+
* Checks every `everyMs` whether the worker is `idle`; when it is, registers the next deadline
|
|
155
|
+
* with the wakeup, replacing what it held (none when there is no deadline at all), and stops the
|
|
156
|
+
* worker, then tells `onStopped`. A failed registration is recorded on the row and logged once for
|
|
157
|
+
* each new message, and the worker keeps running: the next check tries again. A run that arrives
|
|
158
|
+
* while it registers keeps the worker up, since the worker must be idle twice, with the same
|
|
159
|
+
* deadline, before it stops. `close` ends the checks.
|
|
159
160
|
*/
|
|
160
161
|
export function stopWhenIdle(rule: StopRule): { close(): void } {
|
|
161
162
|
let closed = false;
|
|
@@ -171,30 +172,23 @@ export function stopWhenIdle(rule: StopRule): { close(): void } {
|
|
|
171
172
|
const next = rule.deadlines.nextDeadline();
|
|
172
173
|
const at = next?.toISOString() ?? null;
|
|
173
174
|
const db = await rule.db();
|
|
174
|
-
const generation = await nextWakeupGeneration(db, rule.owner);
|
|
175
|
-
if (generation === undefined) {
|
|
176
|
-
// A worker claims the row only when it starts, so this one never gets it back.
|
|
177
|
-
warn("another worker registers this instance's wakeups now, so this one keeps running and stops checking");
|
|
178
|
-
closed = true;
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
175
|
if (at !== null) {
|
|
182
176
|
try {
|
|
183
|
-
await registerWakeup(rule, { at
|
|
177
|
+
await registerWakeup(rule, { at });
|
|
184
178
|
} catch (err) {
|
|
185
179
|
const message = errorMessage(err);
|
|
186
|
-
await recordWakeupError(db,
|
|
180
|
+
await recordWakeupError(db, message);
|
|
187
181
|
problem(`could not register the wakeup at ${at}, so the worker keeps running: ${message}`);
|
|
188
182
|
return false;
|
|
189
183
|
}
|
|
190
|
-
await recordWakeupRegistration(db,
|
|
184
|
+
await recordWakeupRegistration(db, at);
|
|
191
185
|
}
|
|
192
186
|
lastProblem = undefined;
|
|
193
187
|
if (closed || !(await rule.idle()) || rule.deadlines.nextDeadline()?.toISOString() !== next?.toISOString())
|
|
194
188
|
return false;
|
|
195
|
-
await recordWakeupStop(db
|
|
189
|
+
await recordWakeupStop(db);
|
|
196
190
|
await rule.stop().catch((err: unknown) => warn(`the worker could not stop cleanly: ${errorMessage(err)}`));
|
|
197
|
-
rule.onStopped?.({ at
|
|
191
|
+
rule.onStopped?.({ at });
|
|
198
192
|
return true;
|
|
199
193
|
};
|
|
200
194
|
|
|
@@ -222,13 +216,16 @@ export function stopWhenIdle(rule: StopRule): { close(): void } {
|
|
|
222
216
|
|
|
223
217
|
/**
|
|
224
218
|
* Calls the wakeup's register operation, as `SanomaClient.call` calls a driver: its input parsed,
|
|
225
|
-
* a `CallContext` of the runtime's, and no ledger record, which is a run's.
|
|
226
|
-
* is the generation's, so a retried registration is one registration.
|
|
219
|
+
* a `CallContext` of the runtime's, and no ledger record, which is a run's.
|
|
227
220
|
*/
|
|
228
|
-
async function registerWakeup(
|
|
221
|
+
export async function registerWakeup(
|
|
222
|
+
{ register: { op, fn }, secrets, wakeup }: Pick<StopRule, "register" | "secrets" | "wakeup">,
|
|
223
|
+
registration: WakeupRegistration,
|
|
224
|
+
) {
|
|
229
225
|
if (!fn) throw new Error(`${op.id} has no driver: the connector package must attach one`);
|
|
230
|
-
//
|
|
231
|
-
|
|
226
|
+
// Fresh each time: a repeat of an earlier `at` after another registration must re-arm it, so no
|
|
227
|
+
// two registrations share a key (real drivers never dedupe on it anyway).
|
|
228
|
+
const key = `wakeup:${wakeup}:${randomUUID()}`;
|
|
232
229
|
const context: CallContext = { idempotencyKey: key, runId: key, opId: op.id, attempt: 1, secrets, by: WAKEUP_AGENT };
|
|
233
230
|
await fn(op.input.parse(registration), context);
|
|
234
231
|
}
|
package/src/define.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { fileURLToPath } from "node:url";
|
|
|
2
2
|
import { getCallSites } from "node:util";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { isOp, type Op } from "./op.ts";
|
|
5
|
+
import { appOfFile } from "./paths.ts";
|
|
6
|
+
import { type DATA_VENDOR, qualified } from "./shared.ts";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Who is acting: the person or agent that starts a run, calls an operation or decides an
|
|
@@ -171,19 +173,27 @@ export interface StepOptions {
|
|
|
171
173
|
|
|
172
174
|
type OpsOf<U extends readonly Use[]> = Extract<U[number], Op>;
|
|
173
175
|
|
|
174
|
-
type
|
|
175
|
-
readonly [
|
|
176
|
-
readonly [
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
: never;
|
|
183
|
-
};
|
|
176
|
+
type VendorCtx<O extends Op, V> = {
|
|
177
|
+
readonly [R in Extract<O, { vendor: V }>["resource"]]: {
|
|
178
|
+
readonly [N in Extract<O, { vendor: V; resource: R }>["name"]]: Extract<
|
|
179
|
+
O,
|
|
180
|
+
{ vendor: V; resource: R; name: N }
|
|
181
|
+
> extends Op<any, any, any, infer I, infer Out>
|
|
182
|
+
? (input: I, options?: StepOptions) => Promise<Out>
|
|
183
|
+
: never;
|
|
184
184
|
};
|
|
185
185
|
};
|
|
186
186
|
|
|
187
|
+
/**
|
|
188
|
+
* `ctx.<vendor>.<resource>.<op>`. An app's collections are `ctx.data.<app>.<name>` at run time,
|
|
189
|
+
* a folder's name no type knows: that level is untyped (`any`), the root's collections typed.
|
|
190
|
+
*/
|
|
191
|
+
type CtxOps<O extends Op> = {
|
|
192
|
+
readonly [V in O["vendor"]]: V extends typeof DATA_VENDOR
|
|
193
|
+
? VendorCtx<O, V> & { readonly [app: string]: any }
|
|
194
|
+
: VendorCtx<O, V>;
|
|
195
|
+
};
|
|
196
|
+
|
|
187
197
|
interface ApprovalCtx {
|
|
188
198
|
/** Waits, durably, until the approver (or someone in the approver group) approves. Throws `RejectedError` on a rejection, `WithdrawnError` on a withdrawal. */
|
|
189
199
|
approval(title: string, request: ApprovalRequest): Promise<ApprovalResult>;
|
|
@@ -256,7 +266,10 @@ export type Trigger = "manual" | { readonly schedule: string; readonly timezone?
|
|
|
256
266
|
|
|
257
267
|
export interface WorkflowDefinition<U extends readonly Use[] = readonly Use[], S extends z.ZodType = z.ZodType> {
|
|
258
268
|
readonly kind: "workflow";
|
|
269
|
+
/** Its name, as written at the root; in an app's folder, `<app>.<name>` (`app`). */
|
|
259
270
|
readonly name: string;
|
|
271
|
+
/** The app whose folder defined it (`apps/<app>/`), from its `file`; absent for one at the root. */
|
|
272
|
+
readonly app?: string;
|
|
260
273
|
readonly title?: string;
|
|
261
274
|
readonly trigger: Trigger;
|
|
262
275
|
readonly input: S;
|
|
@@ -273,8 +286,13 @@ export interface WorkflowDefinition<U extends readonly Use[] = readonly Use[], S
|
|
|
273
286
|
/** The ids of the operations a workflow `uses`, in the order declared. */
|
|
274
287
|
export const opIdsOf = (wf: Pick<WorkflowDefinition, "uses">): string[] => wf.uses.filter(isOp).map((op) => op.id);
|
|
275
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Defines a workflow in its own file. Its `name` is a lowercase letter, then lowercase letters,
|
|
291
|
+
* digits and dashes; in an app's folder (`apps/<app>/`, `appOfFile`) the runtime names it
|
|
292
|
+
* `<app>.<name>`, so two apps may each have a `sync`.
|
|
293
|
+
*/
|
|
276
294
|
export function defineWorkflow<const U extends readonly Use[], S extends z.ZodType>(
|
|
277
|
-
def: Omit<WorkflowDefinition<U, S>, "kind" | "file">,
|
|
295
|
+
def: Omit<WorkflowDefinition<U, S>, "kind" | "file" | "app">,
|
|
278
296
|
): WorkflowDefinition<U, S> {
|
|
279
297
|
if (!/^[a-z][a-z0-9-]*$/.test(def.name)) {
|
|
280
298
|
throw new Error(`Workflow name "${def.name}" must be lowercase letters, digits and dashes`);
|
|
@@ -293,7 +311,8 @@ export function defineWorkflow<const U extends readonly Use[], S extends z.ZodTy
|
|
|
293
311
|
"its outline is read from the file, and a run is held to the outline",
|
|
294
312
|
);
|
|
295
313
|
}
|
|
296
|
-
|
|
314
|
+
const app = appOfFile(file);
|
|
315
|
+
return Object.freeze({ kind: "workflow", ...def, name: qualified(app, def.name), ...(app && { app }), file });
|
|
297
316
|
}
|
|
298
317
|
|
|
299
318
|
/** True when `{}` parses: a schema zod can check only asynchronously is taken as accepting it, and the run parses it. */
|