@sanoma-ai/workflows 0.8.1 → 0.9.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 +4 -17
- package/dist/apps-list.d.ts +32 -6
- package/dist/apps-list.js +66 -25
- package/dist/apps-list.js.map +1 -1
- package/dist/client.d.ts +1 -13
- package/dist/client.js +2 -22
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +2 -30
- package/dist/config.js +3 -21
- package/dist/config.js.map +1 -1
- package/dist/fake.d.ts +1 -9
- package/dist/fake.js +2 -12
- package/dist/fake.js.map +1 -1
- package/dist/run.d.ts +0 -2
- package/dist/run.js.map +1 -1
- package/dist/sandbox.d.ts +10 -38
- package/dist/sandbox.js +35 -66
- package/dist/sandbox.js.map +1 -1
- package/dist/scenario.d.ts +1 -18
- package/dist/scenario.js +27 -77
- package/dist/scenario.js.map +1 -1
- package/dist/scenarios.d.ts +1 -10
- package/dist/scenarios.js +2 -16
- package/dist/scenarios.js.map +1 -1
- package/dist/sites.d.ts +6 -0
- package/dist/sites.js +49 -10
- package/dist/sites.js.map +1 -1
- package/dist/worker.d.ts +1 -7
- package/dist/worker.js +2 -3
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
- package/src/apps-list.ts +75 -26
- package/src/client.ts +2 -28
- package/src/config.ts +4 -54
- package/src/fake.ts +1 -13
- package/src/run.ts +0 -2
- package/src/sandbox.ts +34 -94
- package/src/scenario.ts +28 -97
- package/src/scenarios.ts +3 -18
- package/src/sites.ts +67 -11
- package/src/worker.ts +2 -9
- package/dist/demo.d.ts +0 -39
- package/dist/demo.js +0 -87
- package/dist/demo.js.map +0 -1
- package/src/demo.ts +0 -118
package/src/sites.ts
CHANGED
|
@@ -63,6 +63,12 @@ export interface SitePage {
|
|
|
63
63
|
* destructured) with `content` from `sanoma/site`.
|
|
64
64
|
*/
|
|
65
65
|
content?: string;
|
|
66
|
+
/**
|
|
67
|
+
* `false` when its route says `staticData: { sitemap: false }`, written out: the sitemap leaves it
|
|
68
|
+
* out. Its index route's `sitemap` (`true` or `false`) decides over a layout's at its path; a
|
|
69
|
+
* pathless or group layout's says nothing.
|
|
70
|
+
*/
|
|
71
|
+
sitemap?: false;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
/** A site, `sites/<name>/` or an app's `apps/<app>/sites/<name>/`, and what it calls. */
|
|
@@ -225,12 +231,27 @@ export function readSites(
|
|
|
225
231
|
});
|
|
226
232
|
}
|
|
227
233
|
|
|
228
|
-
/**
|
|
229
|
-
|
|
234
|
+
/**
|
|
235
|
+
* What a route made by `createFileRoute(id)({ … })` says of the sitemap, `staticData`'s `sitemap`
|
|
236
|
+
* written out (a lazy route's options carry no `staticData`), and how much it is its page's own:
|
|
237
|
+
* an index (2) over a route at its path (1). A pathless or group layout's says nothing.
|
|
238
|
+
*/
|
|
239
|
+
type Says = { listed: boolean; rank: number };
|
|
240
|
+
|
|
241
|
+
/** A route file's page, with what its route says of the sitemap. */
|
|
242
|
+
type FileRoute = Omit<SitePage, "sitemap"> & { says?: Says };
|
|
243
|
+
|
|
244
|
+
/** A site's pages, by path, from its files: a layout's and its index's (one path) merged, the most its own route that says deciding `sitemap`. */
|
|
245
|
+
function pagesOf(read: readonly { route?: FileRoute }[]): Map<string, SitePage> {
|
|
230
246
|
const pages = new Map<string, SitePage>();
|
|
247
|
+
const own = new Map<string, Says>();
|
|
231
248
|
for (const { route } of read) {
|
|
232
|
-
if (route)
|
|
249
|
+
if (!route) continue;
|
|
250
|
+
const { says, ...page } = route;
|
|
251
|
+
pages.set(page.path, { ...pages.get(page.path), ...page });
|
|
252
|
+
if (says && says.rank > (own.get(page.path)?.rank ?? 0)) own.set(page.path, says);
|
|
233
253
|
}
|
|
254
|
+
for (const [path, { listed }] of own) if (!listed) pages.get(path)!.sitemap = false;
|
|
234
255
|
return pages;
|
|
235
256
|
}
|
|
236
257
|
|
|
@@ -318,13 +339,14 @@ type Written = Omit<SiteCall, "target"> & { name: string };
|
|
|
318
339
|
|
|
319
340
|
/**
|
|
320
341
|
* What one file holds: its uses, in its order, and the page it is, from its route's id, with the
|
|
321
|
-
* content kind whose entry its one parameter names (`SitePage.content`)
|
|
322
|
-
*
|
|
342
|
+
* content kind whose entry its one parameter names (`SitePage.content`) and whether its route's
|
|
343
|
+
* options keep it out of the sitemap (`SitePage.sitemap`). A file under `routes/` is read whole;
|
|
344
|
+
* any other only when it imports `api` or `RunForm`.
|
|
323
345
|
*/
|
|
324
346
|
function readSiteFile(
|
|
325
347
|
{ program, at }: SourceFile,
|
|
326
348
|
file: string,
|
|
327
|
-
): { uses: Written[]; route?:
|
|
349
|
+
): { uses: Written[]; route?: FileRoute; warning?: LintProblem & { file: string } } {
|
|
328
350
|
const api = importedAs(program, SITE_SOURCES, "api");
|
|
329
351
|
const runForm = importedAs(program, SITE_SOURCES, "RunForm");
|
|
330
352
|
const content = importedAs(program, SITE_SOURCES, "content");
|
|
@@ -333,6 +355,7 @@ function readSiteFile(
|
|
|
333
355
|
/** Each `content.<kind>.get(arg)`: its kind, its argument and where it is. */
|
|
334
356
|
const gets: { kind: string; arg: Node; start: number }[] = [];
|
|
335
357
|
let page: string | undefined;
|
|
358
|
+
let says: Says | undefined;
|
|
336
359
|
const visit = (node: Node) => {
|
|
337
360
|
if (!node) return;
|
|
338
361
|
if (node.type === "MemberExpression") {
|
|
@@ -341,6 +364,18 @@ function readSiteFile(
|
|
|
341
364
|
if (named) return void found.push({ ...named, via: "api", file, ...at(node.start) });
|
|
342
365
|
} else if (node.type === "CallExpression") {
|
|
343
366
|
const callee = unwrap(node.callee);
|
|
367
|
+
// `createFileRoute(id)({ … })`: its page, and what its options say of the sitemap.
|
|
368
|
+
const maker = callee?.type === "CallExpression" ? unwrap(callee.callee) : undefined;
|
|
369
|
+
const madeId =
|
|
370
|
+
maker?.type === "Identifier" && ROUTE_MAKERS.has(maker.name) && page === undefined
|
|
371
|
+
? stringValue(callee.arguments[0])
|
|
372
|
+
: undefined;
|
|
373
|
+
if (madeId !== undefined) {
|
|
374
|
+
page = pageOf(madeId);
|
|
375
|
+
const listed = maker.name === "createFileRoute" ? sitemapOf(node.arguments[0]) : undefined;
|
|
376
|
+
const rank = madeId.endsWith("/") ? 2 : isPathless(madeId.split("/").at(-1)!) ? 0 : 1;
|
|
377
|
+
if (listed !== undefined && rank) says = { listed, rank };
|
|
378
|
+
}
|
|
344
379
|
if (callee?.type === "Identifier" && ROUTE_MAKERS.has(callee.name) && page === undefined) {
|
|
345
380
|
const id = stringValue(node.arguments[0]);
|
|
346
381
|
if (id !== undefined) page = pageOf(id);
|
|
@@ -360,9 +395,10 @@ function readSiteFile(
|
|
|
360
395
|
visit(program);
|
|
361
396
|
const uses = found.map((use) => (page === undefined ? use : { ...use, page }));
|
|
362
397
|
if (page === undefined) return { uses };
|
|
398
|
+
const route: FileRoute = { path: page, ...(says && { says }) };
|
|
363
399
|
// The one parameter's name (not a splat's), and the kinds whose `get` is given it (`params.slug`, or `slug` destructured).
|
|
364
400
|
const params = page.split("/").filter((segment) => segment.startsWith("$"));
|
|
365
|
-
if (params.length !== 1 || params[0] === "$") return { uses, route
|
|
401
|
+
if (params.length !== 1 || params[0] === "$") return { uses, route };
|
|
366
402
|
const param = params[0]!.slice(1);
|
|
367
403
|
const kinds = new Set(
|
|
368
404
|
gets
|
|
@@ -373,16 +409,33 @@ function readSiteFile(
|
|
|
373
409
|
)
|
|
374
410
|
.map(({ kind }) => kind),
|
|
375
411
|
);
|
|
376
|
-
if (kinds.size === 1) return { uses, route: {
|
|
412
|
+
if (kinds.size === 1) return { uses, route: { ...route, content: [...kinds][0]! } };
|
|
377
413
|
// A read by the parameter in a form the sitemap does not follow (`params.id.toLowerCase()`, two kinds).
|
|
378
414
|
const unfollowed = gets.find(({ arg }) => mentions(arg, param));
|
|
379
|
-
if (!unfollowed) return { uses, route
|
|
415
|
+
if (!unfollowed) return { uses, route };
|
|
380
416
|
const message =
|
|
381
417
|
`the sitemap lists none of this page's entries: it reads content by the page's parameter in a way it cannot follow. ` +
|
|
382
418
|
`Read one kind by it as written, \`content.<kind>.get(params.${param})\` (or \`${param}\` destructured)`;
|
|
383
|
-
return { uses, route
|
|
419
|
+
return { uses, route, warning: { file, ...at(unfollowed.start), message } };
|
|
384
420
|
}
|
|
385
421
|
|
|
422
|
+
/** A route's `options`' `staticData: { sitemap }`, each written out, `sitemap` a boolean; undefined for any other. */
|
|
423
|
+
const sitemapOf = (options: Node): boolean | undefined => {
|
|
424
|
+
const flag = unwrap(valueOf(valueOf(options, "staticData"), "sitemap"));
|
|
425
|
+
return flag?.type === "Literal" && typeof flag.value === "boolean" ? flag.value : undefined;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* The value of an object literal's property `name`, its last, written out; undefined for anything
|
|
430
|
+
* else, or when a spread or a computed key after it could replace it.
|
|
431
|
+
*/
|
|
432
|
+
const valueOf = (object: Node, name: string): Node => {
|
|
433
|
+
const o = unwrap(object);
|
|
434
|
+
const last =
|
|
435
|
+
o?.type === "ObjectExpression" && o.properties.findLast((p: Node) => !isWritten(p) || keyName(p.key) === name);
|
|
436
|
+
return last && isWritten(last) ? last.value : undefined;
|
|
437
|
+
};
|
|
438
|
+
|
|
386
439
|
/** Whether `node` reads the parameter `param` anywhere in it: `params.<param>`, or `param` destructured (not another parameter, nor a property's name, `post.slug` or `{ slug: x }`). */
|
|
387
440
|
const mentions = (node: Node, param: string): boolean => {
|
|
388
441
|
if (!node) return false;
|
|
@@ -555,9 +608,12 @@ function siteOptionsOf(
|
|
|
555
608
|
const pageOf = (id: string): string =>
|
|
556
609
|
`/${id
|
|
557
610
|
.split("/")
|
|
558
|
-
.filter((s) => s &&
|
|
611
|
+
.filter((s) => s && !isPathless(s))
|
|
559
612
|
.map((s) => s.replace(/_$/, ""))
|
|
560
613
|
.join("/")}`;
|
|
561
614
|
|
|
615
|
+
/** A route id's segment the router's path leaves out: a `(group)` or a `_pathless` one. */
|
|
616
|
+
const isPathless = (segment: string): boolean => /^\(.*\)$/.test(segment) || segment.startsWith("_");
|
|
617
|
+
|
|
562
618
|
/** A site's source file: `.ts` or `.tsx`, but a test or a declaration. */
|
|
563
619
|
const isSource = (name: string) => /\.tsx?$/.test(name) && !/\.(test|d)\.tsx?$/.test(name);
|
package/src/worker.ts
CHANGED
|
@@ -93,12 +93,6 @@ export interface WorkerOptions {
|
|
|
93
93
|
* it off, on a database of its own, so no schedule is applied there to start a live run.
|
|
94
94
|
*/
|
|
95
95
|
schedules?: boolean;
|
|
96
|
-
/**
|
|
97
|
-
* The config as the caller resolved it (`resolveConfig(config, options)`), for a worker that
|
|
98
|
-
* shares its drivers with the caller: the instance fakes of `fakes: "instance"`, which a demo's
|
|
99
|
-
* client and reseed call too. The caller closes its `dataStore`, as it made it.
|
|
100
|
-
*/
|
|
101
|
-
resolved?: ResolvedConfig;
|
|
102
96
|
}
|
|
103
97
|
|
|
104
98
|
/**
|
|
@@ -110,7 +104,7 @@ export interface WorkerOptions {
|
|
|
110
104
|
*/
|
|
111
105
|
export async function startWorker(config: SanomaConfig, options: WorkerOptions = {}): Promise<Worker> {
|
|
112
106
|
// Check everything before touching the state a running worker reads.
|
|
113
|
-
const resolved =
|
|
107
|
+
const resolved = resolveConfig(config);
|
|
114
108
|
setLogLevel(options.logLevel);
|
|
115
109
|
// Read once, when it matters: a worker stops itself, registering its next deadline there, only
|
|
116
110
|
// when its caller hears it stop (`onStopped`).
|
|
@@ -132,7 +126,6 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
132
126
|
ops: resolved.ops,
|
|
133
127
|
drivers: resolved.drivers,
|
|
134
128
|
fakes: resolved.fakes,
|
|
135
|
-
...(resolved.instanceFakes && { instanceFakes: resolved.instanceFakes }),
|
|
136
129
|
...(resolved.scenarios && { scenarios: resolved.scenarios }),
|
|
137
130
|
...(options.source && { source: options.source }),
|
|
138
131
|
types: resolved.types,
|
|
@@ -174,7 +167,7 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
174
167
|
() => credentials.close(),
|
|
175
168
|
() => secrets.close(),
|
|
176
169
|
() => ledger.close(),
|
|
177
|
-
async () =>
|
|
170
|
+
async () => resolved.dataStore?.close(),
|
|
178
171
|
]) {
|
|
179
172
|
await close().catch((error: unknown) => (failed ??= { error }));
|
|
180
173
|
}
|
package/dist/demo.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import type { SanomaClient } from "./client.ts";
|
|
2
|
-
import type { ResolvedConfig } from "./config.ts";
|
|
3
|
-
import { type Moment } from "./scenario.ts";
|
|
4
|
-
/** What the last `reseed` of an instance did: what the demo's banner shows. */
|
|
5
|
-
export interface DemoState {
|
|
6
|
-
/** When it was seeded, as an ISO timestamp. */
|
|
7
|
-
seededAt: string;
|
|
8
|
-
/** The worlds applied, by scenario name. */
|
|
9
|
-
worlds: string[];
|
|
10
|
-
/** Each moment started and left waiting: who it is for, where they land, its scenario and its run. */
|
|
11
|
-
moments: (Moment & {
|
|
12
|
-
scenario: string;
|
|
13
|
-
runId: string;
|
|
14
|
-
})[];
|
|
15
|
-
/** What could not be done, one line each: a feature file that did not load, a world or a moment that failed. */
|
|
16
|
-
problems: string[];
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* A half of a demo's seed (`reseed`'s `only`): what goes into its `database` (the runs cancelled,
|
|
20
|
-
* `sanoma_data` emptied, the worlds' collection records, the moments' runs), or into its `memory`
|
|
21
|
-
* (the instance's fakes reset, the worlds' vendor objects and calls). A copy of a seeded database
|
|
22
|
-
* holds the first half but not the second, which a process over it applies to its own fakes.
|
|
23
|
-
*/
|
|
24
|
-
export type SeedHalf = "database" | "memory";
|
|
25
|
-
/**
|
|
26
|
-
* Seeds the demo instance afresh: cancels the app's unfinished runs (`cancelUnfinished`: the last
|
|
27
|
-
* seed's moments among them), empties the collections' table, resets the instance's fakes,
|
|
28
|
-
* applies every world (a scenario with no `When`) to the instance (`applyGivens`: its records
|
|
29
|
-
* saved through the collections' driver, its vendor objects put in the fakes), then starts each
|
|
30
|
-
* moment as a sandbox run by its person, left undriven, so its first approval waits for them.
|
|
31
|
-
* `only` seeds one half (`SeedHalf`). `resolved` is the resolution the worker and `client` run
|
|
32
|
-
* on, made with `fakes: "instance"`. A world or a moment that fails is a problem in the state it
|
|
33
|
-
* returns, and the rest go on.
|
|
34
|
-
*/
|
|
35
|
-
export declare function reseed(client: SanomaClient, resolved: ResolvedConfig, { only }?: {
|
|
36
|
-
only?: SeedHalf;
|
|
37
|
-
}): Promise<DemoState>;
|
|
38
|
-
/** What the instance's last `reseed` did, or undefined before its first. */
|
|
39
|
-
export declare const demoState: (resolved: ResolvedConfig) => DemoState | undefined;
|
package/dist/demo.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { DATA_TABLE } from "./db.js";
|
|
2
|
-
import { applyGivens } from "./sandbox.js";
|
|
3
|
-
import { loadScenarios } from "./scenario.js";
|
|
4
|
-
import { memorySecrets } from "./secret.js";
|
|
5
|
-
import { DATA_VENDOR, errorMessage } from "./shared.js";
|
|
6
|
-
/** Each instance's last reseed, by its resolution. */
|
|
7
|
-
const states = new WeakMap();
|
|
8
|
-
/** Which half a world's `Given` goes to: a collection's operation the database, anything else memory; a fault both, each refusing it. */
|
|
9
|
-
const halfOf = (g, resolved) => "fault" in g ? undefined : "op" in g && resolved.ops.get(g.op)?.vendor === DATA_VENDOR ? "database" : "memory";
|
|
10
|
-
/**
|
|
11
|
-
* Seeds the demo instance afresh: cancels the app's unfinished runs (`cancelUnfinished`: the last
|
|
12
|
-
* seed's moments among them), empties the collections' table, resets the instance's fakes,
|
|
13
|
-
* applies every world (a scenario with no `When`) to the instance (`applyGivens`: its records
|
|
14
|
-
* saved through the collections' driver, its vendor objects put in the fakes), then starts each
|
|
15
|
-
* moment as a sandbox run by its person, left undriven, so its first approval waits for them.
|
|
16
|
-
* `only` seeds one half (`SeedHalf`). `resolved` is the resolution the worker and `client` run
|
|
17
|
-
* on, made with `fakes: "instance"`. A world or a moment that fails is a problem in the state it
|
|
18
|
-
* returns, and the rest go on.
|
|
19
|
-
*/
|
|
20
|
-
export async function reseed(client, resolved, { only } = {}) {
|
|
21
|
-
const fakes = resolved.instanceFakes;
|
|
22
|
-
if (!fakes)
|
|
23
|
-
throw new Error('reseed seeds an instance on fakes: resolve its config with `fakes: "instance"`');
|
|
24
|
-
const into = (half) => only === undefined || only === half;
|
|
25
|
-
// Read first, so scenarios that cannot be read at all leave the instance as it was.
|
|
26
|
-
const { scenarios, errors } = await loadScenarios(resolved);
|
|
27
|
-
if (into("database")) {
|
|
28
|
-
// The last seed's moments, and whatever a visitor started since, end here: nothing is kept.
|
|
29
|
-
await client.cancelUnfinished();
|
|
30
|
-
if (resolved.dataStore)
|
|
31
|
-
await (await resolved.dataStore.db()).query(`TRUNCATE ${DATA_TABLE}`);
|
|
32
|
-
}
|
|
33
|
-
if (into("memory"))
|
|
34
|
-
for (const fake of fakes.values())
|
|
35
|
-
fake.reset();
|
|
36
|
-
const state = {
|
|
37
|
-
seededAt: new Date().toISOString(),
|
|
38
|
-
worlds: [],
|
|
39
|
-
moments: [],
|
|
40
|
-
problems: errors.map((e) => e.message),
|
|
41
|
-
};
|
|
42
|
-
const secrets = memorySecrets();
|
|
43
|
-
for (const world of scenarios.filter((s) => s.workflow === undefined)) {
|
|
44
|
-
// Each Given keeps its place in the world, so its call's idempotency key is the same in either half.
|
|
45
|
-
const kept = [...world.given.entries()].filter(([, g]) => {
|
|
46
|
-
const half = halfOf(g, resolved);
|
|
47
|
-
return half === undefined || into(half);
|
|
48
|
-
});
|
|
49
|
-
try {
|
|
50
|
-
await applyGivens({ name: world.name, given: kept.map(([, g]) => g) }, {
|
|
51
|
-
kind: "instance",
|
|
52
|
-
drivers: resolved.drivers,
|
|
53
|
-
fakes,
|
|
54
|
-
ops: resolved.ops,
|
|
55
|
-
call: (i, opId) => ({
|
|
56
|
-
idempotencyKey: `world:${world.name}:${kept[i][0]}`,
|
|
57
|
-
runId: `world:${world.name}`,
|
|
58
|
-
opId,
|
|
59
|
-
attempt: 1,
|
|
60
|
-
secrets,
|
|
61
|
-
// Who its records are by: whoever it says is acting, else the runtime's own agent.
|
|
62
|
-
by: world.startedBy ?? "scenarios",
|
|
63
|
-
}),
|
|
64
|
-
});
|
|
65
|
-
state.worlds.push(world.name);
|
|
66
|
-
}
|
|
67
|
-
catch (err) {
|
|
68
|
-
state.problems.push(`world "${world.name}": ${errorMessage(err)}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
for (const s of into("database") ? scenarios : []) {
|
|
72
|
-
if (!s.moment || s.workflow === undefined)
|
|
73
|
-
continue;
|
|
74
|
-
try {
|
|
75
|
-
const runId = await client.start(s.workflow, s.input, { sandbox: s.name, startedBy: s.moment.person });
|
|
76
|
-
state.moments.push({ ...s.moment, scenario: s.name, runId });
|
|
77
|
-
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
state.problems.push(`moment "${s.name}": ${errorMessage(err)}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
states.set(resolved, state);
|
|
83
|
-
return state;
|
|
84
|
-
}
|
|
85
|
-
/** What the instance's last `reseed` did, or undefined before its first. */
|
|
86
|
-
export const demoState = (resolved) => states.get(resolved);
|
|
87
|
-
//# sourceMappingURL=demo.js.map
|
package/dist/demo.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"demo.js","sourceRoot":"","sources":["../src/demo.ts"],"names":[],"mappings":"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"}
|
package/src/demo.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import type { SanomaClient } from "./client.ts";
|
|
2
|
-
import type { ResolvedConfig } from "./config.ts";
|
|
3
|
-
import { DATA_TABLE } from "./db.ts";
|
|
4
|
-
import { applyGivens } from "./sandbox.ts";
|
|
5
|
-
import { type Given, loadScenarios, type Moment } from "./scenario.ts";
|
|
6
|
-
import { memorySecrets } from "./secret.ts";
|
|
7
|
-
import { DATA_VENDOR, errorMessage } from "./shared.ts";
|
|
8
|
-
|
|
9
|
-
/*
|
|
10
|
-
* A demo: a company repo run on its connectors' fakes, with the world its scenarios describe.
|
|
11
|
-
* Re-exported from `@sanoma-ai/workflows/scenarios`, server-side, since it loads Cucumber.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/** What the last `reseed` of an instance did: what the demo's banner shows. */
|
|
15
|
-
export interface DemoState {
|
|
16
|
-
/** When it was seeded, as an ISO timestamp. */
|
|
17
|
-
seededAt: string;
|
|
18
|
-
/** The worlds applied, by scenario name. */
|
|
19
|
-
worlds: string[];
|
|
20
|
-
/** Each moment started and left waiting: who it is for, where they land, its scenario and its run. */
|
|
21
|
-
moments: (Moment & { scenario: string; runId: string })[];
|
|
22
|
-
/** What could not be done, one line each: a feature file that did not load, a world or a moment that failed. */
|
|
23
|
-
problems: string[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Each instance's last reseed, by its resolution. */
|
|
27
|
-
const states = new WeakMap<ResolvedConfig, DemoState>();
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* A half of a demo's seed (`reseed`'s `only`): what goes into its `database` (the runs cancelled,
|
|
31
|
-
* `sanoma_data` emptied, the worlds' collection records, the moments' runs), or into its `memory`
|
|
32
|
-
* (the instance's fakes reset, the worlds' vendor objects and calls). A copy of a seeded database
|
|
33
|
-
* holds the first half but not the second, which a process over it applies to its own fakes.
|
|
34
|
-
*/
|
|
35
|
-
export type SeedHalf = "database" | "memory";
|
|
36
|
-
|
|
37
|
-
/** Which half a world's `Given` goes to: a collection's operation the database, anything else memory; a fault both, each refusing it. */
|
|
38
|
-
const halfOf = (g: Given, resolved: ResolvedConfig): SeedHalf | undefined =>
|
|
39
|
-
"fault" in g ? undefined : "op" in g && resolved.ops.get(g.op)?.vendor === DATA_VENDOR ? "database" : "memory";
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Seeds the demo instance afresh: cancels the app's unfinished runs (`cancelUnfinished`: the last
|
|
43
|
-
* seed's moments among them), empties the collections' table, resets the instance's fakes,
|
|
44
|
-
* applies every world (a scenario with no `When`) to the instance (`applyGivens`: its records
|
|
45
|
-
* saved through the collections' driver, its vendor objects put in the fakes), then starts each
|
|
46
|
-
* moment as a sandbox run by its person, left undriven, so its first approval waits for them.
|
|
47
|
-
* `only` seeds one half (`SeedHalf`). `resolved` is the resolution the worker and `client` run
|
|
48
|
-
* on, made with `fakes: "instance"`. A world or a moment that fails is a problem in the state it
|
|
49
|
-
* returns, and the rest go on.
|
|
50
|
-
*/
|
|
51
|
-
export async function reseed(
|
|
52
|
-
client: SanomaClient,
|
|
53
|
-
resolved: ResolvedConfig,
|
|
54
|
-
{ only }: { only?: SeedHalf } = {},
|
|
55
|
-
): Promise<DemoState> {
|
|
56
|
-
const fakes = resolved.instanceFakes;
|
|
57
|
-
if (!fakes) throw new Error('reseed seeds an instance on fakes: resolve its config with `fakes: "instance"`');
|
|
58
|
-
const into = (half: SeedHalf) => only === undefined || only === half;
|
|
59
|
-
// Read first, so scenarios that cannot be read at all leave the instance as it was.
|
|
60
|
-
const { scenarios, errors } = await loadScenarios(resolved);
|
|
61
|
-
if (into("database")) {
|
|
62
|
-
// The last seed's moments, and whatever a visitor started since, end here: nothing is kept.
|
|
63
|
-
await client.cancelUnfinished();
|
|
64
|
-
if (resolved.dataStore) await (await resolved.dataStore.db()).query(`TRUNCATE ${DATA_TABLE}`);
|
|
65
|
-
}
|
|
66
|
-
if (into("memory")) for (const fake of fakes.values()) fake.reset();
|
|
67
|
-
const state: DemoState = {
|
|
68
|
-
seededAt: new Date().toISOString(),
|
|
69
|
-
worlds: [],
|
|
70
|
-
moments: [],
|
|
71
|
-
problems: errors.map((e) => e.message),
|
|
72
|
-
};
|
|
73
|
-
const secrets = memorySecrets();
|
|
74
|
-
for (const world of scenarios.filter((s) => s.workflow === undefined)) {
|
|
75
|
-
// Each Given keeps its place in the world, so its call's idempotency key is the same in either half.
|
|
76
|
-
const kept = [...world.given.entries()].filter(([, g]) => {
|
|
77
|
-
const half = halfOf(g, resolved);
|
|
78
|
-
return half === undefined || into(half);
|
|
79
|
-
});
|
|
80
|
-
try {
|
|
81
|
-
await applyGivens(
|
|
82
|
-
{ name: world.name, given: kept.map(([, g]) => g) },
|
|
83
|
-
{
|
|
84
|
-
kind: "instance",
|
|
85
|
-
drivers: resolved.drivers,
|
|
86
|
-
fakes,
|
|
87
|
-
ops: resolved.ops,
|
|
88
|
-
call: (i, opId) => ({
|
|
89
|
-
idempotencyKey: `world:${world.name}:${kept[i]![0]}`,
|
|
90
|
-
runId: `world:${world.name}`,
|
|
91
|
-
opId,
|
|
92
|
-
attempt: 1,
|
|
93
|
-
secrets,
|
|
94
|
-
// Who its records are by: whoever it says is acting, else the runtime's own agent.
|
|
95
|
-
by: world.startedBy ?? "scenarios",
|
|
96
|
-
}),
|
|
97
|
-
},
|
|
98
|
-
);
|
|
99
|
-
state.worlds.push(world.name);
|
|
100
|
-
} catch (err) {
|
|
101
|
-
state.problems.push(`world "${world.name}": ${errorMessage(err)}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
for (const s of into("database") ? scenarios : []) {
|
|
105
|
-
if (!s.moment || s.workflow === undefined) continue;
|
|
106
|
-
try {
|
|
107
|
-
const runId = await client.start(s.workflow, s.input, { sandbox: s.name, startedBy: s.moment.person });
|
|
108
|
-
state.moments.push({ ...s.moment, scenario: s.name, runId });
|
|
109
|
-
} catch (err) {
|
|
110
|
-
state.problems.push(`moment "${s.name}": ${errorMessage(err)}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
states.set(resolved, state);
|
|
114
|
-
return state;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** What the instance's last `reseed` did, or undefined before its first. */
|
|
118
|
-
export const demoState = (resolved: ResolvedConfig): DemoState | undefined => states.get(resolved);
|