@sanoma-ai/workflows 0.7.1 → 0.8.1
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 +34 -7
- 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 +30 -2
- package/dist/config.js +41 -20
- package/dist/config.js.map +1 -1
- package/dist/db.d.ts +12 -0
- package/dist/db.js +15 -0
- package/dist/db.js.map +1 -1
- package/dist/deadlines.d.ts +5 -2
- 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/index.d.ts +7 -5
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/lint.js +19 -5
- package/dist/lint.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/resources.d.ts +7 -2
- package/dist/resources.js +23 -8
- package/dist/resources.js.map +1 -1
- package/dist/run.d.ts +7 -0
- package/dist/run.js.map +1 -1
- package/dist/sandbox.js +2 -2
- package/dist/sandbox.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 +55 -5
- package/dist/source.js +4 -0
- package/dist/source.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/worker.js +50 -3
- package/dist/worker.js.map +1 -1
- package/oxlint.json +2 -2
- package/package.json +1 -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 +66 -22
- package/src/db.ts +23 -0
- package/src/deadlines.ts +5 -2
- 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/index.ts +14 -0
- package/src/lint.ts +20 -5
- package/src/outline.ts +15 -6
- package/src/paths.ts +57 -2
- package/src/resources.ts +27 -9
- package/src/run.ts +5 -0
- package/src/sandbox.ts +2 -5
- 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 +50 -5
- package/src/version.ts +25 -16
- package/src/worker.ts +46 -3
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,6 +16,7 @@ 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";
|
|
@@ -37,11 +40,12 @@ import {
|
|
|
37
40
|
import { type CedarPolicySource, policyAt } from "./cedar.ts";
|
|
38
41
|
import type { Policy } from "./policy.ts";
|
|
39
42
|
import { type Resource, resourceTypesOf, withoutWriteOnly } from "./resource.ts";
|
|
40
|
-
import { APPLY_WORKFLOW, BUILT_IN_WORKFLOWS, CALL_WORKFLOW, DATA_VENDOR } from "./shared.ts";
|
|
43
|
+
import { APPLY_WORKFLOW, BUILT_IN_WORKFLOWS, CALL_WORKFLOW, CHANGE_WORKFLOW, DATA_VENDOR } from "./shared.ts";
|
|
41
44
|
import { type RemoteHalf, sourceDriver, sourceOps } from "./source.ts";
|
|
45
|
+
import { appsListDriver, appsListOps } from "./apps-list.ts";
|
|
42
46
|
import type { Steps } from "./steps.ts";
|
|
43
47
|
import { checkSchedule } from "./schedules.ts";
|
|
44
|
-
import { computeVersion } from "./version.ts";
|
|
48
|
+
import { changeCodeOf, computeVersion } from "./version.ts";
|
|
45
49
|
import type { DeclaredResource } from "./resources.ts";
|
|
46
50
|
import { checkWakeupOps, type DeclaredWakeup } from "./wakeup.ts";
|
|
47
51
|
import { sanomaWakeupDriver, wakeupSteps } from "./wakeup-driver.ts";
|
|
@@ -56,7 +60,11 @@ import { checkDatabaseOps } from "./database.ts";
|
|
|
56
60
|
* policies are `scenarios/`, `resources/` and `policies/` in `root`.
|
|
57
61
|
*/
|
|
58
62
|
export interface SanomaConfig {
|
|
59
|
-
|
|
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>[];
|
|
60
68
|
/**
|
|
61
69
|
* The connectors (from `defineConnector`, as their packages export them). The worker takes
|
|
62
70
|
* each operation's effect, schemas and retry setting from here, not from the workflow, and
|
|
@@ -106,6 +114,12 @@ export interface SanomaConfig {
|
|
|
106
114
|
* that sets it and declares a wakeup of its own.
|
|
107
115
|
*/
|
|
108
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 };
|
|
109
123
|
}
|
|
110
124
|
|
|
111
125
|
/** A config, checked, with everything a worker, client or app derives from it. */
|
|
@@ -118,6 +132,8 @@ export interface ResolvedConfig {
|
|
|
118
132
|
databaseUrl: string;
|
|
119
133
|
/** The DBOS application version: `<appName>@<DBOS__APPVERSION, or a hash of the workflows>`. */
|
|
120
134
|
version: string;
|
|
135
|
+
/** The hash of the built-in change workflow's own code (`changeCodeOf`), which each change run carries. */
|
|
136
|
+
changeCode: string;
|
|
121
137
|
/** The DBOS queue runs are started on: `sanoma:<appName>`. */
|
|
122
138
|
queueName: string;
|
|
123
139
|
/**
|
|
@@ -151,7 +167,16 @@ export interface ResolvedConfig {
|
|
|
151
167
|
models: ReadonlyMap<string, NonNullable<ConnectorSetup["model"]>>;
|
|
152
168
|
/** The remote halves the connectors attach, made once for this resolution, by vendor id in `connectors` order: `remoteFor` finds a URL's. */
|
|
153
169
|
remotes: ReadonlyMap<string, RemoteHalf>;
|
|
154
|
-
/**
|
|
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
|
+
*/
|
|
155
180
|
scenarios?: URL;
|
|
156
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. */
|
|
157
182
|
types: ReadonlyMap<string, Resource>;
|
|
@@ -258,8 +283,8 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
258
283
|
throw new Error("The config needs `connectors`: the connectors its workflows call, as their packages export them");
|
|
259
284
|
}
|
|
260
285
|
for (const connector of config.connectors as unknown[]) checkConnector(connector);
|
|
261
|
-
if (!Array.isArray(config.workflows)) {
|
|
262
|
-
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");
|
|
263
288
|
}
|
|
264
289
|
// An untyped config naming fake drivers would otherwise run live. Delete once sanoma-ops has migrated.
|
|
265
290
|
if (config.drivers !== undefined || config.fakes !== undefined)
|
|
@@ -279,16 +304,18 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
279
304
|
const running = withBuiltIns(config.connectors);
|
|
280
305
|
const types = resourceTypesOf(running);
|
|
281
306
|
// Built-in: `sanoma.source.resolve`, which apply and drift call for a host's revision.
|
|
282
|
-
const
|
|
307
|
+
const hostTyped = [...types.values()].some((type) => type.host);
|
|
283
308
|
// Built-in: the collections' operations, `data.<name>.<op>`, on the config's database.
|
|
284
309
|
const databaseUrl = resolveDatabaseUrl(config);
|
|
285
310
|
const declared = [...collections.values()];
|
|
286
311
|
const dataStore = declared.length ? lazyPool(databaseUrl, "data") : undefined;
|
|
287
|
-
// 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`).
|
|
288
314
|
const ops = indexConnectors([
|
|
289
315
|
...running,
|
|
290
316
|
changeOps,
|
|
291
|
-
|
|
317
|
+
appsListOps,
|
|
318
|
+
...(hostTyped ? [sourceOps] : []),
|
|
292
319
|
...(dataStore ? [collectionsConnector(declared)] : []),
|
|
293
320
|
]);
|
|
294
321
|
checkWakeupOps(types, ops);
|
|
@@ -298,18 +325,20 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
298
325
|
const driversByVendor = instanceFakes
|
|
299
326
|
? new Map([...instanceFakes].map(([vendor, fake]) => [vendor, fake.driver]))
|
|
300
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()]));
|
|
301
329
|
const drivers = indexDrivers(
|
|
302
330
|
[
|
|
303
331
|
...driversByVendor.values(),
|
|
304
|
-
changeDriver(),
|
|
305
|
-
...
|
|
332
|
+
changeDriver(remotes),
|
|
333
|
+
...sandboxDrivers,
|
|
334
|
+
...(hostTyped ? [sourceDriver] : []),
|
|
306
335
|
...(dataStore ? [collectionsDriver(declared, dataStore)] : []),
|
|
307
336
|
],
|
|
308
337
|
ops,
|
|
309
338
|
);
|
|
310
339
|
if (dataStore) fakes.set(DATA_VENDOR, () => collectionsFake(declared));
|
|
311
340
|
const names = new Map<string, WorkflowDefinition<any, any>>();
|
|
312
|
-
for (const wf of config.workflows) {
|
|
341
|
+
for (const wf of config.workflows ?? []) {
|
|
313
342
|
if (BUILT_IN_WORKFLOWS.has(wf.name)) {
|
|
314
343
|
throw new Error(`A workflow is named "${wf.name}", the name of a built-in workflow; name it otherwise`);
|
|
315
344
|
}
|
|
@@ -324,17 +353,21 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
324
353
|
names.set(wf.name, wf);
|
|
325
354
|
}
|
|
326
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);
|
|
327
360
|
if (types.size) {
|
|
328
|
-
const connectors = connectorsOf(config);
|
|
329
|
-
// The types the data files may declare: the vendors' and the people's, whose resources are left out.
|
|
330
|
-
const declarable = resourceTypesOf(connectors);
|
|
331
|
-
const read = () => readDeclared(root, connectors, declarable, types);
|
|
332
361
|
names.set(
|
|
333
362
|
DRIFT_WORKFLOW,
|
|
334
363
|
driftWorkflow(types, ops, drivers, () => DBOS.runStep(read, { name: "drift:data-files" })),
|
|
335
364
|
);
|
|
336
365
|
names.set(APPLY_WORKFLOW, applyWorkflow(types, ops, drivers, applySteps(appName, root, read)));
|
|
337
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 })));
|
|
338
371
|
// Built-in: a direct call the policy holds for an approval, made a run, when any operation may be called directly.
|
|
339
372
|
const call = callWorkflow(ops, drivers, types);
|
|
340
373
|
if (call.uses.length) names.set(CALL_WORKFLOW, call);
|
|
@@ -344,6 +377,7 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
344
377
|
databaseUrl,
|
|
345
378
|
// The built-in's code and operations are part of the app's version, like the config's own.
|
|
346
379
|
version: computeVersion({ appName, workflows: [...names.values()] }),
|
|
380
|
+
changeCode: changeCodeOf(names.get(CHANGE_WORKFLOW)!),
|
|
347
381
|
queueName: `sanoma:${appName}`,
|
|
348
382
|
ops,
|
|
349
383
|
drivers,
|
|
@@ -352,7 +386,8 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
352
386
|
...(instanceFakes && { instanceFakes }),
|
|
353
387
|
logins: makersOf(config.connectors, "login"),
|
|
354
388
|
models: makersOf(config.connectors, "model"),
|
|
355
|
-
remotes
|
|
389
|
+
remotes,
|
|
390
|
+
hosts,
|
|
356
391
|
...(scenarios && { scenarios }),
|
|
357
392
|
types,
|
|
358
393
|
steps: [...new Set([...running.flatMap((c) => setupOf(c).steps ?? []), ...(config.steps ?? [])])],
|
|
@@ -391,6 +426,12 @@ function collectionsOf(config: SanomaConfig): Map<string, Collection> {
|
|
|
391
426
|
return collections;
|
|
392
427
|
}
|
|
393
428
|
|
|
429
|
+
/**
|
|
430
|
+
* The built-in drivers no fake implements that a sandbox run calls beside its fakes (`seedSandbox`):
|
|
431
|
+
* those that need no credentials and change nothing, as `sanoma.apps.list` (GitHub's public search).
|
|
432
|
+
*/
|
|
433
|
+
export const sandboxDrivers: readonly Driver[] = [appsListDriver];
|
|
434
|
+
|
|
394
435
|
/**
|
|
395
436
|
* `sanoma.wakeup`, the runtime's own: the type's operations and `sanoma.wakeup.register` (effect
|
|
396
437
|
* `write`), which the worker registers each deadline through before it stops, with its live
|
|
@@ -479,11 +520,14 @@ function appNameAt(root: string | undefined): string {
|
|
|
479
520
|
return typeof named === "string" && named.trim() ? named.trim() : "sanoma";
|
|
480
521
|
}
|
|
481
522
|
|
|
482
|
-
/**
|
|
523
|
+
/**
|
|
524
|
+
* The root, as a `file:` URL to the directory (with its trailing slash), when `<root>/scenarios/`
|
|
525
|
+
* or an app's `apps/<app>/scenarios/` is a directory: the scenarios are the feature files there,
|
|
526
|
+
* each named by its path from the root (`scenarioFiles`).
|
|
527
|
+
*/
|
|
483
528
|
function scenariosAt(root: string | undefined): URL | undefined {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
return pathToFileURL(`${dir}/`);
|
|
529
|
+
if (root === undefined || !kindDirs(root, SCENARIOS_DIR).some(isDirectory)) return undefined;
|
|
530
|
+
return pathToFileURL(`${root}/`);
|
|
487
531
|
}
|
|
488
532
|
|
|
489
533
|
/**
|
package/src/db.ts
CHANGED
|
@@ -101,6 +101,29 @@ export function openPool(databaseUrl: string, what: string): Pool {
|
|
|
101
101
|
*/
|
|
102
102
|
export const DBOS_DATABASE_OPTIONS = { systemDatabaseIdleTransactionTimeoutMs: 0 } as const;
|
|
103
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
|
+
|
|
104
127
|
/**
|
|
105
128
|
* A DBOS client of the app `applicationName` on the database at `databaseUrl`, as every client of
|
|
106
129
|
* the runtime, its tests and `@sanoma-ai/testing` make one (`DBOS_DATABASE_OPTIONS`).
|
package/src/deadlines.ts
CHANGED
|
@@ -14,8 +14,11 @@ import type { WakeupRegistration } from "./wakeup.ts";
|
|
|
14
14
|
// blocked in a `ctx` wait, and until when. DBOS sees both as PENDING. A worker that registers a
|
|
15
15
|
// wakeup stops itself once nothing can run before the next deadline (`stopWhenIdle`).
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
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" };
|
|
19
22
|
|
|
20
23
|
/** A deadline besides the sleeping runs', such as a schedule's next fire; undefined for none. */
|
|
21
24
|
export type DeadlineSource = () => Date | undefined;
|
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. */
|
package/src/demo.ts
CHANGED
|
@@ -2,9 +2,9 @@ import type { SanomaClient } from "./client.ts";
|
|
|
2
2
|
import type { ResolvedConfig } from "./config.ts";
|
|
3
3
|
import { DATA_TABLE } from "./db.ts";
|
|
4
4
|
import { applyGivens } from "./sandbox.ts";
|
|
5
|
-
import { loadScenarios, type Moment } from "./scenario.ts";
|
|
5
|
+
import { type Given, loadScenarios, type Moment } from "./scenario.ts";
|
|
6
6
|
import { memorySecrets } from "./secret.ts";
|
|
7
|
-
import { errorMessage } from "./shared.ts";
|
|
7
|
+
import { DATA_VENDOR, errorMessage } from "./shared.ts";
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
10
|
* A demo: a company repo run on its connectors' fakes, with the world its scenarios describe.
|
|
@@ -27,20 +27,43 @@ export interface DemoState {
|
|
|
27
27
|
const states = new WeakMap<ResolvedConfig, DemoState>();
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
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,
|
|
31
44
|
* applies every world (a scenario with no `When`) to the instance (`applyGivens`: its records
|
|
32
45
|
* saved through the collections' driver, its vendor objects put in the fakes), then starts each
|
|
33
46
|
* moment as a sandbox run by its person, left undriven, so its first approval waits for them.
|
|
34
|
-
* `resolved` is the resolution the worker and `client` run
|
|
35
|
-
* A world or a moment that fails is a problem in the state it
|
|
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.
|
|
36
50
|
*/
|
|
37
|
-
export async function reseed(
|
|
51
|
+
export async function reseed(
|
|
52
|
+
client: SanomaClient,
|
|
53
|
+
resolved: ResolvedConfig,
|
|
54
|
+
{ only }: { only?: SeedHalf } = {},
|
|
55
|
+
): Promise<DemoState> {
|
|
38
56
|
const fakes = resolved.instanceFakes;
|
|
39
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;
|
|
40
59
|
// Read first, so scenarios that cannot be read at all leave the instance as it was.
|
|
41
60
|
const { scenarios, errors } = await loadScenarios(resolved);
|
|
42
|
-
if (
|
|
43
|
-
|
|
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();
|
|
44
67
|
const state: DemoState = {
|
|
45
68
|
seededAt: new Date().toISOString(),
|
|
46
69
|
worlds: [],
|
|
@@ -49,28 +72,36 @@ export async function reseed(client: SanomaClient, resolved: ResolvedConfig): Pr
|
|
|
49
72
|
};
|
|
50
73
|
const secrets = memorySecrets();
|
|
51
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
|
+
});
|
|
52
80
|
try {
|
|
53
|
-
await applyGivens(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
);
|
|
68
99
|
state.worlds.push(world.name);
|
|
69
100
|
} catch (err) {
|
|
70
101
|
state.problems.push(`world "${world.name}": ${errorMessage(err)}`);
|
|
71
102
|
}
|
|
72
103
|
}
|
|
73
|
-
for (const s of scenarios) {
|
|
104
|
+
for (const s of into("database") ? scenarios : []) {
|
|
74
105
|
if (!s.moment || s.workflow === undefined) continue;
|
|
75
106
|
try {
|
|
76
107
|
const runId = await client.start(s.workflow, s.input, { sandbox: s.name, startedBy: s.moment.person });
|
package/src/describe.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { z } from "zod";
|
|
3
|
+
import { type AppEntry, appsAt } from "./apps.ts";
|
|
3
4
|
import { type LintProblem, readSource, type SourceFile } from "./ast.ts";
|
|
4
5
|
import { type Field, recordSchemaOf } from "./collection.ts";
|
|
5
6
|
import { connectorsOf, resolveConfig, rootOf, type SanomaConfig } from "./config.ts";
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
type DataFiles,
|
|
15
16
|
type DeclaredCollection,
|
|
16
17
|
type DeclaredResource,
|
|
18
|
+
inAppPeople,
|
|
17
19
|
readDataFiles,
|
|
18
20
|
type ResourceProblem,
|
|
19
21
|
} from "./resources.ts";
|
|
@@ -62,8 +64,19 @@ export {
|
|
|
62
64
|
type Bindings,
|
|
63
65
|
type Rebind,
|
|
64
66
|
} from "./lockfile.ts";
|
|
67
|
+
// The apps under `apps/`, read without running their `app.ts`.
|
|
68
|
+
export { crossAppImports, readApps, type AppEntry } from "./apps.ts";
|
|
65
69
|
// What the sites call, read from their source: a workflow's Used by.
|
|
66
|
-
export {
|
|
70
|
+
export {
|
|
71
|
+
readSites,
|
|
72
|
+
type SiteCall,
|
|
73
|
+
type SiteEntry,
|
|
74
|
+
type SitePage,
|
|
75
|
+
siteContentPages,
|
|
76
|
+
siteIcons,
|
|
77
|
+
siteLocales,
|
|
78
|
+
sitesUnder,
|
|
79
|
+
} from "./sites.ts";
|
|
67
80
|
// The generated SDK, `.sanoma/api.ts`: a site's `api` typed from this description.
|
|
68
81
|
export { API_FILE, generateApi, writeApi, writeGenerated } from "./sdk.ts";
|
|
69
82
|
// The Cedar schema, `.sanoma/schema.cedarschema`: what a company's policies are validated and decided against.
|
|
@@ -76,7 +89,10 @@ export type { ApplyInput, ApplyPlan, ApplyReport, ApplyResult, PlanAnswer, PlanS
|
|
|
76
89
|
|
|
77
90
|
/** What a workflow is, read from its definition: enough to draw a start form and show what it may call. */
|
|
78
91
|
export interface WorkflowEntry {
|
|
92
|
+
/** Its name: `<app>.<name>` for one an app's folder defines. */
|
|
79
93
|
name: string;
|
|
94
|
+
/** The app whose folder defines it (`apps/<app>/workflows/`); absent for one at the root. */
|
|
95
|
+
app?: string;
|
|
80
96
|
title?: string;
|
|
81
97
|
/** `manual`, or the schedule it starts on (`defineWorkflow`'s `trigger`). */
|
|
82
98
|
trigger: Trigger;
|
|
@@ -166,7 +182,10 @@ export interface ResourceTypeEntry {
|
|
|
166
182
|
|
|
167
183
|
/** A collection the config declares (`collection()`). */
|
|
168
184
|
export interface CollectionEntry {
|
|
185
|
+
/** Its name: `<app>.<name>` for one an app's `resources/data.ts` declares. */
|
|
169
186
|
name: string;
|
|
187
|
+
/** The app whose folder declares it (`apps/<app>/resources/data.ts`); absent for one at the root. */
|
|
188
|
+
app?: string;
|
|
170
189
|
title?: string;
|
|
171
190
|
/** The field each record is found by. */
|
|
172
191
|
key: string;
|
|
@@ -204,8 +223,14 @@ export interface ConfigDescription {
|
|
|
204
223
|
resources: DeclaredResource[];
|
|
205
224
|
/** Each binding in the lockfile (`LOCKFILE`) whose key no data file declares, as `readDataFiles` reports it. */
|
|
206
225
|
orphans: DataFiles["orphans"];
|
|
207
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* What is wrong in the data files (`readDataFiles`) and the apps (a folder of `apps/` that is no
|
|
228
|
+
* app, an `app.ts` that cannot be read, a collection or site at the root named as an app is),
|
|
229
|
+
* each at its file, relative to the root, line and column. A workflow's are on its entry.
|
|
230
|
+
*/
|
|
208
231
|
problems: ResourceProblem[];
|
|
232
|
+
/** The apps under `apps/` (`readApps`), by name. Each workflow, collection and site an app declares names it (`app`). */
|
|
233
|
+
apps: AppEntry[];
|
|
209
234
|
/**
|
|
210
235
|
* `defined` when the root has `policies/**\/*.cedar` (else the policy is `allowAll`), with their
|
|
211
236
|
* `version`, or while they have `problems` (`policyProblems`), none: then every call and start is refused.
|
|
@@ -258,6 +283,7 @@ export function describeConfig(config: SanomaConfig): ConfigDescription {
|
|
|
258
283
|
const collections: CollectionEntry[] = [...resolved.collections.values()]
|
|
259
284
|
.map((c) => ({
|
|
260
285
|
name: c.name,
|
|
286
|
+
...(c.app !== undefined && { app: c.app }),
|
|
261
287
|
...(c.title !== undefined && { title: c.title }),
|
|
262
288
|
key: c.key,
|
|
263
289
|
fields: c.fields,
|
|
@@ -294,6 +320,7 @@ export function describeConfig(config: SanomaConfig): ConfigDescription {
|
|
|
294
320
|
];
|
|
295
321
|
return {
|
|
296
322
|
name: wf.name,
|
|
323
|
+
...(wf.app !== undefined && { app: wf.app }),
|
|
297
324
|
title: wf.title,
|
|
298
325
|
trigger: wf.trigger,
|
|
299
326
|
...(builtin && { builtin: true as const }),
|
|
@@ -308,6 +335,7 @@ export function describeConfig(config: SanomaConfig): ConfigDescription {
|
|
|
308
335
|
});
|
|
309
336
|
workflows.sort((a, b) => a.name.localeCompare(b.name));
|
|
310
337
|
addApiNameProblems(workflows, new Set(ops.map((op) => op.vendor)));
|
|
338
|
+
const { apps, problems: appProblems } = appsAt(resolved.root);
|
|
311
339
|
const sites = readSites(
|
|
312
340
|
resolved.root,
|
|
313
341
|
{ workflows, collections, ops },
|
|
@@ -323,9 +351,16 @@ export function describeConfig(config: SanomaConfig): ConfigDescription {
|
|
|
323
351
|
vendors,
|
|
324
352
|
resourceTypes,
|
|
325
353
|
collections,
|
|
326
|
-
|
|
354
|
+
// An app's people are refused (`readPeople`), so the grid and the People page never show them.
|
|
355
|
+
resources: dataFiles.resources.filter((r) => !inAppPeople(r)),
|
|
327
356
|
orphans: dataFiles.orphans,
|
|
328
|
-
problems: [
|
|
357
|
+
problems: [
|
|
358
|
+
...dataFiles.problems,
|
|
359
|
+
...appProblems,
|
|
360
|
+
...appClashes(apps, collections, sites),
|
|
361
|
+
...domainProblems(resolved.root, dataFiles.resources, resources, sites, read),
|
|
362
|
+
],
|
|
363
|
+
apps,
|
|
329
364
|
policy: {
|
|
330
365
|
defined: resolved.policy !== allowAll,
|
|
331
366
|
...(resolved.policy.version === undefined ? {} : { version: resolved.policy.version }),
|
|
@@ -335,6 +370,34 @@ export function describeConfig(config: SanomaConfig): ConfigDescription {
|
|
|
335
370
|
};
|
|
336
371
|
}
|
|
337
372
|
|
|
373
|
+
/** That the root's `kind` `name`, in `file`, is named as an app is. */
|
|
374
|
+
const appClash = (kind: string, name: string, file: string): ResourceProblem => ({
|
|
375
|
+
file,
|
|
376
|
+
line: 1,
|
|
377
|
+
column: 1,
|
|
378
|
+
message: `the ${kind} ${name} is named as the app apps/${name} is: name it otherwise`,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* A collection or site at the root named as an app is: `api.data.<app>` and `/sites/<app>/` are
|
|
383
|
+
* the app's, so the root's would take its place.
|
|
384
|
+
*/
|
|
385
|
+
function appClashes(
|
|
386
|
+
apps: readonly AppEntry[],
|
|
387
|
+
collections: readonly CollectionEntry[],
|
|
388
|
+
sites: readonly SiteEntry[],
|
|
389
|
+
): ResourceProblem[] {
|
|
390
|
+
const names = new Set(apps.map((app) => app.name));
|
|
391
|
+
return [
|
|
392
|
+
...collections
|
|
393
|
+
.filter((c) => c.app === undefined && names.has(c.name))
|
|
394
|
+
.map((c) => appClash("collection", c.name, c.declared?.file ?? "resources/data.ts")),
|
|
395
|
+
...sites
|
|
396
|
+
.filter((site) => site.app === undefined && names.has(site.name))
|
|
397
|
+
.map((site) => appClash("site", site.name, `sites/${site.name}`)),
|
|
398
|
+
];
|
|
399
|
+
}
|
|
400
|
+
|
|
338
401
|
/**
|
|
339
402
|
* Each site a host's `domains` (its type's `host.inputs.domains`) names that is not a public
|
|
340
403
|
* site, and each domain it gives more than one site, at the host's declaration: the app serves
|
package/src/direct.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import type { Op } from "./op.ts";
|
|
2
2
|
import { isResourceOp } from "./resource.ts";
|
|
3
|
-
import { DATA_VENDOR } from "./shared.ts";
|
|
3
|
+
import { APPS_LIST, DATA_VENDOR } from "./shared.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Whether an operation may be called directly, outside a run (`SanomaClient.call`), as the
|
|
7
|
-
* person: a collection's (`data.<name>.<op>`),
|
|
8
|
-
*
|
|
7
|
+
* person: a collection's (`data.<name>.<op>`), the runtime's listing of apps (`sanoma.apps.list`,
|
|
8
|
+
* which a page calls), or a `read` that is neither the runtime's own (the vendor `sanoma`) nor a
|
|
9
|
+
* resource type's (`isResourceOp`), which only drift and apply call.
|
|
9
10
|
* Anything else (an effect at a vendor) is a workflow's. The one rule: the built-in `call`
|
|
10
11
|
* workflow, the SDK and the app read it, from here or from `OpEntry.direct`, and never derive it.
|
|
11
12
|
*/
|
|
12
13
|
export const isDirect = (
|
|
13
|
-
op: Pick<Op, "vendor" | "resource" | "effect">,
|
|
14
|
+
op: Pick<Op, "id" | "vendor" | "resource" | "effect">,
|
|
14
15
|
types: ReadonlyMap<string, unknown>,
|
|
15
|
-
): boolean =>
|
|
16
|
+
): boolean =>
|
|
17
|
+
op.vendor === DATA_VENDOR ||
|
|
18
|
+
op.id === APPS_LIST ||
|
|
19
|
+
(op.effect === "read" && op.vendor !== "sanoma" && !isResourceOp(op, types));
|
package/src/errors.ts
CHANGED
|
@@ -51,6 +51,9 @@ export function errorCode(e: unknown): ErrorCode | undefined {
|
|
|
51
51
|
return code !== undefined && KNOWN.has(code) ? (code as ErrorCode) : undefined;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** A `DriverError`'s `vendorCode` (`no_token`, `bad_signature`), read as a property: undefined for anything else. */
|
|
55
|
+
export const vendorCodeOf = (e: unknown): string | undefined => own<string>(e, "vendorCode", "string");
|
|
56
|
+
|
|
54
57
|
/**
|
|
55
58
|
* True when the error says trying again would only repeat it (`retryable: false`), as a
|
|
56
59
|
* `DriverError` for a vendor's final answer or a ledger store for a corrupt file does. Read as
|