bazis 0.126.0 → 0.127.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.
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
232
232
  if (method === "account/updated") this.modelCache = undefined;
233
233
  }, () => { this.clearLogin(); this.modelCache = undefined; });
234
234
  const initialized = await client.request("initialize", {
235
- clientInfo: { name: "bazis", title: "Bazis", version: "0.126.0" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.127.0" }, capabilities: { experimentalApi: true },
236
236
  }, 15_000, signal);
237
237
  if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
238
238
  client.notify("initialized");
@@ -3,13 +3,15 @@ import { WorkflowDefinitionsModule } from "./definitions_module/WorkflowDefiniti
3
3
  import { IWorkflowDefinitions } from "./definitions_module/services/IWorkflowDefinitions.service";
4
4
  import { WorkflowRuntimeModule } from "./runtime_module/WorkflowRuntime.module";
5
5
  import { IWorkflowRuntime } from "./runtime_module/services/IWorkflowRuntime.service";
6
+ import { WorkflowTasksModule } from "./tasks_module/WorkflowTasks.module";
7
+ import { IWorkflowTasks } from "./tasks_module/services/IWorkflowTasks.service";
6
8
 
7
9
  /**
8
10
  * Business processes (WORKFLOW-001): the pack only assembles its parts
9
11
  * ([passport](MODULE.md)). An application imports it, or only the parts it needs.
10
12
  */
11
13
  @Module({
12
- imports: [WorkflowDefinitionsModule, WorkflowRuntimeModule],
13
- exports: [IWorkflowDefinitions, IWorkflowRuntime],
14
+ imports: [WorkflowDefinitionsModule, WorkflowRuntimeModule, WorkflowTasksModule],
15
+ exports: [IWorkflowDefinitions, IWorkflowRuntime, IWorkflowTasks],
14
16
  })
15
17
  export class WorkflowModule {}
@@ -6,6 +6,9 @@
6
6
  export { WorkflowModule } from "./Workflow.module";
7
7
  export { WorkflowDefinitionsModule } from "./definitions_module/WorkflowDefinitions.module";
8
8
  export { WorkflowRuntimeModule } from "./runtime_module/WorkflowRuntime.module";
9
+ export { WorkflowTasksModule } from "./tasks_module/WorkflowTasks.module";
10
+ export { IWorkflowTasks, type TaskDetails, type TaskListQuery } from "./tasks_module/services/IWorkflowTasks.service";
11
+ export type { TaskStatus } from "./tasks_module/model/WfTask.model";
9
12
  export {
10
13
  IWorkflowDefinitions,
11
14
  type CreateProcess,
@@ -18,6 +21,8 @@ export {
18
21
  } from "./definitions_module/services/IWorkflowDefinitions.service";
19
22
  export {
20
23
  IWorkflowRuntime,
24
+ type EmitOptions,
25
+ type EmitResult,
21
26
  type HistoryEntry,
22
27
  type InstanceDetails,
23
28
  type InstanceListQuery,
@@ -27,9 +32,11 @@ export {
27
32
  } from "./runtime_module/services/IWorkflowRuntime.service";
28
33
  export {
29
34
  WORKFLOW_INSTANCE_FINISHED,
35
+ WORKFLOW_INSTANCES_DELETED,
30
36
  WORKFLOW_TASK_REQUESTED,
31
37
  WORKFLOW_TOKEN_CLOSED,
32
38
  type WorkflowInstanceFinished,
39
+ type WorkflowInstancesDeleted,
33
40
  type WorkflowTaskRequested,
34
41
  type WorkflowTokenClosed,
35
42
  } from "./runtime_module/events/Runtime.events";
@@ -16,6 +16,12 @@ export async function authorize(ctx: HttpContext, action: WorkflowAction, target
16
16
  return actor;
17
17
  }
18
18
 
19
+ /** Whom the caller's tasks are addressed to, from the application's {@link WorkflowAccess}. */
20
+ export async function assigneesOf(ctx: HttpContext, actor: WorkflowActor): Promise<readonly string[]> {
21
+ const access = ctx.services.tryResolve(WorkflowAccess);
22
+ return access === undefined ? [] : await access.assignees(ctx, actor);
23
+ }
24
+
19
25
  /** A route parameter as a string. */
20
26
  export function param(ctx: HttpContext, name: string): string {
21
27
  return String(ctx.params[name] ?? "");
@@ -1,7 +1,8 @@
1
1
  import { BackgroundService, delay } from "../../../background";
2
2
  import type { ServiceProvider } from "../../../di";
3
- import type { ConfigView } from "../../../kernel";
3
+ import { EventBus, type ConfigView } from "../../../kernel";
4
4
  import { IWorkflowDefinitions } from "../../definitions_module/services/IWorkflowDefinitions.service";
5
+ import { WORKFLOW_INSTANCES_DELETED } from "../events/Runtime.events";
5
6
  import { RuntimeDbContext } from "../model/Runtime.dbContext";
6
7
  import type { WorkflowSettings } from "./WorkflowWorker";
7
8
 
@@ -11,6 +12,8 @@ const DAY_MS = 86_400_000;
11
12
  * Deletes `completed` and `cancelled` instances older than their process's
12
13
  * `retentionDays`, with tokens, jobs, subscriptions and history (by cascade),
13
14
  * `retention.batchSize` at a time. `failed` instances wait for a person and stay.
15
+ * Each deleted batch is published as `WORKFLOW_INSTANCES_DELETED`, so parts
16
+ * with their own data (tasks) drop theirs.
14
17
  */
15
18
  export class WorkflowRetention extends BackgroundService {
16
19
  constructor(
@@ -55,6 +58,7 @@ export class WorkflowRetention extends BackgroundService {
55
58
  if (ids.length === 0) break;
56
59
  await db.instances.asNoTracking().where((i) => i.id.in(ids)).executeDelete();
57
60
  deleted += ids.length;
61
+ await scope.resolve(EventBus).publishScoped(scope, WORKFLOW_INSTANCES_DELETED, { instanceIds: ids });
58
62
  if (ids.length < batchSize) break;
59
63
  }
60
64
  }
@@ -32,6 +32,12 @@ export interface WorkflowInstanceFinished {
32
32
  readonly status: Extract<InstanceStatus, "completed" | "cancelled">;
33
33
  }
34
34
 
35
+ /** Retention deleted these finished instances with their tokens and history. */
36
+ export interface WorkflowInstancesDeleted {
37
+ readonly instanceIds: readonly string[];
38
+ }
39
+
35
40
  export const WORKFLOW_TASK_REQUESTED = createEventToken<WorkflowTaskRequested>("workflow.task-requested");
36
41
  export const WORKFLOW_TOKEN_CLOSED = createEventToken<WorkflowTokenClosed>("workflow.token-closed");
37
42
  export const WORKFLOW_INSTANCE_FINISHED = createEventToken<WorkflowInstanceFinished>("workflow.instance-finished");
43
+ export const WORKFLOW_INSTANCES_DELETED = createEventToken<WorkflowInstancesDeleted>("workflow.instances-deleted");
@@ -0,0 +1,29 @@
1
+ import { DI, Module, scoped } from "../../di";
2
+ import { withEventSubscribers } from "../../kernel";
3
+ import { IWorkflowRuntime } from "../runtime_module/services/IWorkflowRuntime.service";
4
+ import { WorkflowRuntimeModule } from "../runtime_module/WorkflowRuntime.module";
5
+ import { TaskEvents } from "./events/TaskEvents";
6
+ import { TasksController } from "./http/Tasks.controller";
7
+ import { TasksDbContext, tasksStore } from "./model/Tasks.dbContext";
8
+ import { WfTask } from "./model/WfTask.model";
9
+ import { IWorkflowTasks } from "./services/IWorkflowTasks.service";
10
+ import { WorkflowTasks } from "./services/WorkflowTasks.service";
11
+
12
+ /**
13
+ * Tasks for people: the inbox of `userTask` steps ([passport](MODULE.md)).
14
+ * Dependencies are explicit: the framework's classes are not covered by the
15
+ * application's codegen.
16
+ */
17
+ @Module({
18
+ imports: [WorkflowRuntimeModule],
19
+ ormBazis: { context: TasksDbContext, entities: [WfTask], ownedStore: tasksStore, registerRepositories: false },
20
+ providers: [
21
+ scoped(WorkflowTasks, WorkflowTasks, [TasksDbContext, IWorkflowRuntime] as const),
22
+ DI.scoped(DI.factoryProviderWithResolver(IWorkflowTasks, [], (resolver) => resolver.resolve(WorkflowTasks))),
23
+ scoped(TaskEvents, TaskEvents, [WorkflowTasks] as const),
24
+ ...withEventSubscribers(TaskEvents),
25
+ ],
26
+ controllers: [DI.bindDeps(TasksController, IWorkflowTasks)],
27
+ exports: [IWorkflowTasks],
28
+ })
29
+ export class WorkflowTasksModule {}
@@ -0,0 +1,23 @@
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";
3
+ import type { WorkflowTasks } from "../services/WorkflowTasks.service";
4
+
5
+ /** Keeps the inbox in line with the steps: a waiting user task gets a task, a closed one cancels it. */
6
+ export class TaskEvents {
7
+ constructor(private readonly tasks: WorkflowTasks) {}
8
+
9
+ @OnEvent(WORKFLOW_TASK_REQUESTED)
10
+ requested(event: WorkflowTaskRequested): Promise<void> {
11
+ return this.tasks.requested(event);
12
+ }
13
+
14
+ @OnEvent(WORKFLOW_TOKEN_CLOSED)
15
+ closed(event: WorkflowTokenClosed): Promise<void> {
16
+ return this.tasks.closed(event);
17
+ }
18
+
19
+ @OnEvent(WORKFLOW_INSTANCES_DELETED)
20
+ deleted(event: WorkflowInstancesDeleted): Promise<void> {
21
+ return this.tasks.deleted(event);
22
+ }
23
+ }
@@ -0,0 +1,67 @@
1
+ import { contextOnlyController } from "../../../http/Routing/contextOnlyControllers";
2
+ import { Controller, Get, Post, type HttpContext } from "../../../http";
3
+ import type { WorkflowActor } from "../../contracts/types";
4
+ import { WorkflowError } from "../../contracts/WorkflowError";
5
+ import { assigneesOf, authorize, body, optionalString, pagingQuery, param } from "../../internal/http";
6
+ import type { TaskStatus } from "../model/WfTask.model";
7
+ import { IWorkflowTasks, type TaskDetails } from "../services/IWorkflowTasks.service";
8
+
9
+ /**
10
+ * The caller's tasks (WORKFLOW-001 §5): those addressed to any of
11
+ * `WorkflowAccess.assignees` or claimed by the caller. Another person's task
12
+ * answers 404, as if it did not exist.
13
+ */
14
+ @Controller("workflow/tasks")
15
+ export class TasksController {
16
+ constructor(private readonly tasks: IWorkflowTasks) {}
17
+
18
+ @Get()
19
+ async list(ctx: HttpContext) {
20
+ const partition = optionalString(ctx.query("partition"), "partition");
21
+ const actor = await authorize(ctx, "tasks.work", { partition: partition ?? null });
22
+ const status = ctx.query("status");
23
+ const page = await this.tasks.list({
24
+ ...pagingQuery(ctx),
25
+ assignees: await assigneesOf(ctx, actor),
26
+ actor,
27
+ ...(status === undefined ? {} : { status: status.split(",") as TaskStatus[] }),
28
+ partition,
29
+ processKey: optionalString(ctx.query("processKey"), "processKey"),
30
+ });
31
+ return { data: page.items, meta: { total: page.total } };
32
+ }
33
+
34
+ @Get(":id")
35
+ async get(ctx: HttpContext) {
36
+ return (await this.visible(ctx)).task;
37
+ }
38
+
39
+ @Post(":id/claim")
40
+ async claim(ctx: HttpContext) {
41
+ const { task, actor } = await this.visible(ctx);
42
+ return await this.tasks.claim(task.id, actor);
43
+ }
44
+
45
+ @Post(":id/release")
46
+ async release(ctx: HttpContext) {
47
+ const { task, actor } = await this.visible(ctx);
48
+ return await this.tasks.release(task.id, actor);
49
+ }
50
+
51
+ @Post(":id/complete")
52
+ async complete(ctx: HttpContext) {
53
+ const { task, actor } = await this.visible(ctx);
54
+ const request = await body(ctx);
55
+ return await this.tasks.complete(task.id, request.output ?? {}, actor);
56
+ }
57
+
58
+ /** The task if the caller may see it: addressed to one of their assignees or claimed by them. */
59
+ private async visible(ctx: HttpContext): Promise<{ task: TaskDetails; actor: WorkflowActor }> {
60
+ const task = await this.tasks.get(param(ctx, "id"));
61
+ const actor = await authorize(ctx, "tasks.work", { processKey: task.processKey, partition: task.partition, instanceId: task.instanceId, taskId: task.id });
62
+ const assignees = await assigneesOf(ctx, actor);
63
+ if (!assignees.includes(task.assignee) && task.claimedBy !== actor.id) throw new WorkflowError("not_found", `Task ${task.id} does not exist`);
64
+ return { task, actor };
65
+ }
66
+ }
67
+ contextOnlyController(TasksController);
@@ -0,0 +1,14 @@
1
+ import { DbContext, defineOrmOwnedStoreV1 } from "../../../orm";
2
+ import { WfTask } from "./WfTask.model";
3
+
4
+ export class TasksDbContext extends DbContext {
5
+ readonly tasks = this.set(WfTask);
6
+ }
7
+
8
+ /** The table of the Tasks part: bazis creates and checks it in the application database. */
9
+ export const tasksStore = defineOrmOwnedStoreV1({
10
+ contract: "bazis.orm-owned-store/v1",
11
+ storeKey: "bazis.workflow.tasks",
12
+ formatVersion: 1,
13
+ ownedScope: { schema: "public", tablePrefix: "bazis_wf_task_" },
14
+ });
@@ -0,0 +1,69 @@
1
+ import { Column, Entity, Index, UUID } from "../../../orm";
2
+ import type { FormSpec } from "../../../../library/workflow";
3
+
4
+ /** `open` waits for someone, `claimed` is taken by one person, `completed` and `cancelled` are done. */
5
+ export type TaskStatus = "open" | "claimed" | "completed" | "cancelled";
6
+
7
+ /**
8
+ * A task for a person: a `userTask` step of an instance waiting for its form.
9
+ * Created from `WORKFLOW_TASK_REQUESTED`; references are text (other stores).
10
+ */
11
+ @Index(["assignee", "status"])
12
+ @Index(["partition", "status"])
13
+ @Entity({ table: "bazis_wf_task_items" })
14
+ export class WfTask {
15
+ @UUID({ version: "v7" })
16
+ id = "";
17
+
18
+ /** The waiting token: one task per token. */
19
+ @Index({ unique: true })
20
+ @Column({ type: "text" })
21
+ tokenId = "";
22
+
23
+ @Index()
24
+ @Column({ type: "text" })
25
+ instanceId = "";
26
+
27
+ @Column({ type: "text" })
28
+ processKey = "";
29
+
30
+ @Column({ type: "text" })
31
+ nodeId = "";
32
+
33
+ @Column({ type: "text", nullable: true })
34
+ partition: string | null = null;
35
+
36
+ @Column({ type: "text" })
37
+ title = "";
38
+
39
+ /** `role:<code>` or `user:<id>`. */
40
+ @Column({ type: "text" })
41
+ assignee = "";
42
+
43
+ @Column({ type: "json", nullable: true })
44
+ form: FormSpec | null = null;
45
+
46
+ @Column({ type: "datetime", nullable: true })
47
+ dueAt: Date | null = null;
48
+
49
+ @Column({ type: "text" })
50
+ status: TaskStatus = "open";
51
+
52
+ @Column({ type: "text", nullable: true })
53
+ claimedById: string | null = null;
54
+
55
+ @Column({ type: "datetime", nullable: true })
56
+ claimedAt: Date | null = null;
57
+
58
+ @Column({ type: "text", nullable: true })
59
+ completedById: string | null = null;
60
+
61
+ @Column({ type: "datetime", nullable: true })
62
+ completedAt: Date | null = null;
63
+
64
+ @Column({ type: "json", nullable: true })
65
+ output: Record<string, unknown> | null = null;
66
+
67
+ @Column({ type: "createdAt" })
68
+ createdAt = new Date(0);
69
+ }
@@ -0,0 +1,52 @@
1
+ import type { FormSpec } from "../../../../library/workflow";
2
+ import type { WorkflowActor, WorkflowPage, WorkflowPaging } from "../../contracts/types";
3
+ import type { TaskStatus } from "../model/WfTask.model";
4
+
5
+ export interface TaskDetails {
6
+ readonly id: string;
7
+ readonly instanceId: string;
8
+ readonly tokenId: string;
9
+ readonly processKey: string;
10
+ readonly nodeId: string;
11
+ readonly partition: string | null;
12
+ readonly title: string;
13
+ readonly assignee: string;
14
+ readonly form: FormSpec | null;
15
+ readonly dueAt: Date | null;
16
+ readonly status: TaskStatus;
17
+ readonly claimedBy: string | null;
18
+ readonly claimedAt: Date | null;
19
+ readonly completedBy: string | null;
20
+ readonly completedAt: Date | null;
21
+ readonly output: Record<string, unknown> | null;
22
+ readonly createdAt: Date;
23
+ }
24
+
25
+ export interface TaskListQuery extends WorkflowPaging {
26
+ /** Tasks addressed to any of these (`role:support`, `user:42`), plus those the actor claimed. */
27
+ readonly assignees: readonly string[];
28
+ /** Whose claimed tasks count as theirs. */
29
+ readonly actor?: WorkflowActor;
30
+ /** `open` and `claimed` by default. */
31
+ readonly status?: readonly TaskStatus[];
32
+ readonly partition?: string;
33
+ readonly processKey?: string;
34
+ }
35
+
36
+ /**
37
+ * Tasks for people (WORKFLOW-001 §5): the inbox of `userTask` steps. A task
38
+ * appears when its step starts waiting and is cancelled when the step closes
39
+ * otherwise. Errors are {@link WorkflowError}: `not_found`, `conflict` (claimed
40
+ * by another person), `wrong_status`, `invalid_input` with the form issues.
41
+ */
42
+ export abstract class IWorkflowTasks {
43
+ /** The tasks of these assignees, oldest first. */
44
+ abstract list(query: TaskListQuery): Promise<WorkflowPage<TaskDetails>>;
45
+ abstract get(taskId: string): Promise<TaskDetails>;
46
+ /** Takes an open task: nobody else can complete it until it is released. */
47
+ abstract claim(taskId: string, actor: WorkflowActor): Promise<TaskDetails>;
48
+ /** Gives a claimed task back. */
49
+ abstract release(taskId: string, actor: WorkflowActor): Promise<TaskDetails>;
50
+ /** Fills the form: the process moves on. */
51
+ abstract complete(taskId: string, output: unknown, actor: WorkflowActor): Promise<TaskDetails>;
52
+ }
@@ -0,0 +1,167 @@
1
+ import { UniqueViolationError } from "../../../orm";
2
+ import { isUuid, pagingOf, type WorkflowActor, type WorkflowPage } from "../../contracts/types";
3
+ import { WorkflowError } from "../../contracts/WorkflowError";
4
+ import type { WorkflowInstancesDeleted, WorkflowTaskRequested, WorkflowTokenClosed } from "../../runtime_module/events/Runtime.events";
5
+ import type { IWorkflowRuntime } from "../../runtime_module/services/IWorkflowRuntime.service";
6
+ import type { TasksDbContext } from "../model/Tasks.dbContext";
7
+ import { WfTask, type TaskStatus } from "../model/WfTask.model";
8
+ import { IWorkflowTasks, type TaskDetails, type TaskListQuery } from "./IWorkflowTasks.service";
9
+
10
+ const STATUSES = new Set<TaskStatus>(["open", "claimed", "completed", "cancelled"]);
11
+
12
+ export class WorkflowTasks extends IWorkflowTasks {
13
+ constructor(
14
+ private readonly db: TasksDbContext,
15
+ private readonly runtime: IWorkflowRuntime,
16
+ ) {
17
+ super();
18
+ }
19
+
20
+ async list(query: TaskListQuery): Promise<WorkflowPage<TaskDetails>> {
21
+ const { limit, offset } = pagingOf(query);
22
+ const statuses = [...(query.status ?? ["open", "claimed"])];
23
+ if (statuses.length === 0 || statuses.some((status) => !STATUSES.has(status))) throw new WorkflowError("invalid_input", "status must be open, claimed, completed or cancelled");
24
+ const assignees = [...query.assignees];
25
+ const mine = query.actor?.id;
26
+ if (assignees.length === 0 && mine === undefined) return { items: [], total: 0 };
27
+ let source = this.db.tasks.asNoTracking().where((t) => t.status.in(statuses));
28
+ source = mine === undefined
29
+ ? source.where((t) => t.assignee.in(assignees))
30
+ : assignees.length === 0
31
+ ? source.where((t) => t.claimedById.eq(mine))
32
+ : source.where((t) => t.assignee.in(assignees).or(t.claimedById.eq(mine)));
33
+ if (query.partition !== undefined) source = source.where((t) => t.partition.eq(query.partition!));
34
+ if (query.processKey !== undefined) source = source.where((t) => t.processKey.eq(query.processKey!));
35
+ const total = await source.count();
36
+ const rows = await source.orderBy((t) => t.id).skip(offset).take(limit).toList();
37
+ return { items: rows.map(detailsOf), total };
38
+ }
39
+
40
+ async get(taskId: string): Promise<TaskDetails> {
41
+ return detailsOf(await this.task(taskId, false));
42
+ }
43
+
44
+ async claim(taskId: string, actor: WorkflowActor): Promise<TaskDetails> {
45
+ return await this.db.transactionScope(async () => {
46
+ const task = await this.locked(taskId);
47
+ if (task.status === "claimed" && task.claimedById === actor.id) return detailsOf(task);
48
+ if (task.status === "claimed") throw new WorkflowError("conflict", "The task is taken by someone else");
49
+ if (task.status !== "open") throw new WorkflowError("wrong_status", `The task is ${task.status}`);
50
+ task.status = "claimed";
51
+ task.claimedById = actor.id;
52
+ task.claimedAt = new Date();
53
+ await this.db.saveChanges();
54
+ return detailsOf(task);
55
+ });
56
+ }
57
+
58
+ async release(taskId: string, actor: WorkflowActor): Promise<TaskDetails> {
59
+ return await this.db.transactionScope(async () => {
60
+ const task = await this.locked(taskId);
61
+ if (task.status !== "claimed" || task.claimedById !== actor.id) throw new WorkflowError("wrong_status", "Only the person who took the task gives it back");
62
+ task.status = "open";
63
+ task.claimedById = null;
64
+ task.claimedAt = null;
65
+ await this.db.saveChanges();
66
+ return detailsOf(task);
67
+ });
68
+ }
69
+
70
+ async complete(taskId: string, output: unknown, actor: WorkflowActor): Promise<TaskDetails> {
71
+ const task = await this.task(taskId, false);
72
+ if (task.status === "claimed" && task.claimedById !== actor.id) throw new WorkflowError("conflict", "The task is taken by someone else");
73
+ if (task.status !== "open" && task.status !== "claimed") throw new WorkflowError("wrong_status", `The task is ${task.status}`);
74
+ let instance;
75
+ try {
76
+ // The form is checked and the process moves on first; the task follows.
77
+ instance = await this.runtime.completeToken(task.tokenId, output, actor);
78
+ } catch (error) {
79
+ if (error instanceof WorkflowError && error.code === "wrong_status") await this.close(task.tokenId, "cancelled");
80
+ throw error;
81
+ }
82
+ const saved = instance.nodes[task.nodeId];
83
+ await this.db.tasks.asNoTracking().where((t) => t.id.eq(task.id)).executeUpdate({
84
+ status: "completed",
85
+ completedById: actor.id,
86
+ completedAt: new Date(),
87
+ output: typeof saved === "object" && saved !== null && !Array.isArray(saved) ? saved as Record<string, unknown> : null,
88
+ });
89
+ return await this.get(task.id);
90
+ }
91
+
92
+ /** A step started waiting for a person: its task. Repeated events of one token make one task. */
93
+ async requested(event: WorkflowTaskRequested): Promise<void> {
94
+ const assignee = "role" in event.assignee ? `role:${event.assignee.role}` : `user:${event.assignee.user}`;
95
+ this.db.tasks.add(Object.assign(new WfTask(), {
96
+ tokenId: event.tokenId,
97
+ instanceId: event.instanceId,
98
+ processKey: event.processKey,
99
+ nodeId: event.nodeId,
100
+ partition: event.partition,
101
+ title: event.title,
102
+ assignee,
103
+ form: event.form,
104
+ dueAt: event.dueAt,
105
+ }));
106
+ try {
107
+ await this.db.saveChanges();
108
+ } catch (error) {
109
+ if (!(error instanceof UniqueViolationError)) throw error;
110
+ }
111
+ }
112
+
113
+ /** The step closed otherwise than by its task: the task is cancelled. */
114
+ async closed(event: WorkflowTokenClosed): Promise<void> {
115
+ await this.close(event.tokenId, "cancelled");
116
+ }
117
+
118
+ /** Retention deleted the instances: their tasks go too. */
119
+ async deleted(event: WorkflowInstancesDeleted): Promise<void> {
120
+ if (event.instanceIds.length > 0) await this.db.tasks.asNoTracking().where((t) => t.instanceId.in([...event.instanceIds])).executeDelete();
121
+ }
122
+
123
+ private async close(tokenId: string, status: "cancelled"): Promise<void> {
124
+ await this.db.tasks.asNoTracking().where((t) => t.tokenId.eq(tokenId).and(t.status.in(["open", "claimed"]))).executeUpdate({ status });
125
+ }
126
+
127
+ private async task(taskId: string, tracked: boolean): Promise<WfTask> {
128
+ if (!isUuid(taskId)) throw notFound(taskId);
129
+ const source = tracked ? this.db.tasks : this.db.tasks.asNoTracking();
130
+ const task = await source.firstOrDefault((t) => t.id.eq(taskId));
131
+ if (!task) throw notFound(taskId);
132
+ return task;
133
+ }
134
+
135
+ private async locked(taskId: string): Promise<WfTask> {
136
+ if (!isUuid(taskId)) throw notFound(taskId);
137
+ const task = await this.db.tasks.where((t) => t.id.eq(taskId)).forUpdate().firstOrDefault();
138
+ if (!task) throw notFound(taskId);
139
+ return task;
140
+ }
141
+ }
142
+
143
+ function notFound(taskId: string): WorkflowError {
144
+ return new WorkflowError("not_found", `Task ${taskId} does not exist`);
145
+ }
146
+
147
+ function detailsOf(row: WfTask): TaskDetails {
148
+ return {
149
+ id: row.id,
150
+ instanceId: row.instanceId,
151
+ tokenId: row.tokenId,
152
+ processKey: row.processKey,
153
+ nodeId: row.nodeId,
154
+ partition: row.partition,
155
+ title: row.title,
156
+ assignee: row.assignee,
157
+ form: row.form,
158
+ dueAt: row.dueAt,
159
+ status: row.status,
160
+ claimedBy: row.claimedById,
161
+ claimedAt: row.claimedAt,
162
+ completedBy: row.completedById,
163
+ completedAt: row.completedAt,
164
+ output: row.output,
165
+ createdAt: row.createdAt,
166
+ };
167
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.126.0",
3
+ "version": "0.127.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",