bazis 0.133.0 → 0.135.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 (52) hide show
  1. package/core/access/contracts/References.ts +2 -0
  2. package/core/agent/AgentToolExecutor.ts +5 -0
  3. package/core/agent/Tool.contract.ts +2 -2
  4. package/core/agent/metadata.ts +11 -0
  5. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  6. package/core/scripts/di-generate.ts +8 -0
  7. package/core/tools/StandardTools.module.ts +14 -0
  8. package/core/tools/generated/deps.ts +10 -0
  9. package/core/tools/index.ts +8 -0
  10. package/core/tools/orm/OrmEntity.references.ts +75 -0
  11. package/core/tools/orm/ormEntityTools.ts +423 -0
  12. package/core/workflow/contracts/WorkflowError.ts +2 -1
  13. package/core/workflow/contracts/WorkflowPartitions.ts +29 -0
  14. package/core/workflow/contracts/WorkflowProcessUsage.ts +8 -0
  15. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +9 -2
  16. package/core/workflow/definitions_module/access/Definitions.permissions.ts +4 -2
  17. package/core/workflow/definitions_module/access/ProcessPartition.lookup.ts +27 -0
  18. package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +13 -1
  19. package/core/workflow/definitions_module/code/inspectProcesses.ts +15 -4
  20. package/core/workflow/definitions_module/events/Definitions.events.ts +1 -1
  21. package/core/workflow/definitions_module/http/Processes.controller.ts +19 -4
  22. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +31 -0
  23. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +80 -6
  24. package/core/workflow/generated/deps.ts +13 -4
  25. package/core/workflow/index.ts +5 -0
  26. package/core/workflow/internal/WorkflowReferences.ts +69 -0
  27. package/core/workflow/runtime_module/WorkflowRuntime.module.ts +5 -0
  28. package/core/workflow/runtime_module/access/Runtime.permissions.ts +1 -1
  29. package/core/workflow/runtime_module/background/WorkflowRetention.ts +15 -11
  30. package/core/workflow/runtime_module/events/Runtime.events.ts +7 -0
  31. package/core/workflow/runtime_module/http/Instances.controller.ts +7 -2
  32. package/core/workflow/runtime_module/internal/ChildCalls.ts +1 -0
  33. package/core/workflow/runtime_module/internal/EventDelivery.ts +234 -0
  34. package/core/workflow/runtime_module/internal/RunLimits.ts +16 -5
  35. package/core/workflow/runtime_module/internal/RuntimeProcessUsage.ts +13 -0
  36. package/core/workflow/runtime_module/internal/Schedules.ts +97 -30
  37. package/core/workflow/runtime_module/internal/ToolStep.ts +16 -2
  38. package/core/workflow/runtime_module/internal/WorkflowEngine.ts +48 -9
  39. package/core/workflow/runtime_module/internal/WorkflowJobs.ts +7 -0
  40. package/core/workflow/runtime_module/model/WfJob.model.ts +7 -6
  41. package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +34 -1
  42. package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +40 -41
  43. package/core/workflow/tasks_module/events/TaskEvents.ts +17 -1
  44. package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +2 -0
  45. package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +2 -1
  46. package/library/workflow/cron.ts +103 -24
  47. package/library/workflow/index.ts +3 -2
  48. package/library/workflow/nodeKinds.ts +64 -9
  49. package/library/workflow/paths.ts +2 -2
  50. package/library/workflow/references.ts +106 -0
  51. package/library/workflow/validateProcess.ts +13 -0
  52. 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,13 +3,16 @@ 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";
9
+ import { WorkflowReferences } from "../internal/WorkflowReferences";
8
10
  import { WorkflowTools } from "../internal/WorkflowTools";
9
11
  import { definitionsPermissions } from "./access/Definitions.permissions";
10
12
  import { FolderReferences } from "./access/Folder.references";
11
13
  import { ProcessReferences } from "./access/Process.references";
12
14
  import { ProcessFolderLookup } from "./access/ProcessFolder.lookup";
15
+ import { ProcessPartitionLookup } from "./access/ProcessPartition.lookup";
13
16
  import { CatalogImport } from "./code/CatalogImport";
