@sanoma-ai/workflows 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -76
- 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 +11 -6
- package/dist/approvals.js +38 -10
- package/dist/approvals.js.map +1 -1
- package/dist/call-workflow.d.ts +16 -0
- package/dist/call-workflow.js +35 -0
- package/dist/call-workflow.js.map +1 -0
- package/dist/call.d.ts +35 -2
- package/dist/call.js +123 -43
- package/dist/call.js.map +1 -1
- package/dist/cedar.d.ts +87 -0
- package/dist/cedar.js +365 -0
- package/dist/cedar.js.map +1 -0
- package/dist/client.d.ts +123 -9
- package/dist/client.js +355 -34
- package/dist/client.js.map +1 -1
- package/dist/collection.d.ts +107 -0
- package/dist/collection.js +306 -0
- package/dist/collection.js.map +1 -0
- package/dist/config.d.ts +42 -14
- package/dist/config.js +109 -28
- package/dist/config.js.map +1 -1
- package/dist/constructors.d.ts +2 -0
- package/dist/constructors.js +5 -0
- package/dist/constructors.js.map +1 -0
- package/dist/credentials.d.ts +25 -0
- package/dist/credentials.js +73 -5
- package/dist/credentials.js.map +1 -1
- package/dist/data.d.ts +2 -0
- package/dist/data.js +3 -0
- package/dist/data.js.map +1 -0
- package/dist/datafile.d.ts +38 -3
- package/dist/datafile.js +166 -13
- package/dist/datafile.js.map +1 -1
- package/dist/db.d.ts +5 -3
- package/dist/db.js +88 -21
- package/dist/db.js.map +1 -1
- package/dist/define.d.ts +10 -4
- package/dist/define.js +1 -5
- package/dist/define.js.map +1 -1
- package/dist/describe.d.ts +54 -8
- package/dist/describe.js +84 -11
- package/dist/describe.js.map +1 -1
- package/dist/direct.d.ts +9 -0
- package/dist/direct.js +11 -0
- package/dist/direct.js.map +1 -0
- package/dist/drift.d.ts +64 -12
- package/dist/drift.js +118 -41
- package/dist/drift.js.map +1 -1
- package/dist/errors.d.ts +16 -1
- package/dist/errors.js +30 -1
- package/dist/errors.js.map +1 -1
- package/dist/expectations.d.ts +4 -4
- package/dist/expectations.js +43 -9
- package/dist/expectations.js.map +1 -1
- 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 +13 -5
- package/dist/index.js +15 -5
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +12 -1
- package/dist/ledger.js +18 -11
- package/dist/ledger.js.map +1 -1
- package/dist/lint.d.ts +5 -4
- package/dist/lint.js +24 -7
- package/dist/lint.js.map +1 -1
- package/dist/lockfile.d.ts +36 -0
- package/dist/lockfile.js +69 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/op.d.ts +9 -0
- package/dist/op.js.map +1 -1
- package/dist/outline.d.ts +2 -0
- package/dist/outline.js +1 -1
- package/dist/outline.js.map +1 -1
- package/dist/paths.d.ts +4 -0
- package/dist/paths.js +17 -3
- package/dist/paths.js.map +1 -1
- package/dist/person.d.ts +61 -0
- package/dist/person.js +69 -0
- package/dist/person.js.map +1 -0
- package/dist/plugin.js +1 -1
- package/dist/plugin.js.map +1 -1
- package/dist/policy.d.ts +51 -55
- package/dist/policy.js +14 -36
- package/dist/policy.js.map +1 -1
- package/dist/resource.d.ts +207 -25
- package/dist/resource.js +184 -30
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +76 -9
- package/dist/resources.js +170 -30
- package/dist/resources.js.map +1 -1
- package/dist/run.d.ts +31 -1
- package/dist/run.js +6 -1
- package/dist/run.js.map +1 -1
- package/dist/sandbox.js +5 -1
- package/dist/sandbox.js.map +1 -1
- package/dist/scenario.d.ts +25 -9
- package/dist/scenario.js +125 -27
- package/dist/scenario.js.map +1 -1
- package/dist/scenarios.d.ts +10 -6
- package/dist/scenarios.js +8 -4
- package/dist/scenarios.js.map +1 -1
- package/dist/schema.d.ts +32 -0
- package/dist/schema.js +225 -0
- package/dist/schema.js.map +1 -0
- package/dist/sdk.d.ts +34 -0
- package/dist/sdk.js +168 -0
- package/dist/sdk.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 +188 -4
- package/dist/shared.js +165 -5
- package/dist/shared.js.map +1 -1
- package/dist/sites.d.ts +52 -0
- package/dist/sites.js +174 -0
- package/dist/sites.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/version.d.ts +3 -2
- package/dist/version.js +3 -2
- package/dist/version.js.map +1 -1
- package/dist/worker.d.ts +6 -2
- package/dist/worker.js +37 -8
- package/dist/worker.js.map +1 -1
- package/dist/writer.d.ts +35 -0
- package/dist/writer.js +201 -0
- package/dist/writer.js.map +1 -0
- package/oxlint.json +5 -16
- package/package.json +11 -1
- package/src/apply-steps.ts +110 -0
- package/src/apply.ts +626 -0
- package/src/approvals.ts +49 -13
- package/src/call-workflow.ts +44 -0
- package/src/call.ts +173 -46
- package/src/cedar.ts +493 -0
- package/src/client.ts +450 -49
- package/src/collection.ts +431 -0
- package/src/config.ts +168 -48
- package/src/constructors.ts +4 -0
- package/src/credentials.ts +90 -5
- package/src/data.ts +10 -0
- package/src/datafile.ts +207 -15
- package/src/db.ts +90 -22
- package/src/define.ts +11 -9
- package/src/describe.ts +146 -23
- package/src/direct.ts +15 -0
- package/src/drift.ts +155 -45
- package/src/errors.ts +36 -1
- package/src/expectations.ts +46 -11
- package/src/host.ts +96 -0
- package/src/index.ts +56 -2
- package/src/ledger.ts +35 -17
- package/src/lint.ts +29 -12
- package/src/lockfile.ts +95 -0
- package/src/op.ts +9 -0
- package/src/outline.ts +1 -1
- package/src/paths.ts +18 -3
- package/src/person.ts +75 -0
- package/src/plugin.ts +1 -1
- package/src/policy.ts +59 -63
- package/src/resource.ts +302 -52
- package/src/resources.ts +226 -30
- package/src/run.ts +33 -1
- package/src/sandbox.ts +5 -1
- package/src/scenario.ts +175 -43
- package/src/scenarios.ts +15 -8
- package/src/schema.ts +261 -0
- package/src/sdk.ts +211 -0
- package/src/secret.ts +92 -0
- package/src/shared.ts +279 -6
- package/src/sites.ts +220 -0
- package/src/source.ts +82 -0
- package/src/version.ts +3 -2
- package/src/worker.ts +45 -11
- package/src/writer.ts +225 -0
package/src/resources.ts
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { parsed } from "./ast.ts";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
type DataFile,
|
|
6
|
+
type DataFileExport,
|
|
7
|
+
type DataValue,
|
|
8
|
+
readDataFile,
|
|
9
|
+
Reference,
|
|
10
|
+
SECRET_ENTRIES,
|
|
11
|
+
} from "./datafile.ts";
|
|
12
|
+
import { type Binding, type Bindings, LOCKFILE, readLockfile } from "./lockfile.ts";
|
|
6
13
|
import { type Connector, VENDOR } from "./op.ts";
|
|
14
|
+
import { filesUnder } from "./paths.ts";
|
|
15
|
+
import { peopleConnector } from "./person.ts";
|
|
7
16
|
import type { Span } from "./outline.ts";
|
|
8
17
|
import { type Declared, type Resource, resourceTypesOf } from "./resource.ts";
|
|
9
|
-
import {
|
|
18
|
+
import { Principal } from "./define.ts";
|
|
19
|
+
import { NotAdmittedError } from "./errors.ts";
|
|
20
|
+
import {
|
|
21
|
+
AGENTS,
|
|
22
|
+
declaresPeople,
|
|
23
|
+
type PeopleResource,
|
|
24
|
+
personOf,
|
|
25
|
+
principalOf,
|
|
26
|
+
problemLine,
|
|
27
|
+
RUNTIME_AGENTS,
|
|
28
|
+
} from "./shared.ts";
|
|
10
29
|
|
|
11
30
|
/** A resource a data file declares, as read from the file without running it. */
|
|
12
31
|
export interface DeclaredResource {
|
|
@@ -16,8 +35,12 @@ export interface DeclaredResource {
|
|
|
16
35
|
file: string;
|
|
17
36
|
vendor: string;
|
|
18
37
|
type: string;
|
|
19
|
-
/**
|
|
38
|
+
/** What people call it: its identity, else its export's name. */
|
|
20
39
|
name: string;
|
|
40
|
+
/** The import id that finds it unbound: what its type's `find` makes of its fields; absent when the type has none. */
|
|
41
|
+
identity?: string;
|
|
42
|
+
/** The vendor's id of the object the lockfile binds it to (`LOCKFILE`); absent when unbound. */
|
|
43
|
+
binding?: string;
|
|
21
44
|
/** Its `export const` statement, as UTF-16 offsets into the file's text. */
|
|
22
45
|
span: Span;
|
|
23
46
|
/** Its fields, as its type checked them: each resource it names as that resource's `name`. */
|
|
@@ -29,6 +52,22 @@ export interface DeclaredResource {
|
|
|
29
52
|
refs: Record<string, string>;
|
|
30
53
|
}
|
|
31
54
|
|
|
55
|
+
/**
|
|
56
|
+
* A collection `resources/data.ts` declares, as read without running it: where it is, for the
|
|
57
|
+
* Resources page. Its fields are the collection's own (`describeConfig`'s `collections`), from
|
|
58
|
+
* the same file, which the runtime imports.
|
|
59
|
+
*/
|
|
60
|
+
export interface DeclaredCollection {
|
|
61
|
+
/** `<file>#<export>`, as a resource's: `resources/data.ts#outbound`. */
|
|
62
|
+
id: string;
|
|
63
|
+
/** `resources/data.ts`. */
|
|
64
|
+
file: string;
|
|
65
|
+
/** The collection's name, its operations' middle segment (`data.<name>.get`). */
|
|
66
|
+
name: string;
|
|
67
|
+
/** Its `export const` statement, as UTF-16 offsets into the file's text. */
|
|
68
|
+
span: Span;
|
|
69
|
+
}
|
|
70
|
+
|
|
32
71
|
/** Something wrong in the data files, or in finding them. */
|
|
33
72
|
export interface ResourceProblem {
|
|
34
73
|
/**
|
|
@@ -43,26 +82,118 @@ export interface ResourceProblem {
|
|
|
43
82
|
message: string;
|
|
44
83
|
}
|
|
45
84
|
|
|
46
|
-
/** What the data files declare: each resource read without problems, and the problems. */
|
|
85
|
+
/** What the data files declare: each resource read without problems, the lockfile's bindings, and the problems. */
|
|
47
86
|
export interface DataFiles {
|
|
48
87
|
/** By file and then in file order, less any with a problem, and any that names one. */
|
|
49
88
|
resources: DeclaredResource[];
|
|
89
|
+
/** The lockfile's bindings, by key (`<file>#<export>`), as read: none when it is missing or has a problem. */
|
|
90
|
+
bindings: Bindings;
|
|
91
|
+
/** Each binding whose key no data file declares, by key. */
|
|
92
|
+
orphans: (Binding & { key: string })[];
|
|
93
|
+
/** The collections `resources/data.ts` declares, in file order. */
|
|
94
|
+
collections: DeclaredCollection[];
|
|
50
95
|
/** By file, line and column. */
|
|
51
96
|
problems: ResourceProblem[];
|
|
52
97
|
}
|
|
53
98
|
|
|
99
|
+
/** What the data files declare of the people: the resources `principalOf` takes, and what is wrong in their files. */
|
|
100
|
+
export interface DeclaredPeople {
|
|
101
|
+
people: DeclaredResource[];
|
|
102
|
+
/**
|
|
103
|
+
* The problems in each file that declares (or tries to declare) a person, agent or group, and
|
|
104
|
+
* an id two of them share or one of the runtime's own agents (`RUNTIME_AGENTS`) takes. While
|
|
105
|
+
* there are any, nobody is admitted: a broken file never admits more than it says.
|
|
106
|
+
*/
|
|
107
|
+
problems: ResourceProblem[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A file that imports the runtime's constructors, `sanoma` (by any local name): the files `readPeople` reads. */
|
|
111
|
+
const DECLARES_PEOPLE = /\{[^}]*\bsanoma\b[^}]*\}\s*from\s*["'](?:sanoma|@sanoma-ai\/workflows)\/resources["']/;
|
|
112
|
+
/** The last read of the people under each root, by what its data files were then (`filesStamp`). */
|
|
113
|
+
const peopleRead = new Map<string, { stamp: string; read: DeclaredPeople }>();
|
|
114
|
+
|
|
115
|
+
/** Each data file under `root` with its size and modification time: unchanged while the files are. */
|
|
116
|
+
const filesStamp = (root: string) =>
|
|
117
|
+
dataFilesUnder(join(root, "resources"))
|
|
118
|
+
.map((path) => {
|
|
119
|
+
// Gone since it was listed (an editor's save by rename): stamped as such, and read next time.
|
|
120
|
+
const stat = statSync(path, { bigint: true, throwIfNoEntry: false });
|
|
121
|
+
return `${path} ${stat?.size} ${stat?.mtimeNs}`;
|
|
122
|
+
})
|
|
123
|
+
.join("\n");
|
|
124
|
+
|
|
54
125
|
/**
|
|
55
|
-
* The
|
|
56
|
-
*
|
|
57
|
-
*
|
|
126
|
+
* The people, agents and groups the data files under `root` declare (`sanoma.person`, `.agent`,
|
|
127
|
+
* `.group`), read without running them, as `principalOf` takes them, with what is wrong in their
|
|
128
|
+
* files; none without a root. Read again only when a data file changed (by its size and time),
|
|
129
|
+
* so a new colleague is admitted once their file is saved, and a request costs a `stat` per file.
|
|
58
130
|
*/
|
|
59
|
-
export function
|
|
60
|
-
|
|
131
|
+
export function readPeople(root: string | undefined): DeclaredPeople {
|
|
132
|
+
if (root === undefined) return { people: [], problems: [] };
|
|
133
|
+
const stamp = filesStamp(root);
|
|
134
|
+
const cached = peopleRead.get(root);
|
|
135
|
+
if (cached?.stamp === stamp) return cached.read;
|
|
136
|
+
const read = readDataFiles(root, [peopleConnector], undefined, (source) => DECLARES_PEOPLE.test(source));
|
|
137
|
+
const people = read.resources;
|
|
138
|
+
// The people are never bound, so the lockfile's problems are not theirs.
|
|
139
|
+
const own = read.problems.filter((p) => p.file !== LOCKFILE);
|
|
140
|
+
const ids = new Map<string, DeclaredResource>();
|
|
141
|
+
for (const r of people) {
|
|
142
|
+
if (r.type === "group") {
|
|
143
|
+
if (r.name === AGENTS) {
|
|
144
|
+
own.push({
|
|
145
|
+
file: r.file,
|
|
146
|
+
message: `${r.id} is named "${AGENTS}", the group every agent is in: give it another name`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const id = r.name;
|
|
152
|
+
const other = ids.get(id);
|
|
153
|
+
if (other || RUNTIME_AGENTS.includes(id)) {
|
|
154
|
+
own.push({
|
|
155
|
+
file: r.file,
|
|
156
|
+
message: other
|
|
157
|
+
? `${r.id} has the id "${id}", which ${other.id} has: give each person and agent their own`
|
|
158
|
+
: `${r.id} has the id "${id}", one of the runtime's own agents': give it another`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
ids.set(id, r);
|
|
162
|
+
}
|
|
163
|
+
const declared = { people, problems: own };
|
|
164
|
+
peopleRead.set(root, { stamp, read: declared });
|
|
165
|
+
return declared;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Who `asking` is admitted as, by the people the data files declare (`readPeople`): the declared
|
|
170
|
+
* principal (`principalOf`) of the id it names, a person's or an agent's; for a sign-in
|
|
171
|
+
* (`signIn`), of the person it matches (`personOf`: email, GitHub login, then id), never an agent.
|
|
172
|
+
* While no person is declared, whoever names an id is taken as given. Throws `NotAdmittedError`
|
|
173
|
+
* for anyone else, and for everyone while the people's files have problems.
|
|
174
|
+
*/
|
|
175
|
+
export function admit(
|
|
176
|
+
asking: { id?: string; email?: string; github?: string; groups?: string[]; onBehalfOf?: string },
|
|
177
|
+
{ people, problems }: { people: readonly PeopleResource[]; problems: readonly ResourceProblem[] },
|
|
178
|
+
signIn = false,
|
|
179
|
+
): Principal {
|
|
180
|
+
const who = asking.github ?? asking.id ?? asking.email ?? "This sign-in";
|
|
61
181
|
if (problems.length) {
|
|
62
|
-
|
|
63
|
-
|
|
182
|
+
// The runtime's own agents are declared by no file, so a broken one does not stop them (`check`'s scenarios).
|
|
183
|
+
if (!signIn && asking.id !== undefined && RUNTIME_AGENTS.includes(asking.id)) {
|
|
184
|
+
return principalOf(asking.id, people)!;
|
|
185
|
+
}
|
|
186
|
+
throw new NotAdmittedError(who, problems.map(problemLine));
|
|
64
187
|
}
|
|
65
|
-
|
|
188
|
+
if (!declaresPeople(people)) {
|
|
189
|
+
const given = Principal.safeParse(asking).data;
|
|
190
|
+
if (given) return given;
|
|
191
|
+
} else {
|
|
192
|
+
const id = signIn ? personOf(asking, people) : asking.id;
|
|
193
|
+
const admitted = id === undefined ? undefined : principalOf(id, people);
|
|
194
|
+
if (admitted) return admitted;
|
|
195
|
+
}
|
|
196
|
+
throw new NotAdmittedError(who);
|
|
66
197
|
}
|
|
67
198
|
|
|
68
199
|
interface FileEntry {
|
|
@@ -82,12 +213,23 @@ interface FileEntry {
|
|
|
82
213
|
*
|
|
83
214
|
* Problems: anything outside the subset, a constructor or data file the config does not know, a
|
|
84
215
|
* type the connector does not declare, a value the type refuses (a reference where its type
|
|
85
|
-
* takes none among them), a reference cycle, and two resources of one type with one
|
|
216
|
+
* takes none among them), a reference cycle, and two resources of one type with one identity.
|
|
217
|
+
*
|
|
218
|
+
* The collections `resources/data.ts` declares are listed as they are read (`collections`),
|
|
219
|
+
* never matched to a connector, a resource type or the lockfile: the runtime imports that file.
|
|
220
|
+
*
|
|
221
|
+
* Each declaration is joined to its binding in the lockfile (`readLockfile`) by its key,
|
|
222
|
+
* `<file>#<export>`; a binding no export declares is an orphan. Problems: the lockfile's, a
|
|
223
|
+
* binding of another type than its declaration's, and two declarations bound to one object.
|
|
224
|
+
*
|
|
225
|
+
* `partial` reads part of the files: only those whose source it passes, and in them, an import of
|
|
226
|
+
* a connector not in `connectors`, or of a file left out, is left unread rather than reported.
|
|
86
227
|
*/
|
|
87
228
|
export function readDataFiles(
|
|
88
229
|
root: string | undefined,
|
|
89
230
|
connectors: readonly Connector<any, any>[],
|
|
90
231
|
types: ReadonlyMap<string, Resource> = resourceTypesOf(connectors),
|
|
232
|
+
partial?: (source: string) => boolean,
|
|
91
233
|
): DataFiles {
|
|
92
234
|
const problems: ResourceProblem[] = [];
|
|
93
235
|
if (root === undefined) {
|
|
@@ -95,13 +237,20 @@ export function readDataFiles(
|
|
|
95
237
|
message:
|
|
96
238
|
"The config has no root, so its data files cannot be found: make it with defineConfig, which records its file, or set its `root`",
|
|
97
239
|
});
|
|
98
|
-
return { resources: [], problems };
|
|
240
|
+
return { resources: [], bindings: {}, orphans: [], collections: [], problems };
|
|
99
241
|
}
|
|
242
|
+
const lock = readLockfile(root);
|
|
243
|
+
problems.push(...lock.problems);
|
|
100
244
|
|
|
101
245
|
const files = new Map<string, FileEntry>();
|
|
246
|
+
const left = new Set<string>();
|
|
102
247
|
for (const path of dataFilesUnder(join(root, "resources"))) {
|
|
103
248
|
const rel = relative(root, path).split(sep).join("/");
|
|
104
249
|
const source = readFileSync(path, "utf8");
|
|
250
|
+
if (partial && !partial(source)) {
|
|
251
|
+
left.add(path);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
105
254
|
const { program, problems: syntax, at } = parsed(source, path);
|
|
106
255
|
const data = readDataFile(program, path, join(root, "resources"));
|
|
107
256
|
const problem = (offset: number, message: string) => problems.push({ file: rel, ...at(offset), message });
|
|
@@ -116,6 +265,8 @@ export function readDataFiles(
|
|
|
116
265
|
const { id, info } = connector[VENDOR];
|
|
117
266
|
if (info?.package) entries.set(`${info.package}/resources`, id);
|
|
118
267
|
}
|
|
268
|
+
// The runtime's own (`peopleConnector`, package `sanoma`) is imported from either name of its entry.
|
|
269
|
+
if (entries.get("sanoma/resources") === "sanoma") for (const entry of SECRET_ENTRIES) entries.set(entry, "sanoma");
|
|
119
270
|
|
|
120
271
|
// What each file's names stand for, and every resource read by its id.
|
|
121
272
|
const vendors = new Map<FileEntry, Map<string, string>>();
|
|
@@ -127,6 +278,7 @@ export function readDataFiles(
|
|
|
127
278
|
for (const imp of file.data.imports) {
|
|
128
279
|
if (imp.kind === "constructors") {
|
|
129
280
|
const vendor = entries.get(imp.source);
|
|
281
|
+
if (vendor === undefined && partial) continue;
|
|
130
282
|
if (vendor === undefined) {
|
|
131
283
|
file.problem(
|
|
132
284
|
imp.start,
|
|
@@ -142,7 +294,9 @@ export function readDataFiles(
|
|
|
142
294
|
}
|
|
143
295
|
continue;
|
|
144
296
|
}
|
|
145
|
-
const
|
|
297
|
+
const target = resolve(dirname(path), imp.source);
|
|
298
|
+
const other = files.get(target);
|
|
299
|
+
if (!other && left.has(target)) continue;
|
|
146
300
|
if (!other) {
|
|
147
301
|
file.problem(imp.start, `"${imp.source}" is not a data file under resources/`);
|
|
148
302
|
} else if (!other.data.declared.includes(imp.imported)) {
|
|
@@ -175,7 +329,7 @@ export function readDataFiles(
|
|
|
175
329
|
visiting.push(id);
|
|
176
330
|
const result = build(file, exp);
|
|
177
331
|
visiting.pop();
|
|
178
|
-
if (result) {
|
|
332
|
+
if (result?.declared.name !== undefined) {
|
|
179
333
|
const { vendor, type, name } = result.declared;
|
|
180
334
|
const key = `${vendor}.${type} ${JSON.stringify(name)}`;
|
|
181
335
|
const first = byName.get(key);
|
|
@@ -231,26 +385,68 @@ export function readDataFiles(
|
|
|
231
385
|
}
|
|
232
386
|
}
|
|
233
387
|
|
|
388
|
+
// Each binding against its declaration: of its type, and the only one bound to its object.
|
|
389
|
+
const boundBy = new Map<string, string>();
|
|
390
|
+
for (const [id, { file, exp }] of declarations) {
|
|
391
|
+
const binding = lock.bindings[id];
|
|
392
|
+
// An unknown constructors import was reported with the import.
|
|
393
|
+
const vendor = vendors.get(file)!.get(exp.vendor);
|
|
394
|
+
if (binding === undefined || vendor === undefined) continue;
|
|
395
|
+
const type = `${vendor}.${exp.type}`;
|
|
396
|
+
const object = `${binding.type} ${binding.id}`;
|
|
397
|
+
const first = boundBy.get(object);
|
|
398
|
+
const why =
|
|
399
|
+
binding.type !== type
|
|
400
|
+
? `${id} is bound to a ${binding.type} in ${LOCKFILE}, but declares a ${type}`
|
|
401
|
+
: first !== undefined
|
|
402
|
+
? `${id} is bound to the same ${object} as ${first} in ${LOCKFILE}: bind each object once`
|
|
403
|
+
: undefined;
|
|
404
|
+
if (why === undefined) boundBy.set(object, id);
|
|
405
|
+
else {
|
|
406
|
+
// Left out, as any declaration with a problem, and so is any that names it.
|
|
407
|
+
file.problem(exp.start, why);
|
|
408
|
+
done.set(id, undefined);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const keys = new Set([...files.values()].flatMap((f) => f.data.declared.map((name) => `${f.rel}#${name}`)));
|
|
412
|
+
const orphans = Object.entries(lock.bindings)
|
|
413
|
+
.filter(([key]) => !keys.has(key))
|
|
414
|
+
.map(([key, binding]) => ({ key, ...binding }));
|
|
415
|
+
|
|
234
416
|
const resources: DeclaredResource[] = [];
|
|
235
417
|
for (const [id, { file, exp }] of declarations) {
|
|
236
418
|
const result = declare(id);
|
|
237
419
|
if (!result) continue;
|
|
238
|
-
const { vendor, type, name, desired } = result.declared;
|
|
239
|
-
|
|
420
|
+
const { vendor, type, name: identity, desired } = result.declared;
|
|
421
|
+
const binding = lock.bindings[id]?.id;
|
|
422
|
+
resources.push({
|
|
423
|
+
id,
|
|
424
|
+
file: file.rel,
|
|
425
|
+
vendor,
|
|
426
|
+
type,
|
|
427
|
+
name: identity ?? exp.name,
|
|
428
|
+
...(identity !== undefined && { identity }),
|
|
429
|
+
...(binding !== undefined && { binding }),
|
|
430
|
+
span: [exp.start, exp.end],
|
|
431
|
+
desired,
|
|
432
|
+
refs: result.refs,
|
|
433
|
+
});
|
|
240
434
|
}
|
|
435
|
+
const collections = [...files.values()].flatMap(({ rel, data }) =>
|
|
436
|
+
data.collections.map((c) => ({
|
|
437
|
+
id: `${rel}#${c.export}`,
|
|
438
|
+
file: rel,
|
|
439
|
+
name: c.name,
|
|
440
|
+
span: [c.start, c.end] as Span,
|
|
441
|
+
})),
|
|
442
|
+
);
|
|
241
443
|
problems.sort(
|
|
242
444
|
(a, b) =>
|
|
243
445
|
(a.file ?? "").localeCompare(b.file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.column ?? 0) - (b.column ?? 0),
|
|
244
446
|
);
|
|
245
|
-
return { resources, problems };
|
|
447
|
+
return { resources, bindings: lock.bindings, orphans, collections, problems };
|
|
246
448
|
}
|
|
247
449
|
|
|
248
|
-
/** The data files under `dir`, sorted: regular `.ts` files, less tests, declarations and `node_modules
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
return readdirSync(dir, { recursive: true, withFileTypes: true })
|
|
252
|
-
.filter((d) => d.isFile() && d.name.endsWith(".ts") && !d.name.endsWith(".test.ts") && !d.name.endsWith(".d.ts"))
|
|
253
|
-
.map((d) => join(d.parentPath, d.name))
|
|
254
|
-
.filter((path) => !relative(dir, path).split(sep).includes("node_modules"))
|
|
255
|
-
.toSorted();
|
|
256
|
-
}
|
|
450
|
+
/** The data files under `dir`, sorted: regular `.ts` files, less tests, declarations and `node_modules`. */
|
|
451
|
+
const dataFilesUnder = (dir: string): string[] =>
|
|
452
|
+
filesUnder(dir, (name) => name.endsWith(".ts") && !name.endsWith(".test.ts") && !name.endsWith(".d.ts"));
|
package/src/run.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { Collection } from "./collection.ts";
|
|
3
|
+
import type { ApprovalState, Approver, Principal, WorkflowDefinition } from "./define.ts";
|
|
2
4
|
import type { Fake } from "./fake.ts";
|
|
3
5
|
import type { LedgerStore } from "./ledger.ts";
|
|
4
6
|
import type { DriverFn, Op } from "./op.ts";
|
|
5
7
|
import type { CallNode } from "./outline.ts";
|
|
6
8
|
import type { Policy } from "./policy.ts";
|
|
7
9
|
import type { Resource } from "./resource.ts";
|
|
10
|
+
import type { Secrets } from "./secret.ts";
|
|
11
|
+
import type { Source } from "./source.ts";
|
|
8
12
|
import type { Steps } from "./steps.ts";
|
|
9
13
|
|
|
10
14
|
/** What the DBOS workflow receives: the workflow's input, who started the run, and for a sandbox run its scenario. */
|
|
@@ -13,6 +17,18 @@ export interface RunArgs {
|
|
|
13
17
|
startedBy: Principal;
|
|
14
18
|
/** The scenario a sandbox run is seeded from; the run calls the fakes instead of the drivers. */
|
|
15
19
|
sandbox?: string;
|
|
20
|
+
/**
|
|
21
|
+
* The policy's hold on the start (`workflow:<name>`), as `SanomaClient.start` was answered: the
|
|
22
|
+
* run waits for this approval before its body runs.
|
|
23
|
+
*/
|
|
24
|
+
hold?: StartHold;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A start the policy held: whom it asks, the approval's title, and the rules that held it. */
|
|
28
|
+
export interface StartHold {
|
|
29
|
+
approver: Approver;
|
|
30
|
+
title: string;
|
|
31
|
+
reasons?: string[];
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
/**
|
|
@@ -31,11 +47,19 @@ export interface WorkerState {
|
|
|
31
47
|
fakes: Map<string, () => Fake<any, any>>;
|
|
32
48
|
/** Where sandbox runs' scenarios are, the resource types their steps may name, and the steps the connectors ship. */
|
|
33
49
|
scenarios?: URL;
|
|
50
|
+
/** The worker's `source`, when it was given one. */
|
|
51
|
+
source?: Source;
|
|
34
52
|
types: ReadonlyMap<string, Resource>;
|
|
53
|
+
/** The collections, by name, which scenarios' steps may name (`data.<name>`). */
|
|
54
|
+
collections: ReadonlyMap<string, Collection>;
|
|
35
55
|
steps: readonly Steps[];
|
|
36
56
|
/** Its `version`, when it has one, is recorded with each of its decisions. */
|
|
37
57
|
policy: Policy;
|
|
58
|
+
/** The config's directory, whose data files declare the people an approval's decider is checked against (`readPeople`). */
|
|
59
|
+
root?: string;
|
|
38
60
|
ledger: LedgerStore;
|
|
61
|
+
/** The stored secrets, for a live run's drivers (`CallContext.secrets`). */
|
|
62
|
+
secrets: Secrets;
|
|
39
63
|
/**
|
|
40
64
|
* The config's workflows by name, the built-in `drift` among them. A run takes its definition
|
|
41
65
|
* from here, by the name it was registered under: a process registers each name with DBOS once,
|
|
@@ -62,6 +86,8 @@ export interface Run {
|
|
|
62
86
|
sandbox?: string;
|
|
63
87
|
/** A sandbox run's own fakes' functions, by operation id, from `seedSandbox`; unset for a live run. */
|
|
64
88
|
fakeDrivers?: Map<string, DriverFn>;
|
|
89
|
+
/** A sandbox run's own secrets, in memory, from `seedSandbox`; unset for a live run. */
|
|
90
|
+
fakeSecrets?: Secrets;
|
|
65
91
|
approvals: ApprovalState[];
|
|
66
92
|
/** The next ledger `seq`. Advanced only outside steps, so a replay counts the same way. */
|
|
67
93
|
seq: number;
|
|
@@ -89,3 +115,9 @@ export interface WorkflowOutline {
|
|
|
89
115
|
lineStarts: readonly number[];
|
|
90
116
|
calls: readonly CallNode[];
|
|
91
117
|
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The run whose workflow body is running: what a built-in's own steps read of it (who started
|
|
121
|
+
* it, whether it is a sandbox run, the worker's source and policy), as no `ctx` member gives them.
|
|
122
|
+
*/
|
|
123
|
+
export const currentRun = new AsyncLocalStorage<Run>();
|
package/src/sandbox.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Fake, ResourceFake } from "./fake.ts";
|
|
|
6
6
|
import { entry, type LedgerRecord, write } from "./ledger.ts";
|
|
7
7
|
import type { DriverFn } from "./op.ts";
|
|
8
8
|
import type { Run } from "./run.ts";
|
|
9
|
+
import { memorySecrets } from "./secret.ts";
|
|
9
10
|
import { type Given, loadScenarios, type Scenario } from "./scenario.ts";
|
|
10
11
|
import { errorMessage } from "./shared.ts";
|
|
11
12
|
|
|
@@ -90,6 +91,8 @@ export function seedResource(
|
|
|
90
91
|
export async function seedSandbox(run: Run, name: string): Promise<void> {
|
|
91
92
|
const { state } = run;
|
|
92
93
|
let copies: Map<string, DriverFn> | undefined;
|
|
94
|
+
// The run's secrets, in memory as its fakes are: seeding and its calls share them.
|
|
95
|
+
const secrets = memorySecrets();
|
|
93
96
|
const { seeds } = await DBOS.runStep(
|
|
94
97
|
async () => {
|
|
95
98
|
// Read in the step, so a recovered run never reads a feature file edited or renamed since.
|
|
@@ -126,7 +129,7 @@ export async function seedSandbox(run: Run, name: string): Promise<void> {
|
|
|
126
129
|
for (const [i, given] of scenario.given.entries()) {
|
|
127
130
|
if ("fault" in given) continue;
|
|
128
131
|
if ("op" in given) {
|
|
129
|
-
const call = { idempotencyKey: `${run.id}:seed:${i}`, runId: run.id, opId: given.op, attempt: 1 };
|
|
132
|
+
const call = { idempotencyKey: `${run.id}:seed:${i}`, runId: run.id, opId: given.op, attempt: 1, secrets };
|
|
130
133
|
const output = await drivers.get(given.op)!(given.input, call);
|
|
131
134
|
seeded.push({ op: given.op, input: given.input, output });
|
|
132
135
|
continue;
|
|
@@ -153,5 +156,6 @@ export async function seedSandbox(run: Run, name: string): Promise<void> {
|
|
|
153
156
|
);
|
|
154
157
|
}
|
|
155
158
|
run.fakeDrivers = copies;
|
|
159
|
+
run.fakeSecrets = secrets;
|
|
156
160
|
await write(run, entry(run, { type: "scenario.seeded", scenario: name, seeds }));
|
|
157
161
|
}
|