bazis 0.134.0 → 0.136.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/core/access/contracts/References.ts +2 -0
- package/core/agent/AgentToolExecutor.ts +3 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/tools/StandardTools.module.ts +10 -2
- package/core/tools/contracts/Tools.config.ts +30 -0
- package/core/tools/generated/deps.ts +5 -0
- package/core/tools/http/HttpRequest.tool.ts +246 -0
- package/core/tools/http/publicAddress.ts +94 -0
- package/core/tools/index.ts +5 -2
- package/core/tools/orm/OrmEntity.references.ts +11 -7
- package/core/tools/secrets/SecretStore.ts +28 -0
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +3 -1
- package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +13 -1
- package/core/workflow/definitions_module/code/inspectProcesses.ts +15 -4
- package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +23 -0
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +56 -6
- package/core/workflow/generated/deps.ts +5 -2
- package/core/workflow/internal/WorkflowReferences.ts +69 -0
- package/core/workflow/runtime_module/access/Runtime.permissions.ts +1 -1
- package/core/workflow/runtime_module/background/WorkflowRetention.ts +15 -11
- package/core/workflow/runtime_module/events/Runtime.events.ts +7 -0
- package/core/workflow/runtime_module/http/Instances.controller.ts +7 -2
- package/core/workflow/runtime_module/internal/ChildCalls.ts +1 -0
- package/core/workflow/runtime_module/internal/RunLimits.ts +4 -3
- package/core/workflow/runtime_module/internal/ToolStep.ts +45 -5
- package/core/workflow/runtime_module/internal/WorkflowEngine.ts +20 -7
- package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +13 -0
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +11 -1
- package/core/workflow/tasks_module/events/TaskEvents.ts +17 -1
- package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +2 -0
- package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +2 -1
- package/library/redaction/index.ts +28 -0
- package/library/workflow/expressions.ts +8 -1
- package/library/workflow/index.ts +2 -1
- package/library/workflow/paths.ts +21 -1
- package/library/workflow/references.ts +106 -0
- package/library/workflow/validateProcess.ts +6 -5
- package/package.json +1 -1
|
@@ -16,6 +16,11 @@ export interface StartInstance extends WorkflowAuditOptions {
|
|
|
16
16
|
readonly actor: WorkflowActor;
|
|
17
17
|
/** The language of the request, for the application's validator; `workflow.language` by default. */
|
|
18
18
|
readonly language?: string;
|
|
19
|
+
/**
|
|
20
|
+
* A trial run (WORKFLOW-002 §5.4): Tools change nothing, the application hears
|
|
21
|
+
* nothing, tasks go only to who started it. It lives for a day.
|
|
22
|
+
*/
|
|
23
|
+
readonly test?: boolean;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
/** One element of a mass start. */
|
|
@@ -63,8 +68,14 @@ export interface InstanceSummary {
|
|
|
63
68
|
/** The instance whose `callProcess` started this one. */
|
|
64
69
|
readonly parentInstanceId: string | null;
|
|
65
70
|
readonly error: { readonly node: string; readonly code: string; readonly message: string } | null;
|
|
71
|
+
/** The step it failed on. */
|
|
72
|
+
readonly failedNode: string | null;
|
|
73
|
+
/** A trial run. */
|
|
74
|
+
readonly test: boolean;
|
|
66
75
|
readonly createdAt: Date;
|
|
67
76
|
readonly endedAt: Date | null;
|
|
77
|
+
/** How long it took by the engine's clock; `null` until it ends. */
|
|
78
|
+
readonly durationMs: number | null;
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
export interface TokenDetails {
|
|
@@ -141,6 +152,8 @@ export interface InstanceListQuery extends WorkflowPaging {
|
|
|
141
152
|
readonly businessKey?: string;
|
|
142
153
|
readonly startedById?: string;
|
|
143
154
|
readonly parentInstanceId?: string;
|
|
155
|
+
/** Trial runs are hidden unless asked for: `true` gives only them. Children of a named parent are shown as they are. */
|
|
156
|
+
readonly test?: boolean;
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
/**
|
|
@@ -55,7 +55,7 @@ export class WorkflowRuntime extends IWorkflowRuntime {
|
|
|
55
55
|
if (!start) throw new WorkflowError("wrong_status", `Process ${processKey} has no manual start`);
|
|
56
56
|
const { value, issues } = await this.validator().validate(inputSpecOf(version.document), options.input, { purpose: "input", processKey, partition, language: options.language ?? this.languages.default, actor: options.actor });
|
|
57
57
|
if (issues.length > 0) throw new WorkflowError("invalid_input", "The process input is invalid", issues);
|
|
58
|
-
const { id, skipped } = await this.engine.begin(this.engine.cache(version), start[0], { input: value, partition, businessKey, startedVia: "manual", actor: options.actor, request: options.request ?? null });
|
|
58
|
+
const { id, skipped } = await this.engine.begin(this.engine.cache(version), start[0], { input: value, partition, businessKey, startedVia: "manual", actor: options.actor, request: options.request ?? null, ...(options.test === true ? { test: true } : {}) });
|
|
59
59
|
if (skipped) throw new WorkflowError("conflict", `Instance ${id} with this businessKey is not finished`);
|
|
60
60
|
return await this.instance(id);
|
|
61
61
|
}
|
|
@@ -160,6 +160,7 @@ export class WorkflowRuntime extends IWorkflowRuntime {
|
|
|
160
160
|
if (query.businessKey !== undefined) source = source.where((i) => i.businessKey.eq(query.businessKey!));
|
|
161
161
|
if (query.startedById !== undefined) source = source.where((i) => i.startedById.eq(query.startedById!));
|
|
162
162
|
if (query.parentInstanceId !== undefined) source = source.where((i) => i.parentInstanceId.eq(query.parentInstanceId!));
|
|
163
|
+
if (query.test !== undefined || query.parentInstanceId === undefined) source = source.where((i) => i.test.eq(query.test === true));
|
|
163
164
|
const { items, total } = await paginate(source, pageQueryOf(query, { field: "id", dir: "desc" }));
|
|
164
165
|
return { items: items.map(summaryOf), total };
|
|
165
166
|
}
|
|
@@ -231,11 +232,20 @@ function summaryOf(row: WfInstance): InstanceSummary {
|
|
|
231
232
|
startedBy: { kind: row.startedByKind, id: row.startedById },
|
|
232
233
|
parentInstanceId: row.parentInstanceId,
|
|
233
234
|
error: row.error,
|
|
235
|
+
failedNode: row.error === null || row.error.node === "" ? null : row.error.node,
|
|
236
|
+
test: row.test,
|
|
234
237
|
createdAt: row.createdAt,
|
|
235
238
|
endedAt: row.endedAt,
|
|
239
|
+
durationMs: durationOf(row),
|
|
236
240
|
};
|
|
237
241
|
}
|
|
238
242
|
|
|
243
|
+
/** From the start by the engine's clock: `createdAt` is the clock of the database. */
|
|
244
|
+
function durationOf(row: WfInstance): number | null {
|
|
245
|
+
const startedAt = typeof row.trigger?.at === "string" ? Date.parse(row.trigger.at) : Number.NaN;
|
|
246
|
+
return row.endedAt === null || Number.isNaN(startedAt) ? null : row.endedAt.getTime() - startedAt;
|
|
247
|
+
}
|
|
248
|
+
|
|
239
249
|
function tokenOf(row: WfToken) {
|
|
240
250
|
return { id: row.id, nodeId: row.nodeId, viaFlowId: row.viaFlowId, parentTokenId: row.parentTokenId, iteration: row.iteration, status: row.status, output: row.output, createdAt: row.createdAt, endedAt: row.endedAt };
|
|
241
251
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OnEvent } from "../../../kernel";
|
|
2
|
-
import { WORKFLOW_INSTANCES_DELETED, WORKFLOW_TASK_REQUESTED, WORKFLOW_TOKEN_CLOSED, type WorkflowInstancesDeleted, type WorkflowTaskRequested, type WorkflowTokenClosed } from "../../runtime_module/events/Runtime.events";
|
|
2
|
+
import { WORKFLOW_INSTANCES_DELETED, WORKFLOW_TASK_REQUESTED, WORKFLOW_TOKEN_CLOSED, WORKFLOW_TRIAL_TASK_REQUESTED, WORKFLOW_TRIAL_TOKEN_CLOSED, type WorkflowInstancesDeleted, type WorkflowTaskRequested, type WorkflowTokenClosed } from "../../runtime_module/events/Runtime.events";
|
|
3
3
|
import type { TasksDbContext } from "../model/Tasks.dbContext";
|
|
4
4
|
import { WfTask } from "../model/WfTask.model";
|
|
5
5
|
|
|
@@ -10,6 +10,16 @@ export class TaskEvents {
|
|
|
10
10
|
/** A step started waiting for a person: its task. Repeated events of one token make one task. */
|
|
11
11
|
@OnEvent(WORKFLOW_TASK_REQUESTED)
|
|
12
12
|
async requested(event: WorkflowTaskRequested): Promise<void> {
|
|
13
|
+
await this.add(event, false);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The task of a trial run: it goes to who started it, and is marked. */
|
|
17
|
+
@OnEvent(WORKFLOW_TRIAL_TASK_REQUESTED)
|
|
18
|
+
async trialRequested(event: WorkflowTaskRequested): Promise<void> {
|
|
19
|
+
await this.add(event, true);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private async add(event: WorkflowTaskRequested, test: boolean): Promise<void> {
|
|
13
23
|
const assignee = "role" in event.assignee ? `role:${event.assignee.role}` : `user:${event.assignee.user}`;
|
|
14
24
|
// Past the change tracker: the scope may go on to update tasks directly, which a context holding one would refuse.
|
|
15
25
|
await this.db.tasks.insertIfAbsent(Object.assign(new WfTask(), {
|
|
@@ -18,6 +28,7 @@ export class TaskEvents {
|
|
|
18
28
|
processKey: event.processKey,
|
|
19
29
|
nodeId: event.nodeId,
|
|
20
30
|
partition: event.partition,
|
|
31
|
+
test,
|
|
21
32
|
title: event.title,
|
|
22
33
|
assignee,
|
|
23
34
|
form: event.form,
|
|
@@ -32,6 +43,11 @@ export class TaskEvents {
|
|
|
32
43
|
await this.db.tasks.asNoTracking().where((t) => t.tokenId.eq(event.tokenId).and(t.status.in(["open", "claimed"]))).executeUpdate({ status: "cancelled" });
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
@OnEvent(WORKFLOW_TRIAL_TOKEN_CLOSED)
|
|
47
|
+
async trialClosed(event: WorkflowTokenClosed): Promise<void> {
|
|
48
|
+
await this.closed(event);
|
|
49
|
+
}
|
|
50
|
+
|
|
35
51
|
/** Retention deleted the instances: their tasks go too. */
|
|
36
52
|
@OnEvent(WORKFLOW_INSTANCES_DELETED)
|
|
37
53
|
async deleted(event: WorkflowInstancesDeleted): Promise<void> {
|
|
@@ -11,6 +11,8 @@ export interface TaskDetails {
|
|
|
11
11
|
readonly nodeId: string;
|
|
12
12
|
readonly partition: string | null;
|
|
13
13
|
readonly title: LocalizedText;
|
|
14
|
+
/** A task of a trial run. */
|
|
15
|
+
readonly test: boolean;
|
|
14
16
|
readonly assignee: string;
|
|
15
17
|
readonly form: FormSpec | null;
|
|
16
18
|
readonly dueAt: Date | null;
|
|
@@ -100,7 +100,7 @@ export class WorkflowTasks extends IWorkflowTasks {
|
|
|
100
100
|
completedAt: this.clock.now(),
|
|
101
101
|
output: typeof saved === "object" && saved !== null && !Array.isArray(saved) ? saved as Record<string, unknown> : null,
|
|
102
102
|
});
|
|
103
|
-
await this.events.publish(WORKFLOW_TASK_COMPLETED, { taskId: task.id, instanceId: task.instanceId, processKey: task.processKey, nodeId: task.nodeId, partition: task.partition, actor, request: options.request ?? null });
|
|
103
|
+
if (!task.test) await this.events.publish(WORKFLOW_TASK_COMPLETED, { taskId: task.id, instanceId: task.instanceId, processKey: task.processKey, nodeId: task.nodeId, partition: task.partition, actor, request: options.request ?? null });
|
|
104
104
|
return await this.get(task.id);
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -157,6 +157,7 @@ function detailsOf(row: WfTask): TaskDetails {
|
|
|
157
157
|
nodeId: row.nodeId,
|
|
158
158
|
partition: row.partition,
|
|
159
159
|
title: row.title,
|
|
160
|
+
test: row.test,
|
|
160
161
|
assignee: row.assignee,
|
|
161
162
|
form: row.form,
|
|
162
163
|
dueAt: row.dueAt,
|
|
@@ -52,6 +52,34 @@ export const MIN_REMEMBERED_SECRET_LENGTH = 10;
|
|
|
52
52
|
const rememberedSecrets = new Set<string>();
|
|
53
53
|
let longestFirst: readonly string[] = [];
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* A copy of `value` with these exact strings cut out of every text and key: the
|
|
57
|
+
* secrets of one call. Unlike {@link rememberSecretValue} it knows nothing of the
|
|
58
|
+
* rest of the process, so it takes values of any length.
|
|
59
|
+
*/
|
|
60
|
+
export function redactValues<T>(value: T, secrets: readonly string[], replacement = DEFAULT_REPLACEMENT): T {
|
|
61
|
+
const list = secrets.filter((item) => item !== "").sort((left, right) => right.length - left.length);
|
|
62
|
+
if (list.length === 0) return value;
|
|
63
|
+
const text = (source: string) => list.reduce((current, secret) => current.split(secret).join(replacement), source);
|
|
64
|
+
const seen = new WeakMap<object, unknown>();
|
|
65
|
+
const walk = (item: unknown): unknown => {
|
|
66
|
+
if (typeof item === "string") return text(item);
|
|
67
|
+
if (typeof item !== "object" || item === null || item instanceof Date) return item;
|
|
68
|
+
if (seen.has(item)) return seen.get(item);
|
|
69
|
+
if (Array.isArray(item)) {
|
|
70
|
+
const copy: unknown[] = [];
|
|
71
|
+
seen.set(item, copy);
|
|
72
|
+
for (const element of item) copy.push(walk(element));
|
|
73
|
+
return copy;
|
|
74
|
+
}
|
|
75
|
+
const copy: Record<string, unknown> = {};
|
|
76
|
+
seen.set(item, copy);
|
|
77
|
+
for (const [key, element] of Object.entries(item)) copy[text(key)] = walk(element);
|
|
78
|
+
return copy;
|
|
79
|
+
};
|
|
80
|
+
return walk(value) as T;
|
|
81
|
+
}
|
|
82
|
+
|
|
55
83
|
/**
|
|
56
84
|
* Remembers a secret value: from now on every redaction removes it wherever it
|
|
57
85
|
* appears, also in plain text without a marker such as `password=`. `Secret`
|
|
@@ -267,6 +267,8 @@ export interface ExpressionScope {
|
|
|
267
267
|
readonly nodes?: ReadonlySet<string>;
|
|
268
268
|
/** Whether it is read inside a loop body: `$item` and `$index` exist only there. */
|
|
269
269
|
readonly loop?: boolean;
|
|
270
|
+
/** Whether it is the input of a Tool: `$secrets` exists only there, a value must go nowhere else. */
|
|
271
|
+
readonly secrets?: boolean;
|
|
270
272
|
}
|
|
271
273
|
|
|
272
274
|
/** What is wrong with an expression, or `null`: its shape, functions, argument types, paths and limits. */
|
|
@@ -333,11 +335,16 @@ function literalArgumentProblem(name: ExprFunctionName, args: readonly unknown[]
|
|
|
333
335
|
|
|
334
336
|
/** What is wrong with a `$` path, or `null`. */
|
|
335
337
|
export function checkPath(path: unknown, scope: ExpressionScope = {}): string | null {
|
|
336
|
-
if (typeof path !== "string" || !DATA_PATH.test(path)) return `path ${JSON.stringify(path)} must look like $input.field, $vars.name, $nodes.Node.field, $event.field, $trigger, $run.id, $now, $item.field or $
|
|
338
|
+
if (typeof path !== "string" || !DATA_PATH.test(path)) return `path ${JSON.stringify(path)} must look like $input.field, $vars.name, $nodes.Node.field, $event.field, $trigger, $run.id, $now, $item.field, $index or $secrets.NAME`;
|
|
337
339
|
const [root, first] = path.split(".");
|
|
338
340
|
if (root === "$now" && first !== undefined) return "$now has no fields";
|
|
339
341
|
if (root === "$index" && first !== undefined) return "$index has no fields";
|
|
340
342
|
if ((root === "$item" || root === "$index") && scope.loop === false) return `path ${path}: ${root} exists only inside a loop body`;
|
|
343
|
+
if (root === "$secrets") {
|
|
344
|
+
if (scope.secrets !== true) return `path ${path}: $secrets exists only in the input of a Tool`;
|
|
345
|
+
if (first === undefined || path.split(".").length !== 2) return "$secrets needs a name: $secrets.NAME";
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
341
348
|
if (root === "$nodes" && first === undefined) return "$nodes needs a node: $nodes.Node.field";
|
|
342
349
|
if (first === undefined) return null;
|
|
343
350
|
if (root === "$input" && scope.input && !scope.input.has(first)) return `path ${path}: the input has no field ${first}`;
|
|
@@ -22,7 +22,7 @@ export {
|
|
|
22
22
|
} from "./ProcessDocument";
|
|
23
23
|
export { NODE_KINDS, nodeKindOf, ISO_DURATION, MAX_ITERATIONS, MAX_LOOP_PARALLEL, PROCESS_KEY, ISO_MOMENT, type JsonSchema, type NodeKindDefinition, type NodeRole } from "./nodeKinds";
|
|
24
24
|
export { PROCESS_LIMITS, validateProcess, hasErrors, type ProcessIssue, type ProcessIssueCode, type ValidateProcessOptions } from "./validateProcess";
|
|
25
|
-
export { DATA_PATH, DATA_PATHS, isPath, readPath, resolveInput, type InstanceData } from "./paths";
|
|
25
|
+
export { DATA_PATH, DATA_PATHS, isPath, readPath, resolveInput, secretNames, type InstanceData } from "./paths";
|
|
26
26
|
export {
|
|
27
27
|
EXPRESSION_FUNCTIONS,
|
|
28
28
|
EXPRESSION_LIMITS,
|
|
@@ -42,6 +42,7 @@ export {
|
|
|
42
42
|
} from "./expressions";
|
|
43
43
|
export { ExpressionSyntaxError, parseExpression, printExpression } from "./expressionSyntax";
|
|
44
44
|
export { durationMs } from "./duration";
|
|
45
|
+
export { documentReferences, inputReferences, schemaReferenceKinds, type DocumentReference, type InputReference } from "./references";
|
|
45
46
|
export { isTimeZone, parseCron, type CronSchedule } from "./cron";
|
|
46
47
|
export { processToMermaid } from "./mermaid";
|
|
47
48
|
export { PERSONAL_MASK, maskByMapping, maskBySchema, masksPersonal, personalPaths, personalVariables, readsPersonal } from "./masking";
|
|
@@ -20,10 +20,12 @@ export interface InstanceData {
|
|
|
20
20
|
readonly item?: unknown;
|
|
21
21
|
/** Inside a loop body: the number of this iteration, from 0. */
|
|
22
22
|
readonly index?: number;
|
|
23
|
+
/** Only while the input of a Tool is resolved: the values of `$secrets.NAME`. */
|
|
24
|
+
readonly secrets?: Readonly<Record<string, string>>;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/** `$input.amount`, `$vars.payment.id`, `$nodes.Pay.paymentId`, `$event.record.id`, `$run.partition`, `$now`, `$item.id`, `$index`. */
|
|
26
|
-
export const DATA_PATH = /^\$(input|vars|nodes|event|trigger|run|now|item|index)((\.[A-Za-z_][A-Za-z0-9_]*)*)$/;
|
|
28
|
+
export const DATA_PATH = /^\$(input|vars|nodes|event|trigger|run|now|item|index|secrets)((\.[A-Za-z_][A-Za-z0-9_]*)*)$/;
|
|
27
29
|
|
|
28
30
|
/** The roots of `$` paths, for a designer. */
|
|
29
31
|
export const DATA_PATHS: readonly { readonly path: string; readonly description: string }[] = Object.freeze([
|
|
@@ -36,8 +38,26 @@ export const DATA_PATHS: readonly { readonly path: string; readonly description:
|
|
|
36
38
|
{ path: "$now", description: "The engine's time" },
|
|
37
39
|
{ path: "$item", description: "Inside a loop body: the element of this iteration" },
|
|
38
40
|
{ path: "$index", description: "Inside a loop body: the number of this iteration, from 0" },
|
|
41
|
+
{ path: "$secrets.<name>", description: "Only in the input of a Tool: the value of a secret, which goes nowhere else" },
|
|
39
42
|
]);
|
|
40
43
|
|
|
44
|
+
/** The secrets a Tool input names, in paths and expressions at any depth. */
|
|
45
|
+
export function secretNames(input: unknown): string[] {
|
|
46
|
+
const names = new Set<string>();
|
|
47
|
+
const walk = (value: unknown): void => {
|
|
48
|
+
if (typeof value === "string") {
|
|
49
|
+
const match = /^\$secrets\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(value);
|
|
50
|
+
if (match) names.add(match[1]!);
|
|
51
|
+
} else if (Array.isArray(value)) {
|
|
52
|
+
value.forEach(walk);
|
|
53
|
+
} else if (typeof value === "object" && value !== null) {
|
|
54
|
+
Object.values(value).forEach(walk);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
walk(input);
|
|
58
|
+
return [...names];
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
/** `true` if the value is a path to instance data. */
|
|
42
62
|
export function isPath(value: unknown): value is string {
|
|
43
63
|
return typeof value === "string" && DATA_PATH.test(value);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { isExpressionNode } from "./expressions";
|
|
2
|
+
import { isPath } from "./paths";
|
|
3
|
+
import type { ProcessDocument } from "./ProcessDocument";
|
|
4
|
+
|
|
5
|
+
/** One reference in a Tool input or a document: what `x-ref` and `x-ref-keys` of a schema mark. */
|
|
6
|
+
export interface InputReference {
|
|
7
|
+
readonly kind: string;
|
|
8
|
+
readonly id: string;
|
|
9
|
+
/** The area it is looked for in: `x-ref-scope`. */
|
|
10
|
+
readonly within?: string;
|
|
11
|
+
/** Where in the input: `/values/status`. */
|
|
12
|
+
readonly path: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A reference of a document, and whose it is. */
|
|
16
|
+
export interface DocumentReference extends InputReference {
|
|
17
|
+
readonly node?: string;
|
|
18
|
+
readonly variable?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Json = Record<string, unknown>;
|
|
22
|
+
|
|
23
|
+
function isObject(value: unknown): value is Json {
|
|
24
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Whether a value is known only when the step runs. */
|
|
28
|
+
function isComputed(value: unknown): boolean {
|
|
29
|
+
return isPath(value) || isExpressionNode(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isId(value: unknown): value is string | number {
|
|
33
|
+
return (typeof value === "string" && value !== "") || typeof value === "number";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The references in a Tool input by the JSON Schema of the input. A value computed
|
|
38
|
+
* when the step runs is left out: it is checked then. `x-ref-scope` beginning
|
|
39
|
+
* with `/` is a JSON Pointer to another field of the input; otherwise it is the area
|
|
40
|
+
* itself. A reference whose area is computed is left out too.
|
|
41
|
+
*/
|
|
42
|
+
export function inputReferences(schema: unknown, input: unknown): InputReference[] {
|
|
43
|
+
const out: InputReference[] = [];
|
|
44
|
+
const within = (source: Json): { readonly known: boolean; readonly value?: string } => {
|
|
45
|
+
const scope = source["x-ref-scope"];
|
|
46
|
+
if (typeof scope !== "string") return { known: true };
|
|
47
|
+
if (!scope.startsWith("/")) return { known: true, value: scope };
|
|
48
|
+
let current: unknown = input;
|
|
49
|
+
for (const key of scope.slice(1).split("/")) {
|
|
50
|
+
current = isObject(current) && Object.hasOwn(current, key) ? current[key] : undefined;
|
|
51
|
+
}
|
|
52
|
+
return isId(current) && !isComputed(current) ? { known: true, value: String(current) } : { known: false };
|
|
53
|
+
};
|
|
54
|
+
const walk = (source: unknown, value: unknown, path: string): void => {
|
|
55
|
+
if (!isObject(source) || value === undefined || value === null || isComputed(value)) return;
|
|
56
|
+
const area = within(source);
|
|
57
|
+
const add = (kind: string, id: string, at: string) => out.push({ kind, id, ...(area.value === undefined ? {} : { within: area.value }), path: at });
|
|
58
|
+
if (typeof source["x-ref"] === "string" && isId(value) && area.known) add(source["x-ref"], String(value), path);
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
value.forEach((item, index) => walk(source.items, item, `${path}/${index}`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!isObject(value)) return;
|
|
64
|
+
if (typeof source["x-ref-keys"] === "string" && area.known) for (const key of Object.keys(value)) add(source["x-ref-keys"], key, `${path}/${key}`);
|
|
65
|
+
if (isObject(source.properties)) {
|
|
66
|
+
for (const [key, item] of Object.entries(source.properties)) if (Object.hasOwn(value, key)) walk(item, value[key], `${path}/${key}`);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
walk(schema, input, "");
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The kinds a schema refers to, whatever the values: each must have an owner. */
|
|
74
|
+
export function schemaReferenceKinds(schema: unknown): string[] {
|
|
75
|
+
const kinds = new Set<string>();
|
|
76
|
+
const walk = (source: unknown): void => {
|
|
77
|
+
if (!isObject(source)) return;
|
|
78
|
+
for (const key of ["x-ref", "x-ref-keys"]) if (typeof source[key] === "string") kinds.add(source[key]);
|
|
79
|
+
if (isObject(source.properties)) Object.values(source.properties).forEach(walk);
|
|
80
|
+
walk(source.items);
|
|
81
|
+
};
|
|
82
|
+
walk(schema);
|
|
83
|
+
return [...kinds];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The references a document holds as literals (WORKFLOW-002 §6.3): in the inputs of
|
|
88
|
+
* its Tools, by their schemas; the defaults of variables of type `ref`; the processes
|
|
89
|
+
* it calls. `inputSchemaOf` gives the input schema of a Tool, if it is known.
|
|
90
|
+
*/
|
|
91
|
+
export function documentReferences(document: ProcessDocument, inputSchemaOf: (tool: string) => unknown): DocumentReference[] {
|
|
92
|
+
const out: DocumentReference[] = [];
|
|
93
|
+
for (const variable of document.variables ?? []) {
|
|
94
|
+
if (variable.type === "ref" && typeof variable.ref === "string" && isId(variable.default)) {
|
|
95
|
+
out.push({ kind: variable.ref, id: String(variable.default), path: "/default", variable: variable.name });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const handler = document.settings?.errorHandler;
|
|
99
|
+
if (handler !== undefined) out.push({ kind: "workflow.process", id: handler, path: "/settings/errorHandler" });
|
|
100
|
+
for (const [node, { kind, config }] of Object.entries(document.nodes)) {
|
|
101
|
+
if (kind === "callProcess" && typeof config?.process === "string") out.push({ kind: "workflow.process", id: config.process, path: "/process", node });
|
|
102
|
+
if (kind !== "serviceTask" || typeof config?.tool !== "string") continue;
|
|
103
|
+
for (const reference of inputReferences(inputSchemaOf(config.tool), config.input)) out.push({ ...reference, node });
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
@@ -33,6 +33,7 @@ export type ProcessIssueCode =
|
|
|
33
33
|
| "config_invalid"
|
|
34
34
|
| "unknown_tool"
|
|
35
35
|
| "unknown_process"
|
|
36
|
+
| "broken_ref"
|
|
36
37
|
| "container_invalid"
|
|
37
38
|
| "flow_across_container"
|
|
38
39
|
| "loop_body"
|
|
@@ -64,8 +65,6 @@ export type ProcessIssueCode =
|
|
|
64
65
|
export const PROCESS_LIMITS = Object.freeze({ nodes: 500, flows: 1000, bytes: 1_000_000 });
|
|
65
66
|
|
|
66
67
|
const ID = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
67
|
-
/** Paths that come with later stages of WORKFLOW-002: until then they are an error, not a silent literal. */
|
|
68
|
-
const NOT_YET = /^\$secrets(\.|$)/;
|
|
69
68
|
/** Who a boundary error may hang on: the steps that fail by an error of their own. */
|
|
70
69
|
const CAN_FAIL: ReadonlySet<string | undefined> = new Set(["serviceTask", "callProcess", "loop"]);
|
|
71
70
|
|
|
@@ -446,7 +445,7 @@ function configExpressionProblems(kind: string, config: Json, scope: ExpressionS
|
|
|
446
445
|
if (problem) out.push(`config.assignments[${index}].value: ${problem}`);
|
|
447
446
|
}
|
|
448
447
|
}
|
|
449
|
-
if ((kind === "serviceTask" || kind === "callProcess") && config.input !== undefined) walkInput(config.input, "config.input", scope, out);
|
|
448
|
+
if ((kind === "serviceTask" || kind === "callProcess") && config.input !== undefined) walkInput(config.input, "config.input", kind === "serviceTask" ? { ...scope, secrets: true } : scope, out);
|
|
450
449
|
if (kind === "loop" && config.over !== undefined) {
|
|
451
450
|
const problem = checkExpression(config.over, scope);
|
|
452
451
|
if (problem) out.push(`config.over: ${problem}`);
|
|
@@ -474,8 +473,10 @@ function configExpressionProblems(kind: string, config: Json, scope: ExpressionS
|
|
|
474
473
|
|
|
475
474
|
function walkInput(value: unknown, at: string, scope: ExpressionScope, out: string[]): void {
|
|
476
475
|
if (typeof value === "string") {
|
|
477
|
-
if (
|
|
478
|
-
|
|
476
|
+
if (value === "$secrets" || value.startsWith("$secrets.")) {
|
|
477
|
+
const problem = checkPath(value, scope);
|
|
478
|
+
if (problem) out.push(`${at}: ${problem}`);
|
|
479
|
+
} else if (isPath(value)) {
|
|
479
480
|
const problem = checkPath(value, scope);
|
|
480
481
|
if (problem) out.push(`${at}: ${problem}`);
|
|
481
482
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.136.0",
|
|
4
4
|
"description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexander Alenin",
|