14
17
  import { CodeProcessCatalog } from "./code/CodeProcessCatalog";
15
18
  import { CodeProcessPublisher } from "./code/CodeProcessPublisher";
@@ -23,7 +26,8 @@ import { WorkflowDefinitions } from "./services/WorkflowDefinitions.service";
23
26
  /**
24
27
  * The process catalog: drafts and published versions, and the processes
25
28
  * declared in code ([passport](MODULE.md)). The application checks its own
26
- * `extensions` by registering its `WorkflowExtensions`.
29
+ * `extensions` by registering its `WorkflowExtensions` and says where processes
30
+ * work by registering its `WorkflowPartitions`.
27
31
  */
28
32
  @Module({
29
33
  imports: [AccessModule],
@@ -37,15 +41,18 @@ import { WorkflowDefinitions } from "./services/WorkflowDefinitions.service";
37
41
  singleton(CodeProcessCatalog),
38
42
  singleton(WorkflowLanguages),
39
43
  singleton(WorkflowExtensions, DefaultWorkflowExtensions),
44
+ singleton(WorkflowPartitions, DefaultWorkflowPartitions),
40
45
  scoped(WorkflowEvents),
46
+ scoped(WorkflowReferences),
41
47
  scoped(CatalogImport),
42
48
  scoped(IWorkflowDefinitions, WorkflowDefinitions),
43
49
  scoped(ProcessReferences),
44
50
  scoped(FolderReferences),
45
51
  scoped(ProcessFolderLookup),
52
+ scoped(ProcessPartitionLookup),
46
53
  singleton(HOSTED_SERVICE, CodeProcessPublisher),
47
54
  ],
48
55
  controllers: [ProcessesController],
49
- exports: [IWorkflowDefinitions, WorkflowTools, WorkflowClock, WorkflowEvents, WorkflowLanguages, WorkflowExtensions, ProcessFolderLookup],
56
+ exports: [IWorkflowDefinitions, WorkflowReferences, WorkflowTools, WorkflowClock, WorkflowEvents, WorkflowLanguages, WorkflowExtensions, WorkflowPartitions, ProcessFolderLookup],
50
57
  })
51
58
  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
+ }
@@ -1,6 +1,7 @@
1
1
  import { markConfigurationError, type ServiceProvider } from "../../../di";
2
2
  import type { HostedService } from "../../../di/extensions/hosted-service";
3
3
  import { LOGGER } from "../../../kernel";
4
+ import { WorkflowReferences } from "../../internal/WorkflowReferences";
4
5
  import { WorkflowTools } from "../../internal/WorkflowTools";
5
6
  import { IWorkflowDefinitions } from "../services/IWorkflowDefinitions.service";
6
7
  import { CatalogImport } from "./CatalogImport";
@@ -12,7 +13,9 @@ import { definitionProblems, WorkflowSetupError } from "./codeProcesses";
12
13
  * (WORKFLOW-002 §9), then publishes the processes of `@Module({ processes })`
13
14
  * (WORKFLOW-001 §14). A document with an unknown Tool or process, or an `extensions`
14
15
  * namespace nobody checks, stops the start with every finding of every process; the same
15
- * document again gives no new version.
16
+ * document again gives no new version. So does a reference kind no module owns, in a Tool
17
+ * or a document. A value that does not exist only warns: it may be data that is yet to come,
18
+ * and the step then fails with `ref_not_found`.
16
19
  */
