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.
Files changed (57) hide show
  1. package/core/agent/AgentToolExecutor.ts +5 -0
  2. package/core/agent/Tool.contract.ts +2 -2
  3. package/core/agent/metadata.ts +11 -0
  4. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  5. package/core/scripts/di-generate.ts +8 -0
  6. package/core/tools/StandardTools.module.ts +14 -0
  7. package/core/tools/generated/deps.ts +10 -0
  8. package/core/tools/index.ts +8 -0
  9. package/core/tools/orm/OrmEntity.references.ts +71 -0
  10. package/core/tools/orm/ormEntityTools.ts +423 -0
  11. package/core/workflow/contracts/WorkflowError.ts +2 -1
  12. package/core/workflow/contracts/WorkflowPartitions.ts +29 -0
  13. package/core/workflow/contracts/WorkflowProcessUsage.ts +8 -0
  14. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +7 -2
  15. package/core/workflow/definitions_module/access/Definitions.permissions.ts +4 -2
  16. package/core/workflow/definitions_module/access/ProcessPartition.lookup.ts +27 -0
  17. package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +5 -4
  18. package/core/workflow/definitions_module/events/Definitions.events.ts +1 -1
  19. package/core/workflow/definitions_module/http/Processes.controller.ts +21 -5
  20. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +10 -0
  21. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +28 -2
  22. package/core/workflow/generated/deps.ts +12 -2
  23. package/core/workflow/index.ts +5 -0
  24. package/core/workflow/runtime_module/WorkflowRuntime.module.ts +9 -0
  25. package/core/workflow/runtime_module/background/WorkflowRetention.ts +2 -2
  26. package/core/workflow/runtime_module/events/Runtime.events.ts +4 -4
  27. package/core/workflow/runtime_module/http/Instances.controller.ts +14 -2
  28. package/core/workflow/runtime_module/internal/ChildCalls.ts +186 -0
  29. package/core/workflow/runtime_module/internal/EventDelivery.ts +234 -0
  30. package/core/workflow/runtime_module/internal/GraphCache.ts +7 -1
  31. package/core/workflow/runtime_module/internal/RunLimits.ts +134 -0
  32. package/core/workflow/runtime_module/internal/RuntimeProcessUsage.ts +13 -0
  33. package/core/workflow/runtime_module/internal/Schedules.ts +97 -30
  34. package/core/workflow/runtime_module/internal/TimerStep.ts +11 -1
  35. package/core/workflow/runtime_module/internal/ToolStep.ts +4 -4
  36. package/core/workflow/runtime_module/internal/WorkflowEngine.ts +544 -132
  37. package/core/workflow/runtime_module/internal/WorkflowJobs.ts +44 -6
  38. package/core/workflow/runtime_module/model/WfHistory.model.ts +1 -1
  39. package/core/workflow/runtime_module/model/WfInstance.model.ts +7 -4
  40. package/core/workflow/runtime_module/model/WfJob.model.ts +18 -8
  41. package/core/workflow/runtime_module/model/WfToken.model.ts +5 -3
  42. package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +59 -4
  43. package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +72 -42
  44. package/core/workflow/tasks_module/events/TaskEvents.ts +4 -8
  45. package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +1 -1
  46. package/library/workflow/ProcessDocument.ts +14 -0
  47. package/library/workflow/cron.ts +103 -24
  48. package/library/workflow/expressions.ts +5 -1
  49. package/library/workflow/index.ts +2 -2
  50. package/library/workflow/masking.ts +17 -3
  51. package/library/workflow/mermaid.ts +22 -6
  52. package/library/workflow/nodeKinds.ts +123 -9
  53. package/library/workflow/palette.ts +3 -1
  54. package/library/workflow/paths.ts +10 -4
  55. package/library/workflow/structure.ts +33 -4
  56. package/library/workflow/validateProcess.ts +141 -17
  57. 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, so these have no partition until `WorkflowPartitions` connects
9
- * processes to partitions (stage 4).
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` namespace
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.objectOf(key));
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.objectOf(key));
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.objectOf(key));
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
- return { issues: await this.definitions.validate(request.document, { actor, language }) };
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 issues = validateProcess(document, { tools: (name) => this.workflowTools.has(name), extensions: (namespace) => namespaces.has(namespace) });
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
- [Schedules, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions]],
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"]],
@@ -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 `cancelled` instances older than their process's
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 timer took over the step. */
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 cancelled. */
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
  }