@sanoma-ai/workflows 0.11.16 → 0.11.18
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 +16 -8
- package/dist/call.d.ts +35 -4
- package/dist/call.js +128 -40
- package/dist/call.js.map +1 -1
- package/dist/client.d.ts +17 -8
- package/dist/client.js +90 -51
- package/dist/client.js.map +1 -1
- package/dist/collection.d.ts +25 -7
- package/dist/collection.js +236 -91
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +24 -1
- package/dist/config.js +48 -10
- package/dist/config.js.map +1 -1
- package/dist/db/company.d.ts +20 -0
- package/dist/db/company.js +9 -1
- package/dist/db/company.js.map +1 -1
- package/dist/describe.js +1 -1
- package/dist/describe.js.map +1 -1
- package/dist/description.d.ts +6 -3
- package/dist/description.js +5 -4
- package/dist/description.js.map +1 -1
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +3 -2
- package/dist/errors.js.map +1 -1
- package/dist/grid.js +3 -2
- package/dist/grid.js.map +1 -1
- package/dist/host-derive.d.ts +13 -8
- package/dist/host-derive.js +26 -13
- package/dist/host-derive.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +7 -0
- package/dist/ledger.js +32 -2
- package/dist/ledger.js.map +1 -1
- package/dist/local-sandbox.d.ts +17 -0
- package/dist/local-sandbox.js +131 -0
- package/dist/local-sandbox.js.map +1 -0
- package/dist/op.d.ts +10 -2
- package/dist/op.js.map +1 -1
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +20 -4
- package/dist/paths.js.map +1 -1
- package/dist/resource.d.ts +7 -1
- package/dist/resource.js +5 -1
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +9 -6
- package/dist/resources.js.map +1 -1
- package/dist/runner-box.d.ts +23 -0
- package/dist/runner-box.js +181 -0
- package/dist/runner-box.js.map +1 -0
- package/dist/runner-protocol.d.ts +73 -0
- package/dist/runner-protocol.js +34 -0
- package/dist/runner-protocol.js.map +1 -0
- package/dist/sandbox-contract.d.ts +109 -0
- package/dist/sandbox-contract.js +26 -0
- package/dist/sandbox-contract.js.map +1 -0
- package/dist/sandbox-runner.cjs +7 -0
- package/dist/scenario.js +16 -7
- package/dist/scenario.js.map +1 -1
- package/dist/schedules.d.ts +11 -3
- package/dist/schedules.js +15 -3
- package/dist/schedules.js.map +1 -1
- package/dist/schema.js +6 -3
- package/dist/schema.js.map +1 -1
- package/dist/secret.d.ts +2 -1
- package/dist/secret.js +3 -2
- package/dist/secret.js.map +1 -1
- package/dist/shared.d.ts +39 -5
- package/dist/shared.js +13 -1
- package/dist/shared.js.map +1 -1
- package/dist/sites.js +10 -8
- package/dist/sites.js.map +1 -1
- package/dist/source.d.ts +21 -0
- package/dist/source.js.map +1 -1
- package/drizzle/company/0003_ledger_target.sql +2 -0
- package/drizzle/company/meta/0003_snapshot.json +845 -0
- package/drizzle/company/meta/_journal.json +7 -0
- package/package.json +5 -2
- package/src/call.ts +181 -60
- package/src/client.ts +118 -67
- package/src/collection.ts +265 -97
- package/src/config.ts +65 -16
- package/src/db/company.ts +9 -1
- package/src/describe.ts +1 -1
- package/src/description.ts +5 -4
- package/src/errors.ts +3 -2
- package/src/grid.ts +3 -1
- package/src/host-derive.ts +38 -18
- package/src/index.ts +15 -2
- package/src/ledger.ts +42 -2
- package/src/local-sandbox.ts +133 -0
- package/src/op.ts +10 -2
- package/src/paths.ts +22 -4
- package/src/resource.ts +16 -2
- package/src/resources.ts +8 -6
- package/src/runner-box.ts +218 -0
- package/src/runner-protocol.ts +61 -0
- package/src/sandbox-contract.ts +121 -0
- package/src/sandbox-runner.ts +187 -0
- package/src/scenario.ts +16 -6
- package/src/schedules.ts +19 -4
- package/src/schema.ts +14 -3
- package/src/secret.ts +6 -2
- package/src/shared.ts +40 -4
- package/src/sites.ts +11 -7
- package/src/source.ts +14 -0
package/src/schedules.ts
CHANGED
|
@@ -140,23 +140,38 @@ interface TimeMatcher {
|
|
|
140
140
|
/** The first instant strictly after `date` that matches; one that does not at the end of its search horizon. */
|
|
141
141
|
nextWakeupTime(date: Date | number): Date;
|
|
142
142
|
}
|
|
143
|
-
type TimeMatcherClass = new (pattern: string, timezone?: string) => TimeMatcher;
|
|
143
|
+
export type TimeMatcherClass = new (pattern: string, timezone?: string) => TimeMatcher;
|
|
144
144
|
|
|
145
145
|
let matcherClass: TimeMatcherClass | undefined;
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
|
-
* DBOS's `TimeMatcher
|
|
149
|
-
* exports leave it out (dbos-sdk 5.2.11), so it is loaded by
|
|
148
|
+
* The file DBOS's `TimeMatcher` is in, `dist/src/scheduler/crontab.js` beside its main module, as
|
|
149
|
+
* `require` resolves the package: its exports leave it out (dbos-sdk 5.2.11), so it is loaded by
|
|
150
|
+
* path, and a test holds it there.
|
|
150
151
|
*/
|
|
152
|
+
export const timeMatcherFile = (require: NodeJS.Require): string =>
|
|
153
|
+
join(dirname(require.resolve("@dbos-inc/dbos-sdk")), "scheduler", "crontab.js");
|
|
154
|
+
|
|
155
|
+
/** DBOS's `TimeMatcher`, required from its file (`timeMatcherFile`), or the one `useTimeMatcher` gave. */
|
|
151
156
|
export function timeMatcher(): TimeMatcherClass {
|
|
152
157
|
if (matcherClass) return matcherClass;
|
|
153
158
|
const require = createRequire(import.meta.url);
|
|
154
|
-
const file =
|
|
159
|
+
const file = timeMatcherFile(require);
|
|
155
160
|
const loaded = (require(file) as { TimeMatcher?: unknown }).TimeMatcher;
|
|
156
161
|
if (typeof loaded !== "function") throw new Error(`DBOS's cron parser is not in ${file} any more`);
|
|
157
162
|
return (matcherClass = loaded as TimeMatcherClass);
|
|
158
163
|
}
|
|
159
164
|
|
|
165
|
+
/**
|
|
166
|
+
* From now on, DBOS's cron parser is `matcher`, where it cannot be required by path: workerd,
|
|
167
|
+
* which bundles the runtime, and imports it by its file.
|
|
168
|
+
*/
|
|
169
|
+
export function useTimeMatcher(matcher: TimeMatcherClass): void {
|
|
170
|
+
if (typeof matcher !== "function")
|
|
171
|
+
throw new Error("DBOS's cron parser (TimeMatcher) is not in its scheduler's crontab.js any more");
|
|
172
|
+
matcherClass = matcher;
|
|
173
|
+
}
|
|
174
|
+
|
|
160
175
|
/**
|
|
161
176
|
* When `schedule` (a cron expression read in `timezone`, UTC without one) fires next, strictly
|
|
162
177
|
* after `from`: as DBOS's scheduler finds it. Throws for a schedule that never fires.
|
package/src/schema.ts
CHANGED
|
@@ -13,7 +13,15 @@ import { recordSchemaOf } from "./collection.ts";
|
|
|
13
13
|
import type { ResolvedConfig } from "./config.ts";
|
|
14
14
|
import { jsonSchemaOf } from "./define.ts";
|
|
15
15
|
import { isObject } from "./resource.ts";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
CHANGE_MERGE,
|
|
18
|
+
DATA_VENDOR,
|
|
19
|
+
EFFECTS,
|
|
20
|
+
groupNamesOf,
|
|
21
|
+
isBatch,
|
|
22
|
+
type PeopleResource,
|
|
23
|
+
WORKFLOW_ACTION,
|
|
24
|
+
} from "./shared.ts";
|
|
17
25
|
|
|
18
26
|
// The Cedar schema a company's policies are validated and decided against: made from the
|
|
19
27
|
// resolved config and the people, in memory for the evaluator, and written as text to
|
|
@@ -90,7 +98,8 @@ const WRITES = new Set(["save", "delete"]);
|
|
|
90
98
|
* - `entity Group enum [...]`, the declared groups, `agents` and `visitors`, so a policy naming another fails
|
|
91
99
|
* validation; `entity Principal in [Group] { onBehalfOf?: Principal }`; `Collection`;
|
|
92
100
|
* `Target in [Collection]`; `Workflow`;
|
|
93
|
-
* - an action per operation id
|
|
101
|
+
* - an action per operation id (a collection's `saveMany` excepted: the policy decides each of
|
|
102
|
+
* its entries as a `save` or `delete`), in its effect's group (`effect:money`), on a `Target`, with
|
|
94
103
|
* `context` `{ input, before? }`: `input` from the operation's input schema, `before` the record
|
|
95
104
|
* as it was for a collection's `save` and `delete`; `sanoma.change.merge` on a `Change`, whose
|
|
96
105
|
* attributes are a change's facts (`ChangeFacts`), so a rule reads `resource.touches`;
|
|
@@ -112,7 +121,9 @@ function buildSchema(
|
|
|
112
121
|
});
|
|
113
122
|
const actions: CedarSchemaJson[string]["actions"] = { [START]: {} };
|
|
114
123
|
for (const effect of EFFECTS) actions[`effect:${effect}`] = {};
|
|
115
|
-
|
|
124
|
+
// A collection's batch is no action: the policy decides each of its entries as a save or a delete.
|
|
125
|
+
const ops = [...resolved.ops.values()].filter((op) => !isBatch(op));
|
|
126
|
+
for (const op of ops.toSorted((a, b) => a.id.localeCompare(b.id))) {
|
|
116
127
|
const collection =
|
|
117
128
|
op.vendor === DATA_VENDOR && WRITES.has(op.name) ? resolved.collections.get(op.resource) : undefined;
|
|
118
129
|
const before = collection && typeOf(recordSchemaOf(collection));
|
package/src/secret.ts
CHANGED
|
@@ -101,14 +101,18 @@ export type Resolved<T> = T extends Secret
|
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* `value` with each secret marker in it replaced by the stored value: generated and stored first
|
|
104
|
-
* for one with `generate` and none stored. A marker naming nothing stored fails the call, for good
|
|
104
|
+
* for one with `generate` and none stored. A marker naming nothing stored fails the call, for good,
|
|
105
|
+
* before the driver changes anything at the vendor (`secret_not_set`, which the ledger tells).
|
|
105
106
|
*/
|
|
106
107
|
export async function resolveSecrets<T>(value: T, secrets: Secrets): Promise<Resolved<T>> {
|
|
107
108
|
const resolve = async (v: unknown): Promise<unknown> => {
|
|
108
109
|
if (isSecret(v)) {
|
|
109
110
|
const stored = v.generate ? await secrets.ensure(v.$secret, generateSecret) : await secrets.get(v.$secret);
|
|
110
111
|
if (stored === undefined) {
|
|
111
|
-
throw new DriverError(
|
|
112
|
+
throw new DriverError(
|
|
113
|
+
`The secret ${v.$secret} has no value: store it (or, where sanoma.config.ts binds it, set what the binding reads), then try again`,
|
|
114
|
+
{ retryable: false, vendorCode: "secret_not_set" },
|
|
115
|
+
);
|
|
112
116
|
}
|
|
113
117
|
return stored;
|
|
114
118
|
}
|
package/src/shared.ts
CHANGED
|
@@ -55,6 +55,14 @@ export const APPLY_WORKFLOW = "apply";
|
|
|
55
55
|
*/
|
|
56
56
|
export const DATA_VENDOR = "data";
|
|
57
57
|
|
|
58
|
+
/** The one operation of a collection that stands for others: `saveMany`, each entry a `save` or `delete`. */
|
|
59
|
+
export const BATCH = "saveMany";
|
|
60
|
+
export const isBatch = (op: Pick<OpEntry, "vendor" | "name">): boolean =>
|
|
61
|
+
op.vendor === DATA_VENDOR && op.name === BATCH;
|
|
62
|
+
/** The ids of the operations a batch's entries are: its collection's `save` and `delete`. */
|
|
63
|
+
export const entryOpsOf = (op: Pick<OpEntry, "resource">): string[] =>
|
|
64
|
+
["save", "delete"].map((name) => `${DATA_VENDOR}.${op.resource}.${name}`);
|
|
65
|
+
|
|
58
66
|
/** Every effect an operation may have, each an action group (`effect:<effect>`) of the Cedar schema. */
|
|
59
67
|
export const EFFECTS = ["read", "write", "publish", "send", "money", "access"] as const satisfies readonly Effect[];
|
|
60
68
|
|
|
@@ -68,11 +76,16 @@ export const WORKFLOW_ACTION = "workflow:";
|
|
|
68
76
|
*/
|
|
69
77
|
export function previewRequests(
|
|
70
78
|
workflow: Pick<WorkflowEntry, "name" | "ops">,
|
|
71
|
-
ops: ReadonlyMap<string, Pick<OpEntry, "effect">>,
|
|
79
|
+
ops: ReadonlyMap<string, Pick<OpEntry, "effect" | "vendor" | "resource" | "name">>,
|
|
72
80
|
): Omit<PreviewRequest, "principal">[] {
|
|
81
|
+
// A batch is asked as its entries, the save and the delete the policy decides: no rule names it.
|
|
82
|
+
const asked = workflow.ops.flatMap((id) => {
|
|
83
|
+
const op = ops.get(id);
|
|
84
|
+
return op && isBatch(op) ? entryOpsOf(op) : [id];
|
|
85
|
+
});
|
|
73
86
|
return [
|
|
74
87
|
{ action: { id: `${WORKFLOW_ACTION}${workflow.name}` } },
|
|
75
|
-
...
|
|
88
|
+
...[...new Set(asked)].map((id) => {
|
|
76
89
|
const effect = ops.get(id)?.effect;
|
|
77
90
|
return { action: effect === undefined ? { id } : { id, effect }, inRun: true };
|
|
78
91
|
}),
|
|
@@ -203,6 +216,12 @@ type Range<V> = { gt?: V; gte?: V; lt?: V; lte?: V };
|
|
|
203
216
|
/** How many records a collection's `list` answers: `default` when the query names no `limit`, `max` at most. */
|
|
204
217
|
export const LIST_LIMIT = { default: 100, max: 1000 } as const;
|
|
205
218
|
|
|
219
|
+
/** How many entries (records and deletes together) a collection's `saveMany` takes at most. */
|
|
220
|
+
export const BATCH_LIMIT = 1000;
|
|
221
|
+
|
|
222
|
+
/** What a collection's `delete` answers, and `saveMany` for each of its deletes: whether there was a record. */
|
|
223
|
+
export type Deleted = { key: string; deleted: boolean };
|
|
224
|
+
|
|
206
225
|
/** The kinds of value a field holds: text, a number, true or false, a date (ISO 8601) or a person's id. */
|
|
207
226
|
export const FIELD_TYPES = ["text", "number", "boolean", "date", "person"] as const;
|
|
208
227
|
|
|
@@ -673,10 +692,27 @@ export interface WorkflowFn<I, O> {
|
|
|
673
692
|
/** An operation a site may call directly (a read, or a collection's write), as the person on the page. */
|
|
674
693
|
export type OpFn<I, O> = (input: I) => Promise<O>;
|
|
675
694
|
|
|
676
|
-
/** A collection as a site's `api.data.<name>` has it: its
|
|
695
|
+
/** A collection as a site's `api.data.<name>` has it: its six operations (`Saved`, `ListQuery`, `Deleted`). */
|
|
677
696
|
export interface CollectionApi<R> {
|
|
678
697
|
get: OpFn<{ key: string }, Saved<R> | null>;
|
|
679
698
|
list(query?: ListQuery<R>): Promise<Saved<R>[]>;
|
|
699
|
+
/** How many records there are, or match `where` (as `list` takes it). */
|
|
700
|
+
count(query?: Pick<ListQuery<R>, "where">): Promise<{ count: number }>;
|
|
680
701
|
save: OpFn<{ record: R; ifAbsent?: boolean; ifRecord?: Record<string, unknown> }, Saved<R>>;
|
|
681
|
-
|
|
702
|
+
/**
|
|
703
|
+
* Saves and deletes together, all or none, at most `BATCH_LIMIT` entries, each key once: the
|
|
704
|
+
* records' receipts then the deletes' answers, in the order sent. The policy decides each entry
|
|
705
|
+
* as its own `save` or `delete`; a deny refuses the batch (`policy_denied`, with the `denied`
|
|
706
|
+
* keys), a hold holds it whole, and a record an entry's `ifRecord` or `ifAbsent` refuses refuses
|
|
707
|
+
* it (status 409, `record_changed`) with every such entry in `conflicts: { key, current }[]`.
|
|
708
|
+
*/
|
|
709
|
+
saveMany: OpFn<
|
|
710
|
+
{
|
|
711
|
+
records?: { record: R; ifAbsent?: boolean; ifRecord?: Record<string, unknown> }[];
|
|
712
|
+
deletes?: { key: string; ifRecord?: Record<string, unknown> }[];
|
|
713
|
+
},
|
|
714
|
+
(Saved<R> | Deleted)[]
|
|
715
|
+
>;
|
|
716
|
+
/** Deletes the record, or with `ifRecord` only while it still matches (status 409, `record_changed`, otherwise). */
|
|
717
|
+
delete: OpFn<{ key: string; ifRecord?: Record<string, unknown> }, Deleted>;
|
|
682
718
|
}
|
package/src/sites.ts
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
BUILT_IN_WORKFLOWS,
|
|
17
17
|
DATA_VENDOR,
|
|
18
18
|
DOMAIN_NAME,
|
|
19
|
+
entryOpsOf,
|
|
20
|
+
isBatch,
|
|
19
21
|
LOCALE,
|
|
20
22
|
SITE_NAME,
|
|
21
23
|
siteKey,
|
|
@@ -80,7 +82,7 @@ export function readSites(
|
|
|
80
82
|
*/
|
|
81
83
|
const refused = (call: SiteCall, login: string | undefined): string | undefined => {
|
|
82
84
|
const why = `a visitor's call to ${KIND_NAMES[call.kind]} "${call.target}" is refused`;
|
|
83
|
-
let
|
|
85
|
+
let actions: string[];
|
|
84
86
|
let group: string;
|
|
85
87
|
if (call.kind === "workflow") {
|
|
86
88
|
if (BUILT_IN_WORKFLOWS.has(call.target)) return `${why}: a visitor may not start the runtime's own workflows`;
|
|
@@ -88,22 +90,24 @@ export function readSites(
|
|
|
88
90
|
if (call.method !== undefined && call.method !== "start") {
|
|
89
91
|
return `${why}: a visitor reads only the result of a run it started`;
|
|
90
92
|
}
|
|
91
|
-
[
|
|
93
|
+
[actions, group] = [[`${WORKFLOW_ACTION}${call.target}`], START];
|
|
92
94
|
} else {
|
|
93
|
-
action = call.kind === "op" ? call.target : `${DATA_VENDOR}.${call.target}.${call.method}`;
|
|
95
|
+
const action = call.kind === "op" ? call.target : `${DATA_VENDOR}.${call.target}.${call.method}`;
|
|
94
96
|
const op = ops.get(action);
|
|
95
97
|
if (!op) return undefined;
|
|
96
|
-
|
|
98
|
+
// A batch is its entries to the policy: the save and the delete each need a permit.
|
|
99
|
+
[actions, group] = [isBatch(op) ? entryOpsOf(op) : [action], `effect:${op.effect}`];
|
|
97
100
|
}
|
|
98
101
|
if (!permits) return undefined;
|
|
99
102
|
const [scopes, principal] =
|
|
100
103
|
login === undefined
|
|
101
104
|
? [[ANONYMOUS], `== Sanoma::Principal::"${ANONYMOUS}"`]
|
|
102
105
|
: [[ANONYMOUS, VISITORS], `in Sanoma::Group::"${VISITORS}"`];
|
|
103
|
-
|
|
106
|
+
const missing = actions.filter((action) => !permits(new Set(scopes), action, group));
|
|
107
|
+
if (!missing.length) return undefined;
|
|
104
108
|
return (
|
|
105
|
-
`${why}: no permit in policies/ names ${scopes.join(" or ")} for ${
|
|
106
|
-
`Add: permit (principal ${principal}, action == Sanoma::Action::"${
|
|
109
|
+
`${why}: no permit in policies/ names ${scopes.join(" or ")} for ${missing.join(" or ")}. ` +
|
|
110
|
+
`Add: ${missing.map((a) => `permit (principal ${principal}, action == Sanoma::Action::"${a}", resource);`).join(" ")}`
|
|
107
111
|
);
|
|
108
112
|
};
|
|
109
113
|
|
package/src/source.ts
CHANGED
|
@@ -40,6 +40,20 @@ export interface Source {
|
|
|
40
40
|
diff?(id: string): Promise<DraftDiff>;
|
|
41
41
|
/** Closes the draft `id`: its checkout removed and its branch deleted, whatever it held; nothing when it is gone. */
|
|
42
42
|
closeDraft?(id: string): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* The draft `id`'s branch as a git bundle (`git bundle create`), for a clone elsewhere (an agent
|
|
45
|
+
* box's) to fetch: its commits since `since`, a commit that clone has, or the whole branch when
|
|
46
|
+
* `since` is absent or a commit this repository lacks; no bytes when the clone has them all.
|
|
47
|
+
* The branch's head beside it. A bundle holds commits and the branch's name alone: no remote,
|
|
48
|
+
* no config, no credential.
|
|
49
|
+
*/
|
|
50
|
+
bundle?(id: string, opts?: { since?: string }): Promise<{ bytes: Uint8Array; head: string }>;
|
|
51
|
+
/**
|
|
52
|
+
* Takes a bundle of the draft `id`'s branch made elsewhere (an agent box's new commits on it):
|
|
53
|
+
* its commits fetched, then the draft's checkout fast-forwarded to them. Refused unless that is
|
|
54
|
+
* a fast-forward that leaves what the checkout holds uncommitted in place. The branch's head after.
|
|
55
|
+
*/
|
|
56
|
+
unbundle?(id: string, bytes: Uint8Array): Promise<{ head: string }>;
|
|
43
57
|
/**
|
|
44
58
|
* `sanoma check` on the draft `id`, as a child in its checkout, one at a time in the process. A
|
|
45
59
|
* check that gives no result (a config that does not load, no install, a timeout) is a result
|