17
20
  export class CodeProcessPublisher implements HostedService {
18
21
  readonly diagnosticName = "workflow:processes";
@@ -25,6 +28,11 @@ export class CodeProcessPublisher implements HostedService {
25
28
  try {
26
29
  const imported = await scope.resolve(CatalogImport).run();
27
30
  if (imported > 0) logger?.info("workflow catalog brought over from the old store", { processes: imported });
31
+ const references = scope.resolve(WorkflowReferences);
32
+ const unowned = references.unownedToolKinds();
33
+ if (unowned.length > 0) {
34
+ throw markConfigurationError(new WorkflowSetupError(`Tools refer to kinds no module owns:\n${unowned.map(({ tool, kind }) => ` - ${tool}: ${kind}${kind.startsWith("orm.") ? " (import StandardToolsModule from bazis/core/tools)" : " (declare it in @Module({ references }))"}`).join("\n")}`));
35
+ }
28
36
  const entries = this.services.resolve(CodeProcessCatalog).list();
29
37
  if (entries.length === 0) return;
30
38
  const tools = this.services.resolve(WorkflowTools);
@@ -35,6 +43,10 @@ export class CodeProcessPublisher implements HostedService {
35
43
  const found = [...definitionProblems(process, (name) => tools.has(name))];
36
44
  for (const issue of await definitions.validate(process.document, { processes })) {
37
45
  if (issue.severity === "error" && (issue.code === "extension_unknown" || issue.code === "extension_invalid" || issue.code === "unknown_process")) found.push(`${issue.message} (${issue.code})`);
46
+ else if (issue.code === "broken_ref") {
47
+ if (issue.message.startsWith("no module owns")) found.push(`${issue.message} (${issue.code})`);
48
+ else logger?.warn("workflow process refers to what does not exist", { process: process.key, node: issue.node, message: issue.message });
49
+ }
38
50
  }
39
51
  if (found.length > 0) problems.push(`${module}: process "${process.key}":\n${found.map((line) => ` - ${line}`).join("\n")}`);
40
52
  }
@@ -1,6 +1,6 @@
1
1
  import { expandModuleMetadata, type BazisModuleRef } from "../../../di";
2
- import { toolMetadataOf } from "../../../agent";
3
- import { validateProcess } from "../../../../library/workflow";
2
+ import { describeTool, toolMetadataOf } from "../../../agent";
3
+ import { documentReferences, schemaReferenceKinds, validateProcess } from "../../../../library/workflow";
4
4
  import { WORKFLOW_CALLER } from "../../internal/WorkflowTools";
5
5
  import { WorkflowDefinitionsModule } from "../WorkflowDefinitions.module";
6
6
  import { definitionProblems, type OwnedCodeProcess } from "./codeProcesses";
@@ -28,16 +28,23 @@ export function inspectProcesses(root: BazisModuleRef): ProcessInspection {
28
28
  const found: OwnedCodeProcess[] = [];
29
29
  const errors: string[] = [];
30
30
  const tools = new Set<string>();
31
+ const schemas = new Map<string, unknown>();
32
+ const kinds = new Set<string>();
31
33
  const visited = new Set<BazisModuleRef>();
32
34
  const visit = (moduleRef: BazisModuleRef): void => {
33
35
  if (visited.has(moduleRef)) return;
34
36
  visited.add(moduleRef);
35
37
  const module = typeof moduleRef === "function" ? moduleRef.name || "<anonymous module>" : "<module>";
36
- const fields = moduleRef as { readonly processes?: unknown; readonly tools?: unknown };
38
+ const fields = moduleRef as { readonly processes?: unknown; readonly tools?: unknown; readonly references?: unknown };
39
+ if (typeof fields.references === "object" && fields.references !== null) for (const kind of Object.keys(fields.references)) kinds.add(kind);
37
40
  if (Array.isArray(fields.tools)) {
38
41
  for (const target of fields.tools) {
39
42
  const metadata = typeof target === "function" ? toolMetadataOf(target as new () => object) : undefined;
40
- if (metadata?.callers.includes(WORKFLOW_CALLER)) tools.add(metadata.name);
43
+ if (metadata?.callers.includes(WORKFLOW_CALLER)) {
44
+ tools.add(metadata.name);
45
+ const input = describeTool({ target: target as new () => object, metadata }).input;
46
+ if (input?.kind === "json-schema") schemas.set(metadata.name, input.schema);
47
+ }
41
48
  }
42
49
  }
43
50
  if (fields.processes !== undefined) {
@@ -54,6 +61,10 @@ export function inspectProcesses(root: BazisModuleRef): ProcessInspection {
54
61
  if (found.length > 0 && !visited.has(WorkflowDefinitionsModule)) {
55
62
  errors.push(`${found[0]!.module} declares processes, but the application does not import WorkflowModule (bazis/core/workflow).`);
56
63
  }
64
+ // The values live in the database: `check` knows only whether someone owns their kind.
65
+ for (const [tool, schema] of schemas) {
66
+ for (const kind of schemaReferenceKinds(schema)) if (!kinds.has(kind)) errors.push(`Tool ${tool} refers to the kind ${kind}, and no module owns it.`);
67
+ }
57
68
  const owners = new Map<string, string>();
58
69
  for (const { process, module } of found) {
59
70
  const other = owners.get(process.key);
@@ -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);
@@ -144,6 +154,11 @@ export class ProcessesController {
144
154
  return { process: key, folder: await this.definitions.folderOf(key) };
145
155
  }
146
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
+
147
162
  private localized(items: readonly ProcessSummary[], language: string, ctx: HttpContext) {
148
163
  return items.map((item) => withTitleText(item, language, defaultLanguage(ctx)));
149
164
  }
@@ -92,6 +92,24 @@ export interface SavedDraft {
92
92
  readonly draftUpdatedAt: Date;
93
93
  }
94
94
 
95
+ export interface ProcessReferenceQuery {
96
+ readonly kind: string;
97
+ readonly id: string;
98
+ /** The area of the value: the table of a field. Without it any area goes. */
99
+ readonly within?: string;
100
+ }
101
+
102
+ /** A process that refers to a value, and where. */
103
+ export interface ProcessReference {
104
+ readonly processKey: string;
105
+ readonly version: number;
106
+ readonly node?: string;
107
+ readonly variable?: string;
108
+ readonly within?: string;
109
+ /** Where in the input of the step: `/values/status`. */
110
+ readonly path: string;
111
+ }
112
+
95
113
  /**
96
114
  * The process catalog: drafts and immutable published versions
97
115
  * (WORKFLOW-001 §3). Errors are {@link WorkflowError}: `not_found`, `conflict`,
@@ -102,6 +120,8 @@ export abstract class IWorkflowDefinitions {
102
120
  abstract get(key: string): Promise<ProcessDetails>;
103
121
  /** The folder of a process, for a check of rights; `null` also for a process that does not exist. */
104
122
  abstract folderOf(key: string): Promise<string | null>;
123
+ /** The partitions a process works in, for a check of rights. */
124
+ abstract partitionsOf(key: string): Promise<readonly (string | null)[]>;
105
125
  /** Every finding of a document, with the application's `extensions` checks; nothing is stored. */
106
126
  abstract validate(document: unknown, options?: ValidateOptions): Promise<readonly ProcessIssue[]>;
107
127
  /** What a designer builds its forms from: node kinds, functions, paths and Tools. */
@@ -109,6 +129,12 @@ export abstract class IWorkflowDefinitions {
109
129
  abstract create(process: CreateProcess, actor: WorkflowActor, options?: WorkflowAuditOptions): Promise<ProcessDetails>;
110
130
  /** A process declared in code takes only `archived`; other fields give `conflict`. */
111
131
  abstract update(key: string, changes: UpdateProcess, actor: WorkflowActor, options?: WorkflowAuditOptions): Promise<ProcessDetails>;
132
+ /**
133
+ * Deletes an archived process with its versions: `wrong_status` when it is not
134
+ * archived, `conflict` when it is declared in code, `in_use` when it has unfinished
135
+ * instances or partitions. Finished instances and their history stay.
136
+ */
137
+ abstract remove(key: string, actor: WorkflowActor, options?: WorkflowAuditOptions): Promise<void>;
112
138
  /** Stores the draft even with errors; `conflict` for a process declared in code and when `expected` is out of date. */
113
139
  abstract saveDraft(key: string, document: unknown, actor: WorkflowActor, options?: SaveDraftOptions): Promise<SavedDraft>;
114
140
  /**
@@ -124,6 +150,11 @@ export abstract class IWorkflowDefinitions {
124
150
  abstract published(): Promise<readonly VersionDetails[]>;
125
151
  /** A version by id, for the engine; `undefined` if there is none. */
126
152
  abstract versionById(id: string): Promise<VersionDetails | undefined>;
153
+ /**
154
+ * Who refers to a value (WORKFLOW-002 §6.3), by the latest published versions: the
155
+ * application forbids or warns before it removes a field or a table.
156
+ */
157
+ abstract referencing(reference: ProcessReferenceQuery): Promise<readonly ProcessReference[]>;
127
158
  /** Processes with a retention period: what the retention service deletes. */
128
159
  abstract retentionPolicies(): Promise<readonly { readonly key: string; readonly retentionDays: number }[]>;
129
160
  /** The Tools a service task may call, with their input and output schemas. */
@@ -2,15 +2,18 @@ import { grantsToWhere } from "../../../access";
2
2
  import { createHash } from "node:crypto";
3
3
  import { paginate, UniqueViolationError } from "../../../orm";
4
4
  import type { AgentToolContract } from "../../../agent";
5
- import { allTexts, checkLocalizedText, emptyProcess, hasErrors, processPalette, validateProcess, type LocalizedText, type ProcessDocument, type ProcessIssue, type ProcessPalette } from "../../../../library/workflow";
5
+ import { allTexts, checkLocalizedText, emptyProcess, hasErrors, processPalette, validateProcess, type DocumentReference, type LocalizedText, type ProcessDocument, type ProcessIssue, type ProcessPalette } from "../../../../library/workflow";
6
6
  import type { ServiceProvider } from "../../../di";
7
7
  import { isUuid, pageQueryOf, type WorkflowActor, type WorkflowAuditOptions, type WorkflowPage } from "../../contracts/types";
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";
16
+ import type { WorkflowReferences } from "../../internal/WorkflowReferences";
14
17
  import type { WorkflowTools } from "../../internal/WorkflowTools";
15
18
  import type { CodeProcessCatalog } from "../code/CodeProcessCatalog";
16
19
  import type { OwnedCodeProcess } from "../code/codeProcesses";
@@ -23,6 +26,8 @@ import {
23
26
  type CreateProcess,
24
27
  type ProcessDetails,
25
28
  type ProcessListQuery,
29
+ type ProcessReference,
30
+ type ProcessReferenceQuery,
26
31
  type ProcessSummary,
27
32
  type SavedDraft,
28
33
  type SaveDraftOptions,
@@ -46,6 +51,7 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
46
51
  private readonly clock: WorkflowClock,
47
52
  private readonly events: WorkflowEvents,
48
53
  private readonly languages: WorkflowLanguages,
54
+ private readonly references: WorkflowReferences,
49
55
  /** The scope: the port is asked here, so that the application's own registration replaces the default. */
50
56
  private readonly services: ServiceProvider,
51
57
  ) {
@@ -83,13 +89,22 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
83
89
  return row?.folder ?? null;
84
90
  }
85
91
 
92
+ async partitionsOf(key: string): Promise<readonly (string | null)[]> {
93
+ const partitions = await this.partitions(key, null);
94
+ return partitions.filter((item) => item.enabled).map((item) => item.partition);
95
+ }
96
+
97
+ private async partitions(key: string, actor: WorkflowActor | null) {
98
+ return await this.services.resolve(WorkflowPartitions).of(key, { partition: null, language: this.languages.default, actor });
99
+ }
100
+
86
101
  async validate(document: unknown, options: ValidateOptions = {}): Promise<readonly ProcessIssue[]> {
87
102
  const port = this.services.resolve(WorkflowExtensions);
88
103
  const namespaces = new Set(port.namespaces());
89
104
  const published = await this.db.processes.asNoTracking().where((p) => p.archivedAt.isNull().and(p.latestVersion.isNotNull())).select((p) => ({ key: p.key })).toList();
90
105
  const processes = new Set([...published.map((row) => row.key), ...options.processes ?? []]);
91
106
  const issues = validateProcess(document, { tools: (name) => this.workflowTools.has(name), processes: (key) => processes.has(key), extensions: (namespace) => namespaces.has(namespace) });
92
- if (namespaces.size === 0 || issues.some((issue) => issue.code === "too_large" || issue.code === "schema")) return issues;
107
+ if (issues.some((issue) => issue.code === "too_large" || issue.code === "schema")) return issues;
93
108
  const context: WorkflowPortContext = { partition: null, language: options.language ?? this.languages.default, actor: options.actor ?? null };
94
109
  const found: ProcessIssue[] = [...issues];
95
110
  const { extensions, variables, nodes } = document as ProcessDocument;
@@ -100,9 +115,52 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
100
115
  for (const message of await port.check(namespace, value, target, context)) found.push({ severity: "error", code: "extension_invalid", message: `${namespace}: ${message}`, ...target });
101
116
  }
102
117
  };
103
- await checkAll(extensions, {});
104
- if (Array.isArray(variables)) for (const variable of variables) await checkAll(variable?.extensions, { variable: variable?.name });
105
- for (const [node, raw] of Object.entries(nodes)) await checkAll(raw?.extensions, { node });
118
+ if (namespaces.size > 0) {
119
+ await checkAll(extensions, {});
120
+ if (Array.isArray(variables)) for (const variable of variables) await checkAll(variable?.extensions, { variable: variable?.name });
121
+ for (const [node, raw] of Object.entries(nodes)) await checkAll(raw?.extensions, { node });
122
+ }
123
+ found.push(...await this.brokenReferences(document as ProcessDocument, context.language));
124
+ return found;
125
+ }
126
+
127
+ /** The literal references no owner knows. The processes a document calls are told above as `unknown_process`. */
128
+ private async brokenReferences(document: ProcessDocument, language: string): Promise<ProcessIssue[]> {
129
+ let references: DocumentReference[];
130
+ try {
131
+ references = this.references.of(document);
132
+ } catch {
133
+ // A document that is not whole yet: its shape has been told above.
134
+ return [];
135
+ }
136
+ const missing = await this.references.missing(references.filter((reference) => reference.kind !== "workflow.process"), { scope: null, language });
137
+ return missing.map((reference) => ({
138
+ severity: "error" as const,
139
+ code: "broken_ref" as const,
140
+ message: this.references.owned(reference.kind)
141
+ ? `${reference.kind} ${JSON.stringify(reference.id)}${reference.within === undefined ? "" : ` in ${reference.within}`} does not exist`
142
+ : `no module owns the reference kind ${reference.kind}`,
143
+ ...(reference.node === undefined ? {} : { node: reference.node }),
144
+ ...(reference.variable === undefined ? {} : { variable: reference.variable }),
145
+ }));
146
+ }
147
+
148
+ async referencing(reference: ProcessReferenceQuery): Promise<readonly ProcessReference[]> {
149
+ const found: ProcessReference[] = [];
150
+ for (const version of await this.published()) {
151
+ for (const item of this.references.of(version.document)) {
152
+ if (item.kind !== reference.kind || item.id !== reference.id) continue;
153
+ if (reference.within !== undefined && item.within !== reference.within) continue;
154
+ found.push({
155
+ processKey: version.key,
156
+ version: version.version,
157
+ path: item.path,
158
+ ...(item.node === undefined ? {} : { node: item.node }),
159
+ ...(item.variable === undefined ? {} : { variable: item.variable }),
160
+ ...(item.within === undefined ? {} : { within: item.within }),
161
+ });
162
+ }
163
+ }
106
164
  return found;
107
165
  }
108
166
 
@@ -153,6 +211,19 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
153
211
  return this.detailsOf(row);
154
212
  }
155
213
 
214
+ async remove(key: string, actor: WorkflowActor, options: WorkflowAuditOptions = {}): Promise<void> {
215
+ const row = await this.process(key, false);
216
+ this.assertNotCode(key);
217
+ if (row.archivedAt === null) throw new WorkflowError("wrong_status", `Process ${key} is not archived`);
218
+ // An archived process starts nothing more, so what is counted here cannot grow.
219
+ const connected = (await this.partitions(key, actor)).filter((item) => item.partition !== null).map((item) => item.partition);
220
+ if (connected.length > 0) throw new WorkflowError("in_use", `Process ${key} is connected to partitions: ${connected.join(", ")}`);
221
+ const live = await this.services.tryResolve(WorkflowProcessUsage)?.liveInstances(key) ?? 0;
222
+ if (live > 0) throw new WorkflowError("in_use", `Process ${key} has ${live} unfinished instances`);
223
+ await this.db.processes.asNoTracking().where((p) => p.key.eq(key)).executeDelete();
224
+ await this.changed("deleted", key, actor, options);
225
+ }
226
+
156
227
  async saveDraft(key: string, document: unknown, actor: WorkflowActor, options: SaveDraftOptions = {}): Promise<SavedDraft> {
157
228
  const issues = await this.validate(document, { actor, ...(options.language === undefined ? {} : { language: options.language }) });
158
229
  if (issues.some((issue) => issue.code === "too_large")) throw new WorkflowError("invalid_document", "The document is too large", issues);
@@ -175,12 +246,15 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
175
246
  }
176
247
 
177
248
  async publish(key: string, actor: WorkflowActor, options: WorkflowAuditOptions & Pick<ValidateOptions, "language"> = {}): Promise<VersionDetails> {
249
+ // Before the transaction: the owners of references and the ports read their own contexts.
250
+ const draft = await this.process(key, false);
251
+ const issues = await this.validate(draft.draft, { actor, processes: [key], ...(options.language === undefined ? {} : { language: options.language }) });
178
252
  return await this.db.transactionScope(async (transaction) => {
179
253
  const row = await this.db.processes.where((p) => p.key.eq(key)).forUpdate().firstOrDefault();
180
254
  if (!row) throw notFound(key);
181
255
  this.assertNotCode(key);
182
256
  if (row.archivedAt !== null) throw new WorkflowError("archived", `Process ${key} is archived`);
183
- const issues = await this.validate(row.draft, { actor, processes: [key], ...(options.language === undefined ? {} : { language: options.language }) });
257
+ if (canonicalJson(row.draft) !== canonicalJson(draft.draft)) throw new WorkflowError("conflict", `The draft of ${key} was saved while it was being published`);
184
258
  if (hasErrors(issues)) throw new WorkflowError("invalid_document", `Process ${key} has errors and cannot be published`, issues);
185
259
  const { version, created } = await this.publishDraft(row, actor);
186
260
  if (created) transaction.afterCommit(() => this.changed("published", key, actor, options, version.version));
@@ -2,12 +2,14 @@
2
2
  // Do not edit manually.
3
3
  import { registerFrameworkClassDeps } from "../../di/module/autoDeps";
4
4
  import { DbContextOptions } from "../../../library/orm/DbContextOptions";
5
+ import { IAccessCatalog } from "../../access/catalog/IAccessCatalog.service";
5
6
  import { ServiceProvider } from "../../di/ServiceProvider";
6
7
  import { EventBus } from "../../kernel/events/EventBus";
7
8
  import { WorkflowClock } from "../contracts/WorkflowClock";
8
9
  import { FolderReferences } from "../definitions_module/access/Folder.references";
9
10
  import { ProcessReferences } from "../definitions_module/access/Process.references";
10
11
  import { ProcessFolderLookup } from "../definitions_module/access/ProcessFolder.lookup";
12
+ import { ProcessPartitionLookup } from "../definitions_module/access/ProcessPartition.lookup";
11
13
  import { CatalogImport } from "../definitions_module/code/CatalogImport";
12
14
  import { CodeProcessCatalog } from "../definitions_module/code/CodeProcessCatalog";
13
15
  import { CodeProcessPublisher } from "../definitions_module/code/CodeProcessPublisher";
@@ -17,14 +19,17 @@ import { IWorkflowDefinitions } from "../definitions_module/services/IWorkflowDe
17
19
  import { WorkflowDefinitions } from "../definitions_module/services/WorkflowDefinitions.service";
18
20
  import { WorkflowEvents } from "../internal/WorkflowEvents";
19
21
  import { WorkflowLanguages } from "../internal/WorkflowLanguages";
22
+ import { WorkflowReferences } from "../internal/WorkflowReferences";
20
23
  import { WorkflowTools } from "../internal/WorkflowTools";
21
24
  import { WorkflowRetention } from "../runtime_module/background/WorkflowRetention";
22
25
  import { WorkflowWorker } from "../runtime_module/background/WorkflowWorker";
23
26
  import { EventsController } from "../runtime_module/http/Events.controller";
24
27
  import { InstancesController } from "../runtime_module/http/Instances.controller";
25
28
  import { ChildCalls } from "../runtime_module/internal/ChildCalls";
29
+ import { EventDelivery } from "../runtime_module/internal/EventDelivery";
26
30
  import { GraphCache } from "../runtime_module/internal/GraphCache";
27
31
  import { RunLimits } from "../runtime_module/internal/RunLimits";
32
+ import { RuntimeProcessUsage } from "../runtime_module/internal/RuntimeProcessUsage";
28
33
  import { Schedules } from "../runtime_module/internal/Schedules";
29
34
  import { TimerStep } from "../runtime_module/internal/TimerStep";
30
35
  import { ToolStep } from "../runtime_module/internal/ToolStep";
@@ -44,26 +49,30 @@ registerFrameworkClassDeps([
44
49
  [CodeProcessCatalog, [ServiceProvider]],
45
50
  [CodeProcessPublisher, [ServiceProvider]],
46
51
  [DefinitionsDbContext, [DbContextOptions]],
52
+ [EventDelivery, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions, WorkflowLanguages, ServiceProvider]],
47
53
  [EventsController, [IWorkflowRuntime]],
48
54
  [FolderReferences, [DefinitionsDbContext]],
49
55
  [InstancesController, [IWorkflowRuntime, IWorkflowDefinitions]],
50
56
  [ProcessesController, [IWorkflowDefinitions]],
51
57
  [ProcessFolderLookup, [DefinitionsDbContext]],
58
+ [ProcessPartitionLookup, [DefinitionsDbContext, WorkflowLanguages, ServiceProvider]],
52
59
  [ProcessReferences, [DefinitionsDbContext]],
53
60
  [RunLimits, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions]],
54
61
  [RuntimeDbContext, [DbContextOptions]],
55
- [Schedules, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions]],
62
+ [RuntimeProcessUsage, [RuntimeDbContext]],
63
+ [Schedules, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions, WorkflowLanguages, ServiceProvider]],
56
64
  [TaskEvents, [TasksDbContext]],
57
65
  [TasksController, [IWorkflowTasks]],
58
66
  [TasksDbContext, [DbContextOptions]],
59
67
  [TimerStep, [RuntimeDbContext, WorkflowEngine]],
60
- [ToolStep, [RuntimeDbContext, WorkflowEngine, WorkflowTools]],
61
- [WorkflowDefinitions, [DefinitionsDbContext, WorkflowTools, CodeProcessCatalog, WorkflowClock, WorkflowEvents, WorkflowLanguages, ServiceProvider]],
68
+ [ToolStep, [RuntimeDbContext, WorkflowEngine, WorkflowTools, WorkflowReferences, WorkflowLanguages]],
69
+ [WorkflowDefinitions, [DefinitionsDbContext, WorkflowTools, CodeProcessCatalog, WorkflowClock, WorkflowEvents, WorkflowLanguages, WorkflowReferences, ServiceProvider]],
62
70
  [WorkflowEngine, [RuntimeDbContext, IWorkflowDefinitions, GraphCache, WorkflowEvents, WorkflowClock]],
63
71
  [WorkflowEvents, [EventBus, ServiceProvider, "optional:Logger"]],
64
72
  [WorkflowLanguages, ["Config:workflow"]],
73
+ [WorkflowReferences, [IAccessCatalog, WorkflowTools, ServiceProvider]],
65
74
  [WorkflowRetention, [ServiceProvider, "Config:workflow"]],
66
- [WorkflowRuntime, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions, ServiceProvider, WorkflowLanguages]],
75
+ [WorkflowRuntime, [RuntimeDbContext, WorkflowEngine, EventDelivery, IWorkflowDefinitions, ServiceProvider, WorkflowLanguages]],
67
76
  [WorkflowTasks, [TasksDbContext, IWorkflowRuntime, WorkflowClock, WorkflowEvents]],
68
77
  [WorkflowTools, [ServiceProvider]],
69
78
  [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