bazis 0.123.0 → 0.124.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/agent/index.ts +1 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/workflow/Workflow.module.ts +15 -0
- package/core/workflow/access/WorkflowAccess.ts +52 -0
- package/core/workflow/contracts/WorkflowError.ts +28 -0
- package/core/workflow/contracts/WorkflowInputValidator.ts +24 -0
- package/core/workflow/contracts/types.ts +30 -0
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +24 -0
- package/core/workflow/definitions_module/http/Processes.controller.ts +92 -0
- package/core/workflow/definitions_module/model/Definitions.dbContext.ts +16 -0
- package/core/workflow/definitions_module/model/WfProcess.model.ts +52 -0
- package/core/workflow/definitions_module/model/WfVersion.model.ts +33 -0
- package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +79 -0
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +234 -0
- package/core/workflow/index.ts +43 -0
- package/core/workflow/internal/WorkflowTools.ts +36 -0
- package/core/workflow/internal/http.ts +63 -0
- package/core/workflow/runtime_module/WorkflowRuntime.module.ts +53 -0
- package/core/workflow/runtime_module/background/WorkflowRetention.ts +66 -0
- package/core/workflow/runtime_module/background/WorkflowWorker.ts +92 -0
- package/core/workflow/runtime_module/config/Runtime.config.ts +27 -0
- package/core/workflow/runtime_module/events/Runtime.events.ts +36 -0
- package/core/workflow/runtime_module/http/Instances.controller.ts +72 -0
- package/core/workflow/runtime_module/internal/GraphCache.ts +37 -0
- package/core/workflow/runtime_module/internal/ToolStep.ts +60 -0
- package/core/workflow/runtime_module/internal/WorkflowEngine.ts +356 -0
- package/core/workflow/runtime_module/model/Runtime.dbContext.ts +25 -0
- package/core/workflow/runtime_module/model/WfHistory.model.ts +38 -0
- package/core/workflow/runtime_module/model/WfInstance.model.ts +70 -0
- package/core/workflow/runtime_module/model/WfJob.model.ts +49 -0
- package/core/workflow/runtime_module/model/WfSubscription.model.ts +30 -0
- package/core/workflow/runtime_module/model/WfToken.model.ts +39 -0
- package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +82 -0
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +167 -0
- package/library/workflow/ProcessDocument.ts +94 -0
- package/library/workflow/conditions.ts +44 -0
- package/library/workflow/duration.ts +10 -0
- package/library/workflow/formFields.ts +112 -0
- package/library/workflow/index.ts +24 -0
- package/library/workflow/nodeKinds.ts +111 -0
- package/library/workflow/paths.ts +39 -0
- package/library/workflow/validateProcess.ts +310 -0
- package/package.json +4 -2
package/core/agent/index.ts
CHANGED
|
@@ -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.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.124.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");
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Module } from "../di";
|
|
2
|
+
import { WorkflowDefinitionsModule } from "./definitions_module/WorkflowDefinitions.module";
|
|
3
|
+
import { IWorkflowDefinitions } from "./definitions_module/services/IWorkflowDefinitions.service";
|
|
4
|
+
import { WorkflowRuntimeModule } from "./runtime_module/WorkflowRuntime.module";
|
|
5
|
+
import { IWorkflowRuntime } from "./runtime_module/services/IWorkflowRuntime.service";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Business processes (WORKFLOW-001): the pack only assembles its parts
|
|
9
|
+
* ([passport](MODULE.md)). An application imports it, or only the parts it needs.
|
|
10
|
+
*/
|
|
11
|
+
@Module({
|
|
12
|
+
imports: [WorkflowDefinitionsModule, WorkflowRuntimeModule],
|
|
13
|
+
exports: [IWorkflowDefinitions, IWorkflowRuntime],
|
|
14
|
+
})
|
|
15
|
+
export class WorkflowModule {}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { HttpContext } from "../../http";
|
|
2
|
+
import type { WorkflowActor } from "../contracts/types";
|
|
3
|
+
|
|
4
|
+
export type WorkflowAction =
|
|
5
|
+
| "processes.read"
|
|
6
|
+
| "processes.manage"
|
|
7
|
+
| "processes.publish"
|
|
8
|
+
| "instances.start"
|
|
9
|
+
| "instances.read"
|
|
10
|
+
| "instances.manage"
|
|
11
|
+
| "events.emit"
|
|
12
|
+
| "tasks.work";
|
|
13
|
+
|
|
14
|
+
/** What an action is about; the fields known for the route. */
|
|
15
|
+
export interface WorkflowAccessTarget {
|
|
16
|
+
readonly processKey?: string;
|
|
17
|
+
readonly partition?: string | null;
|
|
18
|
+
readonly instanceId?: string;
|
|
19
|
+
readonly taskId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Rights and identity of the workflow HTTP (WORKFLOW-001 §6). The framework's
|
|
24
|
+
* controllers do not know how the application signs people in, so they ask this
|
|
25
|
+
* port. It is the DI token too: register the application's implementation in any
|
|
26
|
+
* module, `DI.singleton(DI.classProvider(WorkflowAccess, AppWorkflowAccess))`.
|
|
27
|
+
* Without one every workflow route answers 403. The services never ask it: the
|
|
28
|
+
* application's code that calls them decides itself.
|
|
29
|
+
*/
|
|
30
|
+
export abstract class WorkflowAccess {
|
|
31
|
+
/** Who makes the request; `undefined` answers 401. */
|
|
32
|
+
abstract actor(ctx: HttpContext): WorkflowActor | undefined | Promise<WorkflowActor | undefined>;
|
|
33
|
+
|
|
34
|
+
/** Whether the actor may do `action` on `target`; `false` answers 403. */
|
|
35
|
+
abstract can(ctx: HttpContext, action: WorkflowAction, target: WorkflowAccessTarget): boolean | Promise<boolean>;
|
|
36
|
+
|
|
37
|
+
/** Whom the user's tasks are addressed to: `user:<id>`, `role:<code>`. */
|
|
38
|
+
assignees(_ctx: HttpContext, actor: WorkflowActor): readonly string[] | Promise<readonly string[]> {
|
|
39
|
+
return [`user:${actor.id}`];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The default: nothing is allowed until the application says otherwise. */
|
|
44
|
+
export class DenyWorkflowAccess extends WorkflowAccess {
|
|
45
|
+
actor(): undefined {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
can(): boolean {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { HttpError } from "../../http";
|
|
2
|
+
import type { FormIssue, ProcessIssue } from "../../../library/workflow";
|
|
3
|
+
|
|
4
|
+
export type WorkflowErrorCode = "not_found" | "conflict" | "wrong_status" | "invalid_document" | "invalid_input" | "archived";
|
|
5
|
+
|
|
6
|
+
const STATUS: Readonly<Record<WorkflowErrorCode, number>> = {
|
|
7
|
+
not_found: 404,
|
|
8
|
+
conflict: 409,
|
|
9
|
+
wrong_status: 409,
|
|
10
|
+
archived: 409,
|
|
11
|
+
invalid_document: 422,
|
|
12
|
+
invalid_input: 422,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* An error of the workflow services. Code and HTTP status in one: the
|
|
17
|
+
* framework's controllers throw it as is, the response is
|
|
18
|
+
* `{ error, details: { code, issues? } }`.
|
|
19
|
+
*/
|
|
20
|
+
export class WorkflowError extends HttpError {
|
|
21
|
+
constructor(
|
|
22
|
+
readonly code: WorkflowErrorCode,
|
|
23
|
+
message: string,
|
|
24
|
+
readonly issues?: readonly (ProcessIssue | FormIssue)[],
|
|
25
|
+
) {
|
|
26
|
+
super(STATUS[code], message, issues === undefined ? { code } : { code, issues });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { validateForm, type FormIssue, type FormSpec } from "../../../library/workflow";
|
|
2
|
+
|
|
3
|
+
/** What a value is checked for: a process input or a task form. */
|
|
4
|
+
export interface WorkflowInputContext {
|
|
5
|
+
readonly purpose: "input" | "task";
|
|
6
|
+
readonly processKey: string;
|
|
7
|
+
readonly partition: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks a process input and task forms against their fields (WORKFLOW-001 §9).
|
|
12
|
+
* The default checks the types of `bazis.workflow/v1`; an application with its
|
|
13
|
+
* own field rules registers its implementation of this class in any module.
|
|
14
|
+
*/
|
|
15
|
+
export abstract class WorkflowInputValidator {
|
|
16
|
+
abstract validate(spec: FormSpec | undefined, value: unknown, context: WorkflowInputContext): { readonly value: Record<string, unknown>; readonly issues: readonly FormIssue[] } | Promise<{ readonly value: Record<string, unknown>; readonly issues: readonly FormIssue[] }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The built-in field check. */
|
|
20
|
+
export class DefaultWorkflowInputValidator extends WorkflowInputValidator {
|
|
21
|
+
validate(spec: FormSpec | undefined, value: unknown): { readonly value: Record<string, unknown>; readonly issues: readonly FormIssue[] } {
|
|
22
|
+
return validateForm(spec, value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Who acts: a user or the system (schedules, events). Steps of an instance run on behalf of who started it. */
|
|
2
|
+
export interface WorkflowActor {
|
|
3
|
+
readonly kind: "user" | "system";
|
|
4
|
+
readonly id: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** A page of a list. */
|
|
8
|
+
export interface WorkflowPage<T> {
|
|
9
|
+
readonly items: readonly T[];
|
|
10
|
+
readonly total: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Paging of the list operations: `limit` 1–200 (50 by default), `offset` from 0. */
|
|
14
|
+
export interface WorkflowPaging {
|
|
15
|
+
readonly limit?: number;
|
|
16
|
+
readonly offset?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function pagingOf(paging: WorkflowPaging): { readonly limit: number; readonly offset: number } {
|
|
20
|
+
const limit = paging.limit === undefined ? 50 : Math.trunc(paging.limit);
|
|
21
|
+
const offset = paging.offset === undefined ? 0 : Math.trunc(paging.offset);
|
|
22
|
+
return { limit: Number.isFinite(limit) ? Math.min(Math.max(limit, 1), 200) : 50, offset: Number.isFinite(offset) ? Math.max(offset, 0) : 0 };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
26
|
+
|
|
27
|
+
/** Whether an id can be a row key; anything else would reach PostgreSQL as an invalid uuid. */
|
|
28
|
+
export function isUuid(value: unknown): value is string {
|
|
29
|
+
return typeof value === "string" && UUID.test(value);
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DI, Module, SERVICE_PROVIDER, scoped } from "../../di";
|
|
2
|
+
import { WorkflowTools } from "../internal/WorkflowTools";
|
|
3
|
+
import { ProcessesController } from "./http/Processes.controller";
|
|
4
|
+
import { DefinitionsDbContext, definitionsStore } from "./model/Definitions.dbContext";
|
|
5
|
+
import { WfProcess } from "./model/WfProcess.model";
|
|
6
|
+
import { WfVersion } from "./model/WfVersion.model";
|
|
7
|
+
import { IWorkflowDefinitions } from "./services/IWorkflowDefinitions.service";
|
|
8
|
+
import { WorkflowDefinitions } from "./services/WorkflowDefinitions.service";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The process catalog: drafts and published versions ([passport](MODULE.md)).
|
|
12
|
+
* Dependencies are explicit: the framework's classes are not covered by the
|
|
13
|
+
* application's codegen.
|
|
14
|
+
*/
|
|
15
|
+
@Module({
|
|
16
|
+
ormBazis: { context: DefinitionsDbContext, entities: [WfProcess, WfVersion], ownedStore: definitionsStore, registerRepositories: false },
|
|
17
|
+
providers: [
|
|
18
|
+
DI.singleton(DI.factoryProviderWithResolver(WorkflowTools, [], (resolver) => new WorkflowTools(resolver.resolve(SERVICE_PROVIDER)))),
|
|
19
|
+
scoped(IWorkflowDefinitions, WorkflowDefinitions, [DefinitionsDbContext, WorkflowTools] as const),
|
|
20
|
+
],
|
|
21
|
+
controllers: [DI.bindDeps(ProcessesController, IWorkflowDefinitions)],
|
|
22
|
+
exports: [IWorkflowDefinitions, WorkflowTools],
|
|
23
|
+
})
|
|
24
|
+
export class WorkflowDefinitionsModule {}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Controller, Created, Get, Patch, Post, Put, type HttpContext } from "../../../http";
|
|
2
|
+
import { authorize, body, optionalInteger, optionalString, pagingQuery, param, requiredString } from "../../internal/http";
|
|
3
|
+
import { WorkflowError } from "../../contracts/WorkflowError";
|
|
4
|
+
import { IWorkflowDefinitions } from "../services/IWorkflowDefinitions.service";
|
|
5
|
+
|
|
6
|
+
/** The process catalog over HTTP (WORKFLOW-001 §3); rights come from `WorkflowAccess`. */
|
|
7
|
+
@Controller("workflow")
|
|
8
|
+
export class ProcessesController {
|
|
9
|
+
constructor(private readonly definitions: IWorkflowDefinitions) {}
|
|
10
|
+
|
|
11
|
+
@Get("processes")
|
|
12
|
+
async list(ctx: HttpContext) {
|
|
13
|
+
await authorize(ctx, "processes.read");
|
|
14
|
+
const archived = ctx.query("archived");
|
|
15
|
+
if (archived !== undefined && archived !== "true" && archived !== "false") throw new WorkflowError("invalid_input", "archived must be true or false");
|
|
16
|
+
const page = await this.definitions.list({ ...pagingQuery(ctx), archived: archived === undefined ? undefined : archived === "true", search: optionalString(ctx.query("search"), "search") });
|
|
17
|
+
return { data: page.items, meta: { total: page.total } };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Post("processes")
|
|
21
|
+
async create(ctx: HttpContext) {
|
|
22
|
+
const request = await body(ctx);
|
|
23
|
+
const key = requiredString(request.key, "key", 64);
|
|
24
|
+
const actor = await authorize(ctx, "processes.manage", { processKey: key });
|
|
25
|
+
const process = await this.definitions.create({
|
|
26
|
+
key,
|
|
27
|
+
title: requiredString(request.title, "title"),
|
|
28
|
+
description: optionalString(request.description, "description", 2000) ?? null,
|
|
29
|
+
retentionDays: optionalInteger(request.retentionDays, "retentionDays") ?? null,
|
|
30
|
+
}, actor);
|
|
31
|
+
return Created(`${ctx.path}/${process.key}`, process);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Get("processes/:key")
|
|
35
|
+
async get(ctx: HttpContext) {
|
|
36
|
+
const key = param(ctx, "key");
|
|
37
|
+
await authorize(ctx, "processes.read", { processKey: key });
|
|
38
|
+
return await this.definitions.get(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Patch("processes/:key")
|
|
42
|
+
async update(ctx: HttpContext) {
|
|
43
|
+
const key = param(ctx, "key");
|
|
44
|
+
const request = await body(ctx);
|
|
45
|
+
const actor = await authorize(ctx, "processes.manage", { processKey: key });
|
|
46
|
+
if (request.archived !== undefined && typeof request.archived !== "boolean") throw new WorkflowError("invalid_input", "archived must be a boolean");
|
|
47
|
+
return await this.definitions.update(key, {
|
|
48
|
+
...(request.title === undefined ? {} : { title: requiredString(request.title, "title") }),
|
|
49
|
+
...(request.description === undefined ? {} : { description: optionalString(request.description, "description", 2000) ?? null }),
|
|
50
|
+
...(request.retentionDays === undefined ? {} : { retentionDays: optionalInteger(request.retentionDays, "retentionDays") ?? null }),
|
|
51
|
+
...(request.archived === undefined ? {} : { archived: request.archived as boolean }),
|
|
52
|
+
}, actor);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Put("processes/:key/draft")
|
|
56
|
+
async saveDraft(ctx: HttpContext) {
|
|
57
|
+
const key = param(ctx, "key");
|
|
58
|
+
const request = await body(ctx);
|
|
59
|
+
const actor = await authorize(ctx, "processes.manage", { processKey: key });
|
|
60
|
+
return await this.definitions.saveDraft(key, request.document, actor);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Post("processes/:key/publish")
|
|
64
|
+
async publish(ctx: HttpContext) {
|
|
65
|
+
const key = param(ctx, "key");
|
|
66
|
+
const actor = await authorize(ctx, "processes.publish", { processKey: key });
|
|
67
|
+
const version = await this.definitions.publish(key, actor);
|
|
68
|
+
return Created(`${ctx.path.replace(/\/publish$/, "")}/versions/${version.version}`, version);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Get("processes/:key/versions")
|
|
72
|
+
async versions(ctx: HttpContext) {
|
|
73
|
+
const key = param(ctx, "key");
|
|
74
|
+
await authorize(ctx, "processes.read", { processKey: key });
|
|
75
|
+
return { data: await this.definitions.versions(key) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Get("processes/:key/versions/:version")
|
|
79
|
+
async version(ctx: HttpContext) {
|
|
80
|
+
const key = param(ctx, "key");
|
|
81
|
+
await authorize(ctx, "processes.read", { processKey: key });
|
|
82
|
+
const version = optionalInteger(param(ctx, "version"), "version");
|
|
83
|
+
if (version === undefined) throw new WorkflowError("invalid_input", "version must be an integer");
|
|
84
|
+
return await this.definitions.version(key, version);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Get("tools")
|
|
88
|
+
async tools(ctx: HttpContext) {
|
|
89
|
+
await authorize(ctx, "processes.read");
|
|
90
|
+
return { data: this.definitions.tools() };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DbContext, defineOrmOwnedStoreV1 } from "../../../orm";
|
|
2
|
+
import { WfProcess } from "./WfProcess.model";
|
|
3
|
+
import { WfVersion } from "./WfVersion.model";
|
|
4
|
+
|
|
5
|
+
export class DefinitionsDbContext extends DbContext {
|
|
6
|
+
readonly processes = this.set(WfProcess);
|
|
7
|
+
readonly versions = this.set(WfVersion);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The tables of the Definitions part: bazis creates and checks them in the application database. */
|
|
11
|
+
export const definitionsStore = defineOrmOwnedStoreV1({
|
|
12
|
+
contract: "bazis.orm-owned-store/v1",
|
|
13
|
+
storeKey: "bazis.workflow.definitions",
|
|
14
|
+
formatVersion: 1,
|
|
15
|
+
ownedScope: { schema: "public", tablePrefix: "bazis_wf_def_" },
|
|
16
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Column, Entity, Index, UUID } from "../../../orm";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A process in the catalog: the key it is started by, its names, the draft a
|
|
5
|
+
* designer edits and the retention of finished instances. Changes; the
|
|
6
|
+
* published versions ({@link WfVersion}) do not.
|
|
7
|
+
*/
|
|
8
|
+
@Entity({ table: "bazis_wf_def_processes" })
|
|
9
|
+
export class WfProcess {
|
|
10
|
+
@UUID({ version: "v7" })
|
|
11
|
+
id = "";
|
|
12
|
+
|
|
13
|
+
@Index({ unique: true })
|
|
14
|
+
@Column({ type: "text" })
|
|
15
|
+
key = "";
|
|
16
|
+
|
|
17
|
+
@Column({ type: "text" })
|
|
18
|
+
title = "";
|
|
19
|
+
|
|
20
|
+
@Column({ type: "text", nullable: true })
|
|
21
|
+
description: string | null = null;
|
|
22
|
+
|
|
23
|
+
/** The draft document; kept with validation errors too. */
|
|
24
|
+
@Column({ type: "json", nullable: true })
|
|
25
|
+
draft: unknown = null;
|
|
26
|
+
|
|
27
|
+
@Column({ type: "datetime", nullable: true })
|
|
28
|
+
draftUpdatedAt: Date | null = null;
|
|
29
|
+
|
|
30
|
+
@Column({ type: "text", nullable: true })
|
|
31
|
+
draftUpdatedByKind: string | null = null;
|
|
32
|
+
|
|
33
|
+
@Column({ type: "text", nullable: true })
|
|
34
|
+
draftUpdatedById: string | null = null;
|
|
35
|
+
|
|
36
|
+
@Column({ type: "integer", nullable: true })
|
|
37
|
+
latestVersion: number | null = null;
|
|
38
|
+
|
|
39
|
+
/** Days to keep completed and cancelled instances; `null` keeps them. */
|
|
40
|
+
@Column({ type: "integer", nullable: true })
|
|
41
|
+
retentionDays: number | null = null;
|
|
42
|
+
|
|
43
|
+
/** An archived process is not started. */
|
|
44
|
+
@Column({ type: "datetime", nullable: true })
|
|
45
|
+
archivedAt: Date | null = null;
|
|
46
|
+
|
|
47
|
+
@Column({ type: "createdAt" })
|
|
48
|
+
createdAt = new Date(0);
|
|
49
|
+
|
|
50
|
+
@Column({ type: "updatedAt" })
|
|
51
|
+
updatedAt = new Date(0);
|
|
52
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Column, Entity, ForeignKey, Index, UUID } from "../../../orm";
|
|
2
|
+
import { WfProcess } from "./WfProcess.model";
|
|
3
|
+
|
|
4
|
+
/** A published version of a process: an immutable snapshot of its document. */
|
|
5
|
+
@ForeignKey(() => WfProcess, { properties: ["processId"], onDelete: "cascade" })
|
|
6
|
+
@Index(["processId", "version"], { unique: true })
|
|
7
|
+
@Entity({ table: "bazis_wf_def_versions" })
|
|
8
|
+
export class WfVersion {
|
|
9
|
+
@UUID({ version: "v7" })
|
|
10
|
+
id = "";
|
|
11
|
+
|
|
12
|
+
@Column({ type: "uuid" })
|
|
13
|
+
processId = "";
|
|
14
|
+
|
|
15
|
+
@Column({ type: "integer" })
|
|
16
|
+
version = 0;
|
|
17
|
+
|
|
18
|
+
@Column({ type: "json" })
|
|
19
|
+
document: unknown = null;
|
|
20
|
+
|
|
21
|
+
/** sha-256 of the document: publishing the same document again adds no version. */
|
|
22
|
+
@Column({ type: "text" })
|
|
23
|
+
documentHash = "";
|
|
24
|
+
|
|
25
|
+
@Column({ type: "createdAt" })
|
|
26
|
+
publishedAt = new Date(0);
|
|
27
|
+
|
|
28
|
+
@Column({ type: "text" })
|
|
29
|
+
publishedByKind = "";
|
|
30
|
+
|
|
31
|
+
@Column({ type: "text" })
|
|
32
|
+
publishedById = "";
|
|
33
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { AgentToolContract } from "../../../agent";
|
|
2
|
+
import type { ProcessDocument, ProcessIssue } from "../../../../library/workflow";
|
|
3
|
+
import type { WorkflowActor, WorkflowPage, WorkflowPaging } from "../../contracts/types";
|
|
4
|
+
|
|
5
|
+
export interface ProcessSummary {
|
|
6
|
+
readonly key: string;
|
|
7
|
+
readonly title: string;
|
|
8
|
+
readonly description: string | null;
|
|
9
|
+
readonly latestVersion: number | null;
|
|
10
|
+
readonly retentionDays: number | null;
|
|
11
|
+
readonly archived: boolean;
|
|
12
|
+
readonly createdAt: Date;
|
|
13
|
+
readonly updatedAt: Date;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProcessDetails extends ProcessSummary {
|
|
17
|
+
/** The draft a designer edits; may hold validation errors. */
|
|
18
|
+
readonly draft: unknown;
|
|
19
|
+
readonly draftUpdatedAt: Date | null;
|
|
20
|
+
readonly draftUpdatedBy: WorkflowActor | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface VersionSummary {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly key: string;
|
|
26
|
+
readonly version: number;
|
|
27
|
+
readonly publishedAt: Date;
|
|
28
|
+
readonly publishedBy: WorkflowActor;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface VersionDetails extends VersionSummary {
|
|
32
|
+
readonly document: ProcessDocument;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CreateProcess {
|
|
36
|
+
readonly key: string;
|
|
37
|
+
readonly title: string;
|
|
38
|
+
readonly description?: string | null;
|
|
39
|
+
readonly retentionDays?: number | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UpdateProcess {
|
|
43
|
+
readonly title?: string;
|
|
44
|
+
readonly description?: string | null;
|
|
45
|
+
readonly retentionDays?: number | null;
|
|
46
|
+
readonly archived?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ProcessListQuery extends WorkflowPaging {
|
|
50
|
+
readonly archived?: boolean;
|
|
51
|
+
/** Part of the key or title, case-insensitive. */
|
|
52
|
+
readonly search?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The process catalog: drafts and immutable published versions
|
|
57
|
+
* (WORKFLOW-001 §3). Errors are {@link WorkflowError}: `not_found`, `conflict`,
|
|
58
|
+
* `invalid_document` with the issues, `invalid_input`.
|
|
59
|
+
*/
|
|
60
|
+
export abstract class IWorkflowDefinitions {
|
|
61
|
+
abstract list(query?: ProcessListQuery): Promise<WorkflowPage<ProcessSummary>>;
|
|
62
|
+
abstract get(key: string): Promise<ProcessDetails>;
|
|
63
|
+
abstract create(process: CreateProcess, actor: WorkflowActor): Promise<ProcessDetails>;
|
|
64
|
+
abstract update(key: string, changes: UpdateProcess, actor: WorkflowActor): Promise<ProcessDetails>;
|
|
65
|
+
/** Stores the draft even with errors and returns every validation finding. */
|
|
66
|
+
abstract saveDraft(key: string, document: unknown, actor: WorkflowActor): Promise<{ readonly issues: readonly ProcessIssue[] }>;
|
|
67
|
+
/** Validates the draft and stores it as the next version; the same document again returns the latest version. */
|
|
68
|
+
abstract publish(key: string, actor: WorkflowActor): Promise<VersionDetails>;
|
|
69
|
+
abstract versions(key: string): Promise<readonly VersionSummary[]>;
|
|
70
|
+
abstract version(key: string, version: number): Promise<VersionDetails>;
|
|
71
|
+
/** The latest published version of a process that is not archived. */
|
|
72
|
+
abstract latest(key: string): Promise<VersionDetails>;
|
|
73
|
+
/** A version by id, for the engine; `undefined` if there is none. */
|
|
74
|
+
abstract versionById(id: string): Promise<VersionDetails | undefined>;
|
|
75
|
+
/** Processes with a retention period: what the retention service deletes. */
|
|
76
|
+
abstract retentionPolicies(): Promise<readonly { readonly key: string; readonly retentionDays: number }[]>;
|
|
77
|
+
/** The Tools a service task may call, with their input and output schemas. */
|
|
78
|
+
abstract tools(): readonly AgentToolContract[];
|
|
79
|
+
}
|