@sanoma-ai/workflows 0.9.0 → 0.10.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 +169 -52
- package/dist/apply-steps.js +5 -2
- package/dist/apply-steps.js.map +1 -1
- package/dist/apply.d.ts +3 -0
- package/dist/apply.js +35 -21
- package/dist/apply.js.map +1 -1
- package/dist/approvals.d.ts +1 -0
- package/dist/approvals.js +6 -1
- package/dist/approvals.js.map +1 -1
- package/dist/apps.d.ts +7 -24
- package/dist/apps.js +51 -67
- package/dist/apps.js.map +1 -1
- package/dist/ast.d.ts +9 -7
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -1
- package/dist/authentication.d.ts +24 -0
- package/dist/authentication.js +68 -0
- package/dist/authentication.js.map +1 -0
- package/dist/call-workflow.d.ts +5 -0
- package/dist/call-workflow.js +4 -3
- package/dist/call-workflow.js.map +1 -1
- package/dist/call.d.ts +3 -2
- package/dist/call.js +34 -7
- package/dist/call.js.map +1 -1
- package/dist/cedar.js +14 -1
- package/dist/cedar.js.map +1 -1
- package/dist/change-steps.js +7 -5
- package/dist/change-steps.js.map +1 -1
- package/dist/change.d.ts +7 -0
- package/dist/change.js +7 -0
- package/dist/change.js.map +1 -1
- package/dist/client.d.ts +10 -6
- package/dist/client.js +96 -43
- package/dist/client.js.map +1 -1
- package/dist/collection.d.ts +7 -0
- package/dist/collection.js +5 -2
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +14 -3
- package/dist/config.js +55 -14
- package/dist/config.js.map +1 -1
- package/dist/connection.d.ts +168 -0
- package/dist/connection.js +327 -0
- package/dist/connection.js.map +1 -0
- package/dist/credentials.d.ts +7 -53
- package/dist/credentials.js +84 -247
- package/dist/credentials.js.map +1 -1
- package/dist/cucumber.d.ts +4 -2
- package/dist/cucumber.js +8 -3
- package/dist/cucumber.js.map +1 -1
- package/dist/db.js +21 -2
- package/dist/db.js.map +1 -1
- package/dist/define.d.ts +9 -2
- package/dist/define.js.map +1 -1
- package/dist/definitions.d.ts +20 -0
- package/dist/definitions.js +16 -0
- package/dist/definitions.js.map +1 -0
- package/dist/deployment-key.d.ts +2 -0
- package/dist/deployment-key.js +22 -0
- package/dist/deployment-key.js.map +1 -0
- package/dist/describe.d.ts +9 -177
- package/dist/describe.js +71 -34
- package/dist/describe.js.map +1 -1
- package/dist/description.d.ts +532 -0
- package/dist/description.js +381 -0
- package/dist/description.js.map +1 -0
- package/dist/direct.js +3 -0
- package/dist/direct.js.map +1 -1
- package/dist/drift.d.ts +1 -1
- package/dist/drift.js +1 -1
- package/dist/drift.js.map +1 -1
- package/dist/env.d.ts +2 -0
- package/dist/env.js +2 -2
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +2 -0
- package/dist/ledger.js +1 -1
- package/dist/ledger.js.map +1 -1
- package/dist/lint.d.ts +0 -9
- package/dist/lint.js +1 -16
- package/dist/lint.js.map +1 -1
- package/dist/load.d.ts +74 -0
- package/dist/load.js +357 -0
- package/dist/load.js.map +1 -0
- package/dist/lockfile.d.ts +2 -0
- package/dist/lockfile.js +8 -3
- package/dist/lockfile.js.map +1 -1
- package/dist/op.d.ts +10 -1
- package/dist/op.js +1 -1
- package/dist/op.js.map +1 -1
- package/dist/outline.js +13 -4
- package/dist/outline.js.map +1 -1
- package/dist/paths.d.ts +18 -0
- package/dist/paths.js +21 -3
- package/dist/paths.js.map +1 -1
- package/dist/plugin.d.ts +0 -11
- package/dist/plugin.js +3 -18
- package/dist/plugin.js.map +1 -1
- package/dist/policy.d.ts +2 -0
- package/dist/policy.js.map +1 -1
- package/dist/resource.d.ts +27 -4
- package/dist/resource.js +49 -4
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +33 -76
- package/dist/resources.js +219 -227
- package/dist/resources.js.map +1 -1
- package/dist/run.d.ts +2 -0
- package/dist/run.js.map +1 -1
- package/dist/sandbox.d.ts +1 -1
- package/dist/sandbox.js +7 -3
- package/dist/sandbox.js.map +1 -1
- package/dist/scenario.d.ts +7 -0
- package/dist/scenario.js +33 -3
- package/dist/scenario.js.map +1 -1
- package/dist/scenarios.js +7 -1
- package/dist/scenarios.js.map +1 -1
- package/dist/schema.js +8 -0
- package/dist/schema.js.map +1 -1
- package/dist/secret-store-check.d.ts +11 -0
- package/dist/secret-store-check.js +76 -0
- package/dist/secret-store-check.js.map +1 -0
- package/dist/secret-store.d.ts +106 -0
- package/dist/secret-store.js +228 -0
- package/dist/secret-store.js.map +1 -0
- package/dist/shared.d.ts +17 -11
- package/dist/shared.js +34 -7
- package/dist/shared.js.map +1 -1
- package/dist/sites.d.ts +4 -93
- package/dist/sites.js +61 -84
- package/dist/sites.js.map +1 -1
- package/dist/source.d.ts +6 -0
- package/dist/source.js +13 -0
- package/dist/source.js.map +1 -1
- package/dist/steps.d.ts +3 -1
- package/dist/steps.js.map +1 -1
- package/dist/version.d.ts +2 -1
- package/dist/version.js +2 -1
- package/dist/version.js.map +1 -1
- package/dist/wakeup-driver.js +15 -6
- package/dist/wakeup-driver.js.map +1 -1
- package/dist/worker.js +12 -8
- package/dist/worker.js.map +1 -1
- package/dist/writer.d.ts +53 -14
- package/dist/writer.js +113 -43
- package/dist/writer.js.map +1 -1
- package/oxlint.json +0 -5
- package/package.json +6 -2
- package/src/apply-steps.ts +5 -2
- package/src/apply.ts +70 -29
- package/src/approvals.ts +7 -1
- package/src/apps.ts +54 -85
- package/src/ast.ts +6 -6
- package/src/authentication.ts +74 -0
- package/src/call-workflow.ts +5 -4
- package/src/call.ts +53 -9
- package/src/cedar.ts +14 -1
- package/src/change-steps.ts +7 -4
- package/src/change.ts +10 -0
- package/src/client.ts +108 -41
- package/src/collection.ts +10 -2
- package/src/config.ts +72 -13
- package/src/connection.ts +446 -0
- package/src/credentials.ts +94 -281
- package/src/cucumber.ts +16 -3
- package/src/db.ts +21 -2
- package/src/define.ts +7 -2
- package/src/definitions.ts +31 -0
- package/src/deployment-key.ts +23 -0
- package/src/describe.ts +102 -230
- package/src/description.ts +426 -0
- package/src/direct.ts +3 -0
- package/src/drift.ts +10 -2
- package/src/env.ts +4 -2
- package/src/index.ts +42 -1
- package/src/ledger.ts +3 -1
- package/src/lint.ts +1 -16
- package/src/load.ts +437 -0
- package/src/lockfile.ts +10 -3
- package/src/op.ts +10 -1
- package/src/outline.ts +18 -5
- package/src/paths.ts +24 -2
- package/src/plugin.ts +3 -24
- package/src/policy.ts +2 -0
- package/src/resource.ts +67 -6
- package/src/resources.ts +246 -301
- package/src/run.ts +2 -0
- package/src/sandbox.ts +11 -3
- package/src/scenario.ts +45 -3
- package/src/scenarios.ts +7 -1
- package/src/schema.ts +10 -0
- package/src/secret-store-check.ts +93 -0
- package/src/secret-store.ts +314 -0
- package/src/shared.ts +35 -10
- package/src/sites.ts +70 -180
- package/src/source.ts +14 -0
- package/src/steps.ts +3 -1
- package/src/version.ts +2 -1
- package/src/wakeup-driver.ts +22 -6
- package/src/worker.ts +12 -8
- package/src/writer.ts +145 -56
- package/dist/datafile.d.ts +0 -97
- package/dist/datafile.js +0 -479
- package/dist/datafile.js.map +0 -1
- package/src/datafile.ts +0 -601
package/src/sites.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
-
import { basename, join
|
|
3
|
-
import {
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { childrenOf, keyName, type LintProblem, type Node, readSource, type SourceFile } from "./ast.ts";
|
|
4
5
|
import { type CedarPolicySource, visitorPermits } from "./cedar.ts";
|
|
5
|
-
import {
|
|
6
|
-
import type { ConfigDescription } from "./describe.ts";
|
|
6
|
+
import type { ConfigDescription, SiteCall, SiteEntry, SitePage } from "./description.ts";
|
|
7
7
|
import { stringValue, unwrap } from "./outline.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { exportsOf, loadModules } from "./load.ts";
|
|
9
|
+
import { APPS_DIR, appFolders, filesUnder, isDirectory, relPath, SITE_ENTRIES } from "./paths.ts";
|
|
10
|
+
import type { ResourceProblem } from "./resources.ts";
|
|
9
11
|
import { START } from "./schema.ts";
|
|
10
12
|
import {
|
|
11
13
|
ANONYMOUS,
|
|
@@ -19,100 +21,14 @@ import {
|
|
|
19
21
|
WORKFLOW_ACTION,
|
|
20
22
|
} from "./shared.ts";
|
|
21
23
|
|
|
22
|
-
// What a company's sites call, read from their source with oxc and never run
|
|
23
|
-
// a workflow's page shows as Used by, and
|
|
24
|
+
// What a company's sites call, read from their source with oxc and never run (their pages are
|
|
25
|
+
// browser code, the site's outline): the reverse index a workflow's page shows as Used by, and
|
|
26
|
+
// the names `sanoma check` holds to the config. A site's `site.ts` is loaded.
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
export interface SiteCall {
|
|
27
|
-
/** What it calls: a workflow, a collection (`api.data.<name>.<op>`) or an operation (`api.<vendor>.<resource>.<op>`). */
|
|
28
|
-
kind: "workflow" | "collection" | "op";
|
|
29
|
-
/**
|
|
30
|
-
* The workflow's name (`RunForm`'s `workflow`, or the workflow whose `apiNameOf` is the name
|
|
31
|
-
* `api` gives; that name itself when none is), the collection's name, or the operation's id.
|
|
32
|
-
*/
|
|
33
|
-
target: string;
|
|
34
|
-
/**
|
|
35
|
-
* What it calls of the workflow (`start`, or a read of its runs: `result`, `latest`, …) or the
|
|
36
|
-
* collection (`list`, `save`, …), as written; absent for a workflow called as itself or by
|
|
37
|
-
* `RunForm`, which starts it, and for an operation.
|
|
38
|
-
*/
|
|
39
|
-
method?: string;
|
|
40
|
-
/** `<RunForm workflow="…" />` or a call on `api`, from `sanoma/site`. */
|
|
41
|
-
via: "RunForm" | "api";
|
|
42
|
-
/** The file, relative to the config's root, with `/`: `sites/sales/routes/index.tsx`. */
|
|
43
|
-
file: string;
|
|
44
|
-
/** From 1. */
|
|
45
|
-
line: number;
|
|
46
|
-
/** From 1, in UTF-16 code units. */
|
|
47
|
-
column: number;
|
|
48
|
-
/**
|
|
49
|
-
* The path of the page the file is, from its `createFileRoute` (or `createLazyFileRoute`) id: groups and pathless
|
|
50
|
-
* segments left out, no trailing slash (`/sites/sales`). Absent for a file that is no page,
|
|
51
|
-
* such as a component's.
|
|
52
|
-
*/
|
|
53
|
-
page?: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** A page of a site: a route file's path, and for a page per content entry the kind it reads. */
|
|
57
|
-
export interface SitePage {
|
|
58
|
-
/** As `SiteCall.page` has it: `/sites/www`, `/sites/www/blog/$slug`. */
|
|
59
|
-
path: string;
|
|
60
|
-
/**
|
|
61
|
-
* For a path with one parameter (`$slug`), the content kind whose entries are its pages: the one
|
|
62
|
-
* kind its file reads by that parameter, `content.<kind>.get(params.slug)` (or `slug`,
|
|
63
|
-
* destructured) with `content` from `sanoma/site`.
|
|
64
|
-
*/
|
|
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;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** A site, `sites/<name>/` or an app's `apps/<app>/sites/<name>/`, and what it calls. */
|
|
75
|
-
export interface SiteEntry {
|
|
76
|
-
/** Its folder's name, `<app>/<name>` for an app's (`siteKey`): where it is mounted, `/sites/<name>`. */
|
|
77
|
-
name: string;
|
|
78
|
-
/** The app whose folder holds it (`apps/<app>/sites/`); absent for one at the root. */
|
|
79
|
-
app?: string;
|
|
80
|
-
/** `defineSite({ title })` in its `site.ts`, when that is a string literal. */
|
|
81
|
-
title?: string;
|
|
82
|
-
/** `defineSite({ icon })` in its `site.ts`: a Lucide icon's name, as written. */
|
|
83
|
-
icon?: string;
|
|
84
|
-
/** `defineSite({ public: true })`: served to visitors, not people. */
|
|
85
|
-
public?: true;
|
|
86
|
-
/** `defineSite({ login })`, the connector whose login half signs visitors in, as written: one that names none is among `problems`. */
|
|
87
|
-
login?: string;
|
|
88
|
-
/**
|
|
89
|
-
* `defineSite({ locales })`, a public site's languages, the first its default: kept only when
|
|
90
|
-
* every one is a `LOCALE`, written once, on a public site with no login; else a problem.
|
|
91
|
-
*/
|
|
92
|
-
locales?: string[];
|
|
93
|
-
/** The host a public site answers on: set by `startApp` from its `domains`, never by `readSites`. */
|
|
94
|
-
domain?: string;
|
|
95
|
-
/** Its pages, by path: one for each path its `routes/` files give (a layout and its index are one). */
|
|
96
|
-
pages: SitePage[];
|
|
97
|
-
/** Its calls, by file and then in the file's order. */
|
|
98
|
-
calls: SiteCall[];
|
|
99
|
-
/**
|
|
100
|
-
* Each call to a workflow, a collection or an operation the config does not have, or to an
|
|
101
|
-
* operation a page may not call directly, or, on a public site, that no policy lets a visitor
|
|
102
|
-
* make, at its file; then what is wrong in its `site.ts`.
|
|
103
|
-
*/
|
|
104
|
-
problems: (LintProblem & { file: string })[];
|
|
105
|
-
/**
|
|
106
|
-
* On a public site, each page with one parameter whose file reads content the sitemap cannot
|
|
107
|
-
* tie to it (`SitePage.content`), so the sitemap lists none of its pages: at the first read.
|
|
108
|
-
*/
|
|
109
|
-
warnings: (LintProblem & { file: string })[];
|
|
110
|
-
}
|
|
28
|
+
export type { SiteCall, SiteEntry, SitePage };
|
|
111
29
|
|
|
112
30
|
/** Where a site's code is: its pages and its components. */
|
|
113
31
|
const SITE_DIRS = ["routes", "components"];
|
|
114
|
-
/** Where `api`, `RunForm` and `defineSite` come from: `sanoma/site`, which re-exports the app's. */
|
|
115
|
-
const SITE_SOURCES: ReadonlySet<string> = new Set(["sanoma/site", "@sanoma-ai/app/site"]);
|
|
116
32
|
const KIND_NAMES = { workflow: "workflow", collection: "collection", op: "operation" } as const;
|
|
117
33
|
/** What a route's loader and head name its path's parameters by. */
|
|
118
34
|
const PARAMS: ReadonlySet<string> = new Set(["params"]);
|
|
@@ -212,15 +128,14 @@ export function readSites(
|
|
|
212
128
|
const read = readSiteFiles(root, dir);
|
|
213
129
|
const resolved = read.flatMap(({ uses }) => uses.filter((use) => !isAppCollection(use)).map(resolve));
|
|
214
130
|
const pages = pagesOf(read);
|
|
215
|
-
const
|
|
216
|
-
const { problems: optionProblems, ...options } = siteOptionsOf(join(root, siteFile), connectors);
|
|
131
|
+
const { problems: optionProblems, ...options } = siteDefinitionOf(root, dir, connectors);
|
|
217
132
|
const calls = resolved.map((r) => r.call);
|
|
218
133
|
const problems = [
|
|
219
134
|
...resolved.flatMap(({ call, problem }) => {
|
|
220
135
|
const message = problem ?? (options.public ? refused(call, options.login) : undefined);
|
|
221
136
|
return message === undefined ? [] : [{ file: call.file, line: call.line, column: call.column, message }];
|
|
222
137
|
}),
|
|
223
|
-
...optionProblems
|
|
138
|
+
...optionProblems,
|
|
224
139
|
];
|
|
225
140
|
const sorted = [...pages.keys()].toSorted().map((path) => pages.get(path)!);
|
|
226
141
|
// A page's file it warns of, unless another of its files (a lazy one, its layout) gives its kind.
|
|
@@ -261,7 +176,7 @@ const readSiteFiles = (root: string, dir: string) =>
|
|
|
261
176
|
.toSorted()
|
|
262
177
|
.flatMap((path) => {
|
|
263
178
|
const file = readSource(path);
|
|
264
|
-
return typeof file === "string" ? [] : [readSiteFile(file,
|
|
179
|
+
return typeof file === "string" ? [] : [readSiteFile(file, relPath(root, path))];
|
|
265
180
|
});
|
|
266
181
|
|
|
267
182
|
/**
|
|
@@ -282,13 +197,13 @@ export function siteContentPages(root: string): Record<string, string> {
|
|
|
282
197
|
/**
|
|
283
198
|
* Each site's languages (`defineSite({ locales })`, the first its default) under `root`, by name,
|
|
284
199
|
* for the sites whose `site.ts` gives them without a problem there: what the app's build gives
|
|
285
|
-
* its router (`virtual:sanoma/locales`).
|
|
200
|
+
* its router (`virtual:sanoma/locales`). Loaded as `readSites` loads them.
|
|
286
201
|
*/
|
|
287
202
|
export function siteLocales(root: string): Record<string, string[]> {
|
|
288
203
|
const none = new Set<string>();
|
|
289
204
|
return Object.fromEntries(
|
|
290
205
|
sitesUnder(root).flatMap(({ name, dir }) => {
|
|
291
|
-
const { locales } =
|
|
206
|
+
const { locales } = siteDefinitionOf(root, dir, { vendors: none, logins: none });
|
|
292
207
|
return locales ? [[name, locales]] : [];
|
|
293
208
|
}),
|
|
294
209
|
);
|
|
@@ -297,13 +212,13 @@ export function siteLocales(root: string): Record<string, string[]> {
|
|
|
297
212
|
/**
|
|
298
213
|
* Each site's icon (`defineSite({ icon })`, a Lucide icon's name as written) under `root`, by name,
|
|
299
214
|
* for the sites whose `site.ts` gives one: the icons the app's build bundles (`virtual:sanoma/icons`).
|
|
300
|
-
*
|
|
215
|
+
* Loaded as `readSites` loads it.
|
|
301
216
|
*/
|
|
302
217
|
export function siteIcons(root: string): Record<string, string> {
|
|
303
218
|
const none = new Set<string>();
|
|
304
219
|
return Object.fromEntries(
|
|
305
220
|
sitesUnder(root).flatMap(({ name, dir }) => {
|
|
306
|
-
const { icon } =
|
|
221
|
+
const { icon } = siteDefinitionOf(root, dir, { vendors: none, logins: none });
|
|
307
222
|
return icon === undefined ? [] : [[name, icon]];
|
|
308
223
|
}),
|
|
309
224
|
);
|
|
@@ -347,9 +262,9 @@ function readSiteFile(
|
|
|
347
262
|
{ program, at }: SourceFile,
|
|
348
263
|
file: string,
|
|
349
264
|
): { uses: Written[]; route?: FileRoute; warning?: LintProblem & { file: string } } {
|
|
350
|
-
const api = importedAs(program,
|
|
351
|
-
const runForm = importedAs(program,
|
|
352
|
-
const content = importedAs(program,
|
|
265
|
+
const api = importedAs(program, SITE_ENTRIES, "api");
|
|
266
|
+
const runForm = importedAs(program, SITE_ENTRIES, "RunForm");
|
|
267
|
+
const content = importedAs(program, SITE_ENTRIES, "content");
|
|
353
268
|
if (!api.size && !runForm.size && !file.includes("/routes/")) return { uses: [] };
|
|
354
269
|
const found: Omit<Written, "page">[] = [];
|
|
355
270
|
/** Each `content.<kind>.get(arg)`: its kind, its argument and where it is. */
|
|
@@ -504,99 +419,74 @@ type SiteConnectors = { vendors: ReadonlySet<string>; logins: ReadonlySet<string
|
|
|
504
419
|
/** A property of an object literal that is written out: not a spread, its name not computed. */
|
|
505
420
|
const isWritten = (p: Node) => p.type === "Property" && !p.computed;
|
|
506
421
|
|
|
507
|
-
/** What `site.ts`
|
|
508
|
-
const
|
|
422
|
+
/** What `site.ts`'s default export, `defineSite({ … })`, may say. */
|
|
423
|
+
const SiteDefinitionSchema = z.strictObject({
|
|
424
|
+
title: z.string().optional(),
|
|
425
|
+
icon: z.string().optional(),
|
|
426
|
+
public: z.boolean().optional(),
|
|
427
|
+
login: z.string().optional(),
|
|
428
|
+
locales: z.array(z.string()).optional(),
|
|
429
|
+
});
|
|
509
430
|
|
|
510
431
|
/**
|
|
511
|
-
* What
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
* `locales` is kept only when it has no problem.
|
|
432
|
+
* What the site in `dir` (relative to `root`) says of itself in its `site.ts`, loaded
|
|
433
|
+
* (`loadModules`, `defineSite` served from `sanoma/site`), and what is wrong there: its default
|
|
434
|
+
* export, `defineSite({ … })`, held to its schema; then a `login` on a site that is not public, or
|
|
435
|
+
* naming no connector of the config with a login half, and `locales` that are not a public site's
|
|
436
|
+
* (with no login) language tags, each once. `locales` is kept only when it has no problem. Nothing
|
|
437
|
+
* for no file, or no default export.
|
|
516
438
|
*/
|
|
517
|
-
function
|
|
518
|
-
|
|
439
|
+
function siteDefinitionOf(
|
|
440
|
+
root: string,
|
|
441
|
+
dir: string,
|
|
519
442
|
connectors: SiteConnectors,
|
|
520
|
-
): Pick<SiteEntry, "title" | "icon" | "public" | "login" | "locales"> & {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
defineSite
|
|
536
|
-
(options === undefined || options.type === "ObjectExpression");
|
|
537
|
-
if (!written) {
|
|
538
|
-
problem(
|
|
539
|
-
exported,
|
|
540
|
-
unread("its default export", '`defineSite({ … })` from "sanoma/site", its options an object literal'),
|
|
541
|
-
);
|
|
542
|
-
return { problems };
|
|
543
|
-
}
|
|
544
|
-
for (const p of options?.properties ?? []) {
|
|
545
|
-
if (!isWritten(p)) problem(p, unread("each of `defineSite`'s options", "a property with its name"));
|
|
443
|
+
): Pick<SiteEntry, "at" | "title" | "icon" | "public" | "login" | "locales"> & {
|
|
444
|
+
problems: (ResourceProblem & { file: string })[];
|
|
445
|
+
} {
|
|
446
|
+
const file = `${dir}/site.ts`;
|
|
447
|
+
if (!existsSync(join(root, file))) return { problems: [] };
|
|
448
|
+
// A helper it imports threw: said at the helper.
|
|
449
|
+
const loaded = exportsOf(loadModules(root, [join(root, file)], []), file);
|
|
450
|
+
if (loaded === undefined) return { problems: [] };
|
|
451
|
+
if ("problem" in loaded) return { problems: [loaded.problem] };
|
|
452
|
+
const value = loaded.exports.find(([name]) => name === "default")?.[1];
|
|
453
|
+
if (value === undefined) return { problems: [] };
|
|
454
|
+
const problem = (message: string) => ({ file, export: "default", message });
|
|
455
|
+
const parsed = SiteDefinitionSchema.safeParse(value);
|
|
456
|
+
if (!parsed.success) {
|
|
457
|
+
const why = z.prettifyError(parsed.error).replaceAll(/\s*\n\s*/g, " ");
|
|
458
|
+
return { problems: [problem(`its default export is \`defineSite({ … })\` from "sanoma/site": ${why}`)] };
|
|
546
459
|
}
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
options?.properties.findLast((p: Node) => isWritten(p) && keyName(p.key) === name);
|
|
550
|
-
|
|
551
|
-
const title = stringValue(property("title")?.value);
|
|
552
|
-
// Cosmetic: one that is not a string (tsc refuses it) is shown as none, never a problem that stops a site.
|
|
553
|
-
const icon = stringValue(property("icon")?.value);
|
|
554
|
-
const publicAt = property("public");
|
|
555
|
-
const flag = unwrap(publicAt?.value);
|
|
556
|
-
const isPublic = flag?.type === "Literal" && flag.value === true;
|
|
557
|
-
const publicUnread = publicAt !== undefined && !(flag?.type === "Literal" && typeof flag.value === "boolean");
|
|
558
|
-
if (publicUnread) problem(publicAt, unread("`public`", "`true` or `false`"));
|
|
559
|
-
|
|
560
|
-
const loginAt = property("login");
|
|
561
|
-
const login = stringValue(loginAt?.value);
|
|
460
|
+
const { title, icon, public: isPublic, login, locales } = parsed.data;
|
|
461
|
+
const problems: (ResourceProblem & { file: string })[] = [];
|
|
562
462
|
const loginProblem = (): string | undefined => {
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
if (!
|
|
566
|
-
if (!connectors.vendors.has(login)) return `login names "${login}", which is not a connector in sanoma.config.ts`;
|
|
567
|
-
if (!connectors.logins.has(login)) return `login names "${login}", whose connector has no login half`;
|
|
463
|
+
if (!isPublic) return "a login is for a public site: add `public: true`, or remove `login`";
|
|
464
|
+
if (!connectors.vendors.has(login!)) return `login names "${login}", which is not a connector in sanoma.config.ts`;
|
|
465
|
+
if (!connectors.logins.has(login!)) return `login names "${login}", whose connector has no login half`;
|
|
568
466
|
return undefined;
|
|
569
467
|
};
|
|
570
|
-
const
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
// A hole or a spread is no string.
|
|
576
|
-
const tags: (string | undefined)[] | undefined =
|
|
577
|
-
list?.type === "ArrayExpression" ? list.elements.map((e: Node) => stringValue(e)) : undefined;
|
|
578
|
-
const locales = tags?.every((tag): tag is string => tag !== undefined) ? tags : undefined;
|
|
579
|
-
const localesProblem = (): string | undefined => {
|
|
580
|
-
if (locales === undefined) return unread("`locales`", "an array of strings");
|
|
581
|
-
if (!locales.length) return "`locales` names a site's languages, its default first: give one or more, or remove it";
|
|
582
|
-
const bad = locales.find((locale) => !LOCALE.test(locale));
|
|
468
|
+
const loginMessage = login !== undefined ? loginProblem() : undefined;
|
|
469
|
+
if (loginMessage) problems.push(problem(loginMessage));
|
|
470
|
+
const localesProblem = (list: string[]): string | undefined => {
|
|
471
|
+
if (!list.length) return "`locales` names a site's languages, its default first: give one or more, or remove it";
|
|
472
|
+
const bad = list.find((locale) => !LOCALE.test(locale));
|
|
583
473
|
if (bad !== undefined) return `"${bad}" is not a language tag such as "en", "fr" or "pt-BR"`;
|
|
584
|
-
const twice =
|
|
474
|
+
const twice = list.find((locale, i) => list.indexOf(locale) !== i);
|
|
585
475
|
if (twice !== undefined) return `"${twice}" is in \`locales\` twice`;
|
|
586
|
-
|
|
587
|
-
if ((!isPublic && !publicUnread) || loginAt) {
|
|
476
|
+
if (!isPublic || login !== undefined) {
|
|
588
477
|
return "`locales` are for a public site with no login: add `public: true`, or remove `locales`";
|
|
589
478
|
}
|
|
590
479
|
return undefined;
|
|
591
480
|
};
|
|
592
|
-
const localesMessage =
|
|
593
|
-
if (localesMessage) problem(
|
|
481
|
+
const localesMessage = locales && localesProblem(locales);
|
|
482
|
+
if (localesMessage) problems.push(problem(localesMessage));
|
|
594
483
|
return {
|
|
484
|
+
at: { file, export: "default" },
|
|
595
485
|
...(title !== undefined && { title }),
|
|
596
486
|
...(icon !== undefined && { icon }),
|
|
597
487
|
...(isPublic && { public: true as const }),
|
|
598
488
|
...(login !== undefined && { login }),
|
|
599
|
-
...(
|
|
489
|
+
...(locales && !localesMessage && { locales }),
|
|
600
490
|
problems,
|
|
601
491
|
};
|
|
602
492
|
}
|
package/src/source.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { promisify } from "node:util";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import type { PermissionsGrid } from "./grid.ts";
|
|
@@ -155,6 +156,19 @@ export const sourceDriver = defineDriver(sourceOps, {
|
|
|
155
156
|
|
|
156
157
|
const COMMIT = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
157
158
|
|
|
159
|
+
/**
|
|
160
|
+
* The fake of `sanoma.source.resolve`, which a sandbox run calls (`sandboxDrivers`): no git and no
|
|
161
|
+
* network, a full commit id is itself as the driver has it, and any other ref the SHA-1 of
|
|
162
|
+
* `<url>#<ref>`, so a scenario's revisions are the same on every run.
|
|
163
|
+
*/
|
|
164
|
+
export const fakeSourceDriver = defineDriver(sourceOps, {
|
|
165
|
+
source: {
|
|
166
|
+
resolve: async ({ url, ref }) => ({
|
|
167
|
+
revision: COMMIT.test(ref) ? ref : createHash("sha1").update(`${url}#${ref}`).digest("hex"),
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
158
172
|
/** The commit `ref` (a branch, a tag, `HEAD`, a full ref or a commit id) names in the repository at `url`. */
|
|
159
173
|
export async function resolveRevision(url: string, ref: string): Promise<string> {
|
|
160
174
|
if (COMMIT.test(ref)) return ref;
|
package/src/steps.ts
CHANGED
|
@@ -14,7 +14,9 @@ type AnyOp<I = any, O = any> = Op<string, string, string, I, O>;
|
|
|
14
14
|
* What a `Given` step sets up before the run: a call through an operation of the run's fake,
|
|
15
15
|
* with the fields of its input to set (the rest is made up), or a fault to inject into its next
|
|
16
16
|
* call. `as` names a call's output, so a later step's table of an operation's own test can use
|
|
17
|
-
* its fields: `$draft.id`.
|
|
17
|
+
* its fields: `$draft.id`. In an operation's own test, a resource type's `create` seeds the object
|
|
18
|
+
* as apply makes it: the type's `afterCreate` calls follow, each polled as apply polls it, the
|
|
19
|
+
* seed's name (else the type's) as the export.
|
|
18
20
|
*/
|
|
19
21
|
export type Seed =
|
|
20
22
|
| { op: AnyOp; input: Record<string, unknown>; as?: string }
|
package/src/version.ts
CHANGED
|
@@ -18,8 +18,9 @@ export const RUNTIME_VERSION = (
|
|
|
18
18
|
* 3: a start the policy holds waits on its approval before the body (2026-10-21).
|
|
19
19
|
* 4: a sleep for a duration reads DBOS's clock for its start (2026-10-25).
|
|
20
20
|
* 5: a scheduled run admits its starter and asks the policy in two steps before the body (2026-10-25).
|
|
21
|
+
* 6: pin a named connection before policy and recheck its binding before use (2026-10-29).
|
|
21
22
|
*/
|
|
22
|
-
export const STEP_LAYOUT =
|
|
23
|
+
export const STEP_LAYOUT = 6;
|
|
23
24
|
|
|
24
25
|
let dbosVersion: string | undefined;
|
|
25
26
|
|
package/src/wakeup-driver.ts
CHANGED
|
@@ -154,8 +154,11 @@ const env = z.object({
|
|
|
154
154
|
});
|
|
155
155
|
|
|
156
156
|
/** The variable `name`, read when an operation is called, or the driver's refusal. */
|
|
157
|
-
function required(
|
|
158
|
-
|
|
157
|
+
function required(
|
|
158
|
+
values: Record<string, string | undefined>,
|
|
159
|
+
name: "SANOMA_WAKEUP_URL" | "SANOMA_WAKEUP_ACCOUNT_TOKEN",
|
|
160
|
+
): string {
|
|
161
|
+
const value = values[name];
|
|
159
162
|
if (!value) throw refused(`${name} is not set (${env.shape[name].description})`);
|
|
160
163
|
return value;
|
|
161
164
|
}
|
|
@@ -167,10 +170,10 @@ const Problem = z.object({ error: z.string() });
|
|
|
167
170
|
const instance = (id: string) => `/wakeup/instances/${encodeURIComponent(id)}`;
|
|
168
171
|
|
|
169
172
|
/** The service at `SANOMA_WAKEUP_URL`, read when an operation is called: each request's failure a `DriverError`. */
|
|
170
|
-
function serviceApi(timeoutMs: number): WakeupApi {
|
|
171
|
-
const base = required("SANOMA_WAKEUP_URL").replace(/\/+$/, "");
|
|
173
|
+
function serviceApi(timeoutMs: number, values: Record<string, string | undefined>): WakeupApi {
|
|
174
|
+
const base = required(values, "SANOMA_WAKEUP_URL").replace(/\/+$/, "");
|
|
172
175
|
if (!URL.canParse(base)) throw refused("SANOMA_WAKEUP_URL is not a URL");
|
|
173
|
-
const account = () => required("SANOMA_WAKEUP_ACCOUNT_TOKEN");
|
|
176
|
+
const account = () => required(values, "SANOMA_WAKEUP_ACCOUNT_TOKEN");
|
|
174
177
|
/**
|
|
175
178
|
* One request: its reply parsed by `reply` (none: not read), a 404 `undefined` where `absent`
|
|
176
179
|
* says it means no such instance, and any other answer but a 2xx a `DriverError`.
|
|
@@ -257,7 +260,20 @@ function serviceApi(timeoutMs: number): WakeupApi {
|
|
|
257
260
|
* `register` send). `timeoutMs` is how long one request may take: 10 s by default.
|
|
258
261
|
*/
|
|
259
262
|
export function sanomaWakeupDriver({ timeoutMs = 10_000 }: { timeoutMs?: number } = {}) {
|
|
260
|
-
|
|
263
|
+
const call = async (name: keyof ReturnType<typeof wakeupCalls>, input: unknown, context: CallContext) => {
|
|
264
|
+
const values = Object.fromEntries(
|
|
265
|
+
await Promise.all(Object.keys(env.shape).map(async (key) => [key, await context.secrets.get(key)])),
|
|
266
|
+
);
|
|
267
|
+
const methods = wakeupCalls(() => serviceApi(timeoutMs, values));
|
|
268
|
+
return (methods[name] as (input: unknown, context: CallContext) => Promise<unknown>)(input, context);
|
|
269
|
+
};
|
|
270
|
+
const methods = Object.fromEntries(
|
|
271
|
+
["import", "read", "plan", "create", "update", "delete", "register"].map((name) => [
|
|
272
|
+
name,
|
|
273
|
+
(input: unknown, context: CallContext) => call(name as keyof ReturnType<typeof wakeupCalls>, input, context),
|
|
274
|
+
]),
|
|
275
|
+
);
|
|
276
|
+
return defineDriver(wakeupOps, { wakeup: methods } as never, { env });
|
|
261
277
|
}
|
|
262
278
|
|
|
263
279
|
/** The wakeup's step for Gherkin tests: an instance at the service before the run, named `wakeup` (`$wakeup.id`). */
|
package/src/worker.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { awaitApproval } from "./approvals.ts";
|
|
|
3
3
|
import { buildCtx, decideStart, isInfrastructureError, startHoldOf } from "./call.ts";
|
|
4
4
|
import { dbosStatusesOf } from "./client.ts";
|
|
5
5
|
import { declaredWakeup, type ResolvedConfig, resolveConfig, type SanomaConfig } from "./config.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { configuredSecrets } from "./credentials.ts";
|
|
7
7
|
import { type Db, DBOS_DATABASE_OPTIONS, lazyPool, prepareDatabase, stampVersion } from "./db.ts";
|
|
8
8
|
import { deadlineRegistry, type Deadlines, idle, onOwnQueue, type SelfStop, stopWhenIdle } from "./deadlines.ts";
|
|
9
9
|
import type { WorkflowDefinition } from "./define.ts";
|
|
@@ -123,6 +123,7 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
123
123
|
const horizonMs = options.idleHorizonMs ?? 60_000;
|
|
124
124
|
const state: WorkerState = {
|
|
125
125
|
app: resolved.appName,
|
|
126
|
+
connections: resolved.connections,
|
|
126
127
|
ops: resolved.ops,
|
|
127
128
|
drivers: resolved.drivers,
|
|
128
129
|
fakes: resolved.fakes,
|
|
@@ -134,7 +135,7 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
134
135
|
policy: resolved.policy,
|
|
135
136
|
...(resolved.root !== undefined && { root: resolved.root }),
|
|
136
137
|
ledger: ledger.store,
|
|
137
|
-
secrets:
|
|
138
|
+
secrets: configuredSecrets(resolved.secrets, () => secrets.db()),
|
|
138
139
|
workflows: resolved.workflows,
|
|
139
140
|
outlines: new Map([...resolved.workflows].map(([name, wf]) => [name, outlineOf(wf)])),
|
|
140
141
|
// `registered` is filled below, before any run starts: every name of this config's is registered then.
|
|
@@ -146,7 +147,6 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
146
147
|
stopped: false,
|
|
147
148
|
deadlines: deadlineRegistry(),
|
|
148
149
|
};
|
|
149
|
-
const credentials = await watchCredentials(resolved.databaseUrl, resolved.driversByVendor);
|
|
150
150
|
const worker: Worker = {
|
|
151
151
|
unconfigured: [...credentialsOf(resolved.driversByVendor, (name) => process.env[name])]
|
|
152
152
|
.filter(([vendor, statuses]) => unconfigured(vendor, statuses))
|
|
@@ -164,8 +164,8 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
164
164
|
let failed: { error: unknown } | undefined;
|
|
165
165
|
for (const close of [
|
|
166
166
|
() => DBOS.shutdown(),
|
|
167
|
-
() => credentials.close(),
|
|
168
167
|
() => secrets.close(),
|
|
168
|
+
() => resolved.connectionPool.close(),
|
|
169
169
|
() => ledger.close(),
|
|
170
170
|
async () => resolved.dataStore?.close(),
|
|
171
171
|
]) {
|
|
@@ -184,6 +184,7 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
184
184
|
const previous = current;
|
|
185
185
|
let launched = false;
|
|
186
186
|
try {
|
|
187
|
+
await configuredSecrets(resolved.secrets, () => secrets.db()).validate();
|
|
187
188
|
for (const name of resolved.workflows.keys()) {
|
|
188
189
|
if (!registered.has(name)) registered.set(name, register(name));
|
|
189
190
|
}
|
|
@@ -252,9 +253,12 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
252
253
|
let stopFailed: { error: unknown } | undefined;
|
|
253
254
|
if (launched) await worker.stop().catch((error: unknown) => (stopFailed = { error }));
|
|
254
255
|
else {
|
|
255
|
-
await
|
|
256
|
-
.close()
|
|
257
|
-
.
|
|
256
|
+
await Promise.all([
|
|
257
|
+
secrets.close(),
|
|
258
|
+
ledger.close(),
|
|
259
|
+
resolved.dataStore?.close(),
|
|
260
|
+
resolved.connectionPool.close(),
|
|
261
|
+
]);
|
|
258
262
|
}
|
|
259
263
|
current = previous;
|
|
260
264
|
if (stopFailed) {
|
|
@@ -438,7 +442,7 @@ async function runOn(state: WorkerState, name: string, start: RunArgs | Date): P
|
|
|
438
442
|
if (sandbox !== undefined) {
|
|
439
443
|
// Imported here, so a live worker never loads the Gherkin parser or faker.
|
|
440
444
|
const { seedSandbox } = await import("./sandbox.ts");
|
|
441
|
-
await seedSandbox(run, sandbox);
|
|
445
|
+
await seedSandbox(run, sandbox, parsed);
|
|
442
446
|
}
|
|
443
447
|
output = await currentRun.run(run, () => wf.run(buildCtx(wf, run), parsed));
|
|
444
448
|
} finally {
|