bazis 0.132.0 → 0.134.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/AgentToolExecutor.ts +5 -0
- package/core/agent/Tool.contract.ts +2 -2
- package/core/agent/metadata.ts +11 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/scripts/di-generate.ts +8 -0
- package/core/tools/StandardTools.module.ts +14 -0
- package/core/tools/generated/deps.ts +10 -0
- package/core/tools/index.ts +8 -0
- package/core/tools/orm/OrmEntity.references.ts +71 -0
- package/core/tools/orm/ormEntityTools.ts +423 -0
- package/core/workflow/contracts/WorkflowError.ts +2 -1
- package/core/workflow/contracts/WorkflowPartitions.ts +29 -0
- package/core/workflow/contracts/WorkflowProcessUsage.ts +8 -0
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +7 -2
- package/core/workflow/definitions_module/access/Definitions.permissions.ts +4 -2
- package/core/workflow/definitions_module/access/ProcessPartition.lookup.ts +27 -0
- package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +5 -4
- package/core/workflow/definitions_module/events/Definitions.events.ts +1 -1
- package/core/workflow/definitions_module/http/Processes.controller.ts +21 -5
- package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +10 -0
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +28 -2
- package/core/workflow/generated/deps.ts +12 -2
- package/core/workflow/index.ts +5 -0
- package/core/workflow/runtime_module/WorkflowRuntime.module.ts +9 -0
- package/core/workflow/runtime_module/background/WorkflowRetention.ts +2 -2
- package/core/workflow/runtime_module/events/Runtime.events.ts +4 -4
- package/core/workflow/runtime_module/http/Instances.controller.ts +14 -2
- package/core/workflow/runtime_module/internal/ChildCalls.ts +186 -0
- package/core/workflow/runtime_module/internal/EventDelivery.ts +234 -0
- package/core/workflow/runtime_module/internal/GraphCache.ts +7 -1
- package/core/workflow/runtime_module/internal/RunLimits.ts +134 -0
- package/core/workflow/runtime_module/internal/RuntimeProcessUsage.ts +13 -0
- package/core/workflow/runtime_module/internal/Schedules.ts +97 -30
- package/core/workflow/runtime_module/internal/TimerStep.ts +11 -1
- package/core/workflow/runtime_module/internal/ToolStep.ts +4 -4
- package/core/workflow/runtime_module/internal/WorkflowEngine.ts +544 -132
- package/core/workflow/runtime_module/internal/WorkflowJobs.ts +44 -6
- package/core/workflow/runtime_module/model/WfHistory.model.ts +1 -1
- package/core/workflow/runtime_module/model/WfInstance.model.ts +7 -4
- package/core/workflow/runtime_module/model/WfJob.model.ts +18 -8
- package/core/workflow/runtime_module/model/WfToken.model.ts +5 -3
- package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +59 -4
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +72 -42
- package/core/workflow/tasks_module/events/TaskEvents.ts +4 -8
- package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +1 -1
- package/library/workflow/ProcessDocument.ts +14 -0
- package/library/workflow/cron.ts +103 -24
- package/library/workflow/expressions.ts +5 -1
- package/library/workflow/index.ts +2 -2
- package/library/workflow/masking.ts +17 -3
- package/library/workflow/mermaid.ts +22 -6
- package/library/workflow/nodeKinds.ts +123 -9
- package/library/workflow/palette.ts +3 -1
- package/library/workflow/paths.ts +10 -4
- package/library/workflow/structure.ts +33 -4
- package/library/workflow/validateProcess.ts +141 -17
- package/package.json +2 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { WorkflowPortContext } from "./WorkflowExtensions";
|
|
2
|
+
|
|
3
|
+
/** A partition a process is connected to. */
|
|
4
|
+
export interface ProcessPartition {
|
|
5
|
+
/** `null`: the process runs without a partition. */
|
|
6
|
+
readonly partition: string | null;
|
|
7
|
+
/** An IANA time zone for the schedules of this partition; a time zone on the node goes first. */
|
|
8
|
+
readonly timezone?: string;
|
|
9
|
+
/** A process that is switched off here takes no events and no scheduled starts. */
|
|
10
|
+
readonly enabled: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Where a process works (WORKFLOW-002 §6): tenants, spaces, organizations. An
|
|
15
|
+
* event starts a process only in a partition listed here, a schedule with
|
|
16
|
+
* `partitions: "all"` runs in every enabled one, and a process connected to a
|
|
17
|
+
* partition cannot be deleted. The application registers its implementation in
|
|
18
|
+
* any module.
|
|
19
|
+
*/
|
|
20
|
+
export abstract class WorkflowPartitions {
|
|
21
|
+
abstract of(processKey: string, context: WorkflowPortContext): readonly ProcessPartition[] | Promise<readonly ProcessPartition[]>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** By default a process has one place to work: no partition. */
|
|
25
|
+
export class DefaultWorkflowPartitions extends WorkflowPartitions {
|
|
26
|
+
of(): readonly ProcessPartition[] {
|
|
27
|
+
return [{ partition: null, enabled: true }];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the catalog asks before it deletes a process. Runtime answers: the catalog
|
|
3
|
+
* knows nothing of instances, and without Runtime there are none.
|
|
4
|
+
*/
|
|
5
|
+
export abstract class WorkflowProcessUsage {
|
|
6
|
+
/** How many `pending`, `running` and `failed` instances the process has. */
|
|
7
|
+
abstract liveInstances(processKey: string): Promise<number>;
|
|
8
|
+
}
|
|
@@ -3,6 +3,7 @@ import { HOSTED_SERVICE, Module, scoped, singleton } from "../../di";
|
|
|
3
3
|
import { SystemWorkflowClock, WorkflowClock } from "../contracts/WorkflowClock";
|
|
4
4
|
import { workflowConfig } from "../contracts/Workflow.config";
|
|
5
5
|
import { DefaultWorkflowExtensions, WorkflowExtensions } from "../contracts/WorkflowExtensions";
|
|
6
|
+
import { DefaultWorkflowPartitions, WorkflowPartitions } from "../contracts/WorkflowPartitions";
|
|
6
7
|
import { WorkflowEvents } from "../internal/WorkflowEvents";
|
|
7
8
|
import { WorkflowLanguages } from "../internal/WorkflowLanguages";
|
|
8
9
|
import { WorkflowTools } from "../internal/WorkflowTools";
|
|
@@ -10,6 +11,7 @@ import { definitionsPermissions } from "./access/Definitions.permissions";
|
|
|
10
11
|
import { FolderReferences } from "./access/Folder.references";
|
|
11
12
|
import { ProcessReferences } from "./access/Process.references";
|
|
12
13
|
import { ProcessFolderLookup } from "./access/ProcessFolder.lookup";
|
|
14
|
+
import { ProcessPartitionLookup } from "./access/ProcessPartition.lookup";
|
|
13
15
|
import { CatalogImport } from "./code/CatalogImport";
|
|
14
16
|
import { CodeProcessCatalog } from "./code/CodeProcessCatalog";
|
|
15
17
|
import { CodeProcessPublisher } from "./code/CodeProcessPublisher";
|
|
@@ -23,7 +25,8 @@ import { WorkflowDefinitions } from "./services/WorkflowDefinitions.service";
|
|
|
23
25
|
/**
|
|
24
26
|
* The process catalog: drafts and published versions, and the processes
|
|
25
27
|
* declared in code ([passport](MODULE.md)). The application checks its own
|
|
26
|
-
* `extensions` by registering its `WorkflowExtensions
|
|
28
|
+
* `extensions` by registering its `WorkflowExtensions` and says where processes
|
|
29
|
+
* work by registering its `WorkflowPartitions`.
|
|
27
30
|
*/
|
|
28
31
|
@Module({
|
|
29
32
|
imports: [AccessModule],
|
|
@@ -37,15 +40,17 @@ import { WorkflowDefinitions } from "./services/WorkflowDefinitions.service";
|
|
|
37
40
|
singleton(CodeProcessCatalog),
|
|
38
41
|
singleton(WorkflowLanguages),
|
|
39
42
|
singleton(WorkflowExtensions, DefaultWorkflowExtensions),
|
|
43
|
+
singleton(WorkflowPartitions, DefaultWorkflowPartitions),
|
|
40
44
|
scoped(WorkflowEvents),
|
|
41
45
|
scoped(CatalogImport),
|
|
42
46
|
scoped(IWorkflowDefinitions, WorkflowDefinitions),
|
|
43
47
|
scoped(ProcessReferences),
|
|
44
48
|
scoped(FolderReferences),
|
|
45
49
|
scoped(ProcessFolderLookup),
|
|
50
|
+
scoped(ProcessPartitionLookup),
|
|
46
51
|
singleton(HOSTED_SERVICE, CodeProcessPublisher),
|
|
47
52
|
],
|
|
48
53
|
controllers: [ProcessesController],
|
|
49
|
-
exports: [IWorkflowDefinitions, WorkflowTools, WorkflowClock, WorkflowEvents, WorkflowLanguages, WorkflowExtensions, ProcessFolderLookup],
|
|
54
|
+
exports: [IWorkflowDefinitions, WorkflowTools, WorkflowClock, WorkflowEvents, WorkflowLanguages, WorkflowExtensions, WorkflowPartitions, ProcessFolderLookup],
|
|
50
55
|
})
|
|
51
56
|
export class WorkflowDefinitionsModule {}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { definePermissions } from "../../../access";
|
|
2
|
+
import { ProcessPartitionLookup } from "./ProcessPartition.lookup";
|
|
2
3
|
|
|
3
4
|
const process = { ref: "workflow.process", column: "key" };
|
|
4
5
|
const folder = { ref: "workflow.folder", column: "folder" };
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* The rights of the process catalog (WORKFLOW-002 §6.2). A process is one per
|
|
8
|
-
* application
|
|
9
|
-
*
|
|
9
|
+
* application: only reading it has partitions, those `WorkflowPartitions`
|
|
10
|
+
* connects it to.
|
|
10
11
|
*/
|
|
11
12
|
export const definitionsPermissions = definePermissions({
|
|
12
13
|
"workflow.processes.read": {
|
|
13
14
|
title: { en: "See processes", ru: "Видеть процессы" },
|
|
15
|
+
scope: { lookup: ProcessPartitionLookup },
|
|
14
16
|
on: { process, folder },
|
|
15
17
|
},
|
|
16
18
|
"workflow.processes.manage": {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AttributeLookup, type AccessValue, type LookupColumn } from "../../../access";
|
|
2
|
+
import type { ServiceProvider } from "../../../di";
|
|
3
|
+
import { WorkflowPartitions } from "../../contracts/WorkflowPartitions";
|
|
4
|
+
import type { WorkflowLanguages } from "../../internal/WorkflowLanguages";
|
|
5
|
+
import type { DefinitionsDbContext } from "../model/Definitions.dbContext";
|
|
6
|
+
|
|
7
|
+
/** The partitions of a process are not a column: partition → the keys of the processes connected to it (WORKFLOW-002 §6.2). */
|
|
8
|
+
export class ProcessPartitionLookup extends AttributeLookup {
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly db: DefinitionsDbContext,
|
|
11
|
+
private readonly languages: WorkflowLanguages,
|
|
12
|
+
private readonly services: ServiceProvider,
|
|
13
|
+
) {
|
|
14
|
+
super();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async toColumn(values: readonly AccessValue[]): Promise<LookupColumn> {
|
|
18
|
+
const port = this.services.resolve(WorkflowPartitions);
|
|
19
|
+
const rows = await this.db.processes.asNoTracking().select((p) => ({ key: p.key })).toList();
|
|
20
|
+
const keys: string[] = [];
|
|
21
|
+
for (const { key } of rows) {
|
|
22
|
+
const partitions = await port.of(key, { partition: null, language: this.languages.default, actor: null });
|
|
23
|
+
if (partitions.some((item) => item.enabled && values.includes(item.partition))) keys.push(key);
|
|
24
|
+
}
|
|
25
|
+
return { column: "key", in: keys };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -10,8 +10,8 @@ import { definitionProblems, WorkflowSetupError } from "./codeProcesses";
|
|
|
10
10
|
/**
|
|
11
11
|
* At start, after the tables exist: brings over the catalog of the old store
|
|
12
12
|
* (WORKFLOW-002 §9), then publishes the processes of `@Module({ processes })`
|
|
13
|
-
* (WORKFLOW-001 §14). A document with an unknown Tool or an `extensions`
|
|
14
|
-
* nobody checks stops the start with every finding of every process; the same
|
|
13
|
+
* (WORKFLOW-001 §14). A document with an unknown Tool or process, or an `extensions`
|
|
14
|
+
* namespace nobody checks, stops the start with every finding of every process; the same
|
|
15
15
|
* document again gives no new version.
|
|
16
16
|
*/
|
|
17
17
|
export class CodeProcessPublisher implements HostedService {
|
|
@@ -30,10 +30,11 @@ export class CodeProcessPublisher implements HostedService {
|
|
|
30
30
|
const tools = this.services.resolve(WorkflowTools);
|
|
31
31
|
const definitions = scope.resolve(IWorkflowDefinitions);
|
|
32
32
|
const problems: string[] = [];
|
|
33
|
+
const processes = entries.map(({ process }) => process.key);
|
|
33
34
|
for (const { process, module } of entries) {
|
|
34
35
|
const found = [...definitionProblems(process, (name) => tools.has(name))];
|
|
35
|
-
for (const issue of await definitions.validate(process.document)) {
|
|
36
|
-
if (issue.severity === "error" && (issue.code === "extension_unknown" || issue.code === "extension_invalid")) found.push(`${issue.message} (${issue.code})`);
|
|
36
|
+
for (const issue of await definitions.validate(process.document, { processes })) {
|
|
37
|
+
if (issue.severity === "error" && (issue.code === "extension_unknown" || issue.code === "extension_invalid" || issue.code === "unknown_process")) found.push(`${issue.message} (${issue.code})`);
|
|
37
38
|
}
|
|
38
39
|
if (found.length > 0) problems.push(`${module}: process "${process.key}":\n${found.map((line) => ` - ${line}`).join("\n")}`);
|
|
39
40
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createEventToken } from "../../../kernel";
|
|
2
2
|
import type { WorkflowActor, WorkflowRequest } from "../../contracts/types";
|
|
3
3
|
|
|
4
|
-
export type ProcessChange = "created" | "updated" | "draftSaved" | "published" | "archived" | "restored";
|
|
4
|
+
export type ProcessChange = "created" | "updated" | "draftSaved" | "published" | "archived" | "restored" | "deleted";
|
|
5
5
|
|
|
6
6
|
/** A process of the catalog changed, for the application's audit. Published after the commit. */
|
|
7
7
|
export interface WorkflowProcessChanged {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { contextOnlyController } from "../../../http/Routing/contextOnlyControllers";
|
|
2
2
|
import { can, type AccessObject } from "../../../access";
|
|
3
3
|
import type { LocalizedText } from "../../../../library/workflow";
|
|
4
|
-
import { Authorize, Controller, Created, Get, Patch, Post, Put, type HttpContext } from "../../../http";
|
|
4
|
+
import { Authorize, Controller, Created, Delete, Get, Patch, Post, Put, type HttpContext } from "../../../http";
|
|
5
5
|
import { buildListDocument } from "../../../../library/jsonapi";
|
|
6
6
|
import { body, callerOf, defaultLanguage, filterOf, listQuery, optionalInteger, optionalString, optionalText, pagingOfList, param, requiredString, withTitleText } from "../../internal/http";
|
|
7
7
|
import { WorkflowError } from "../../contracts/WorkflowError";
|
|
@@ -52,10 +52,20 @@ export class ProcessesController {
|
|
|
52
52
|
@Authorize(can("workflow.processes.read"))
|
|
53
53
|
async get(ctx: HttpContext) {
|
|
54
54
|
const key = param(ctx, "key");
|
|
55
|
-
const { language } = await callerOf(ctx, "workflow.processes.read", await this.
|
|
55
|
+
const { language } = await callerOf(ctx, "workflow.processes.read", await this.readObjectOf(key));
|
|
56
56
|
return withTitleText(await this.definitions.get(key), language, defaultLanguage(ctx));
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** An archived process without unfinished instances and partitions (WORKFLOW-002 §4.3). */
|
|
60
|
+
@Delete("processes/:key")
|
|
61
|
+
@Authorize(can("workflow.processes.manage"))
|
|
62
|
+
async remove(ctx: HttpContext) {
|
|
63
|
+
const key = param(ctx, "key");
|
|
64
|
+
const { actor, request } = await callerOf(ctx, "workflow.processes.manage", await this.objectOf(key));
|
|
65
|
+
await this.definitions.remove(key, actor, { request });
|
|
66
|
+
ctx.res.status(204);
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
@Patch("processes/:key")
|
|
60
70
|
@Authorize(can("workflow.processes.manage"))
|
|
61
71
|
async update(ctx: HttpContext) {
|
|
@@ -102,7 +112,7 @@ export class ProcessesController {
|
|
|
102
112
|
@Authorize(can("workflow.processes.read"))
|
|
103
113
|
async versions(ctx: HttpContext) {
|
|
104
114
|
const key = param(ctx, "key");
|
|
105
|
-
await callerOf(ctx, "workflow.processes.read", await this.
|
|
115
|
+
await callerOf(ctx, "workflow.processes.read", await this.readObjectOf(key));
|
|
106
116
|
return { data: await this.definitions.versions(key) };
|
|
107
117
|
}
|
|
108
118
|
|
|
@@ -110,7 +120,7 @@ export class ProcessesController {
|
|
|
110
120
|
@Authorize(can("workflow.processes.read"))
|
|
111
121
|
async version(ctx: HttpContext) {
|
|
112
122
|
const key = param(ctx, "key");
|
|
113
|
-
await callerOf(ctx, "workflow.processes.read", await this.
|
|
123
|
+
await callerOf(ctx, "workflow.processes.read", await this.readObjectOf(key));
|
|
114
124
|
const version = optionalInteger(param(ctx, "version"), "version");
|
|
115
125
|
if (version === undefined) throw new WorkflowError("invalid_input", "version must be an integer");
|
|
116
126
|
return await this.definitions.version(key, version);
|
|
@@ -122,7 +132,8 @@ export class ProcessesController {
|
|
|
122
132
|
async validate(ctx: HttpContext) {
|
|
123
133
|
const request = await body(ctx);
|
|
124
134
|
const { actor, language } = await callerOf(ctx);
|
|
125
|
-
|
|
135
|
+
const key = optionalString(request.key, "key", 64);
|
|
136
|
+
return { issues: await this.definitions.validate(request.document, { actor, language, ...(key === undefined ? {} : { processes: [key] }) }) };
|
|
126
137
|
}
|
|
127
138
|
|
|
128
139
|
/** What a designer builds its forms from (WORKFLOW-002 §4.2). */
|
|
@@ -143,6 +154,11 @@ export class ProcessesController {
|
|
|
143
154
|
return { process: key, folder: await this.definitions.folderOf(key) };
|
|
144
155
|
}
|
|
145
156
|
|
|
157
|
+
/** Reading a process is allowed by the partitions it works in. */
|
|
158
|
+
private async readObjectOf(key: string): Promise<AccessObject> {
|
|
159
|
+
return { ...await this.objectOf(key), scope: await this.definitions.partitionsOf(key) };
|
|
160
|
+
}
|
|
161
|
+
|
|
146
162
|
private localized(items: readonly ProcessSummary[], language: string, ctx: HttpContext) {
|
|
147
163
|
return items.map((item) => withTitleText(item, language, defaultLanguage(ctx)));
|
|
148
164
|
}
|
|
@@ -76,6 +76,8 @@ export interface ProcessListQuery extends WorkflowPaging {
|
|
|
76
76
|
export interface ValidateOptions {
|
|
77
77
|
readonly language?: string;
|
|
78
78
|
readonly actor?: WorkflowActor;
|
|
79
|
+
/** Keys that count as published besides the catalog: the process itself, processes of the code about to be published. */
|
|
80
|
+
readonly processes?: readonly string[];
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
export interface SaveDraftOptions extends WorkflowAuditOptions, Pick<ValidateOptions, "language"> {
|
|
@@ -100,6 +102,8 @@ export abstract class IWorkflowDefinitions {
|
|
|
100
102
|
abstract get(key: string): Promise<ProcessDetails>;
|
|
101
103
|
/** The folder of a process, for a check of rights; `null` also for a process that does not exist. */
|
|
102
104
|
abstract folderOf(key: string): Promise<string | null>;
|
|
105
|
+
/** The partitions a process works in, for a check of rights. */
|
|
106
|
+
abstract partitionsOf(key: string): Promise<readonly (string | null)[]>;
|
|
103
107
|
/** Every finding of a document, with the application's `extensions` checks; nothing is stored. */
|
|
104
108
|
abstract validate(document: unknown, options?: ValidateOptions): Promise<readonly ProcessIssue[]>;
|
|
105
109
|
/** What a designer builds its forms from: node kinds, functions, paths and Tools. */
|
|
@@ -107,6 +111,12 @@ export abstract class IWorkflowDefinitions {
|
|
|
107
111
|
abstract create(process: CreateProcess, actor: WorkflowActor, options?: WorkflowAuditOptions): Promise<ProcessDetails>;
|
|
108
112
|
/** A process declared in code takes only `archived`; other fields give `conflict`. */
|
|
109
113
|
abstract update(key: string, changes: UpdateProcess, actor: WorkflowActor, options?: WorkflowAuditOptions): Promise<ProcessDetails>;
|
|
114
|
+
/**
|
|
115
|
+
* Deletes an archived process with its versions: `wrong_status` when it is not
|
|
116
|
+
* archived, `conflict` when it is declared in code, `in_use` when it has unfinished
|
|
117
|
+
* instances or partitions. Finished instances and their history stay.
|
|
118
|
+
*/
|
|
119
|
+
abstract remove(key: string, actor: WorkflowActor, options?: WorkflowAuditOptions): Promise<void>;
|
|
110
120
|
/** Stores the draft even with errors; `conflict` for a process declared in code and when `expected` is out of date. */
|
|
111
121
|
abstract saveDraft(key: string, document: unknown, actor: WorkflowActor, options?: SaveDraftOptions): Promise<SavedDraft>;
|
|
112
122
|
/**
|
|
@@ -8,6 +8,8 @@ import { isUuid, pageQueryOf, type WorkflowActor, type WorkflowAuditOptions, typ
|
|
|
8
8
|
import type { WorkflowClock } from "../../contracts/WorkflowClock";
|
|
9
9
|
import { WorkflowError } from "../../contracts/WorkflowError";
|
|
10
10
|
import { WorkflowExtensions, type ExtensionTarget, type WorkflowPortContext } from "../../contracts/WorkflowExtensions";
|
|
11
|
+
import { WorkflowPartitions } from "../../contracts/WorkflowPartitions";
|
|
12
|
+
import { WorkflowProcessUsage } from "../../contracts/WorkflowProcessUsage";
|
|
11
13
|
import { canonicalJson } from "../../internal/canonicalJson";
|
|
12
14
|
import type { WorkflowEvents } from "../../internal/WorkflowEvents";
|
|
13
15
|
import type { WorkflowLanguages } from "../../internal/WorkflowLanguages";
|
|
@@ -83,10 +85,21 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
83
85
|
return row?.folder ?? null;
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
async partitionsOf(key: string): Promise<readonly (string | null)[]> {
|
|
89
|
+
const partitions = await this.partitions(key, null);
|
|
90
|
+
return partitions.filter((item) => item.enabled).map((item) => item.partition);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async partitions(key: string, actor: WorkflowActor | null) {
|
|
94
|
+
return await this.services.resolve(WorkflowPartitions).of(key, { partition: null, language: this.languages.default, actor });
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
async validate(document: unknown, options: ValidateOptions = {}): Promise<readonly ProcessIssue[]> {
|
|
87
98
|
const port = this.services.resolve(WorkflowExtensions);
|
|
88
99
|
const namespaces = new Set(port.namespaces());
|
|
89
|
-
const
|
|
100
|
+
const published = await this.db.processes.asNoTracking().where((p) => p.archivedAt.isNull().and(p.latestVersion.isNotNull())).select((p) => ({ key: p.key })).toList();
|
|
101
|
+
const processes = new Set([...published.map((row) => row.key), ...options.processes ?? []]);
|
|
102
|
+
const issues = validateProcess(document, { tools: (name) => this.workflowTools.has(name), processes: (key) => processes.has(key), extensions: (namespace) => namespaces.has(namespace) });
|
|
90
103
|
if (namespaces.size === 0 || issues.some((issue) => issue.code === "too_large" || issue.code === "schema")) return issues;
|
|
91
104
|
const context: WorkflowPortContext = { partition: null, language: options.language ?? this.languages.default, actor: options.actor ?? null };
|
|
92
105
|
const found: ProcessIssue[] = [...issues];
|
|
@@ -151,6 +164,19 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
151
164
|
return this.detailsOf(row);
|
|
152
165
|
}
|
|
153
166
|
|
|
167
|
+
async remove(key: string, actor: WorkflowActor, options: WorkflowAuditOptions = {}): Promise<void> {
|
|
168
|
+
const row = await this.process(key, false);
|
|
169
|
+
this.assertNotCode(key);
|
|
170
|
+
if (row.archivedAt === null) throw new WorkflowError("wrong_status", `Process ${key} is not archived`);
|
|
171
|
+
// An archived process starts nothing more, so what is counted here cannot grow.
|
|
172
|
+
const connected = (await this.partitions(key, actor)).filter((item) => item.partition !== null).map((item) => item.partition);
|
|
173
|
+
if (connected.length > 0) throw new WorkflowError("in_use", `Process ${key} is connected to partitions: ${connected.join(", ")}`);
|
|
174
|
+
const live = await this.services.tryResolve(WorkflowProcessUsage)?.liveInstances(key) ?? 0;
|
|
175
|
+
if (live > 0) throw new WorkflowError("in_use", `Process ${key} has ${live} unfinished instances`);
|
|
176
|
+
await this.db.processes.asNoTracking().where((p) => p.key.eq(key)).executeDelete();
|
|
177
|
+
await this.changed("deleted", key, actor, options);
|
|
178
|
+
}
|
|
179
|
+
|
|
154
180
|
async saveDraft(key: string, document: unknown, actor: WorkflowActor, options: SaveDraftOptions = {}): Promise<SavedDraft> {
|
|
155
181
|
const issues = await this.validate(document, { actor, ...(options.language === undefined ? {} : { language: options.language }) });
|
|
156
182
|
if (issues.some((issue) => issue.code === "too_large")) throw new WorkflowError("invalid_document", "The document is too large", issues);
|
|
@@ -178,7 +204,7 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
178
204
|
if (!row) throw notFound(key);
|
|
179
205
|
this.assertNotCode(key);
|
|
180
206
|
if (row.archivedAt !== null) throw new WorkflowError("archived", `Process ${key} is archived`);
|
|
181
|
-
const issues = await this.validate(row.draft, { actor, ...(options.language === undefined ? {} : { language: options.language }) });
|
|
207
|
+
const issues = await this.validate(row.draft, { actor, processes: [key], ...(options.language === undefined ? {} : { language: options.language }) });
|
|
182
208
|
if (hasErrors(issues)) throw new WorkflowError("invalid_document", `Process ${key} has errors and cannot be published`, issues);
|
|
183
209
|
const { version, created } = await this.publishDraft(row, actor);
|
|
184
210
|
if (created) transaction.afterCommit(() => this.changed("published", key, actor, options, version.version));
|
|
@@ -8,6 +8,7 @@ import { WorkflowClock } from "../contracts/WorkflowClock";
|
|
|
8
8
|
import { FolderReferences } from "../definitions_module/access/Folder.references";
|
|
9
9
|
import { ProcessReferences } from "../definitions_module/access/Process.references";
|
|
10
10
|
import { ProcessFolderLookup } from "../definitions_module/access/ProcessFolder.lookup";
|
|
11
|
+
import { ProcessPartitionLookup } from "../definitions_module/access/ProcessPartition.lookup";
|
|
11
12
|
import { CatalogImport } from "../definitions_module/code/CatalogImport";
|
|
12
13
|
import { CodeProcessCatalog } from "../definitions_module/code/CodeProcessCatalog";
|
|
13
14
|
import { CodeProcessPublisher } from "../definitions_module/code/CodeProcessPublisher";
|
|
@@ -22,7 +23,11 @@ import { WorkflowRetention } from "../runtime_module/background/WorkflowRetentio
|
|
|
22
23
|
import { WorkflowWorker } from "../runtime_module/background/WorkflowWorker";
|
|
23
24
|
import { EventsController } from "../runtime_module/http/Events.controller";
|
|
24
25
|
import { InstancesController } from "../runtime_module/http/Instances.controller";
|
|
26
|
+
import { ChildCalls } from "../runtime_module/internal/ChildCalls";
|
|
27
|
+
import { EventDelivery } from "../runtime_module/internal/EventDelivery";
|
|
25
28
|
import { GraphCache } from "../runtime_module/internal/GraphCache";
|
|
29
|
+
import { RunLimits } from "../runtime_module/internal/RunLimits";
|
|
30
|
+
import { RuntimeProcessUsage } from "../runtime_module/internal/RuntimeProcessUsage";
|
|
26
31
|
import { Schedules } from "../runtime_module/internal/Schedules";
|
|
27
32
|
import { TimerStep } from "../runtime_module/internal/TimerStep";
|
|
28
33
|
import { ToolStep } from "../runtime_module/internal/ToolStep";
|
|
@@ -38,17 +43,22 @@ import { WorkflowTasks } from "../tasks_module/services/WorkflowTasks.service";
|
|
|
38
43
|
|
|
39
44
|
registerFrameworkClassDeps([
|
|
40
45
|
[CatalogImport, [DefinitionsDbContext, WorkflowLanguages]],
|
|
46
|
+
[ChildCalls, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions, ServiceProvider, WorkflowLanguages]],
|
|
41
47
|
[CodeProcessCatalog, [ServiceProvider]],
|
|
42
48
|
[CodeProcessPublisher, [ServiceProvider]],
|
|
43
49
|
[DefinitionsDbContext, [DbContextOptions]],
|
|
50
|
+
[EventDelivery, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions, WorkflowLanguages, ServiceProvider]],
|
|
44
51
|
[EventsController, [IWorkflowRuntime]],
|
|
45
52
|
[FolderReferences, [DefinitionsDbContext]],
|
|
46
53
|
[InstancesController, [IWorkflowRuntime, IWorkflowDefinitions]],
|
|
47
54
|
[ProcessesController, [IWorkflowDefinitions]],
|
|
48
55
|
[ProcessFolderLookup, [DefinitionsDbContext]],
|
|
56
|
+
[ProcessPartitionLookup, [DefinitionsDbContext, WorkflowLanguages, ServiceProvider]],
|
|
49
57
|
[ProcessReferences, [DefinitionsDbContext]],
|
|
58
|
+
[RunLimits, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions]],
|
|
50
59
|
[RuntimeDbContext, [DbContextOptions]],
|
|
51
|
-
[
|
|
60
|
+
[RuntimeProcessUsage, [RuntimeDbContext]],
|
|
61
|
+
[Schedules, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions, WorkflowLanguages, ServiceProvider]],
|
|
52
62
|
[TaskEvents, [TasksDbContext]],
|
|
53
63
|
[TasksController, [IWorkflowTasks]],
|
|
54
64
|
[TasksDbContext, [DbContextOptions]],
|
|
@@ -59,7 +69,7 @@ registerFrameworkClassDeps([
|
|
|
59
69
|
[WorkflowEvents, [EventBus, ServiceProvider, "optional:Logger"]],
|
|
60
70
|
[WorkflowLanguages, ["Config:workflow"]],
|
|
61
71
|
[WorkflowRetention, [ServiceProvider, "Config:workflow"]],
|
|
62
|
-
[WorkflowRuntime, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions, ServiceProvider, WorkflowLanguages]],
|
|
72
|
+
[WorkflowRuntime, [RuntimeDbContext, WorkflowEngine, EventDelivery, IWorkflowDefinitions, ServiceProvider, WorkflowLanguages]],
|
|
63
73
|
[WorkflowTasks, [TasksDbContext, IWorkflowRuntime, WorkflowClock, WorkflowEvents]],
|
|
64
74
|
[WorkflowTools, [ServiceProvider]],
|
|
65
75
|
[WorkflowWorker, [ServiceProvider, "Config:workflow"]],
|
package/core/workflow/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export {
|
|
|
33
33
|
export {
|
|
34
34
|
IWorkflowRuntime,
|
|
35
35
|
type CancelInstance,
|
|
36
|
+
type EmitCause,
|
|
36
37
|
type EmitOptions,
|
|
37
38
|
type EmitResult,
|
|
38
39
|
type HistoryEntry,
|
|
@@ -41,6 +42,9 @@ export {
|
|
|
41
42
|
type InstanceSummary,
|
|
42
43
|
type RetryInstance,
|
|
43
44
|
type StartInstance,
|
|
45
|
+
type StartMany,
|
|
46
|
+
type StartManyItem,
|
|
47
|
+
type StartManyResult,
|
|
44
48
|
type TokenDetails,
|
|
45
49
|
} from "./runtime_module/services/IWorkflowRuntime.service";
|
|
46
50
|
export {
|
|
@@ -65,6 +69,7 @@ export type { InstanceStatus, StartedVia } from "./runtime_module/model/WfInstan
|
|
|
65
69
|
export type { TokenStatus } from "./runtime_module/model/WfToken.model";
|
|
66
70
|
export { WorkflowInputValidator, DefaultWorkflowInputValidator, type WorkflowInputContext } from "./contracts/WorkflowInputValidator";
|
|
67
71
|
export { WorkflowExtensions, DefaultWorkflowExtensions, type ExtensionTarget, type WorkflowPortContext } from "./contracts/WorkflowExtensions";
|
|
72
|
+
export { WorkflowPartitions, DefaultWorkflowPartitions, type ProcessPartition } from "./contracts/WorkflowPartitions";
|
|
68
73
|
export { WorkflowError, type WorkflowErrorCode } from "./contracts/WorkflowError";
|
|
69
74
|
export type { WorkflowActor, WorkflowAuditOptions, WorkflowPage, WorkflowPaging, WorkflowRequest, WorkflowToolMetadata } from "./contracts/types";
|
|
70
75
|
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { AccessModule } from "../../access";
|
|
2
2
|
import { Module, scoped, singleton } from "../../di";
|
|
3
3
|
import { DefaultWorkflowInputValidator, WorkflowInputValidator } from "../contracts/WorkflowInputValidator";
|
|
4
|
+
import { WorkflowProcessUsage } from "../contracts/WorkflowProcessUsage";
|
|
4
5
|
import { WorkflowDefinitionsModule } from "../definitions_module/WorkflowDefinitions.module";
|
|
5
6
|
import { runtimePermissions } from "./access/Runtime.permissions";
|
|
6
7
|
import { WorkflowRetention } from "./background/WorkflowRetention";
|
|
7
8
|
import { WorkflowWorker } from "./background/WorkflowWorker";
|
|
8
9
|
import { EventsController } from "./http/Events.controller";
|
|
9
10
|
import { InstancesController } from "./http/Instances.controller";
|
|
11
|
+
import { ChildCalls } from "./internal/ChildCalls";
|
|
12
|
+
import { EventDelivery } from "./internal/EventDelivery";
|
|
10
13
|
import { GraphCache } from "./internal/GraphCache";
|
|
14
|
+
import { RunLimits } from "./internal/RunLimits";
|
|
15
|
+
import { RuntimeProcessUsage } from "./internal/RuntimeProcessUsage";
|
|
11
16
|
import { Schedules } from "./internal/Schedules";
|
|
12
17
|
import { TimerStep } from "./internal/TimerStep";
|
|
13
18
|
import { ToolStep } from "./internal/ToolStep";
|
|
@@ -31,6 +36,10 @@ import { WorkflowRuntime } from "./services/WorkflowRuntime.service";
|
|
|
31
36
|
scoped(ToolStep),
|
|
32
37
|
scoped(TimerStep),
|
|
33
38
|
scoped(Schedules),
|
|
39
|
+
scoped(ChildCalls),
|
|
40
|
+
scoped(RunLimits),
|
|
41
|
+
scoped(EventDelivery),
|
|
42
|
+
scoped(WorkflowProcessUsage, RuntimeProcessUsage),
|
|
34
43
|
singleton(WorkflowInputValidator, DefaultWorkflowInputValidator),
|
|
35
44
|
scoped(IWorkflowRuntime, WorkflowRuntime),
|
|
36
45
|
],
|
|
@@ -10,7 +10,7 @@ import type { WorkflowSettings } from "../../contracts/Workflow.config";
|
|
|
10
10
|
const DAY_MS = 86_400_000;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Deletes `completed` and `
|
|
13
|
+
* Deletes `completed`, `cancelled` and `timedOut` instances older than their process's
|
|
14
14
|
* `retentionDays`, with tokens, jobs, subscriptions and history (by cascade),
|
|
15
15
|
* `retention.batchSize` at a time. `failed` instances wait for a person and stay.
|
|
16
16
|
* Each deleted batch is published as `WORKFLOW_INSTANCES_DELETED`, so parts
|
|
@@ -52,7 +52,7 @@ export class WorkflowRetention extends BackgroundService {
|
|
|
52
52
|
for (;;) {
|
|
53
53
|
if (signal?.aborted) return deleted;
|
|
54
54
|
const ids = (await db.instances.asNoTracking()
|
|
55
|
-
.where((i) => i.processKey.eq(policy.key).and(i.status.in(["completed", "cancelled"])).and(i.endedAt.lt(cutoff)))
|
|
55
|
+
.where((i) => i.processKey.eq(policy.key).and(i.status.in(["completed", "cancelled", "timedOut"])).and(i.endedAt.lt(cutoff)))
|
|
56
56
|
.orderBy((i) => i.id)
|
|
57
57
|
.take(batchSize)
|
|
58
58
|
.select((i) => ({ id: i.id }))
|
|
@@ -21,16 +21,16 @@ export interface WorkflowTaskRequested {
|
|
|
21
21
|
export interface WorkflowTokenClosed {
|
|
22
22
|
readonly instanceId: string;
|
|
23
23
|
readonly tokenId: string;
|
|
24
|
-
/** `cancelled` — the instance was cancelled; `interrupted` — a boundary
|
|
25
|
-
readonly reason: "cancelled" | "interrupted";
|
|
24
|
+
/** `cancelled` — the instance was cancelled; `interrupted` — a boundary event or a skipped iteration closed the step; `timedOut` — the run took too long. */
|
|
25
|
+
readonly reason: "cancelled" | "interrupted" | "timedOut";
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/** An instance finished: completed or
|
|
28
|
+
/** An instance finished: completed, cancelled or out of time. */
|
|
29
29
|
export interface WorkflowInstanceFinished {
|
|
30
30
|
readonly instanceId: string;
|
|
31
31
|
readonly processKey: string;
|
|
32
32
|
readonly partition: string | null;
|
|
33
|
-
readonly status: Extract<InstanceStatus, "completed" | "cancelled">;
|
|
33
|
+
readonly status: Extract<InstanceStatus, "completed" | "cancelled" | "timedOut">;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/** Retention deleted these finished instances with their tokens and history. */
|
|
@@ -5,7 +5,7 @@ import { buildListDocument } from "../../../../library/jsonapi";
|
|
|
5
5
|
import { body, callerOf, filterOf, listQuery, optionalString, pagingOfList, param, requiredString } from "../../internal/http";
|
|
6
6
|
import type { InstanceStatus } from "../model/WfInstance.model";
|
|
7
7
|
import { IWorkflowDefinitions } from "../../definitions_module/services/IWorkflowDefinitions.service";
|
|
8
|
-
import { IWorkflowRuntime, type InstanceSummary } from "../services/IWorkflowRuntime.service";
|
|
8
|
+
import { IWorkflowRuntime, type InstanceSummary, type StartManyItem } from "../services/IWorkflowRuntime.service";
|
|
9
9
|
|
|
10
10
|
/** Instances over HTTP (WORKFLOW-001 §4); rights are `workflow.instances.*` (WORKFLOW-002 §6.2). */
|
|
11
11
|
@Controller("workflow/instances")
|
|
@@ -33,12 +33,23 @@ export class InstancesController {
|
|
|
33
33
|
return Created(`${ctx.path}/${instance.id}`, instance);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** A start per element, for an action over many records (WORKFLOW-002 §5.1). */
|
|
37
|
+
@Post("bulk")
|
|
38
|
+
@Authorize(can("workflow.instances.start"))
|
|
39
|
+
async startMany(ctx: HttpContext) {
|
|
40
|
+
const request = await body(ctx);
|
|
41
|
+
const processKey = requiredString(request.processKey, "processKey", 64);
|
|
42
|
+
const partition = optionalString(request.partition, "partition") ?? null;
|
|
43
|
+
const { actor, request: from, language } = await callerOf(ctx, "workflow.instances.start", { scope: partition, process: processKey, folder: await this.definitions.folderOf(processKey) });
|
|
44
|
+
return { data: await this.runtime.startMany(processKey, { items: request.items as readonly StartManyItem[], partition, actor, request: from, language }) };
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
@Get()
|
|
37
48
|
@Authorize(can("workflow.instances.read"))
|
|
38
49
|
async list(ctx: HttpContext) {
|
|
39
50
|
const { access } = await callerOf(ctx);
|
|
40
51
|
const list = listQuery(ctx, {
|
|
41
|
-
filter: { partition: ["eq"], processKey: ["eq"], status: ["eq"], businessKey: ["eq"], startedById: ["eq"] },
|
|
52
|
+
filter: { partition: ["eq"], processKey: ["eq"], status: ["eq"], businessKey: ["eq"], startedById: ["eq"], parentInstanceId: ["eq"] },
|
|
42
53
|
sort: ["createdAt", "endedAt", "status", "processKey"],
|
|
43
54
|
});
|
|
44
55
|
const page = await this.runtime.instances({
|
|
@@ -49,6 +60,7 @@ export class InstancesController {
|
|
|
49
60
|
status: filterOf(list, "status") as InstanceStatus | undefined,
|
|
50
61
|
businessKey: filterOf(list, "businessKey"),
|
|
51
62
|
startedById: filterOf(list, "startedById"),
|
|
63
|
+
parentInstanceId: filterOf(list, "parentInstanceId"),
|
|
52
64
|
});
|
|
53
65
|
return buildListDocument(page.items, list, page.total, { basePath: ctx.path });
|
|
54
66
|
}
|