bazis 0.131.0 → 0.132.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.
Files changed (66) hide show
  1. package/cli/templates/process.ts +1 -1
  2. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  3. package/core/workflow/{runtime_module/config/Runtime.config.ts → contracts/Workflow.config.ts} +15 -6
  4. package/core/workflow/contracts/WorkflowExtensions.ts +40 -0
  5. package/core/workflow/contracts/WorkflowInputValidator.ts +6 -5
  6. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +16 -3
  7. package/core/workflow/definitions_module/access/Definitions.permissions.ts +4 -3
  8. package/core/workflow/definitions_module/access/Folder.references.ts +37 -0
  9. package/core/workflow/definitions_module/access/Process.references.ts +1 -1
  10. package/core/workflow/definitions_module/access/ProcessFolder.lookup.ts +20 -0
  11. package/core/workflow/definitions_module/code/CatalogImport.ts +95 -0
  12. package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +26 -16
  13. package/core/workflow/definitions_module/code/codeProcesses.ts +7 -3
  14. package/core/workflow/definitions_module/code/defineProcess.ts +10 -4
  15. package/core/workflow/definitions_module/http/Processes.controller.ts +65 -23
  16. package/core/workflow/definitions_module/model/Definitions.dbContext.ts +2 -2
  17. package/core/workflow/definitions_module/model/WfProcess.model.ts +19 -5
  18. package/core/workflow/definitions_module/model/WfVersion.model.ts +1 -1
  19. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +33 -10
  20. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +88 -28
  21. package/core/workflow/generated/deps.ts +11 -3
  22. package/core/workflow/index.ts +4 -2
  23. package/core/workflow/internal/WorkflowLanguages.ts +35 -0
  24. package/core/workflow/internal/canonicalJson.ts +8 -0
  25. package/core/workflow/internal/http.ts +20 -1
  26. package/core/workflow/runtime_module/WorkflowRuntime.module.ts +0 -2
  27. package/core/workflow/runtime_module/access/Runtime.permissions.ts +5 -3
  28. package/core/workflow/runtime_module/background/WorkflowRetention.ts +1 -1
  29. package/core/workflow/runtime_module/background/WorkflowWorker.ts +2 -2
  30. package/core/workflow/runtime_module/events/Runtime.events.ts +2 -2
  31. package/core/workflow/runtime_module/http/Instances.controller.ts +16 -11
  32. package/core/workflow/runtime_module/internal/Schedules.ts +2 -2
  33. package/core/workflow/runtime_module/internal/ToolStep.ts +40 -9
  34. package/core/workflow/runtime_module/internal/WorkflowEngine.ts +60 -10
  35. package/core/workflow/runtime_module/internal/WorkflowJobs.ts +2 -4
  36. package/core/workflow/runtime_module/model/Runtime.dbContext.ts +2 -2
  37. package/core/workflow/runtime_module/model/WfHistory.model.ts +2 -2
  38. package/core/workflow/runtime_module/model/WfInstance.model.ts +28 -3
  39. package/core/workflow/runtime_module/model/WfJob.model.ts +17 -1
  40. package/core/workflow/runtime_module/model/WfSubscription.model.ts +1 -1
  41. package/core/workflow/runtime_module/model/WfToken.model.ts +11 -1
  42. package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +5 -1
  43. package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +9 -7
  44. package/core/workflow/tasks_module/http/Tasks.controller.ts +14 -13
  45. package/core/workflow/tasks_module/model/Tasks.dbContext.ts +2 -2
  46. package/core/workflow/tasks_module/model/WfTask.model.ts +8 -4
  47. package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +3 -3
  48. package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +3 -3
  49. package/library/openapi/codegen.ts +3 -0
  50. package/library/validation/types/ValidatorOptions.ts +2 -0
  51. package/library/workflow/ProcessDocument.ts +59 -23
  52. package/library/workflow/expressionSyntax.ts +243 -0
  53. package/library/workflow/expressions.ts +343 -0
  54. package/library/workflow/formFields.ts +118 -33
  55. package/library/workflow/index.ts +48 -9
  56. package/library/workflow/localized.ts +35 -0
  57. package/library/workflow/masking.ts +58 -0
  58. package/library/workflow/mermaid.ts +18 -8
  59. package/library/workflow/nodeKinds.ts +88 -17
  60. package/library/workflow/palette.ts +108 -0
  61. package/library/workflow/paths.ts +31 -6
  62. package/library/workflow/structure.ts +282 -0
  63. package/library/workflow/upgradeDocument.ts +84 -0
  64. package/library/workflow/validateProcess.ts +130 -48
  65. package/package.json +1 -1
  66. package/library/workflow/conditions.ts +0 -44
