@sanoma-ai/workflows 0.2.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 +121 -62
- 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 +33 -4
- package/dist/call.js +107 -42
- 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 +85 -7
- package/dist/client.js +289 -25
- 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 +82 -18
- 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/data.d.ts +2 -0
- package/dist/data.js +3 -0
- package/dist/data.js.map +1 -0
- package/dist/datafile.d.ts +28 -2
- package/dist/datafile.js +118 -19
- 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 +45 -5
- package/dist/describe.js +72 -7
- 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/errors.d.ts +16 -1
- package/dist/errors.js +30 -1
- package/dist/errors.js.map +1 -1
- package/dist/expectations.d.ts +3 -3
- package/dist/expectations.js +16 -7
- package/dist/expectations.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +10 -4
- 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/op.d.ts +6 -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 +21 -5
- package/dist/resource.js +12 -3
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +58 -6
- package/dist/resources.js +119 -26
- package/dist/resources.js.map +1 -1
- package/dist/run.d.ts +17 -1
- package/dist/run.js.map +1 -1
- package/dist/scenario.d.ts +16 -9
- package/dist/scenario.js +93 -20
- 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/shared.d.ts +181 -5
- package/dist/shared.js +160 -7
- 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/version.d.ts +3 -2
- package/dist/version.js +3 -2
- package/dist/version.js.map +1 -1
- package/dist/worker.d.ts +3 -2
- package/dist/worker.js +24 -4
- package/dist/worker.js.map +1 -1
- package/oxlint.json +5 -16
- package/package.json +8 -3
- package/src/approvals.ts +49 -13
- package/src/call-workflow.ts +44 -0
- package/src/call.ts +157 -45
- package/src/cedar.ts +493 -0
- package/src/client.ts +368 -37
- package/src/collection.ts +431 -0
- package/src/config.ts +131 -34
- package/src/constructors.ts +4 -0
- package/src/data.ts +10 -0
- package/src/datafile.ts +141 -21
- package/src/db.ts +90 -22
- package/src/define.ts +11 -9
- package/src/describe.ts +113 -10
- package/src/direct.ts +15 -0
- package/src/errors.ts +36 -1
- package/src/expectations.ts +17 -8
- package/src/index.ts +33 -2
- package/src/ledger.ts +35 -17
- package/src/lint.ts +29 -12
- package/src/op.ts +6 -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 +35 -8
- package/src/resources.ts +165 -24
- package/src/run.ts +18 -1
- package/src/scenario.ts +128 -33
- package/src/scenarios.ts +15 -8
- package/src/schema.ts +261 -0
- package/src/sdk.ts +211 -0
- package/src/shared.ts +271 -8
- package/src/sites.ts +220 -0
- package/src/version.ts +3 -2
- package/src/worker.ts +29 -6
package/src/resource.ts
CHANGED
|
@@ -24,8 +24,8 @@ export interface ResourceFields {
|
|
|
24
24
|
|
|
25
25
|
/** Where a field names another declared resource: see `ResourceFields.references`. */
|
|
26
26
|
export interface FieldReference {
|
|
27
|
-
/** The `<vendor>.<type>` of the resource it names. */
|
|
28
|
-
readonly type: string;
|
|
27
|
+
/** The `<vendor>.<type>` of the resource it names, or the types it may name one of. */
|
|
28
|
+
readonly type: string | readonly string[];
|
|
29
29
|
/**
|
|
30
30
|
* The fields of that resource's state the vendor may hold here instead of its `name`, such as
|
|
31
31
|
* GitHub's `node_id`: a drift check takes any of their values, read in the same run, as the
|
|
@@ -85,6 +85,12 @@ export interface ResourceSpec<S extends z.ZodObject = z.ZodObject> {
|
|
|
85
85
|
* recreated database looks fine and has lost its data. Default: true for a host, else false.
|
|
86
86
|
*/
|
|
87
87
|
stateless?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* True when there is no vendor object behind a declaration: the repo is all there is of it (the
|
|
90
|
+
* runtime's people, `sanoma.person`). Such a type has no operations, so no driver, and drift and
|
|
91
|
+
* apply pass its resources by. Default false.
|
|
92
|
+
*/
|
|
93
|
+
vendorless?: boolean;
|
|
88
94
|
/**
|
|
89
95
|
* Operations apply calls, in order, right after it creates one of its objects and binds it: a
|
|
90
96
|
* Supabase project's health, until it is healthy, then its database URL stored.
|
|
@@ -156,12 +162,14 @@ type DeclaringAt<T, R extends References, P extends string> = T extends readonly
|
|
|
156
162
|
: WithSecrets<T>;
|
|
157
163
|
|
|
158
164
|
type DeclaringField<V, R extends References, P extends string> = P extends keyof R
|
|
159
|
-
?
|
|
165
|
+
? V extends readonly (infer I)[]
|
|
166
|
+
? (WithSecrets<I> | DeclaredOf<R[P]["type"]>)[]
|
|
167
|
+
: WithSecrets<V> | DeclaredOf<R[P]["type"]>
|
|
160
168
|
: [Extract<keyof R, `${P}.${string}`>] extends [never]
|
|
161
169
|
? WithSecrets<V>
|
|
162
170
|
: DeclaringAt<V, R, P>;
|
|
163
171
|
|
|
164
|
-
type DeclaredOf<Id> = Id extends string ? Declared<Id> : never;
|
|
172
|
+
type DeclaredOf<Id> = Id extends string ? Declared<Id> : Id extends readonly (infer E)[] ? DeclaredOf<E> : never;
|
|
165
173
|
|
|
166
174
|
const importId = z.string().min(1);
|
|
167
175
|
/** What a driver keeps beside a state (an OpenTofu provider's private data and state version), passed back unchanged. */
|
|
@@ -239,6 +247,15 @@ export function isChangeOp(
|
|
|
239
247
|
);
|
|
240
248
|
}
|
|
241
249
|
|
|
250
|
+
/**
|
|
251
|
+
* True for an operation of a declared resource type (`read`, `import`, and a writable type's
|
|
252
|
+
* `plan` and change operations), which only drift and apply call: by its vendor and resource.
|
|
253
|
+
*/
|
|
254
|
+
export const isResourceOp = (
|
|
255
|
+
op: { readonly vendor: string; readonly resource: string },
|
|
256
|
+
types: ReadonlyMap<string, unknown>,
|
|
257
|
+
): boolean => types.has(`${op.vendor}.${op.resource}`);
|
|
258
|
+
|
|
242
259
|
/**
|
|
243
260
|
* A resource type: call it to declare a resource in a data file, and give it to
|
|
244
261
|
* `defineConnector` as a group, under its `type`: `defineConnector("github", { repository })`.
|
|
@@ -250,7 +267,11 @@ export interface Resource<
|
|
|
250
267
|
W extends boolean = boolean,
|
|
251
268
|
>
|
|
252
269
|
extends
|
|
253
|
-
Readonly<
|
|
270
|
+
Readonly<
|
|
271
|
+
Required<
|
|
272
|
+
Omit<ResourceSpec<S>, "identity" | "find" | "writable" | "host" | "stateless" | "afterCreate" | "vendorless">
|
|
273
|
+
>
|
|
274
|
+
>,
|
|
254
275
|
Readonly<Pick<ResourceSpec<S>, "identity" | "find" | "host" | "afterCreate">> {
|
|
255
276
|
(desired: Declaring<z.input<S>, R>): Declared<Id, z.input<S>>;
|
|
256
277
|
/** `read` and `import`, each effect `read`, idempotent, with the id as `target`; for a writable type, the change operations too. */
|
|
@@ -259,6 +280,8 @@ export interface Resource<
|
|
|
259
280
|
readonly writable: W;
|
|
260
281
|
/** True when apply may recreate or replace its objects as routine: they keep no data. */
|
|
261
282
|
readonly stateless: boolean;
|
|
283
|
+
/** True when there is no vendor object behind it (`ResourceSpec.vendorless`): it has no operations. */
|
|
284
|
+
readonly vendorless: boolean;
|
|
262
285
|
readonly [RESOURCE]: true;
|
|
263
286
|
}
|
|
264
287
|
|
|
@@ -517,6 +540,8 @@ export function defineResource<
|
|
|
517
540
|
const writable = (spec.writable ?? false) as W;
|
|
518
541
|
const what: `${V}.${T}` = `${vendor}.${type}`;
|
|
519
542
|
if (host) checkHost(what, host, spec);
|
|
543
|
+
if (spec.vendorless && (writable || host))
|
|
544
|
+
throw new Error(`${what}: a vendorless type has no object to change or host`);
|
|
520
545
|
const title = spec.title.toLowerCase();
|
|
521
546
|
const io = opsOf(schema);
|
|
522
547
|
// The policy's `target` for a create: the type, as no object has an id yet.
|
|
@@ -572,7 +597,7 @@ export function defineResource<
|
|
|
572
597
|
opaque,
|
|
573
598
|
},
|
|
574
599
|
} satisfies Specs<S, "read" | "import">;
|
|
575
|
-
const ops = (writable ? { ...base, ...changeOps } : base) as unknown as ResourceOps<S, W>;
|
|
600
|
+
const ops = (spec.vendorless ? {} : writable ? { ...base, ...changeOps } : base) as unknown as ResourceOps<S, W>;
|
|
576
601
|
|
|
577
602
|
const named = new Set(identity?.match(/[A-Za-z_][\w.]*/g));
|
|
578
603
|
const owned = new Set(fields.vendorOwned.filter((path) => !named.has(path)));
|
|
@@ -599,8 +624,9 @@ export function defineResource<
|
|
|
599
624
|
const id = value[DECLARED];
|
|
600
625
|
const wants = references[path];
|
|
601
626
|
if (wants === undefined) throw new Error(`${what}: ${at} takes a value, not a resource (${id} ${value.name})`);
|
|
602
|
-
|
|
603
|
-
|
|
627
|
+
const types = [wants.type].flat();
|
|
628
|
+
if (!types.includes(id))
|
|
629
|
+
throw new Error(`${what}: ${at} names a resource of type ${types.join(" or ")}, not ${id} (${value.name})`);
|
|
604
630
|
if (value.name === undefined)
|
|
605
631
|
throw new Error(`${what}: ${at} names a ${id}, which has no identity to name it by`);
|
|
606
632
|
refs[at] = `${id}:${value.name}`;
|
|
@@ -652,6 +678,7 @@ export function defineResource<
|
|
|
652
678
|
writable,
|
|
653
679
|
...(host && { host }),
|
|
654
680
|
stateless: spec.stateless ?? host !== undefined,
|
|
681
|
+
vendorless: spec.vendorless ?? false,
|
|
655
682
|
...(spec.afterCreate && { afterCreate: spec.afterCreate }),
|
|
656
683
|
ops,
|
|
657
684
|
// A symbol, so `defineConnector` tells a resource type from a group of operations.
|
package/src/resources.ts
CHANGED
|
@@ -1,13 +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";
|
|
6
12
|
import { type Binding, type Bindings, LOCKFILE, readLockfile } from "./lockfile.ts";
|
|
7
13
|
import { type Connector, VENDOR } from "./op.ts";
|
|
14
|
+
import { filesUnder } from "./paths.ts";
|
|
15
|
+
import { peopleConnector } from "./person.ts";
|
|
8
16
|
import type { Span } from "./outline.ts";
|
|
9
17
|
import { type Declared, type Resource, resourceTypesOf } from "./resource.ts";
|
|
10
|
-
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";
|
|
11
29
|
|
|
12
30
|
/** A resource a data file declares, as read from the file without running it. */
|
|
13
31
|
export interface DeclaredResource {
|
|
@@ -34,6 +52,22 @@ export interface DeclaredResource {
|
|
|
34
52
|
refs: Record<string, string>;
|
|
35
53
|
}
|
|
36
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
|
+
|
|
37
71
|
/** Something wrong in the data files, or in finding them. */
|
|
38
72
|
export interface ResourceProblem {
|
|
39
73
|
/**
|
|
@@ -56,22 +90,110 @@ export interface DataFiles {
|
|
|
56
90
|
bindings: Bindings;
|
|
57
91
|
/** Each binding whose key no data file declares, by key. */
|
|
58
92
|
orphans: (Binding & { key: string })[];
|
|
93
|
+
/** The collections `resources/data.ts` declares, in file order. */
|
|
94
|
+
collections: DeclaredCollection[];
|
|
59
95
|
/** By file, line and column. */
|
|
60
96
|
problems: ResourceProblem[];
|
|
61
97
|
}
|
|
62
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
|
+
|
|
63
125
|
/**
|
|
64
|
-
* The
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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.
|
|
67
130
|
*/
|
|
68
|
-
export function
|
|
69
|
-
|
|
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";
|
|
70
181
|
if (problems.length) {
|
|
71
|
-
|
|
72
|
-
|
|
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));
|
|
187
|
+
}
|
|
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;
|
|
73
195
|
}
|
|
74
|
-
|
|
196
|
+
throw new NotAdmittedError(who);
|
|
75
197
|
}
|
|
76
198
|
|
|
77
199
|
interface FileEntry {
|
|
@@ -93,14 +215,21 @@ interface FileEntry {
|
|
|
93
215
|
* type the connector does not declare, a value the type refuses (a reference where its type
|
|
94
216
|
* takes none among them), a reference cycle, and two resources of one type with one identity.
|
|
95
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
|
+
*
|
|
96
221
|
* Each declaration is joined to its binding in the lockfile (`readLockfile`) by its key,
|
|
97
222
|
* `<file>#<export>`; a binding no export declares is an orphan. Problems: the lockfile's, a
|
|
98
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.
|
|
99
227
|
*/
|
|
100
228
|
export function readDataFiles(
|
|
101
229
|
root: string | undefined,
|
|
102
230
|
connectors: readonly Connector<any, any>[],
|
|
103
231
|
types: ReadonlyMap<string, Resource> = resourceTypesOf(connectors),
|
|
232
|
+
partial?: (source: string) => boolean,
|
|
104
233
|
): DataFiles {
|
|
105
234
|
const problems: ResourceProblem[] = [];
|
|
106
235
|
if (root === undefined) {
|
|
@@ -108,15 +237,20 @@ export function readDataFiles(
|
|
|
108
237
|
message:
|
|
109
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`",
|
|
110
239
|
});
|
|
111
|
-
return { resources: [], bindings: {}, orphans: [], problems };
|
|
240
|
+
return { resources: [], bindings: {}, orphans: [], collections: [], problems };
|
|
112
241
|
}
|
|
113
242
|
const lock = readLockfile(root);
|
|
114
243
|
problems.push(...lock.problems);
|
|
115
244
|
|
|
116
245
|
const files = new Map<string, FileEntry>();
|
|
246
|
+
const left = new Set<string>();
|
|
117
247
|
for (const path of dataFilesUnder(join(root, "resources"))) {
|
|
118
248
|
const rel = relative(root, path).split(sep).join("/");
|
|
119
249
|
const source = readFileSync(path, "utf8");
|
|
250
|
+
if (partial && !partial(source)) {
|
|
251
|
+
left.add(path);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
120
254
|
const { program, problems: syntax, at } = parsed(source, path);
|
|
121
255
|
const data = readDataFile(program, path, join(root, "resources"));
|
|
122
256
|
const problem = (offset: number, message: string) => problems.push({ file: rel, ...at(offset), message });
|
|
@@ -131,6 +265,8 @@ export function readDataFiles(
|
|
|
131
265
|
const { id, info } = connector[VENDOR];
|
|
132
266
|
if (info?.package) entries.set(`${info.package}/resources`, id);
|
|
133
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");
|
|
134
270
|
|
|
135
271
|
// What each file's names stand for, and every resource read by its id.
|
|
136
272
|
const vendors = new Map<FileEntry, Map<string, string>>();
|
|
@@ -142,6 +278,7 @@ export function readDataFiles(
|
|
|
142
278
|
for (const imp of file.data.imports) {
|
|
143
279
|
if (imp.kind === "constructors") {
|
|
144
280
|
const vendor = entries.get(imp.source);
|
|
281
|
+
if (vendor === undefined && partial) continue;
|
|
145
282
|
if (vendor === undefined) {
|
|
146
283
|
file.problem(
|
|
147
284
|
imp.start,
|
|
@@ -157,7 +294,9 @@ export function readDataFiles(
|
|
|
157
294
|
}
|
|
158
295
|
continue;
|
|
159
296
|
}
|
|
160
|
-
const
|
|
297
|
+
const target = resolve(dirname(path), imp.source);
|
|
298
|
+
const other = files.get(target);
|
|
299
|
+
if (!other && left.has(target)) continue;
|
|
161
300
|
if (!other) {
|
|
162
301
|
file.problem(imp.start, `"${imp.source}" is not a data file under resources/`);
|
|
163
302
|
} else if (!other.data.declared.includes(imp.imported)) {
|
|
@@ -293,19 +432,21 @@ export function readDataFiles(
|
|
|
293
432
|
refs: result.refs,
|
|
294
433
|
});
|
|
295
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
|
+
);
|
|
296
443
|
problems.sort(
|
|
297
444
|
(a, b) =>
|
|
298
445
|
(a.file ?? "").localeCompare(b.file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.column ?? 0) - (b.column ?? 0),
|
|
299
446
|
);
|
|
300
|
-
return { resources, bindings: lock.bindings, orphans, problems };
|
|
447
|
+
return { resources, bindings: lock.bindings, orphans, collections, problems };
|
|
301
448
|
}
|
|
302
449
|
|
|
303
|
-
/** The data files under `dir`, sorted: regular `.ts` files, less tests, declarations and `node_modules
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return readdirSync(dir, { recursive: true, withFileTypes: true })
|
|
307
|
-
.filter((d) => d.isFile() && d.name.endsWith(".ts") && !d.name.endsWith(".test.ts") && !d.name.endsWith(".d.ts"))
|
|
308
|
-
.map((d) => join(d.parentPath, d.name))
|
|
309
|
-
.filter((path) => !relative(dir, path).split(sep).includes("node_modules"))
|
|
310
|
-
.toSorted();
|
|
311
|
-
}
|
|
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,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Collection } from "./collection.ts";
|
|
3
|
+
import type { ApprovalState, Approver, Principal, WorkflowDefinition } from "./define.ts";
|
|
3
4
|
import type { Fake } from "./fake.ts";
|
|
4
5
|
import type { LedgerStore } from "./ledger.ts";
|
|
5
6
|
import type { DriverFn, Op } from "./op.ts";
|
|
@@ -16,6 +17,18 @@ export interface RunArgs {
|
|
|
16
17
|
startedBy: Principal;
|
|
17
18
|
/** The scenario a sandbox run is seeded from; the run calls the fakes instead of the drivers. */
|
|
18
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[];
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
/**
|
|
@@ -37,9 +50,13 @@ export interface WorkerState {
|
|
|
37
50
|
/** The worker's `source`, when it was given one. */
|
|
38
51
|
source?: Source;
|
|
39
52
|
types: ReadonlyMap<string, Resource>;
|
|
53
|
+
/** The collections, by name, which scenarios' steps may name (`data.<name>`). */
|
|
54
|
+
collections: ReadonlyMap<string, Collection>;
|
|
40
55
|
steps: readonly Steps[];
|
|
41
56
|
/** Its `version`, when it has one, is recorded with each of its decisions. */
|
|
42
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;
|
|
43
60
|
ledger: LedgerStore;
|
|
44
61
|
/** The stored secrets, for a live run's drivers (`CallContext.secrets`). */
|
|
45
62
|
secrets: Secrets;
|