@sanoma-ai/workflows 0.0.0-stage → 0.2.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 +544 -2
- package/dist/apply-steps.d.ts +6 -0
- package/dist/apply-steps.js +93 -0
- package/dist/apply-steps.js.map +1 -0
- package/dist/apply.d.ts +197 -0
- package/dist/apply.js +470 -0
- package/dist/apply.js.map +1 -0
- 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 +37 -0
- package/dist/call.js +417 -0
- package/dist/call.js.map +1 -0
- package/dist/client.d.ts +203 -0
- package/dist/client.js +457 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +126 -0
- package/dist/config.js +264 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +55 -0
- package/dist/credentials.js +261 -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 +71 -0
- package/dist/datafile.js +380 -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 +141 -0
- package/dist/describe.js +135 -0
- package/dist/describe.js.map +1 -0
- package/dist/drift.d.ts +127 -0
- package/dist/drift.js +167 -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 +162 -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/host.d.ts +56 -0
- package/dist/host.js +45 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +29 -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/lockfile.d.ts +36 -0
- package/dist/lockfile.js +69 -0
- package/dist/lockfile.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 +260 -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 +378 -0
- package/dist/resource.js +430 -0
- package/dist/resource.js.map +1 -0
- package/dist/resources.d.ts +77 -0
- package/dist/resources.js +244 -0
- package/dist/resources.js.map +1 -0
- package/dist/run.d.ts +101 -0
- package/dist/run.js +7 -0
- package/dist/run.js.map +1 -0
- package/dist/sandbox.d.ts +33 -0
- package/dist/sandbox.js +127 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/scenario.d.ts +221 -0
- package/dist/scenario.js +1104 -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/secret.d.ts +42 -0
- package/dist/secret.js +58 -0
- package/dist/secret.js.map +1 -0
- package/dist/shared.d.ts +69 -0
- package/dist/shared.js +84 -0
- package/dist/shared.js.map +1 -0
- package/dist/source.d.ts +58 -0
- package/dist/source.js +54 -0
- package/dist/source.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 +37 -0
- package/dist/worker.js +228 -0
- package/dist/worker.js.map +1 -0
- package/dist/writer.d.ts +35 -0
- package/dist/writer.js +201 -0
- package/dist/writer.js.map +1 -0
- package/oxlint.json +128 -0
- package/package.json +96 -3
- package/src/apply-steps.ts +110 -0
- package/src/apply.ts +626 -0
- package/src/approvals.ts +157 -0
- package/src/ast.ts +115 -0
- package/src/call.ts +483 -0
- package/src/client.ts +604 -0
- package/src/config.ts +404 -0
- package/src/credentials.ts +296 -0
- package/src/cucumber.ts +230 -0
- package/src/datafile.ts +481 -0
- package/src/db.ts +197 -0
- package/src/define.ts +285 -0
- package/src/describe.ts +282 -0
- package/src/drift.ts +259 -0
- package/src/env.ts +122 -0
- package/src/errors.ts +202 -0
- package/src/expectations.ts +191 -0
- package/src/fake.ts +156 -0
- package/src/fill.ts +24 -0
- package/src/host.ts +96 -0
- package/src/index.ts +143 -0
- package/src/ledger.ts +419 -0
- package/src/lint.ts +208 -0
- package/src/lockfile.ts +95 -0
- package/src/log.ts +82 -0
- package/src/op.ts +402 -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 +670 -0
- package/src/resources.ts +311 -0
- package/src/run.ts +106 -0
- package/src/sandbox.ts +161 -0
- package/src/scenario.ts +1365 -0
- package/src/scenarios.ts +111 -0
- package/src/secret.ts +92 -0
- package/src/shared.ts +126 -0
- package/src/source.ts +82 -0
- package/src/steps.ts +90 -0
- package/src/version.ts +86 -0
- package/src/worker.ts +264 -0
- package/src/writer.ts +225 -0
package/src/approvals.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { type ApprovalRequest, type ApprovalResult, type ApprovalState, notApprover, Principal } from "./define.ts";
|
|
4
|
+
import { RejectedError } from "./errors.ts";
|
|
5
|
+
import { entry, write } from "./ledger.ts";
|
|
6
|
+
import { warn } from "./log.ts";
|
|
7
|
+
import type { Placed } from "./call.ts";
|
|
8
|
+
import type { Run } from "./run.ts";
|
|
9
|
+
import { approverLabel, mayDecide } from "./shared.ts";
|
|
10
|
+
|
|
11
|
+
// The wire contract between a run waiting on approvals and whoever decides them. The client
|
|
12
|
+
// imports this file and never the worker's.
|
|
13
|
+
|
|
14
|
+
/** The DBOS event a run publishes its approvals on, as `ApprovalState[]`. */
|
|
15
|
+
export const APPROVALS_EVENT = "approvals";
|
|
16
|
+
/** The `recv` topic a decision on an approval is sent to. */
|
|
17
|
+
export const topicOf = (approvalId: string) => approvalId;
|
|
18
|
+
/** The DBOS event an approval's decision is published on, as its decided `ApprovalState`, once made. */
|
|
19
|
+
export const decisionEventOf = (approvalId: string) => `approval:${approvalId}`;
|
|
20
|
+
/**
|
|
21
|
+
* The idempotency key a decision message is sent with, so a retried send queues it once. DBOS
|
|
22
|
+
* scopes the key per run, not per topic, so it names the approval too: one message id reused
|
|
23
|
+
* for two approvals of a run would otherwise drop the second send.
|
|
24
|
+
*/
|
|
25
|
+
export const messageKeyOf = (approvalId: string, messageId: string) => `${approvalId}:${messageId}`;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A decision, as sent to a run. `id` names the message, so whoever sent it can tell whether
|
|
29
|
+
* the run decided with it or with another; `SanomaClient.decide` sets one.
|
|
30
|
+
*/
|
|
31
|
+
export const ApprovalMessage = z.object({
|
|
32
|
+
id: z.string().optional(),
|
|
33
|
+
decision: z.enum(["approve", "reject"]),
|
|
34
|
+
by: Principal,
|
|
35
|
+
note: z.string().optional(),
|
|
36
|
+
});
|
|
37
|
+
export type ApprovalMessage = z.infer<typeof ApprovalMessage>;
|
|
38
|
+
|
|
39
|
+
const statusOf = (decision: ApprovalMessage["decision"]) => (decision === "approve" ? "approved" : "rejected");
|
|
40
|
+
|
|
41
|
+
/** The sender's id, from a message that may not be a valid decision. */
|
|
42
|
+
function senderOf(raw: unknown): string | undefined {
|
|
43
|
+
const by = typeof raw === "object" && raw !== null ? (raw as { by?: unknown }).by : undefined;
|
|
44
|
+
const id = typeof by === "object" && by !== null ? (by as { id?: unknown }).id : undefined;
|
|
45
|
+
return typeof id === "string" && id ? id : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** An approval request as checked: who may decide it, and the operations it covers, by id. */
|
|
49
|
+
export interface CheckedApproval extends Omit<ApprovalRequest, "covers"> {
|
|
50
|
+
covers: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A policy's hold: the call held, and the ledger `seq` its `op.called` record takes. */
|
|
54
|
+
export interface HeldCall {
|
|
55
|
+
op: string;
|
|
56
|
+
seq: number;
|
|
57
|
+
input: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Waits, durably, for a decision from someone who may make it. Throws `RejectedError` on a
|
|
62
|
+
* rejection. Without `held`, the workflow asked (`ctx.approval`); with it, the policy did, and
|
|
63
|
+
* `place` is the held call's outline node and name.
|
|
64
|
+
*/
|
|
65
|
+
export async function awaitApproval(
|
|
66
|
+
run: Run,
|
|
67
|
+
title: string,
|
|
68
|
+
req: CheckedApproval,
|
|
69
|
+
place: Placed,
|
|
70
|
+
held?: HeldCall,
|
|
71
|
+
): Promise<ApprovalResult> {
|
|
72
|
+
const { covers } = req;
|
|
73
|
+
const all = run.approvals;
|
|
74
|
+
const state: ApprovalState = {
|
|
75
|
+
id: `approval-${all.length + 1}`,
|
|
76
|
+
title,
|
|
77
|
+
approver: req.approver,
|
|
78
|
+
...(req.links === undefined ? {} : { links: req.links }),
|
|
79
|
+
...(req.details === undefined ? {} : { details: req.details }),
|
|
80
|
+
requestedBy: held ? "policy" : "workflow",
|
|
81
|
+
covers,
|
|
82
|
+
...(held ? { op: held.op, opSeq: held.seq, input: held.input } : {}),
|
|
83
|
+
status: "pending",
|
|
84
|
+
requestedAt: 0,
|
|
85
|
+
refused: [],
|
|
86
|
+
};
|
|
87
|
+
all.push(state);
|
|
88
|
+
state.requestedAt = await DBOS.now();
|
|
89
|
+
await DBOS.setEvent(APPROVALS_EVENT, all);
|
|
90
|
+
await write(
|
|
91
|
+
run,
|
|
92
|
+
entry(
|
|
93
|
+
run,
|
|
94
|
+
{
|
|
95
|
+
type: "approval.requested",
|
|
96
|
+
approval: state.id,
|
|
97
|
+
...place,
|
|
98
|
+
title,
|
|
99
|
+
approver: state.approver,
|
|
100
|
+
requestedBy: state.requestedBy,
|
|
101
|
+
covers,
|
|
102
|
+
...(held ? { op: held.op, opSeq: held.seq } : {}),
|
|
103
|
+
},
|
|
104
|
+
{ key: state.id, at: state.requestedAt },
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
for (;;) {
|
|
108
|
+
const raw = await DBOS.recv<unknown>(topicOf(state.id), { timeoutSeconds: 24 * 60 * 60 });
|
|
109
|
+
if (raw === null || raw === undefined) {
|
|
110
|
+
warn(`run ${run.id}: approval ${state.id} ("${title}") is still waiting for ${approverLabel(state.approver)}`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const at = await DBOS.now();
|
|
114
|
+
const parsed = ApprovalMessage.safeParse(raw);
|
|
115
|
+
const msg = parsed.success ? parsed.data : undefined;
|
|
116
|
+
if (!msg || !mayDecide(state, msg.by)) {
|
|
117
|
+
const by = msg?.by.id ?? senderOf(raw);
|
|
118
|
+
const reason = msg ? notApprover(state, msg.by) : "not a valid decision message";
|
|
119
|
+
state.refused.push({
|
|
120
|
+
...(by === undefined ? {} : { by }),
|
|
121
|
+
...(msg?.id === undefined ? {} : { id: msg.id }),
|
|
122
|
+
at,
|
|
123
|
+
reason,
|
|
124
|
+
});
|
|
125
|
+
await DBOS.setEvent(APPROVALS_EVENT, all);
|
|
126
|
+
await write(
|
|
127
|
+
run,
|
|
128
|
+
entry(
|
|
129
|
+
run,
|
|
130
|
+
{ type: "approval.refused", approval: state.id, ...(by === undefined ? {} : { by }), reason },
|
|
131
|
+
{ key: `${state.id}:refused:${state.refused.length}`, at },
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
Object.assign(state, {
|
|
137
|
+
status: statusOf(msg.decision),
|
|
138
|
+
decidedBy: msg.by.id,
|
|
139
|
+
decidedAt: at,
|
|
140
|
+
note: msg.note,
|
|
141
|
+
decidedWith: msg.id,
|
|
142
|
+
});
|
|
143
|
+
// The list first, then the ledger, then the decision event: whoever sees the event sees both.
|
|
144
|
+
await DBOS.setEvent(APPROVALS_EVENT, all);
|
|
145
|
+
await write(
|
|
146
|
+
run,
|
|
147
|
+
entry(
|
|
148
|
+
run,
|
|
149
|
+
{ type: "approval.decided", approval: state.id, decision: msg.decision, by: msg.by.id, note: msg.note },
|
|
150
|
+
{ key: state.id, at },
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
await DBOS.setEvent(decisionEventOf(state.id), state);
|
|
154
|
+
if (msg.decision === "reject") throw new RejectedError(title, msg.by, msg.note, state.id);
|
|
155
|
+
return { approvedBy: msg.by.id, at, note: msg.note };
|
|
156
|
+
}
|
|
157
|
+
}
|
package/src/ast.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { parseSync, visitorKeys } from "oxc-parser";
|
|
3
|
+
import { errorMessage } from "./shared.ts";
|
|
4
|
+
|
|
5
|
+
// What the lints, the outline and the data-file reader share: one way to parse a file, walk it
|
|
6
|
+
// and say where in it a problem is.
|
|
7
|
+
|
|
8
|
+
/** oxc-parser's ESTree nodes, read by shape. */
|
|
9
|
+
export type Node = any;
|
|
10
|
+
|
|
11
|
+
/** Parses TypeScript, which covers the JavaScript a built package holds, as a module; a `.tsx` or `.jsx` file with its JSX. */
|
|
12
|
+
export const parse = (filename: string, source: string) =>
|
|
13
|
+
parseSync(filename, source, {
|
|
14
|
+
lang: /\.[jt]sx$/.test(filename) ? "tsx" : "ts",
|
|
15
|
+
sourceType: "module",
|
|
16
|
+
preserveParens: false,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// The keys that hold types, not code: nothing the lint or the outline looks for is in them.
|
|
20
|
+
const TYPE_KEYS = new Set([
|
|
21
|
+
"typeAnnotation",
|
|
22
|
+
"typeParameters",
|
|
23
|
+
"returnType",
|
|
24
|
+
"typeArguments",
|
|
25
|
+
"superTypeArguments",
|
|
26
|
+
"implements",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const CODE_KEYS = new Map(
|
|
30
|
+
Object.entries(visitorKeys).map(([type, keys]) => [type, keys.filter((key) => !TYPE_KEYS.has(key))]),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/** A node's children in source order, by oxc-parser's `visitorKeys`, without its types. A hole in a list is `null`. */
|
|
34
|
+
export const childrenOf = (node: Node): Node[] => (CODE_KEYS.get(node.type) ?? []).flatMap((key) => node[key] ?? []);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The offset each line of `source` starts at, from line 1, and one past the end of the text as
|
|
38
|
+
* the start of the line after the last: where each line ends is the next start, less its `\n`.
|
|
39
|
+
*/
|
|
40
|
+
export const lineStartsOf = (source: string): number[] => {
|
|
41
|
+
const lineStarts = [0];
|
|
42
|
+
for (let i = 0; i < source.length; i++) if (source[i] === "\n") lineStarts.push(i + 1);
|
|
43
|
+
lineStarts.push(source.length + 1);
|
|
44
|
+
return lineStarts;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Where an offset into `source` is, as a line and a column, both from 1. */
|
|
48
|
+
export function locator(source: string): (offset: number) => { line: number; column: number } {
|
|
49
|
+
const lineStarts = lineStartsOf(source);
|
|
50
|
+
return (offset) => {
|
|
51
|
+
let line = 0;
|
|
52
|
+
while (line + 1 < lineStarts.length && lineStarts[line + 1]! <= offset) line++;
|
|
53
|
+
return { line: line + 1, column: offset - lineStarts[line]! + 1 };
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The offset into a text of a line and column, both from 1, as a stack frame gives them, from
|
|
59
|
+
* the text's `lineStartsOf`; or undefined when the text has no such line, or the line no such
|
|
60
|
+
* column (a frame mapped wrong must not land in the next line). The column is taken as a UTF-16
|
|
61
|
+
* unit index, as V8 counts it.
|
|
62
|
+
*/
|
|
63
|
+
export function offsetOf(lineStarts: readonly number[], line: number, column: number): number | undefined {
|
|
64
|
+
const start = lineStarts[line - 1];
|
|
65
|
+
const next = lineStarts[line];
|
|
66
|
+
if (line < 1 || column < 1 || start === undefined || next === undefined) return undefined;
|
|
67
|
+
const offset = start + column - 1;
|
|
68
|
+
return offset < next - 1 ? offset : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Something wrong in a file, at a line and column from 1. */
|
|
72
|
+
export interface LintProblem {
|
|
73
|
+
line: number;
|
|
74
|
+
column: number;
|
|
75
|
+
message: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A file parsed, its syntax errors as problems, and offsets as lines and columns from 1. */
|
|
79
|
+
export function parsed(source: string, filename: string) {
|
|
80
|
+
const { program, errors } = parse(filename, source);
|
|
81
|
+
const at = locator(source);
|
|
82
|
+
const problems: LintProblem[] = errors.map((e) => ({
|
|
83
|
+
...at(e.labels?.[0]?.start ?? 0),
|
|
84
|
+
message: `syntax: ${e.message}`,
|
|
85
|
+
}));
|
|
86
|
+
return { program: program as Node, problems, at };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A file read and parsed once, for the outline and the lint alike: its text with `\n` line
|
|
91
|
+
* endings (CodeMirror counts a line break as one unit, oxc counts `\r\n` as two, and spans need
|
|
92
|
+
* `\n` alone), its program, offsets as lines and columns, and its syntax problems.
|
|
93
|
+
*/
|
|
94
|
+
export interface SourceFile {
|
|
95
|
+
file: string;
|
|
96
|
+
source: string;
|
|
97
|
+
program: Node;
|
|
98
|
+
at: (offset: number) => { line: number; column: number };
|
|
99
|
+
problems: LintProblem[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Reads and parses `file`; or why it could not be read, as a message naming it. */
|
|
103
|
+
export function readSource(file: string): SourceFile | string {
|
|
104
|
+
let source: string;
|
|
105
|
+
try {
|
|
106
|
+
source = readFileSync(file, "utf8").replaceAll("\r\n", "\n");
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return `${file} could not be read: ${errorMessage(err)}`;
|
|
109
|
+
}
|
|
110
|
+
return { file, source, ...parsed(source, file) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Problems in the order of their place in the file. */
|
|
114
|
+
export const byPosition = <P extends Omit<LintProblem, "message">>(problems: P[]): P[] =>
|
|
115
|
+
problems.toSorted((a, b) => a.line - b.line || a.column - b.column);
|