@@ -7,13 +7,14 @@ export type StartedVia = "manual" | "event" | "schedule";
7
7
 
8
8
  /**
9
9
  * One run of a process. Bound to the version it started with: a new publication
10
- * does not touch it. Its data: the input, the variables `saveTo` writes and the
11
- * result of each step by node id.
10
+ * does not touch it. Its data: the input, the variables and the result of each
11
+ * step by node id.
12
12
  */
13
13
  @Index(["partition", "status"])
14
14
  @Index(["processKey", "status"])
15
15
  @Index(["status", "endedAt"])
16
- @Entity({ table: "bazis_wf_run_instances" })
16
+ @Index(["test", "createdAt"])
17
+ @Entity({ table: "bazis_wf2_run_instances" })
17
18
  export class WfInstance {
18
19
  @UUID({ version: "v7" })
19
20
  id = "";
@@ -45,6 +46,30 @@ export class WfInstance {
45
46
  @Column({ type: "json" })
46
47
  nodes: Record<string, unknown> = {};
47
48
 
49
+ /** What the process gave back: its `out` variables when it completed. */
50
+ @Column({ type: "json", nullable: true })
51
+ output: Record<string, unknown> | null = null;
52
+
53
+ /** `$event`: the data of the event that started or last continued it. */
54
+ @Column({ type: "json", nullable: true })
55
+ event: unknown = null;
56
+
57
+ /** `$trigger`: how it was started. */
58
+ @Column({ type: "json", nullable: true })
59
+ trigger: Record<string, unknown> | null = null;
60
+
61
+ /** A trial run (WORKFLOW-002 §5.4, stage 5). */
62
+ @Column({ type: "boolean" })
63
+ test = false;
64
+
65
+ /** The instance whose `callProcess` started this one (stage 3). */
66
+ @Index()
67
+ @Column({ type: "text", nullable: true })
68
+ parentInstanceId: string | null = null;
69
+
70
+ @Column({ type: "text", nullable: true })
71
+ parentTokenId: string | null = null;
72
+
48
73
  @Index()
49
74
  @Column({ type: "text", nullable: true })
50
75
  businessKey: string | null = null;
@@ -9,7 +9,7 @@ export type JobKind = "tool" | "timer" | "retry" | "schedule";
9
9
  * token. Application instances claim it with `FOR UPDATE SKIP LOCKED`.
10
10
  */
11
11
  @ForeignKey(() => WfInstance, { properties: ["instanceId"], onDelete: "cascade" })
12
- @Entity({ table: "bazis_wf_run_jobs" })
12
+ @Entity({ table: "bazis_wf2_run_jobs" })
13
13
  export class WfJob {
14
14
  @UUID({ version: "v7" })
15
15
  id = "";
@@ -34,6 +34,22 @@ export class WfJob {
34
34
  @Column({ type: "datetime" })
35
35
  dueAt = new Date(0);
36
36
 
37
+ /** What a job carries besides its token: an event to deliver (stage 4). */
38
+ @Column({ type: "json", nullable: true })
39
+ payload: unknown = null;
40
+
41
+ /** Jobs with one key merge into the last: `debounce` of an event start (stage 4). */
42
+ @Index()
43
+ @Column({ type: "text", nullable: true })
44
+ dedupeKey: string | null = null;
45
+
46
+ /** The partition and the start node of a `schedule` job (stage 4). */
47
+ @Column({ type: "text", nullable: true })
48
+ partition: string | null = null;
49
+
50
+ @Column({ type: "text", nullable: true })
51
+ nodeId: string | null = null;
52
+
37
53
  @Column({ type: "integer" })
38
54
  attempts = 0;
39
55
 
@@ -4,7 +4,7 @@ import { WfInstance } from "./WfInstance.model";
4
4
  /** A token waiting for an event: `eventWait` or `boundaryEvent`. Matched by key and correlation. */
5
5
  @ForeignKey(() => WfInstance, { properties: ["instanceId"], onDelete: "cascade" })
6
6
  @Index(["eventKey", "correlation"])
7
- @Entity({ table: "bazis_wf_run_subscriptions" })
7
+ @Entity({ table: "bazis_wf2_run_subscriptions" })
8
8
  export class WfSubscription {
9
9
  @UUID({ version: "v7" })
10
10
  id = "";
@@ -10,7 +10,7 @@ export type TokenStatus = "active" | "waiting" | "done" | "cancelled" | "failed"
10
10
  /** Where execution is. After a parallel gateway an instance has several. Its id is the idempotency key of a Tool call. */
11
11
  @ForeignKey(() => WfInstance, { properties: ["instanceId"], onDelete: "cascade" })
12
12
  @Index(["instanceId", "status"])
13
- @Entity({ table: "bazis_wf_run_tokens" })
13
+ @Entity({ table: "bazis_wf2_run_tokens" })
14
14
  export class WfToken {
15
15
  @UUID({ version: "v7" })
16
16
  id = "";
@@ -31,6 +31,16 @@ export class WfToken {
31
31
  @Column({ type: "json", nullable: true })
32
32
  output: unknown = null;
33
33
 
34
+ /** The loop token this one runs inside, with its iteration and what only the body sees (stage 3). */
35
+ @Column({ type: "text", nullable: true })
36
+ parentTokenId: string | null = null;
37
+
38
+ @Column({ type: "integer", nullable: true })
39
+ iteration: number | null = null;
40
+
41
+ @Column({ type: "json", nullable: true })
42
+ locals: Record<string, unknown> | null = null;
43
+
34
44
  @Column({ type: "createdAt" })
35
45
  createdAt = new Date(0);
36
46
 
@@ -13,6 +13,8 @@ export interface StartInstance extends WorkflowAuditOptions {
13
13
  readonly businessKey?: string | null;
14
14
  /** On whose behalf the steps run. */
15
15
  readonly actor: WorkflowActor;
16
+ /** The language of the request, for the application's validator; `workflow.language` by default. */
17
+ readonly language?: string;
16
18
  }
17
19
 
18
20
  export interface CancelInstance extends WorkflowAuditOptions {
@@ -52,6 +54,8 @@ export interface InstanceDetails extends InstanceSummary {
52
54
  readonly input: Record<string, unknown>;
53
55
  readonly vars: Record<string, unknown>;
54
56
  readonly nodes: Record<string, unknown>;
57
+ /** What the process gave back: its `out` variables; `null` until it completes. */
58
+ readonly output: Record<string, unknown> | null;
55
59
  /** Every token, oldest first. */
56
60
  readonly tokens: readonly TokenDetails[];
57
61
  }
@@ -105,7 +109,7 @@ export abstract class IWorkflowRuntime {
105
109
  /** Retries the failed steps of a `failed` instance. */
106
110
  abstract retry(instanceId: string, options: RetryInstance): Promise<InstanceDetails>;
107
111
  /** Finishes a user task's wait with the form output and moves on; `wrong_status` when the token no longer waits. */
108
- abstract completeToken(tokenId: string, output: unknown, actor: WorkflowActor): Promise<InstanceDetails>;
112
+ abstract completeToken(tokenId: string, output: unknown, actor: WorkflowActor, options?: { readonly language?: string }): Promise<InstanceDetails>;
109
113
  /**
110
114
  * Delivers an event: starts processes whose latest version has an
111
115
  * `eventStart` for the key, and moves the instances waiting for it.
@@ -1,11 +1,12 @@
1
1
  import { grantsToWhere } from "../../../access";
2
2
  import { paginate } from "../../../orm";
3
- import type { FormIssue, FormSpec } from "../../../../library/workflow";
3
+ import { inputSpecOf, type FormIssue, type FormSpec } from "../../../../library/workflow";
4
4
  import { isUuid, pageQueryOf, type WorkflowActor, type WorkflowPage } from "../../contracts/types";
5
5
  import { WorkflowError } from "../../contracts/WorkflowError";
6
6
  import type { ServiceProvider } from "../../../di";
7
7
  import { WorkflowInputValidator } from "../../contracts/WorkflowInputValidator";
8
8
  import type { IWorkflowDefinitions } from "../../definitions_module/services/IWorkflowDefinitions.service";
9
+ import type { WorkflowLanguages } from "../../internal/WorkflowLanguages";
9
10
  import type { WorkflowEngine } from "../internal/WorkflowEngine";
10
11
  import type { RuntimeDbContext } from "../model/Runtime.dbContext";
11
12
  import type { WfHistory } from "../model/WfHistory.model";
@@ -34,6 +35,7 @@ export class WorkflowRuntime extends IWorkflowRuntime {
34
35
  private readonly definitions: IWorkflowDefinitions,
35
36
  /** The scope: the input validator is asked here, so that the application's own registration replaces the default. */
36
37
  private readonly services: ServiceProvider,
38
+ private readonly languages: WorkflowLanguages,
37
39
  ) {
38
40
  super();
39
41
  }
@@ -45,7 +47,7 @@ export class WorkflowRuntime extends IWorkflowRuntime {
45
47
  const version = await this.definitions.latest(processKey);
46
48
  const start = Object.entries(version.document.nodes).find(([, node]) => node.kind === "manualStart");
47
49
  if (!start) throw new WorkflowError("wrong_status", `Process ${processKey} has no manual start`);
48
- const { value, issues } = await this.validator().validate(version.document.input, options.input, { purpose: "input", processKey, partition });
50
+ 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 });
49
51
  if (issues.length > 0) throw new WorkflowError("invalid_input", "The process input is invalid", issues);
50
52
  const id = await this.engine.begin(this.engine.cache(version), start[0], { input: value, partition, businessKey, startedVia: "manual", actor: options.actor, request: options.request ?? null });
51
53
  return await this.instance(id);
@@ -76,7 +78,7 @@ export class WorkflowRuntime extends IWorkflowRuntime {
76
78
  return await this.instance(instanceId);
77
79
  }
78
80
 
79
- async completeToken(tokenId: string, output: unknown, actor: WorkflowActor): Promise<InstanceDetails> {
81
+ async completeToken(tokenId: string, output: unknown, actor: WorkflowActor, options: { readonly language?: string } = {}): Promise<InstanceDetails> {
80
82
  actorOf(actor);
81
83
  if (!isUuid(tokenId)) throw new WorkflowError("not_found", `Token ${tokenId} does not exist`);
82
84
  const token = await this.db.tokens.asNoTracking().firstOrDefault((t) => t.id.eq(tokenId));
@@ -87,7 +89,7 @@ export class WorkflowRuntime extends IWorkflowRuntime {
87
89
  if (node?.kind !== "userTask" || !current || current.status !== "waiting" || step.instance.status !== "running") {
88
90
  throw new WorkflowError("wrong_status", "The token does not wait for a task");
89
91
  }
90
- const { value, issues } = await this.validator().validate(node.config?.form as FormSpec | undefined, output, { purpose: "task", processKey: step.instance.processKey, partition: step.instance.partition });
92
+ const { value, issues } = await this.validator().validate(node.config?.form as FormSpec | undefined, output, { purpose: "task", processKey: step.instance.processKey, partition: step.instance.partition, language: options.language ?? this.languages.default, actor });
91
93
  if (issues.length > 0) throw new WorkflowError("invalid_input", "The task form is invalid", issues);
92
94
  await this.engine.complete(step, tokenId, value, actor);
93
95
  return true;
@@ -123,13 +125,13 @@ export class WorkflowRuntime extends IWorkflowRuntime {
123
125
  for (const [nodeId, node] of Object.entries(version.document.nodes)) {
124
126
  if (node.kind !== "eventStart" || node.config?.event !== eventKey) continue;
125
127
  // An event carries more than a process needs: fields the input does not declare are dropped, not refused.
126
- const { value, issues: all } = await this.validator().validate(version.document.input, data, { purpose: "input", processKey: version.key, partition });
128
+ const { value, issues: all } = await this.validator().validate(inputSpecOf(version.document), data, { purpose: "input", processKey: version.key, partition, language: this.languages.default, actor });
127
129
  const issues = all.filter((issue) => issue.code !== "unknown");
128
130
  if (issues.length > 0) {
129
131
  rejected.push({ processKey: version.key, issues });
130
132
  continue;
131
133
  }
132
- started.push(await this.engine.begin(this.engine.cache(version), nodeId, { input: value, partition, businessKey: correlation, startedVia: "event", actor, request: options.request ?? null }));
134
+ started.push(await this.engine.begin(this.engine.cache(version), nodeId, { input: value, partition, businessKey: correlation, startedVia: "event", actor, request: options.request ?? null, event: { key: eventKey, payload: data } }));
133
135
  }
134
136
  }
135
137
  return { started, continued: [...continued], rejected };
@@ -155,7 +157,7 @@ export class WorkflowRuntime extends IWorkflowRuntime {
155
157
  const row = await this.db.instances.asNoTracking().firstOrDefault((i) => i.id.eq(instanceId));
156
158
  if (!row) throw notFound(instanceId);
157
159
  const tokens = await this.db.tokens.asNoTracking().where((t) => t.instanceId.eq(instanceId)).orderBy((t) => t.id).toList();
158
- return { ...summaryOf(row), input: row.input, vars: row.vars, nodes: row.nodes, tokens: tokens.map(tokenOf) };
160
+ return { ...summaryOf(row), input: row.input, vars: row.vars, nodes: row.nodes, output: row.output, tokens: tokens.map(tokenOf) };
159
161
  }
160
162
 
161
163
  async history(instanceId: string): Promise<readonly HistoryEntry[]> {
@@ -4,7 +4,7 @@ import { Authorize, Controller, Get, Post, type HttpContext } from "../../../htt
4
4
  import type { WorkflowActor, WorkflowRequest } from "../../contracts/types";
5
5
  import { WorkflowError } from "../../contracts/WorkflowError";
6
6
  import { buildListDocument } from "../../../../library/jsonapi";
7
- import { body, callerOf, filterOf, listQuery, pagingOfList, param } from "../../internal/http";
7
+ import { body, callerOf, defaultLanguage, filterOf, listQuery, pagingOfList, param, withTitleText } from "../../internal/http";
8
8
  import type { TaskStatus } from "../model/WfTask.model";
9
9
  import { IWorkflowTasks, type TaskDetails } from "../services/IWorkflowTasks.service";
10
10
 
@@ -20,7 +20,7 @@ export class TasksController {
20
20
  @Get()
21
21
  @Authorize(can("workflow.tasks.work"))
22
22
  async list(ctx: HttpContext) {
23
- const { access, actor } = await callerOf(ctx);
23
+ const { access, actor, language } = await callerOf(ctx);
24
24
  const list = listQuery(ctx, { filter: { partition: ["eq"], processKey: ["eq"], status: ["eq", "in"] }, sort: ["createdAt", "dueAt", "status", "processKey"] });
25
25
  const partition = filterOf(list, "partition");
26
26
  const status = filterOf(list, "status");
@@ -35,44 +35,45 @@ export class TasksController {
35
35
  partition,
36
36
  processKey: filterOf(list, "processKey"),
37
37
  });
38
- return buildListDocument(page.items, list, page.total, { basePath: ctx.path });
38
+ return buildListDocument(page.items.map((item) => withTitleText(item, language, defaultLanguage(ctx))), list, page.total, { basePath: ctx.path });
39
39
  }
40
40
 
41
41
  @Get(":id")
42
42
  @Authorize(can("workflow.tasks.work"))
43
43
  async get(ctx: HttpContext) {
44
- return (await this.visible(ctx)).task;
44
+ const { task, language } = await this.visible(ctx);
45
+ return withTitleText(task, language, defaultLanguage(ctx));
45
46
  }
46
47
 
47
48
  @Post(":id/claim")
48
49
  @Authorize(can("workflow.tasks.work"))
49
50
  async claim(ctx: HttpContext) {
50
- const { task, actor } = await this.visible(ctx);
51
- return await this.tasks.claim(task.id, actor);
51
+ const { task, actor, language } = await this.visible(ctx);
52
+ return withTitleText(await this.tasks.claim(task.id, actor), language, defaultLanguage(ctx));
52
53
  }
53
54
 
54
55
  @Post(":id/release")
55
56
  @Authorize(can("workflow.tasks.work"))
56
57
  async release(ctx: HttpContext) {
57
- const { task, actor } = await this.visible(ctx);
58
- return await this.tasks.release(task.id, actor);
58
+ const { task, actor, language } = await this.visible(ctx);
59
+ return withTitleText(await this.tasks.release(task.id, actor), language, defaultLanguage(ctx));
59
60
  }
60
61
 
61
62
  @Post(":id/complete")
62
63
  @Authorize(can("workflow.tasks.work"))
63
64
  async complete(ctx: HttpContext) {
64
- const { task, actor, request: from } = await this.visible(ctx);
65
+ const { task, actor, request: from, language } = await this.visible(ctx);
65
66
  const request = await body(ctx);
66
- return await this.tasks.complete(task.id, request.output ?? {}, actor, { request: from });
67
+ return withTitleText(await this.tasks.complete(task.id, request.output ?? {}, actor, { request: from, language }), language, defaultLanguage(ctx));
67
68
  }
68
69
 
69
70
  /** The task if the caller may see it: addressed to one of their principals or claimed by them. */
70
- private async visible(ctx: HttpContext): Promise<{ task: TaskDetails; actor: WorkflowActor; request: WorkflowRequest }> {
71
+ private async visible(ctx: HttpContext): Promise<{ task: TaskDetails; actor: WorkflowActor; request: WorkflowRequest; language: string }> {
71
72
  const task = await this.tasks.get(param(ctx, "id"));
72
- const { access, actor, request } = await callerOf(ctx, "workflow.tasks.work", { scope: task.partition, process: task.processKey });
73
+ const { access, actor, request, language } = await callerOf(ctx, "workflow.tasks.work", { scope: task.partition, process: task.processKey });
73
74
  const addressees = await access.principals([task.partition]);
74
75
  if (!addressees.some(({ principals }) => principals.includes(task.assignee)) && task.claimedBy !== actor.id) throw new WorkflowError("not_found", `Task ${task.id} does not exist`);
75
- return { task, actor, request };
76
+ return { task, actor, request, language };
76
77
  }
77
78
  }
78
79
  contextOnlyController(TasksController);
@@ -8,7 +8,7 @@ export class TasksDbContext extends DbContext {
8
8
  /** The table of the Tasks part: bazis creates and checks it in the application database. */
9
9
  export const tasksStore = defineOrmOwnedStoreV1({
10
10
  contract: "bazis.orm-owned-store/v1",
11
- storeKey: "bazis.workflow.tasks",
11
+ storeKey: "bazis.workflow.tasks.v2",
12
12
  formatVersion: 1,
13
- ownedScope: { schema: "public", tablePrefix: "bazis_wf_task_" },
13
+ ownedScope: { schema: "public", tablePrefix: "bazis_wf2_task_" },
14
14
  });
@@ -1,5 +1,5 @@
1
1
  import { Column, Entity, Index, UUID } from "../../../orm";
2
- import type { FormSpec } from "../../../../library/workflow";
2
+ import type { FormSpec, LocalizedText } from "../../../../library/workflow";
3
3
 
4
4
  /** `open` waits for someone, `claimed` is taken by one person, `completed` and `cancelled` are done. */
5
5
  export type TaskStatus = "open" | "claimed" | "completed" | "cancelled";
@@ -10,7 +10,7 @@ export type TaskStatus = "open" | "claimed" | "completed" | "cancelled";
10
10
  */
11
11
  @Index(["assignee", "status"])
12
12
  @Index(["partition", "status"])
13
- @Entity({ table: "bazis_wf_task_items" })
13
+ @Entity({ table: "bazis_wf2_task_items" })
14
14
  export class WfTask {
15
15
  @UUID({ version: "v7" })
16
16
  id = "";
@@ -33,8 +33,12 @@ export class WfTask {
33
33
  @Column({ type: "text", nullable: true })
34
34
  partition: string | null = null;
35
35
 
36
- @Column({ type: "text" })
37
- title = "";
36
+ @Column({ type: "json" })
37
+ title: LocalizedText = "";
38
+
39
+ /** The task of a trial run (WORKFLOW-002 §5.4, stage 5). */
40
+ @Column({ type: "boolean" })
41
+ test = false;
38
42
 
39
43
  /** `role:<code>` or `user:<id>`. */
40
44
  @Column({ type: "text" })
@@ -1,4 +1,4 @@
1
- import type { FormSpec } from "../../../../library/workflow";
1
+ import type { FormSpec, LocalizedText } from "../../../../library/workflow";
2
2
  import type { AccessFilter, ScopePrincipals } from "../../../access";
3
3
  import type { WorkflowActor, WorkflowAuditOptions, WorkflowPage, WorkflowPaging } from "../../contracts/types";
4
4
  import type { TaskStatus } from "../model/WfTask.model";
@@ -10,7 +10,7 @@ export interface TaskDetails {
10
10
  readonly processKey: string;
11
11
  readonly nodeId: string;
12
12
  readonly partition: string | null;
13
- readonly title: string;
13
+ readonly title: LocalizedText;
14
14
  readonly assignee: string;
15
15
  readonly form: FormSpec | null;
16
16
  readonly dueAt: Date | null;
@@ -53,5 +53,5 @@ export abstract class IWorkflowTasks {
53
53
  /** Gives a claimed task back. */
54
54
  abstract release(taskId: string, actor: WorkflowActor): Promise<TaskDetails>;
55
55
  /** Fills the form: the process moves on. */
56
- abstract complete(taskId: string, output: unknown, actor: WorkflowActor, options?: WorkflowAuditOptions): Promise<TaskDetails>;
56
+ abstract complete(taskId: string, output: unknown, actor: WorkflowActor, options?: WorkflowAuditOptions & { readonly language?: string }): Promise<TaskDetails>;
57
57
  }
@@ -47,7 +47,7 @@ export class WorkflowTasks extends IWorkflowTasks {
47
47
 
48
48
  async partitions(query: { readonly status?: readonly TaskStatus[] } = {}): Promise<readonly (string | null)[]> {
49
49
  const statuses = statusesOf(query.status);
50
- const rows = await this.db.database.querySqlRaw(`SELECT DISTINCT "partition" FROM "bazis_wf_task_items" WHERE "status" IN (${statuses.map((_, index) => `{${index}}`).join(", ")})`, ...statuses);
50
+ const rows = await this.db.database.querySqlRaw(`SELECT DISTINCT "partition" FROM "bazis_wf2_task_items" WHERE "status" IN (${statuses.map((_, index) => `{${index}}`).join(", ")})`, ...statuses);
51
51
  return rows.map((row) => row.partition as string | null);
52
52
  }
53
53
 
@@ -81,14 +81,14 @@ export class WorkflowTasks extends IWorkflowTasks {
81
81
  });
82
82
  }
83
83
 
84
- async complete(taskId: string, output: unknown, actor: WorkflowActor, options: WorkflowAuditOptions = {}): Promise<TaskDetails> {
84
+ async complete(taskId: string, output: unknown, actor: WorkflowActor, options: WorkflowAuditOptions & { readonly language?: string } = {}): Promise<TaskDetails> {
85
85
  const task = await this.task(taskId, false);
86
86
  if (task.status === "claimed" && task.claimedById !== actor.id) throw new WorkflowError("conflict", "The task is taken by someone else");
87
87
  if (task.status !== "open" && task.status !== "claimed") throw new WorkflowError("wrong_status", `The task is ${task.status}`);
88
88
  let instance;
89
89
  try {
90
90
  // The form is checked and the process moves on first; the task follows.
91
- instance = await this.runtime.completeToken(task.tokenId, output, actor);
91
+ instance = await this.runtime.completeToken(task.tokenId, output, actor, options.language === undefined ? {} : { language: options.language });
92
92
  } catch (error) {
93
93
  if (error instanceof WorkflowError && error.code === "wrong_status") await this.close(task.tokenId, "cancelled");
94
94
  throw error;
@@ -695,6 +695,9 @@ function applyValidatorOptions(schema: OpenApiCodegenSchema, options: Record<str
695
695
  if (typeof options.refScope === "string") {
696
696
  out["x-ref-scope"] = options.refScope;
697
697
  }
698
+ if (options.personal === true) {
699
+ out["x-personal"] = true;
700
+ }
698
701
  return out;
699
702
  }
700
703
 
@@ -160,4 +160,6 @@ export interface ValidatorOptions {
160
160
  refKeys?: string;
161
161
  /** JSON Pointer to the sibling input that sets their area (`x-ref-scope`): `/table`. */
162
162
  refScope?: `/${string}`;
163
+ /** Personal data (`x-personal`): a process masks the value in its history. */
164
+ personal?: boolean;
163
165
  }
@@ -1,19 +1,23 @@
1
- import type { FormSpec } from "./formFields";
1
+ import type { Expr } from "./expressions";
2
+ import type { VariableSpec } from "./formFields";
3
+ import type { LocalizedText } from "./localized";
2
4
 
3
5
  /**
4
6
  * A process document: the framework's own JSON, not BPMN XML (a designer may
5
- * render BPMN from it). One object holds the process input (`input`), the graph
6
- * (`nodes`, `flows`) and the canvas layout (`diagram`).
7
+ * render BPMN from it). One object holds the variables, the settings, the graph
8
+ * (`nodes`, `flows`), the canvas layout (`diagram`) and the application's data
9
+ * (`extensions`).
7
10
  */
8
- export const PROCESS_SCHEMA_VERSION = "bazis.workflow/v1";
11
+ export const PROCESS_SCHEMA_VERSION = "bazis.workflow/v2";
9
12
 
10
- /** The node kinds of version 1; closed: a new kind comes with a new document version. */
13
+ /** The node kinds of version 2; closed: a new kind comes with a new document version. */
11
14
  export type NodeKind =
12
15
  | "manualStart"
13
16
  | "scheduleStart"
14
17
  | "eventStart"
15
18
  | "userTask"
16
19
  | "serviceTask"
20
+ | "assign"
17
21
  | "timerCatch"
18
22
  | "eventWait"
19
23
  | "exclusiveGateway"
@@ -25,35 +29,24 @@ export type NodeKind =
25
29
 
26
30
  export interface ProcessNode {
27
31
  readonly kind: NodeKind;
28
- readonly title: string;
32
+ readonly title: LocalizedText;
29
33
  /** Boundary nodes only: the step the node is attached to. */
30
34
  readonly attachedTo?: string;
31
35
  readonly config?: Readonly<Record<string, unknown>>;
36
+ /** The application's data by namespace; the engine does not read it. */
37
+ readonly extensions?: Readonly<Record<string, unknown>>;
32
38
  }
33
39
 
34
40
  export interface ProcessFlow {
35
41
  readonly source: string;
36
42
  readonly target: string;
37
- readonly title?: string;
43
+ readonly title?: LocalizedText;
38
44
  /** Condition of a flow that leaves an exclusive gateway. */
39
- readonly condition?: Condition;
45
+ readonly condition?: Expr;
40
46
  /** The flow an exclusive gateway takes when no condition holds. */
41
47
  readonly isDefault?: boolean;
42
48
  }
43
49
 
44
- /** Comparison operators of conditions. */
45
- export type ConditionOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "contains" | "empty" | "notEmpty";
46
-
47
- /**
48
- * A condition: a tree of `all` / `any` / `not` and comparisons. `path` addresses
49
- * instance data: `$input.<field>`, `$vars.<name>`, `$nodes.<node>.<field>`.
50
- */
51
- export type Condition =
52
- | { readonly all: readonly Condition[] }
53
- | { readonly any: readonly Condition[] }
54
- | { readonly not: Condition }
55
- | { readonly path: string; readonly op: ConditionOp; readonly value?: unknown };
56
-
57
50
  export interface Bounds {
58
51
  readonly x: number;
59
52
  readonly y: number;
@@ -72,13 +65,33 @@ export interface ProcessDiagram {
72
65
  readonly edges: Readonly<Record<string, { readonly waypoints: readonly Point[] }>>;
73
66
  }
74
67
 
68
+ /** What goes to the history of an instance. */
69
+ export interface ProcessLogSettings {
70
+ /** The values of variables an `assign` step sets; `false` by default. */
71
+ readonly variables?: boolean;
72
+ /** The input and output of Tools; `false` by default. */
73
+ readonly toolPayloads?: boolean;
74
+ /** Personal data shows as `***`; `true` by default. */
75
+ readonly maskPersonal?: boolean;
76
+ }
77
+
78
+ /** Settings of a process: they are part of the version. */
79
+ export interface ProcessSettings {
80
+ /** The default `timeout` of steps with a Tool: an ISO duration. */
81
+ readonly stepTimeout?: string;
82
+ readonly log?: ProcessLogSettings;
83
+ }
84
+
75
85
  export interface ProcessDocument {
76
86
  readonly schemaVersion: typeof PROCESS_SCHEMA_VERSION;
77
- /** Fields the process starts with; without them it starts with no data. */
78
- readonly input?: FormSpec;
87
+ /** `in` — the input of a start, `local` — inside the process, `out` — what it gives back. */
88
+ readonly variables?: readonly VariableSpec[];
89
+ readonly settings?: ProcessSettings;
79
90
  readonly nodes: Readonly<Record<string, ProcessNode>>;
80
91
  readonly flows: Readonly<Record<string, ProcessFlow>>;
81
92
  readonly diagram?: ProcessDiagram;
93
+ /** The application's data by namespace; the engine does not read it. */
94
+ readonly extensions?: Readonly<Record<string, unknown>>;
82
95
  }
83
96
 
84
97
  /** A new process: a manual start leading straight to the end. It already passes validation. */
@@ -92,3 +105,26 @@ export function emptyProcess(): ProcessDocument {
92
105
  flows: { StartToEnd: { source: "Start", target: "End" } },
93
106
  };
94
107
  }
108
+
109
+ /** The fields a start takes: the `in` variables. */
110
+ export function inputSpecOf(document: Pick<ProcessDocument, "variables">): { readonly fields: readonly VariableSpec[] } {
111
+ return { fields: (document.variables ?? []).filter((variable) => variable.direction === "in") };
112
+ }
113
+
114
+ /** What `$vars` holds at the start: the `local` and `out` variables with a default. */
115
+ export function initialVarsOf(document: Pick<ProcessDocument, "variables">): Record<string, unknown> {
116
+ const vars: Record<string, unknown> = {};
117
+ for (const variable of document.variables ?? []) {
118
+ if (variable.direction !== "in" && variable.default !== undefined) vars[variable.name] = variable.default;
119
+ }
120
+ return vars;
121
+ }
122
+
123
+ /** What a finished process gives back: its `out` variables. */
124
+ export function outputOf(document: Pick<ProcessDocument, "variables">, vars: Readonly<Record<string, unknown>>): Record<string, unknown> {
125
+ const output: Record<string, unknown> = {};
126
+ for (const variable of document.variables ?? []) {
127
+ if (variable.direction === "out") output[variable.name] = Object.hasOwn(vars, variable.name) ? vars[variable.name] : null;
128
+ }
129
+ return output;
130
+ }