@sanoma-ai/workflows 0.0.0-stage → 0.1.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/LICENSE +105 -0
- package/README.md +493 -2
- package/dist/approvals.d.ts +46 -0
- package/dist/approvals.js +111 -0
- package/dist/approvals.js.map +1 -0
- package/dist/ast.d.ts +57 -0
- package/dist/ast.js +81 -0
- package/dist/ast.js.map +1 -0
- package/dist/call.d.ts +33 -0
- package/dist/call.js +402 -0
- package/dist/call.js.map +1 -0
- package/dist/client.d.ts +167 -0
- package/dist/client.js +400 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +126 -0
- package/dist/config.js +247 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +30 -0
- package/dist/credentials.js +193 -0
- package/dist/credentials.js.map +1 -0
- package/dist/cucumber.d.ts +64 -0
- package/dist/cucumber.js +158 -0
- package/dist/cucumber.js.map +1 -0
- package/dist/datafile.d.ts +62 -0
- package/dist/datafile.js +326 -0
- package/dist/datafile.js.map +1 -0
- package/dist/db.d.ts +58 -0
- package/dist/db.js +179 -0
- package/dist/db.js.map +1 -0
- package/dist/define.d.ts +205 -0
- package/dist/define.js +103 -0
- package/dist/define.js.map +1 -0
- package/dist/describe.d.ts +135 -0
- package/dist/describe.js +127 -0
- package/dist/describe.js.map +1 -0
- package/dist/drift.d.ts +75 -0
- package/dist/drift.js +90 -0
- package/dist/drift.js.map +1 -0
- package/dist/env.d.ts +49 -0
- package/dist/env.js +83 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +88 -0
- package/dist/errors.js +148 -0
- package/dist/errors.js.map +1 -0
- package/dist/expectations.d.ts +26 -0
- package/dist/expectations.js +137 -0
- package/dist/expectations.js.map +1 -0
- package/dist/fake.d.ts +55 -0
- package/dist/fake.js +87 -0
- package/dist/fake.js.map +1 -0
- package/dist/fill.d.ts +9 -0
- package/dist/fill.js +17 -0
- package/dist/fill.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger.d.ts +217 -0
- package/dist/ledger.js +251 -0
- package/dist/ledger.js.map +1 -0
- package/dist/lint.d.ts +32 -0
- package/dist/lint.js +168 -0
- package/dist/lint.js.map +1 -0
- package/dist/log.d.ts +24 -0
- package/dist/log.js +83 -0
- package/dist/log.js.map +1 -0
- package/dist/op.d.ts +257 -0
- package/dist/op.js +188 -0
- package/dist/op.js.map +1 -0
- package/dist/outline.d.ts +179 -0
- package/dist/outline.js +506 -0
- package/dist/outline.js.map +1 -0
- package/dist/paths.d.ts +11 -0
- package/dist/paths.js +30 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugin.d.ts +28 -0
- package/dist/plugin.js +25 -0
- package/dist/plugin.js.map +1 -0
- package/dist/policy.d.ts +114 -0
- package/dist/policy.js +60 -0
- package/dist/policy.js.map +1 -0
- package/dist/resource.d.ts +212 -0
- package/dist/resource.js +285 -0
- package/dist/resource.js.map +1 -0
- package/dist/resources.d.ts +62 -0
- package/dist/resources.js +197 -0
- package/dist/resources.js.map +1 -0
- package/dist/run.d.ts +87 -0
- package/dist/run.js +2 -0
- package/dist/run.js.map +1 -0
- package/dist/sandbox.d.ts +33 -0
- package/dist/sandbox.js +123 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/scenario.d.ts +212 -0
- package/dist/scenario.js +1079 -0
- package/dist/scenario.js.map +1 -0
- package/dist/scenarios.d.ts +55 -0
- package/dist/scenarios.js +71 -0
- package/dist/scenarios.js.map +1 -0
- package/dist/shared.d.ts +61 -0
- package/dist/shared.js +77 -0
- package/dist/shared.js.map +1 -0
- package/dist/steps.d.ts +76 -0
- package/dist/steps.js +23 -0
- package/dist/steps.js.map +1 -0
- package/dist/version.d.ts +29 -0
- package/dist/version.js +76 -0
- package/dist/version.js.map +1 -0
- package/dist/worker.d.ts +34 -0
- package/dist/worker.js +219 -0
- package/dist/worker.js.map +1 -0
- package/oxlint.json +128 -0
- package/package.json +91 -3
- package/src/approvals.ts +157 -0
- package/src/ast.ts +115 -0
- package/src/call.ts +468 -0
- package/src/client.ts +534 -0
- package/src/config.ts +381 -0
- package/src/credentials.ts +211 -0
- package/src/cucumber.ts +230 -0
- package/src/datafile.ts +409 -0
- package/src/db.ts +197 -0
- package/src/define.ts +285 -0
- package/src/describe.ts +262 -0
- package/src/drift.ts +149 -0
- package/src/env.ts +122 -0
- package/src/errors.ts +202 -0
- package/src/expectations.ts +165 -0
- package/src/fake.ts +156 -0
- package/src/fill.ts +24 -0
- package/src/index.ts +120 -0
- package/src/ledger.ts +419 -0
- package/src/lint.ts +208 -0
- package/src/log.ts +82 -0
- package/src/op.ts +399 -0
- package/src/outline.ts +620 -0
- package/src/paths.ts +32 -0
- package/src/plugin.ts +37 -0
- package/src/policy.ts +139 -0
- package/src/resource.ts +447 -0
- package/src/resources.ts +256 -0
- package/src/run.ts +91 -0
- package/src/sandbox.ts +157 -0
- package/src/scenario.ts +1328 -0
- package/src/scenarios.ts +111 -0
- package/src/shared.ts +116 -0
- package/src/steps.ts +90 -0
- package/src/version.ts +86 -0
- package/src/worker.ts +253 -0
package/dist/lint.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { basename, dirname, join } from "node:path";
|
|
2
|
+
import { byPosition, childrenOf, parsed } from "./ast.js";
|
|
3
|
+
import { readDataFile } from "./datafile.js";
|
|
4
|
+
import { constsOf, outlineBody, workflowLiterals } from "./outline.js";
|
|
5
|
+
import { inside, nearestDir, RELATIVE } from "./paths.js";
|
|
6
|
+
// From src/ and from dist/ alike, the package's own oxlint.json: the one list of allowed names.
|
|
7
|
+
import oxlint from "../oxlint.json" with { type: "json" };
|
|
8
|
+
/**
|
|
9
|
+
* A workflow must call vendors through `ctx`, so the policy sees every call. These are the
|
|
10
|
+
* import rules oxlint's built-in rules cannot express; the clock, randomness, the network
|
|
11
|
+
* and the environment are oxlint's (the package's `oxlint.json`).
|
|
12
|
+
*/
|
|
13
|
+
const THROUGH_CTX = "a workflow must call vendors through ctx, so the policy sees every call";
|
|
14
|
+
/**
|
|
15
|
+
* The packages the author surface is imported from, each allowing the same names: `sanoma`
|
|
16
|
+
* re-exports what `@sanoma-ai/workflows` allows (and `z`). Adding one here accepts it.
|
|
17
|
+
*/
|
|
18
|
+
const AUTHOR_PACKAGES = ["@sanoma-ai/workflows", "sanoma"];
|
|
19
|
+
/** The packages a workflow may import from at all: the author packages, connectors and zod. */
|
|
20
|
+
const ALLOWED_PACKAGE = new RegExp(`^(${[...AUTHOR_PACKAGES, "@sanoma-ai/connector-[a-z0-9-]+", "zod"].join("|")})$`);
|
|
21
|
+
/** Packages refused by name, with the reason. */
|
|
22
|
+
const REFUSED_PACKAGES = [
|
|
23
|
+
[/^@sanoma-ai\/testing(\/|$)/, `fakes and test helpers belong in tests; ${THROUGH_CTX}`],
|
|
24
|
+
[/^@sanoma-ai\/app(\/|$)/, "the app starts runs and decides approvals, so a workflow could approve itself"],
|
|
25
|
+
[/^@sanoma-ai\/connector-[^/]+\/(fake|driver)(\/|$)/, `${THROUGH_CTX}; import the connector itself`],
|
|
26
|
+
[/^sanoma\//, "`sanoma` has no entry points for a workflow: import its names from `sanoma` itself"],
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* What a workflow or policy may import from an author package, besides types: the one list,
|
|
30
|
+
* `allowImportNames` of oxlint.json's `@sanoma-ai/workflows` entry (`sanoma`'s generated
|
|
31
|
+
* `oxlint.json` derives its own entry from it). Everything else is
|
|
32
|
+
* refused: the client, the worker and the app could start runs or approve the run's own
|
|
33
|
+
* approvals, a ledger store could forge the audit record (a store keeps the first record per
|
|
34
|
+
* id), and config helpers can read credentials.
|
|
35
|
+
*/
|
|
36
|
+
const WORKFLOW_IMPORTS = restrictedImports(oxlint).find((p) => p.name === "@sanoma-ai/workflows")?.allowImportNames ?? [];
|
|
37
|
+
const ALLOWED_NAMES = new Set(WORKFLOW_IMPORTS);
|
|
38
|
+
const NOT_ALLOWED = `a workflow or policy imports only ${WORKFLOW_IMPORTS.join(", ")} and types from ` +
|
|
39
|
+
`${AUTHOR_PACKAGES.join(" or ")}: the rest could start runs, approve its own approvals, forge the ledger or read credentials`;
|
|
40
|
+
/**
|
|
41
|
+
* Error classes a run must not test with `instanceof`: on a replay DBOS rethrows a serialized
|
|
42
|
+
* copy, which is no instance of them, so a branch on it goes another way than the first time
|
|
43
|
+
* and the run's steps fall out of step. Read the code with `errorCode(err)` instead.
|
|
44
|
+
*/
|
|
45
|
+
const ERROR_CLASSES = new Set(["DriverError", "SanomaError", "PolicyDeniedError", "RejectedError"]);
|
|
46
|
+
export function lintWorkflow(input, path) {
|
|
47
|
+
const filename = typeof input === "string" ? path : input.file;
|
|
48
|
+
const read = typeof input === "string" ? parsed(input, filename ?? "workflow.ts") : input;
|
|
49
|
+
const { program, at } = read;
|
|
50
|
+
const problems = [...read.problems];
|
|
51
|
+
const root = filename === undefined ? undefined : nearestDir(filename, ["workflows", "policies"]);
|
|
52
|
+
const checkSource = (node) => {
|
|
53
|
+
const spec = node.source.value;
|
|
54
|
+
const refusal = whyRefused(spec, filename, root);
|
|
55
|
+
if (refusal) {
|
|
56
|
+
problems.push({ ...at(node.source.start), message: `import "${spec}" is not allowed in a workflow: ${refusal}` });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!AUTHOR_PACKAGES.includes(spec))
|
|
60
|
+
return;
|
|
61
|
+
if (node.type === "ExportAllDeclaration") {
|
|
62
|
+
problems.push({
|
|
63
|
+
...at(node.start),
|
|
64
|
+
message: `export * from "${spec}" is not allowed in a workflow: it would re-export what a workflow may not import`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
for (const s of node.specifiers ?? []) {
|
|
68
|
+
if (s.type === "ImportNamespaceSpecifier") {
|
|
69
|
+
problems.push({
|
|
70
|
+
...at(s.start),
|
|
71
|
+
message: `import * from "${spec}" is not allowed in a workflow: import the names you use, so the rest stays out`,
|
|
72
|
+
});
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// `import type { X }`, `import { type X }` and `export type { X }` bring in no values.
|
|
76
|
+
if (node.importKind === "type" ||
|
|
77
|
+
node.exportKind === "type" ||
|
|
78
|
+
s.importKind === "type" ||
|
|
79
|
+
s.exportKind === "type") {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// What the module exports under: `imported` for an import, `local` for a re-export.
|
|
83
|
+
const named = s.imported ?? s.local;
|
|
84
|
+
const imported = s.type === "ImportDefaultSpecifier" ? "default" : (named?.name ?? named?.value);
|
|
85
|
+
if (!ALLOWED_NAMES.has(imported)) {
|
|
86
|
+
problems.push({ ...at(s.start), message: `${imported} is not allowed in a workflow: ${NOT_ALLOWED}` });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const visit = (node) => {
|
|
91
|
+
if (!node)
|
|
92
|
+
return;
|
|
93
|
+
if ((node.type === "ImportDeclaration" ||
|
|
94
|
+
node.type === "ExportNamedDeclaration" ||
|
|
95
|
+
node.type === "ExportAllDeclaration") &&
|
|
96
|
+
node.source) {
|
|
97
|
+
checkSource(node);
|
|
98
|
+
}
|
|
99
|
+
else if (node.type === "ImportExpression") {
|
|
100
|
+
problems.push({ ...at(node.start), message: "dynamic import is not allowed in a workflow" });
|
|
101
|
+
}
|
|
102
|
+
else if (node.type === "BinaryExpression" && node.operator === "instanceof") {
|
|
103
|
+
checkInstanceof(node);
|
|
104
|
+
}
|
|
105
|
+
for (const child of childrenOf(node))
|
|
106
|
+
visit(child);
|
|
107
|
+
};
|
|
108
|
+
// A workflow imports none of these classes, so they are refused by the name they have.
|
|
109
|
+
const checkInstanceof = (node) => {
|
|
110
|
+
const right = node.right;
|
|
111
|
+
const name = right?.type === "Identifier"
|
|
112
|
+
? right.name
|
|
113
|
+
: right?.type === "MemberExpression" && !right.computed
|
|
114
|
+
? right.property?.name
|
|
115
|
+
: undefined;
|
|
116
|
+
if (name !== undefined && ERROR_CLASSES.has(name)) {
|
|
117
|
+
problems.push({
|
|
118
|
+
...at(node.start),
|
|
119
|
+
message: `instanceof ${name} is not allowed in a workflow: on a replay DBOS rethrows a copy of the error, ` +
|
|
120
|
+
"which is no instance of it, so the run would take another branch; read `errorCode(err)`",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
visit(program);
|
|
125
|
+
// Each workflow's `run` as the worker reads it when it starts: what the outline cannot draw,
|
|
126
|
+
// the worker refuses to start with. One reading, so the lint and the worker agree.
|
|
127
|
+
const outlined = { consts: constsOf(program), source: typeof input === "string" ? input : input.source };
|
|
128
|
+
for (const { run } of workflowLiterals(program))
|
|
129
|
+
for (const { start, message } of outlineBody(run, outlined).problems)
|
|
130
|
+
problems.push({ ...at(start), message });
|
|
131
|
+
return byPosition(problems);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Checks a data file under `resources/`: only named imports (a connector's constructors from its
|
|
135
|
+
* `…/resources` entry, and resources from other data files inside the same `resources/`
|
|
136
|
+
* directory), `export const <name> = <vendor>.<type>({ … })` with literal fields and names of
|
|
137
|
+
* declared resources, and `export default [ … ]`. Each message says what to write instead.
|
|
138
|
+
* It is the subset `readResources` reads (`readDataFile`), without the config: whether the
|
|
139
|
+
* connector, the type and the values are known is the reader's to say.
|
|
140
|
+
*/
|
|
141
|
+
export function lintResources(source, filename) {
|
|
142
|
+
const { program, problems, at } = parsed(source, filename);
|
|
143
|
+
for (const p of readDataFile(program, filename).problems)
|
|
144
|
+
problems.push({ ...at(p.start), message: p.message });
|
|
145
|
+
return byPosition(problems);
|
|
146
|
+
}
|
|
147
|
+
/** The `paths` of oxlint.json's `no-restricted-imports` rule. */
|
|
148
|
+
function restrictedImports(config) {
|
|
149
|
+
// JSON types the rule's [severity, options] as an array of either.
|
|
150
|
+
return config.overrides.flatMap((o) => "no-restricted-imports" in o.rules
|
|
151
|
+
? o.rules["no-restricted-imports"][1].paths
|
|
152
|
+
: []);
|
|
153
|
+
}
|
|
154
|
+
function whyRefused(spec, filename, root) {
|
|
155
|
+
if (RELATIVE.test(spec)) {
|
|
156
|
+
if (filename === undefined || root === undefined || inside(root, join(dirname(filename), spec)))
|
|
157
|
+
return undefined;
|
|
158
|
+
const where = basename(root) === "workflows" || basename(root) === "policies" ? `${basename(root)}/` : "this directory";
|
|
159
|
+
return `it reaches outside ${where}, toward the config and its drivers; ${THROUGH_CTX}`;
|
|
160
|
+
}
|
|
161
|
+
for (const [pattern, reason] of REFUSED_PACKAGES)
|
|
162
|
+
if (pattern.test(spec))
|
|
163
|
+
return reason;
|
|
164
|
+
if (ALLOWED_PACKAGE.test(spec))
|
|
165
|
+
return undefined;
|
|
166
|
+
return "import only from `sanoma` (or `@sanoma-ai/workflows`), a `@sanoma-ai/connector-*` package, zod, or a file beside this one";
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=lint.js.map
|
package/dist/lint.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint.js","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"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"}
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DLogger } from "@dbos-inc/dbos-sdk";
|
|
2
|
+
/** Shows the messages at `level` and above from now on; an unknown level as `warn`. */
|
|
3
|
+
export declare function setLogLevel(level?: string): void;
|
|
4
|
+
/** Tells the operator what the runtime did, such as loading credentials (never their values); shown at `info`. */
|
|
5
|
+
export declare const info: (message: string) => void;
|
|
6
|
+
/** Tells the operator something went wrong or was skipped, without failing anything; shown at `warn`. */
|
|
7
|
+
export declare const warn: (message: string) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Runs a workflow's body, as a sandbox run's when `sandbox`: `dbosLogger` then shows none of DBOS's
|
|
10
|
+
* warnings from inside it (its steps' failed tries among them), nor the error it fails with. A
|
|
11
|
+
* scenario's fakes fail on purpose; the run's ledger and its status say what happened. The run
|
|
12
|
+
* fails with a copy of its error (same prototype and properties), made for it, so the same error
|
|
13
|
+
* object thrown by a live run is still shown.
|
|
14
|
+
*/
|
|
15
|
+
export declare function quietIfSandbox<T>(sandbox: boolean, body: () => Promise<T>): Promise<T>;
|
|
16
|
+
/**
|
|
17
|
+
* DBOS's logger (`DBOS.setConfig`'s `logger`): its lines at the last `startWorker`'s `logLevel`,
|
|
18
|
+
* printed as DBOS's own console logger (`DBOSConsoleLogger`) prints them, except a sandbox run's
|
|
19
|
+
* (`quietIfSandbox`). A live run's failed tries and failure show as they always did. Installed
|
|
20
|
+
* only while DBOS exports no logs over OTLP: a custom logger replaces that transport.
|
|
21
|
+
*/
|
|
22
|
+
export declare const dbosLogger: DLogger;
|
|
23
|
+
/** A value as JSON, or as a string when it isn't JSON, cut to `max` characters, for a message. */
|
|
24
|
+
export declare function shown(value: unknown, max?: number): string;
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
// DBOS's levels (winston's), most severe first. A message shows when its level is at or above the
|
|
3
|
+
// last `startWorker`'s `logLevel`, as DBOS's own do: `warn` by default. One threshold for the
|
|
4
|
+
// process, as DBOS's own logger is one per process.
|
|
5
|
+
const LEVELS = ["error", "warn", "info", "http", "verbose", "debug", "silly"];
|
|
6
|
+
const levelsUpTo = (level) => new Set(LEVELS.slice(0, LEVELS.indexOf(level) + 1));
|
|
7
|
+
let visible = levelsUpTo("warn");
|
|
8
|
+
/** Shows the messages at `level` and above from now on; an unknown level as `warn`. */
|
|
9
|
+
export function setLogLevel(level = "warn") {
|
|
10
|
+
visible = levelsUpTo(LEVELS.includes(level) ? level : "warn");
|
|
11
|
+
}
|
|
12
|
+
const shows = (level) => visible.has(level);
|
|
13
|
+
/** Tells the operator what the runtime did, such as loading credentials (never their values); shown at `info`. */
|
|
14
|
+
export const info = (message) => {
|
|
15
|
+
if (shows("info"))
|
|
16
|
+
console.info(`sanoma: ${message}`);
|
|
17
|
+
};
|
|
18
|
+
/** Tells the operator something went wrong or was skipped, without failing anything; shown at `warn`. */
|
|
19
|
+
export const warn = (message) => {
|
|
20
|
+
if (shows("warn"))
|
|
21
|
+
console.warn(`sanoma: ${message}`);
|
|
22
|
+
};
|
|
23
|
+
/** Set while a sandbox run's body runs. */
|
|
24
|
+
const sandboxRun = new AsyncLocalStorage();
|
|
25
|
+
/** What sandbox runs failed with: each a copy made for its run (`quietIfSandbox`). */
|
|
26
|
+
const sandboxErrors = new WeakSet();
|
|
27
|
+
/**
|
|
28
|
+
* Runs a workflow's body, as a sandbox run's when `sandbox`: `dbosLogger` then shows none of DBOS's
|
|
29
|
+
* warnings from inside it (its steps' failed tries among them), nor the error it fails with. A
|
|
30
|
+
* scenario's fakes fail on purpose; the run's ledger and its status say what happened. The run
|
|
31
|
+
* fails with a copy of its error (same prototype and properties), made for it, so the same error
|
|
32
|
+
* object thrown by a live run is still shown.
|
|
33
|
+
*/
|
|
34
|
+
export function quietIfSandbox(sandbox, body) {
|
|
35
|
+
if (!sandbox)
|
|
36
|
+
return body();
|
|
37
|
+
return sandboxRun.run(true, body).catch((err) => {
|
|
38
|
+
if (typeof err !== "object" || err === null)
|
|
39
|
+
throw err;
|
|
40
|
+
const copy = Object.create(Object.getPrototypeOf(err), Object.getOwnPropertyDescriptors(err));
|
|
41
|
+
// V8's `stack` accessor reads the original's internal slot, which the copy lacks.
|
|
42
|
+
if ("stack" in err)
|
|
43
|
+
Object.defineProperty(copy, "stack", { value: err.stack, writable: true, configurable: true });
|
|
44
|
+
sandboxErrors.add(copy);
|
|
45
|
+
throw copy;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* DBOS's logger (`DBOS.setConfig`'s `logger`): its lines at the last `startWorker`'s `logLevel`,
|
|
50
|
+
* printed as DBOS's own console logger (`DBOSConsoleLogger`) prints them, except a sandbox run's
|
|
51
|
+
* (`quietIfSandbox`). A live run's failed tries and failure show as they always did. Installed
|
|
52
|
+
* only while DBOS exports no logs over OTLP: a custom logger replaces that transport.
|
|
53
|
+
*/
|
|
54
|
+
export const dbosLogger = {
|
|
55
|
+
info: (entry) => void (shows("info") && console.log(entry)),
|
|
56
|
+
debug: (entry) => void (shows("debug") && console.debug(entry)),
|
|
57
|
+
warn(entry) {
|
|
58
|
+
if (shows("warn") && !sandboxRun.getStore())
|
|
59
|
+
console.warn(entry);
|
|
60
|
+
},
|
|
61
|
+
// DBOS's GlobalLogger passes a string, with the stack in `metadata.stack` and an error in `metadata.error`.
|
|
62
|
+
error(entry, metadata) {
|
|
63
|
+
const error = metadata?.error;
|
|
64
|
+
if (!shows("error") || (typeof error === "object" && error !== null && sandboxErrors.has(error)))
|
|
65
|
+
return;
|
|
66
|
+
if (metadata?.stack)
|
|
67
|
+
console.error(entry, "\n", metadata.stack);
|
|
68
|
+
else
|
|
69
|
+
console.error(entry);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
/** A value as JSON, or as a string when it isn't JSON, cut to `max` characters, for a message. */
|
|
73
|
+
export function shown(value, max = Number.POSITIVE_INFINITY) {
|
|
74
|
+
let text;
|
|
75
|
+
try {
|
|
76
|
+
text = JSON.stringify(value) ?? String(value);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
text = String(value);
|
|
80
|
+
}
|
|
81
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=log.js.map
|
package/dist/log.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGrD,kGAAkG;AAClG,8FAA8F;AAC9F,oDAAoD;AACpD,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAC9E,MAAM,UAAU,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC1F,IAAI,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;AAEjC,uFAAuF;AACvF,MAAM,UAAU,WAAW,CAAC,KAAK,GAAG,MAAM;IACxC,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAEpD,kHAAkH;AAClH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,OAAe,EAAE,EAAE;IACtC,IAAI,KAAK,CAAC,MAAM,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,WAAW,OAAO,EAAE,CAAC,CAAC;AACxD,CAAC,CAAC;AAEF,yGAAyG;AACzG,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,OAAe,EAAE,EAAE;IACtC,IAAI,KAAK,CAAC,MAAM,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,WAAW,OAAO,EAAE,CAAC,CAAC;AACxD,CAAC,CAAC;AAEF,2CAA2C;AAC3C,MAAM,UAAU,GAAG,IAAI,iBAAiB,EAAQ,CAAC;AACjD,sFAAsF;AACtF,MAAM,aAAa,GAAG,IAAI,OAAO,EAAU,CAAC;AAE5C;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAI,OAAgB,EAAE,IAAsB;IACxE,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,EAAE,CAAC;IAC5B,OAAO,UAAU,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACvD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;YAAE,MAAM,GAAG,CAAC;QACvD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAW,CAAC;QACxG,kFAAkF;QAClF,IAAI,OAAO,IAAI,GAAG;YAAE,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;QACnH,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxB,MAAM,IAAI,CAAC;IACb,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAY;IACjC,IAAI,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC3D,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK;QACR,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE;YAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnE,CAAC;IACD,4GAA4G;IAC5G,KAAK,CAAC,KAAK,EAAE,QAAQ;QACnB,MAAM,KAAK,GAAG,QAAQ,EAAE,KAAK,CAAC;QAC9B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAAE,OAAO;QACzG,IAAI,QAAQ,EAAE,KAAK;YAAE,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;;YAC3D,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;CACF,CAAC;AAEF,kGAAkG;AAClG,MAAM,UAAU,KAAK,CAAC,KAAc,EAAE,GAAG,GAAG,MAAM,CAAC,iBAAiB;IAClE,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7D,CAAC"}
|
package/dist/op.d.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { Fake } from "./fake.ts";
|
|
3
|
+
import type { Steps } from "./steps.ts";
|
|
4
|
+
/**
|
|
5
|
+
* What an operation does to the outside world. It sets the badge a reviewer
|
|
6
|
+
* sees and, later, the Cedar action the call is checked against.
|
|
7
|
+
*/
|
|
8
|
+
export type Effect = "read" | "write" | "publish" | "send" | "money" | "access";
|
|
9
|
+
export interface OpSpec<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType> {
|
|
10
|
+
effect: Effect;
|
|
11
|
+
input: I;
|
|
12
|
+
output: O;
|
|
13
|
+
/** Safe to retry: the vendor dedupes repeated calls. Otherwise a failed call stops the run. */
|
|
14
|
+
idempotent?: boolean;
|
|
15
|
+
description?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The resource instance a call acts on, from its parsed input, such as a post's id. The
|
|
18
|
+
* policy sees it as `target`, so it can decide per instance. Must be deterministic.
|
|
19
|
+
*
|
|
20
|
+
* Its input is typed `any` inside `defineConnector`: TypeScript gives an inline function the
|
|
21
|
+
* type `Specs` declares, not the one its own spec's schema implies. Annotate it to have it
|
|
22
|
+
* checked: `target: ({ id }: { id: string }) => \`post/${id}\``.
|
|
23
|
+
*/
|
|
24
|
+
target?: (input: any) => string;
|
|
25
|
+
/**
|
|
26
|
+
* How a scenario says this operation was set up or called, in Gherkin steps: `given` seeds
|
|
27
|
+
* the fake through it, `expect` expects a call to it. Cucumber expressions whose parameters are
|
|
28
|
+
* fields of the input, such as `{ given: "a post titled {title} exists" }`.
|
|
29
|
+
*/
|
|
30
|
+
phrases?: {
|
|
31
|
+
given?: string;
|
|
32
|
+
expect?: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Top-level fields of its input and output that are the driver's own data, not the vendor's
|
|
36
|
+
* (a resource type's `handle`): the ledger records each as `"<name>"`, never its value.
|
|
37
|
+
*/
|
|
38
|
+
opaque?: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
/** One vendor operation, such as `ghost.post.publish`. Declares the contract only; drivers implement it. */
|
|
41
|
+
export interface Op<V extends string = string, R extends string = string, N extends string = string, I = any, O = any> {
|
|
42
|
+
readonly kind: "op";
|
|
43
|
+
readonly id: `${V}.${R}.${N}`;
|
|
44
|
+
readonly vendor: V;
|
|
45
|
+
readonly resource: R;
|
|
46
|
+
readonly name: N;
|
|
47
|
+
readonly effect: Effect;
|
|
48
|
+
readonly idempotent: boolean;
|
|
49
|
+
readonly description?: string;
|
|
50
|
+
/** From the spec: the resource instance a call acts on. */
|
|
51
|
+
readonly target?: (input: any) => string;
|
|
52
|
+
/** From the spec: how a scenario's steps name it. */
|
|
53
|
+
readonly phrases?: {
|
|
54
|
+
readonly given?: string;
|
|
55
|
+
readonly expect?: string;
|
|
56
|
+
};
|
|
57
|
+
/** From the spec: fields the ledger records by name only. */
|
|
58
|
+
readonly opaque?: readonly string[];
|
|
59
|
+
readonly input: z.ZodType<any, I>;
|
|
60
|
+
readonly output: z.ZodType<O>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Where a resource type keeps its brand. A symbol, so `defineConnector` tells a resource type
|
|
64
|
+
* (`defineResource`) from a group of operations; a registered one (`Symbol.for`), so a copy of
|
|
65
|
+
* this package reads what another copy made, as `VENDOR`.
|
|
66
|
+
*/
|
|
67
|
+
export declare const RESOURCE: unique symbol;
|
|
68
|
+
/** What `defineConnector` reads of a resource type; `defineResource` makes them. */
|
|
69
|
+
export interface ResourceGroup {
|
|
70
|
+
readonly [RESOURCE]: true;
|
|
71
|
+
readonly vendor: string;
|
|
72
|
+
readonly type: string;
|
|
73
|
+
readonly ops: Readonly<Record<string, OpSpec>>;
|
|
74
|
+
}
|
|
75
|
+
/** A connector's groups: operations by name, or a resource type, whose operations are `read` and `import`. */
|
|
76
|
+
export type Specs = Record<string, Record<string, OpSpec> | ResourceGroup>;
|
|
77
|
+
/** The operations of one group of `Specs`. */
|
|
78
|
+
export type GroupOps<G> = G extends ResourceGroup ? G["ops"] : G;
|
|
79
|
+
type InputOf<T> = T extends OpSpec<infer I, any> ? I : never;
|
|
80
|
+
type OutputOf<T> = T extends OpSpec<any, infer O> ? O : never;
|
|
81
|
+
/**
|
|
82
|
+
* Where a connector keeps its vendor. A symbol, so it names no resource: `Object.values` and
|
|
83
|
+
* `keyof` over a connector see its resources only. A registered one (`Symbol.for`): a process
|
|
84
|
+
* may load two copies of this package (a connector's peer linked into a sanoma checkout, the
|
|
85
|
+
* app's from its tarball), and each must read the connectors the other made.
|
|
86
|
+
*/
|
|
87
|
+
export declare const VENDOR: unique symbol;
|
|
88
|
+
export type Connector<V extends string, S extends Specs> = {
|
|
89
|
+
readonly [R in keyof S & string]: {
|
|
90
|
+
readonly [N in keyof GroupOps<S[R]> & string]: Op<V, R, N, z.input<InputOf<GroupOps<S[R]>[N]>>, z.output<OutputOf<GroupOps<S[R]>[N]>>>;
|
|
91
|
+
};
|
|
92
|
+
} & {
|
|
93
|
+
readonly [VENDOR]: ConnectorVendor<V>;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* The shape of a connector's `[VENDOR]` record. A process may load two builds of this package,
|
|
97
|
+
* and `resolveConfig` reads only a record of its own shape (`checkConnector`): change it with
|
|
98
|
+
* the record, so a build refuses a connector another build made in another shape.
|
|
99
|
+
*/
|
|
100
|
+
export declare const CONNECTOR_FORMAT = 1;
|
|
101
|
+
/**
|
|
102
|
+
* A connector's vendor: its id, who it is (`defineConnector`'s third argument, for a UI), its
|
|
103
|
+
* resource types, and the driver and fake its package attaches (`withSetup`).
|
|
104
|
+
*/
|
|
105
|
+
export interface ConnectorVendor<V extends string = string> {
|
|
106
|
+
/** The record's shape (`CONNECTOR_FORMAT`). */
|
|
107
|
+
readonly format: typeof CONNECTOR_FORMAT;
|
|
108
|
+
readonly id: V;
|
|
109
|
+
readonly info?: VendorInfo;
|
|
110
|
+
/** The resource types among its groups, in the order given. */
|
|
111
|
+
readonly resources: readonly ResourceGroup[];
|
|
112
|
+
/** What `withSetup` attached: how the config's resolver makes its driver and its fake. */
|
|
113
|
+
readonly setup?: ConnectorSetup;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Who a connector's vendor is, and where the connector lives, for a UI to show beside its
|
|
117
|
+
* operations. `logo` is the vendor's mark as inline SVG markup (one `<svg>…</svg>` element), and
|
|
118
|
+
* `dark` its variant for dark backgrounds. The app shows a logo only as an image (a `data:` URL
|
|
119
|
+
* in an `<img>`), where an SVG's scripts and external references never run or load.
|
|
120
|
+
*/
|
|
121
|
+
export interface VendorInfo {
|
|
122
|
+
/** The vendor's name as it writes it, such as "Resend". */
|
|
123
|
+
title?: string;
|
|
124
|
+
logo?: {
|
|
125
|
+
svg: string;
|
|
126
|
+
dark?: string;
|
|
127
|
+
};
|
|
128
|
+
/** The connector's npm package name, such as "@sanoma-ai/connector-resend". */
|
|
129
|
+
package?: string;
|
|
130
|
+
/** The connector package's `homepage` from its package.json: where its code and README are, as an https URL. */
|
|
131
|
+
homepage?: string;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Declares a vendor's operations, grouped by resource: `defineConnector("ghost", { post: { create: {...} } })`.
|
|
135
|
+
* A group may be a resource type from `defineResource`, under its own `type`, for its `read` and
|
|
136
|
+
* `import`: `defineConnector("github", { repository }, { package: "@sanoma-ai/connector-github" })`;
|
|
137
|
+
* it must be of the same vendor, and the connector needs `info.package`, whose `/resources`
|
|
138
|
+
* entry exports the types for data files.
|
|
139
|
+
* `info` says who the vendor is and where the connector lives, for a UI:
|
|
140
|
+
* `{ title: "Resend", logo: { svg }, package: "@sanoma-ai/connector-resend", homepage: "https://…" }`.
|
|
141
|
+
*/
|
|
142
|
+
export declare function defineConnector<const V extends string, const S extends Specs>(vendor: V, specs: S, info?: VendorInfo): Connector<V, S>;
|
|
143
|
+
/**
|
|
144
|
+
* What the runtime tells a driver about the call it is making. `idempotencyKey` is the same
|
|
145
|
+
* on every replay and retry of one call (`<runId>:<seq>`), so a driver can hand it to the
|
|
146
|
+
* vendor, or dedupe on it, and a crash between the vendor's reply and the checkpoint does
|
|
147
|
+
* not repeat the side effect.
|
|
148
|
+
*/
|
|
149
|
+
export interface CallContext {
|
|
150
|
+
idempotencyKey: string;
|
|
151
|
+
runId: string;
|
|
152
|
+
opId: string;
|
|
153
|
+
/** 1 on the first try; retries count up (idempotent operations only). */
|
|
154
|
+
attempt: number;
|
|
155
|
+
}
|
|
156
|
+
export type DriverFn<I = any, O = unknown> = (input: I, call: CallContext) => Promise<O>;
|
|
157
|
+
/**
|
|
158
|
+
* A vendor's answer that the driver understood. `retryable` tells the runtime whether to try
|
|
159
|
+
* again (an idempotent operation on a timeout or a 5xx) or to stop (a 4xx). `status` and
|
|
160
|
+
* `vendorCode` keep what the vendor said, so the ledger and the UI can show it.
|
|
161
|
+
*/
|
|
162
|
+
export declare class DriverError extends Error {
|
|
163
|
+
readonly code: "driver_failed";
|
|
164
|
+
readonly retryable: boolean;
|
|
165
|
+
readonly status?: number;
|
|
166
|
+
readonly vendorCode?: string;
|
|
167
|
+
constructor(message: string, options: {
|
|
168
|
+
retryable: boolean;
|
|
169
|
+
status?: number;
|
|
170
|
+
vendorCode?: string;
|
|
171
|
+
cause?: unknown;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Whether a vendor's HTTP status is worth retrying: a request timeout (408), a rate limit (429)
|
|
176
|
+
* or a server error (5xx). It is the runtime's default rule; a driver adds its vendor's
|
|
177
|
+
* exceptions around it, such as a quota that does not lift in the seconds a retry waits.
|
|
178
|
+
*/
|
|
179
|
+
export declare function retryableStatus(status: number): boolean;
|
|
180
|
+
/**
|
|
181
|
+
* The environment variables a driver reads, as a Zod object of string schemas keyed by variable
|
|
182
|
+
* name, such as `z.object({ ACME_API_KEY: z.string().describe("an API key, from Settings, API keys") })`:
|
|
183
|
+
* each `.describe()` is the variable's one-line manual (what to put there, where to get it),
|
|
184
|
+
* `.optional()` marks one that may be unset. `resolveConfig` checks it against `process.env` and
|
|
185
|
+
* never keeps a value. The driver still reads the raw environment when it is called, so a
|
|
186
|
+
* `.default()` or `.transform()` is not applied to what it reads (use `.optional()` and name the
|
|
187
|
+
* default in `.describe()`), and a `.refine()` or `.superRefine()` on the object is not evaluated,
|
|
188
|
+
* only its fields.
|
|
189
|
+
*/
|
|
190
|
+
export type DriverEnv = z.ZodObject<Record<string, z.ZodType<string | undefined, string | undefined>>>;
|
|
191
|
+
/**
|
|
192
|
+
* Implements a vendor's operations, keyed `"resource.name"`. A driver reads its credentials
|
|
193
|
+
* from the environment when it is called, never from the config file; `env` declares them.
|
|
194
|
+
*/
|
|
195
|
+
export interface Driver {
|
|
196
|
+
readonly vendor: string;
|
|
197
|
+
readonly ops: Readonly<Record<string, DriverFn>>;
|
|
198
|
+
readonly env?: DriverEnv;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* What a connector package attaches to its connector (`withSetup`), for the config's resolver
|
|
202
|
+
* to call: the config then names the connector alone, and never a driver, a fake or steps. Any may
|
|
203
|
+
* be missing: a workflow using an operation of a connector with no driver is refused, and a
|
|
204
|
+
* connector with no fake has none for sandbox runs.
|
|
205
|
+
*/
|
|
206
|
+
export interface ConnectorSetup {
|
|
207
|
+
/** Makes the live driver, which reads its credentials from the environment when called: once per resolved config. */
|
|
208
|
+
driver?: () => Driver;
|
|
209
|
+
/** Makes a fake of the vendor (`defineFake`): once per sandbox run, its own. */
|
|
210
|
+
fake?: () => Fake<any, any>;
|
|
211
|
+
/** The connector's steps for Gherkin tests (`defineSteps`): a config's scenarios and the connector's own features can use them. */
|
|
212
|
+
steps?: Steps;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The connector with `setup` laid over what it already carries, as a connector package exports
|
|
216
|
+
* it: `export const ghost = withSetup(ghostOps, { driver: ghostDriver, fake: fakeGhost, steps: ghostSteps })`. A
|
|
217
|
+
* config that needs a driver made otherwise (another bridge, other options) swaps that one and
|
|
218
|
+
* keeps the fake: `withSetup(github, { driver: () => githubDriver({ bridge }) })`. The operations
|
|
219
|
+
* are the same objects, and the type is the connector's. The setup is kept on the connector's
|
|
220
|
+
* vendor record (`[VENDOR]`), so a spread copy of the connector keeps it.
|
|
221
|
+
*/
|
|
222
|
+
export declare function withSetup<C extends Connector<any, any>>(connector: C, setup: Partial<ConnectorSetup>): C;
|
|
223
|
+
/**
|
|
224
|
+
* Throws unless `value` is a connector this package can read: one `defineConnector` made, in this
|
|
225
|
+
* copy of the package or another of the same `CONNECTOR_FORMAT`. One an older build made (its
|
|
226
|
+
* `VENDOR` an unregistered symbol, or its record without a format) is refused as such.
|
|
227
|
+
*/
|
|
228
|
+
export declare function checkConnector(value: unknown): asserts value is Connector<any, any>;
|
|
229
|
+
/** What `withSetup` attached to a connector: nothing when it attached nothing. */
|
|
230
|
+
export declare const setupOf: (connector: Connector<any, any>) => ConnectorSetup;
|
|
231
|
+
export declare function isOp(x: unknown): x is Op;
|
|
232
|
+
/**
|
|
233
|
+
* What a driver for a connector must implement: every operation, by resource and name. Each
|
|
234
|
+
* function gets the input as the operation's schema parses it and returns what its output
|
|
235
|
+
* schema accepts.
|
|
236
|
+
*/
|
|
237
|
+
export type DriverImpl<S extends Specs> = {
|
|
238
|
+
[R in keyof S & string]: {
|
|
239
|
+
[N in keyof GroupOps<S[R]> & string]: DriverFn<z.output<InputOf<GroupOps<S[R]>[N]>>, z.input<OutputOf<GroupOps<S[R]>[N]>>>;
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
/** The ids of a connector's operations, such as `"ghost.post.create" | "ghost.post.publish"`. */
|
|
243
|
+
export type OpIdOf<V extends string, S extends Specs> = {
|
|
244
|
+
[R in keyof S & string]: {
|
|
245
|
+
[N in keyof GroupOps<S[R]> & string]: `${V}.${R}.${N}`;
|
|
246
|
+
}[keyof GroupOps<S[R]> & string];
|
|
247
|
+
}[keyof S & string];
|
|
248
|
+
/**
|
|
249
|
+
* Implements a connector's operations, typed by its schemas. A driver must be complete: an
|
|
250
|
+
* operation the connector declares that `impl` leaves out, or one `impl` adds that the
|
|
251
|
+
* connector doesn't declare, is refused here rather than when a run calls it. `options.env`
|
|
252
|
+
* declares the environment variables it reads (see `DriverEnv`).
|
|
253
|
+
*/
|
|
254
|
+
export declare function defineDriver<V extends string, S extends Specs>(connector: Connector<V, S>, impl: DriverImpl<S>, options?: {
|
|
255
|
+
env?: DriverEnv;
|
|
256
|
+
}): Driver;
|
|
257
|
+
export {};
|