bazis 0.130.0 → 0.132.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/cli/templates/process.ts +1 -1
- package/core/agent/AgentToolExecutor.ts +3 -0
- package/core/agent/Tool.contract.ts +1 -1
- package/core/agent/index.ts +1 -0
- package/core/agent/metadata.ts +9 -0
- package/core/agent/semantic.ts +8 -1
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/workflow/{runtime_module/config/Runtime.config.ts → contracts/Workflow.config.ts} +15 -6
- package/core/workflow/contracts/WorkflowExtensions.ts +40 -0
- package/core/workflow/contracts/WorkflowInputValidator.ts +6 -5
- package/core/workflow/contracts/types.ts +37 -1
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +18 -3
- package/core/workflow/definitions_module/access/Definitions.permissions.ts +4 -3
- package/core/workflow/definitions_module/access/Folder.references.ts +37 -0
- package/core/workflow/definitions_module/access/Process.references.ts +1 -1
- package/core/workflow/definitions_module/access/ProcessFolder.lookup.ts +20 -0
- package/core/workflow/definitions_module/code/CatalogImport.ts +95 -0
- package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +26 -16
- package/core/workflow/definitions_module/code/codeProcesses.ts +7 -3
- package/core/workflow/definitions_module/code/defineProcess.ts +10 -4
- package/core/workflow/definitions_module/events/Definitions.events.ts +16 -0
- package/core/workflow/definitions_module/http/Processes.controller.ts +72 -25
- package/core/workflow/definitions_module/model/Definitions.dbContext.ts +2 -2
- package/core/workflow/definitions_module/model/WfProcess.model.ts +19 -5
- package/core/workflow/definitions_module/model/WfVersion.model.ts +1 -1
- package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +49 -14
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +128 -45
- package/core/workflow/generated/deps.ts +15 -5
- package/core/workflow/index.ts +17 -3
- package/core/workflow/internal/WorkflowEvents.ts +23 -0
- package/core/workflow/internal/WorkflowLanguages.ts +35 -0
- package/core/workflow/internal/canonicalJson.ts +8 -0
- package/core/workflow/internal/http.ts +53 -5
- package/core/workflow/runtime_module/WorkflowRuntime.module.ts +0 -2
- package/core/workflow/runtime_module/access/Runtime.permissions.ts +5 -3
- package/core/workflow/runtime_module/background/WorkflowRetention.ts +1 -1
- package/core/workflow/runtime_module/background/WorkflowWorker.ts +2 -2
- package/core/workflow/runtime_module/events/Runtime.events.ts +30 -3
- package/core/workflow/runtime_module/http/Events.controller.ts +2 -1
- package/core/workflow/runtime_module/http/Instances.controller.ts +32 -21
- package/core/workflow/runtime_module/internal/Schedules.ts +2 -2
- package/core/workflow/runtime_module/internal/ToolStep.ts +42 -10
- package/core/workflow/runtime_module/internal/WorkflowEngine.ts +83 -31
- package/core/workflow/runtime_module/internal/WorkflowJobs.ts +2 -4
- package/core/workflow/runtime_module/model/Runtime.dbContext.ts +2 -2
- package/core/workflow/runtime_module/model/WfHistory.model.ts +2 -2
- package/core/workflow/runtime_module/model/WfInstance.model.ts +28 -3
- package/core/workflow/runtime_module/model/WfJob.model.ts +17 -1
- package/core/workflow/runtime_module/model/WfSubscription.model.ts +1 -1
- package/core/workflow/runtime_module/model/WfToken.model.ts +11 -1
- package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +19 -6
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +20 -17
- package/core/workflow/tasks_module/events/Tasks.events.ts +15 -0
- package/core/workflow/tasks_module/http/Tasks.controller.ts +21 -18
- package/core/workflow/tasks_module/model/Tasks.dbContext.ts +2 -2
- package/core/workflow/tasks_module/model/WfTask.model.ts +8 -4
- package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +4 -4
- package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +11 -8
- package/library/openapi/codegen.ts +12 -0
- package/library/validation/types/ValidatorOptions.ts +12 -0
- package/library/workflow/ProcessDocument.ts +59 -23
- package/library/workflow/expressionSyntax.ts +243 -0
- package/library/workflow/expressions.ts +343 -0
- package/library/workflow/formFields.ts +120 -34
- package/library/workflow/index.ts +48 -9
- package/library/workflow/localized.ts +35 -0
- package/library/workflow/masking.ts +58 -0
- package/library/workflow/mermaid.ts +18 -8
- package/library/workflow/nodeKinds.ts +88 -17
- package/library/workflow/palette.ts +108 -0
- package/library/workflow/paths.ts +31 -6
- package/library/workflow/structure.ts +282 -0
- package/library/workflow/upgradeDocument.ts +84 -0
- package/library/workflow/validateProcess.ts +130 -48
- package/package.json +1 -1
- package/library/workflow/conditions.ts +0 -44
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { grantsToWhere } from "../../../access";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { UniqueViolationError } from "../../../orm";
|
|
3
|
+
import { paginate, UniqueViolationError } from "../../../orm";
|
|
4
4
|
import type { AgentToolContract } from "../../../agent";
|
|
5
|
-
import { emptyProcess, hasErrors, validateProcess, type ProcessDocument, type ProcessIssue } from "../../../../library/workflow";
|
|
6
|
-
import
|
|
5
|
+
import { allTexts, checkLocalizedText, emptyProcess, hasErrors, processPalette, validateProcess, type LocalizedText, type ProcessDocument, type ProcessIssue, type ProcessPalette } from "../../../../library/workflow";
|
|
6
|
+
import type { ServiceProvider } from "../../../di";
|
|
7
|
+
import { isUuid, pageQueryOf, type WorkflowActor, type WorkflowAuditOptions, type WorkflowPage } from "../../contracts/types";
|
|
7
8
|
import type { WorkflowClock } from "../../contracts/WorkflowClock";
|
|
8
9
|
import { WorkflowError } from "../../contracts/WorkflowError";
|
|
10
|
+
import { WorkflowExtensions, type ExtensionTarget, type WorkflowPortContext } from "../../contracts/WorkflowExtensions";
|
|
11
|
+
import { canonicalJson } from "../../internal/canonicalJson";
|
|
12
|
+
import type { WorkflowEvents } from "../../internal/WorkflowEvents";
|
|
13
|
+
import type { WorkflowLanguages } from "../../internal/WorkflowLanguages";
|
|
9
14
|
import type { WorkflowTools } from "../../internal/WorkflowTools";
|
|
10
15
|
import type { CodeProcessCatalog } from "../code/CodeProcessCatalog";
|
|
11
16
|
import type { OwnedCodeProcess } from "../code/codeProcesses";
|
|
17
|
+
import { WORKFLOW_PROCESS_CHANGED, type ProcessChange } from "../events/Definitions.events";
|
|
12
18
|
import type { DefinitionsDbContext } from "../model/Definitions.dbContext";
|
|
13
19
|
import { WfProcess } from "../model/WfProcess.model";
|
|
14
20
|
import { WfVersion } from "../model/WfVersion.model";
|
|
@@ -18,12 +24,17 @@ import {
|
|
|
18
24
|
type ProcessDetails,
|
|
19
25
|
type ProcessListQuery,
|
|
20
26
|
type ProcessSummary,
|
|
27
|
+
type SavedDraft,
|
|
28
|
+
type SaveDraftOptions,
|
|
21
29
|
type UpdateProcess,
|
|
30
|
+
type ValidateOptions,
|
|
22
31
|
type VersionDetails,
|
|
23
32
|
type VersionSummary,
|
|
24
33
|
} from "./IWorkflowDefinitions.service";
|
|
25
34
|
|
|
26
35
|
const KEY = /^[a-z][a-z0-9_.-]{0,63}$/;
|
|
36
|
+
/** A path of segments without empty ones: `Sales/Orders`. */
|
|
37
|
+
const FOLDER = /^[^/\s](?:[^/]*[^/\s])?(?:\/[^/\s](?:[^/]*[^/\s])?)*$/;
|
|
27
38
|
/** Who publishes the processes declared in code. */
|
|
28
39
|
const CODE_ACTOR: WorkflowActor = { kind: "system", id: "code" };
|
|
29
40
|
|
|
@@ -33,12 +44,15 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
33
44
|
private readonly workflowTools: WorkflowTools,
|
|
34
45
|
private readonly code: CodeProcessCatalog,
|
|
35
46
|
private readonly clock: WorkflowClock,
|
|
47
|
+
private readonly events: WorkflowEvents,
|
|
48
|
+
private readonly languages: WorkflowLanguages,
|
|
49
|
+
/** The scope: the port is asked here, so that the application's own registration replaces the default. */
|
|
50
|
+
private readonly services: ServiceProvider,
|
|
36
51
|
) {
|
|
37
52
|
super();
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
async list(query: ProcessListQuery = {}): Promise<WorkflowPage<ProcessSummary>> {
|
|
41
|
-
const { limit, offset } = pagingOf(query);
|
|
42
56
|
const where = query.access === undefined ? "all" : grantsToWhere(query.access);
|
|
43
57
|
if (where === "none") return { items: [], total: 0 };
|
|
44
58
|
let source = this.db.processes.asNoTracking();
|
|
@@ -47,23 +61,61 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
47
61
|
if (query.archived === false) source = source.where((p) => p.archivedAt.isNull());
|
|
48
62
|
if (query.search !== undefined && query.search.trim() !== "") {
|
|
49
63
|
const text = query.search.trim();
|
|
50
|
-
source = source.where((p) => p.key.contains(text, { ignoreCase: true }).or(p.
|
|
64
|
+
source = source.where((p) => p.key.contains(text, { ignoreCase: true }).or(p.titleText.contains(text, { ignoreCase: true })));
|
|
51
65
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
if (query.folder !== undefined) {
|
|
67
|
+
const folder = query.folder;
|
|
68
|
+
source = source.where((p) => p.folder.eq(folder).or(p.folder.startsWith(`${folder}/`)));
|
|
69
|
+
}
|
|
70
|
+
if (query.owner !== undefined) source = source.where((p) => p.owner.eq(query.owner!));
|
|
71
|
+
// The list sorts by a title as `titleText`: `title` itself is JSON.
|
|
72
|
+
const sort = query.sort?.map((field) => (field.field === "title" ? { ...field, field: "titleText" } : field));
|
|
73
|
+
const { items, total } = await paginate(source, pageQueryOf({ ...query, ...(sort === undefined ? {} : { sort }) }, { field: "key", dir: "asc" }));
|
|
74
|
+
return { items: items.map((row) => this.summaryOf(row)), total };
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
async get(key: string): Promise<ProcessDetails> {
|
|
58
78
|
return this.detailsOf(await this.process(key, false));
|
|
59
79
|
}
|
|
60
80
|
|
|
61
|
-
async
|
|
81
|
+
async folderOf(key: string): Promise<string | null> {
|
|
82
|
+
const row = await this.db.processes.asNoTracking().firstOrDefault((p) => p.key.eq(key));
|
|
83
|
+
return row?.folder ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async validate(document: unknown, options: ValidateOptions = {}): Promise<readonly ProcessIssue[]> {
|
|
87
|
+
const port = this.services.resolve(WorkflowExtensions);
|
|
88
|
+
const namespaces = new Set(port.namespaces());
|
|
89
|
+
const issues = validateProcess(document, { tools: (name) => this.workflowTools.has(name), extensions: (namespace) => namespaces.has(namespace) });
|
|
90
|
+
if (namespaces.size === 0 || issues.some((issue) => issue.code === "too_large" || issue.code === "schema")) return issues;
|
|
91
|
+
const context: WorkflowPortContext = { partition: null, language: options.language ?? this.languages.default, actor: options.actor ?? null };
|
|
92
|
+
const found: ProcessIssue[] = [...issues];
|
|
93
|
+
const { extensions, variables, nodes } = document as ProcessDocument;
|
|
94
|
+
const checkAll = async (data: Readonly<Record<string, unknown>> | undefined, target: ExtensionTarget): Promise<void> => {
|
|
95
|
+
if (typeof data !== "object" || data === null) return;
|
|
96
|
+
for (const [namespace, value] of Object.entries(data)) {
|
|
97
|
+
if (!namespaces.has(namespace)) continue;
|
|
98
|
+
for (const message of await port.check(namespace, value, target, context)) found.push({ severity: "error", code: "extension_invalid", message: `${namespace}: ${message}`, ...target });
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
await checkAll(extensions, {});
|
|
102
|
+
if (Array.isArray(variables)) for (const variable of variables) await checkAll(variable?.extensions, { variable: variable?.name });
|
|
103
|
+
for (const [node, raw] of Object.entries(nodes)) await checkAll(raw?.extensions, { node });
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
palette(): ProcessPalette {
|
|
108
|
+
return processPalette(this.tools());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async create(process: CreateProcess, actor: WorkflowActor, options: WorkflowAuditOptions = {}): Promise<ProcessDetails> {
|
|
62
112
|
if (typeof process.key !== "string" || !KEY.test(process.key)) throw new WorkflowError("invalid_input", "key must match ^[a-z][a-z0-9_.-]{0,63}$");
|
|
63
113
|
const row = Object.assign(new WfProcess(), {
|
|
64
114
|
key: process.key,
|
|
65
|
-
|
|
115
|
+
...this.titled(process.title),
|
|
66
116
|
description: description(process.description),
|
|
117
|
+
folder: folder(process.folder),
|
|
118
|
+
owner: owner(process.owner),
|
|
67
119
|
retentionDays: retention(process.retentionDays),
|
|
68
120
|
draft: emptyProcess(),
|
|
69
121
|
draftUpdatedAt: this.clock.now(),
|
|
@@ -77,43 +129,60 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
77
129
|
if (error instanceof UniqueViolationError) throw new WorkflowError("conflict", `Process ${process.key} already exists`);
|
|
78
130
|
throw error;
|
|
79
131
|
}
|
|
132
|
+
await this.changed("created", row.key, actor, options);
|
|
80
133
|
return this.detailsOf(row);
|
|
81
134
|
}
|
|
82
135
|
|
|
83
|
-
async update(key: string, changes: UpdateProcess,
|
|
136
|
+
async update(key: string, changes: UpdateProcess, actor: WorkflowActor, options: WorkflowAuditOptions = {}): Promise<ProcessDetails> {
|
|
84
137
|
const row = await this.process(key, true);
|
|
85
138
|
const { archived, ...fields } = changes;
|
|
86
139
|
if (Object.values(fields).some((value) => value !== undefined)) this.assertNotCode(key);
|
|
87
|
-
|
|
140
|
+
const card = (): string => canonicalJson({ title: row.title, description: row.description, folder: row.folder, owner: row.owner, retentionDays: row.retentionDays });
|
|
141
|
+
const before = { card: card(), archived: row.archivedAt !== null };
|
|
142
|
+
if (changes.title !== undefined) Object.assign(row, this.titled(changes.title));
|
|
88
143
|
if (changes.description !== undefined) row.description = description(changes.description);
|
|
144
|
+
if (changes.folder !== undefined) row.folder = folder(changes.folder);
|
|
145
|
+
if (changes.owner !== undefined) row.owner = owner(changes.owner);
|
|
89
146
|
if (changes.retentionDays !== undefined) row.retentionDays = retention(changes.retentionDays);
|
|
90
147
|
if (archived !== undefined) row.archivedAt = archived ? row.archivedAt ?? this.clock.now() : null;
|
|
91
148
|
await this.db.saveChanges();
|
|
149
|
+
if (card() !== before.card) await this.changed("updated", key, actor, options);
|
|
150
|
+
if (archived !== undefined && archived !== before.archived) await this.changed(archived ? "archived" : "restored", key, actor, options);
|
|
92
151
|
return this.detailsOf(row);
|
|
93
152
|
}
|
|
94
153
|
|
|
95
|
-
async saveDraft(key: string, document: unknown, actor: WorkflowActor
|
|
96
|
-
const issues = this.validate(document);
|
|
154
|
+
async saveDraft(key: string, document: unknown, actor: WorkflowActor, options: SaveDraftOptions = {}): Promise<SavedDraft> {
|
|
155
|
+
const issues = await this.validate(document, { actor, ...(options.language === undefined ? {} : { language: options.language }) });
|
|
97
156
|
if (issues.some((issue) => issue.code === "too_large")) throw new WorkflowError("invalid_document", "The document is too large", issues);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
157
|
+
return await this.db.transactionScope(async (transaction) => {
|
|
158
|
+
const row = await this.db.processes.where((p) => p.key.eq(key)).forUpdate().firstOrDefault();
|
|
159
|
+
if (!row) throw notFound(key);
|
|
160
|
+
this.assertNotCode(key);
|
|
161
|
+
const previous = row.draftUpdatedAt?.getTime();
|
|
162
|
+
if (options.expected !== undefined && previous !== options.expected.getTime()) throw new WorkflowError("conflict", `The draft of ${key} was saved by someone else`);
|
|
163
|
+
// Later than the last save even within a millisecond: `expected` tells any two saves apart.
|
|
164
|
+
const draftUpdatedAt = new Date(Math.max(this.clock.now().getTime(), (previous ?? 0) + 1));
|
|
165
|
+
row.draft = document;
|
|
166
|
+
row.draftUpdatedAt = draftUpdatedAt;
|
|
167
|
+
row.draftUpdatedByKind = actor.kind;
|
|
168
|
+
row.draftUpdatedById = actor.id;
|
|
169
|
+
await this.db.saveChanges();
|
|
170
|
+
transaction.afterCommit(() => this.changed("draftSaved", key, actor, options));
|
|
171
|
+
return { issues, draftUpdatedAt };
|
|
172
|
+
});
|
|
106
173
|
}
|
|
107
174
|
|
|
108
|
-
async publish(key: string, actor: WorkflowActor): Promise<VersionDetails> {
|
|
109
|
-
return await this.db.transactionScope(async () => {
|
|
175
|
+
async publish(key: string, actor: WorkflowActor, options: WorkflowAuditOptions & Pick<ValidateOptions, "language"> = {}): Promise<VersionDetails> {
|
|
176
|
+
return await this.db.transactionScope(async (transaction) => {
|
|
110
177
|
const row = await this.db.processes.where((p) => p.key.eq(key)).forUpdate().firstOrDefault();
|
|
111
178
|
if (!row) throw notFound(key);
|
|
112
179
|
this.assertNotCode(key);
|
|
113
180
|
if (row.archivedAt !== null) throw new WorkflowError("archived", `Process ${key} is archived`);
|
|
114
|
-
const issues = this.validate(row.draft);
|
|
181
|
+
const issues = await this.validate(row.draft, { actor, ...(options.language === undefined ? {} : { language: options.language }) });
|
|
115
182
|
if (hasErrors(issues)) throw new WorkflowError("invalid_document", `Process ${key} has errors and cannot be published`, issues);
|
|
116
|
-
|
|
183
|
+
const { version, created } = await this.publishDraft(row, actor);
|
|
184
|
+
if (created) transaction.afterCommit(() => this.changed("published", key, actor, options, version.version));
|
|
185
|
+
return version;
|
|
117
186
|
});
|
|
118
187
|
}
|
|
119
188
|
|
|
@@ -124,7 +193,7 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
124
193
|
* and unpublished. The caller has checked the documents with the Tools.
|
|
125
194
|
*/
|
|
126
195
|
async publishCode(entries: readonly OwnedCodeProcess[]): Promise<readonly { readonly key: string; readonly version: number | null; readonly published: boolean }[]> {
|
|
127
|
-
return await this.db.transactionScope(async () => {
|
|
196
|
+
return await this.db.transactionScope(async (transaction) => {
|
|
128
197
|
await this.db.database.querySqlRaw("SELECT pg_advisory_xact_lock(hashtext('bazis.workflow.code'))");
|
|
129
198
|
const results: { key: string; version: number | null; published: boolean }[] = [];
|
|
130
199
|
for (const { process } of [...entries].sort((a, b) => a.process.key.localeCompare(b.process.key))) {
|
|
@@ -132,10 +201,15 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
132
201
|
if (!row) {
|
|
133
202
|
row = Object.assign(new WfProcess(), { key: process.key });
|
|
134
203
|
this.db.processes.add(row);
|
|
204
|
+
transaction.afterCommit(() => this.changed("created", process.key, CODE_ACTOR));
|
|
135
205
|
}
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
if (row.
|
|
206
|
+
const named = this.titled(process.title);
|
|
207
|
+
const about = description(process.description);
|
|
208
|
+
if (canonicalJson(row.title) !== canonicalJson(named.title)) Object.assign(row, named);
|
|
209
|
+
if (canonicalJson(row.description) !== canonicalJson(about)) row.description = about;
|
|
210
|
+
const fields = { folder: folder(process.folder), owner: owner(process.owner), retentionDays: retention(process.retentionDays) };
|
|
211
|
+
if (row.folder !== fields.folder) row.folder = fields.folder;
|
|
212
|
+
if (row.owner !== fields.owner) row.owner = fields.owner;
|
|
139
213
|
if (row.retentionDays !== fields.retentionDays) row.retentionDays = fields.retentionDays;
|
|
140
214
|
if (row.draft === null || canonicalJson(row.draft) !== canonicalJson(process.document)) {
|
|
141
215
|
row.draft = process.document;
|
|
@@ -149,6 +223,7 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
149
223
|
continue;
|
|
150
224
|
}
|
|
151
225
|
const { version, created } = await this.publishDraft(row, CODE_ACTOR);
|
|
226
|
+
if (created) transaction.afterCommit(() => this.changed("published", process.key, CODE_ACTOR, {}, version.version));
|
|
152
227
|
results.push({ key: row.key, version: version.version, published: created });
|
|
153
228
|
}
|
|
154
229
|
return results;
|
|
@@ -226,6 +301,10 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
226
301
|
return this.workflowTools.list();
|
|
227
302
|
}
|
|
228
303
|
|
|
304
|
+
private async changed(change: ProcessChange, key: string, actor: WorkflowActor, options: WorkflowAuditOptions = {}, version: number | null = null): Promise<void> {
|
|
305
|
+
await this.events.publish(WORKFLOW_PROCESS_CHANGED, { change, key, version, actor, request: options.request ?? null });
|
|
306
|
+
}
|
|
307
|
+
|
|
229
308
|
/** A process declared in code changes only through the code. */
|
|
230
309
|
private assertNotCode(key: string): void {
|
|
231
310
|
const module = this.code.moduleOf(key);
|
|
@@ -237,6 +316,8 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
237
316
|
key: row.key,
|
|
238
317
|
title: row.title,
|
|
239
318
|
description: row.description,
|
|
319
|
+
folder: row.folder,
|
|
320
|
+
owner: row.owner,
|
|
240
321
|
latestVersion: row.latestVersion,
|
|
241
322
|
retentionDays: row.retentionDays,
|
|
242
323
|
archived: row.archivedAt !== null,
|
|
@@ -255,8 +336,11 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
255
336
|
};
|
|
256
337
|
}
|
|
257
338
|
|
|
258
|
-
|
|
259
|
-
|
|
339
|
+
/** A title with the column a list searches and sorts by. */
|
|
340
|
+
private titled(value: unknown): { readonly title: LocalizedText; readonly titleText: string } {
|
|
341
|
+
const problem = checkLocalizedText(value, 200);
|
|
342
|
+
if (problem) throw new WorkflowError("invalid_input", `title ${problem}`);
|
|
343
|
+
return { title: value as LocalizedText, titleText: allTexts(value as LocalizedText, this.languages.default) };
|
|
260
344
|
}
|
|
261
345
|
|
|
262
346
|
private async process(key: string, tracked: boolean): Promise<WfProcess> {
|
|
@@ -271,14 +355,22 @@ function notFound(key: string): WorkflowError {
|
|
|
271
355
|
return new WorkflowError("not_found", `Process ${key} does not exist`);
|
|
272
356
|
}
|
|
273
357
|
|
|
274
|
-
function
|
|
275
|
-
if (
|
|
358
|
+
function description(value: unknown): LocalizedText | null {
|
|
359
|
+
if (value === undefined || value === null) return null;
|
|
360
|
+
const problem = checkLocalizedText(value, 2000, 0);
|
|
361
|
+
if (problem) throw new WorkflowError("invalid_input", `description ${problem}`);
|
|
362
|
+
return value as LocalizedText;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function folder(value: unknown): string | null {
|
|
366
|
+
if (value === undefined || value === null) return null;
|
|
367
|
+
if (typeof value !== "string" || value.length > 500 || !FOLDER.test(value)) throw new WorkflowError("invalid_input", "folder must be a path like Sales/Orders");
|
|
276
368
|
return value;
|
|
277
369
|
}
|
|
278
370
|
|
|
279
|
-
function
|
|
371
|
+
function owner(value: unknown): string | null {
|
|
280
372
|
if (value === undefined || value === null) return null;
|
|
281
|
-
if (typeof value !== "string" || value.length >
|
|
373
|
+
if (typeof value !== "string" || value === "" || value.length > 200) throw new WorkflowError("invalid_input", "owner must be up to 200 characters");
|
|
282
374
|
return value;
|
|
283
375
|
}
|
|
284
376
|
|
|
@@ -298,12 +390,3 @@ function versionOf(key: string, row: WfVersion): VersionDetails {
|
|
|
298
390
|
document: row.document as ProcessDocument,
|
|
299
391
|
};
|
|
300
392
|
}
|
|
301
|
-
|
|
302
|
-
/** JSON with sorted keys: the same document gives the same hash whatever the key order. */
|
|
303
|
-
function canonicalJson(value: unknown): string {
|
|
304
|
-
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
305
|
-
if (typeof value === "object" && value !== null) {
|
|
306
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson((value as Record<string, unknown>)[key])}`).join(",")}}`;
|
|
307
|
-
}
|
|
308
|
-
return JSON.stringify(value) ?? "null";
|
|
309
|
-
}
|
|
@@ -5,13 +5,18 @@ import { DbContextOptions } from "../../../library/orm/DbContextOptions";
|
|
|
5
5
|
import { ServiceProvider } from "../../di/ServiceProvider";
|
|
6
6
|
import { EventBus } from "../../kernel/events/EventBus";
|
|
7
7
|
import { WorkflowClock } from "../contracts/WorkflowClock";
|
|
8
|
+
import { FolderReferences } from "../definitions_module/access/Folder.references";
|
|
8
9
|
import { ProcessReferences } from "../definitions_module/access/Process.references";
|
|
10
|
+
import { ProcessFolderLookup } from "../definitions_module/access/ProcessFolder.lookup";
|
|
11
|
+
import { CatalogImport } from "../definitions_module/code/CatalogImport";
|
|
9
12
|
import { CodeProcessCatalog } from "../definitions_module/code/CodeProcessCatalog";
|
|
10
13
|
import { CodeProcessPublisher } from "../definitions_module/code/CodeProcessPublisher";
|
|
11
14
|
import { ProcessesController } from "../definitions_module/http/Processes.controller";
|
|
12
15
|
import { DefinitionsDbContext } from "../definitions_module/model/Definitions.dbContext";
|
|
13
16
|
import { IWorkflowDefinitions } from "../definitions_module/services/IWorkflowDefinitions.service";
|
|
14
17
|
import { WorkflowDefinitions } from "../definitions_module/services/WorkflowDefinitions.service";
|
|
18
|
+
import { WorkflowEvents } from "../internal/WorkflowEvents";
|
|
19
|
+
import { WorkflowLanguages } from "../internal/WorkflowLanguages";
|
|
15
20
|
import { WorkflowTools } from "../internal/WorkflowTools";
|
|
16
21
|
import { WorkflowRetention } from "../runtime_module/background/WorkflowRetention";
|
|
17
22
|
import { WorkflowWorker } from "../runtime_module/background/WorkflowWorker";
|
|
@@ -32,12 +37,15 @@ import { IWorkflowTasks } from "../tasks_module/services/IWorkflowTasks.service"
|
|
|
32
37
|
import { WorkflowTasks } from "../tasks_module/services/WorkflowTasks.service";
|
|
33
38
|
|
|
34
39
|
registerFrameworkClassDeps([
|
|
40
|
+
[CatalogImport, [DefinitionsDbContext, WorkflowLanguages]],
|
|
35
41
|
[CodeProcessCatalog, [ServiceProvider]],
|
|
36
42
|
[CodeProcessPublisher, [ServiceProvider]],
|
|
37
43
|
[DefinitionsDbContext, [DbContextOptions]],
|
|
38
44
|
[EventsController, [IWorkflowRuntime]],
|
|
39
|
-
[
|
|
45
|
+
[FolderReferences, [DefinitionsDbContext]],
|
|
46
|
+
[InstancesController, [IWorkflowRuntime, IWorkflowDefinitions]],
|
|
40
47
|
[ProcessesController, [IWorkflowDefinitions]],
|
|
48
|
+
[ProcessFolderLookup, [DefinitionsDbContext]],
|
|
41
49
|
[ProcessReferences, [DefinitionsDbContext]],
|
|
42
50
|
[RuntimeDbContext, [DbContextOptions]],
|
|
43
51
|
[Schedules, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions]],
|
|
@@ -46,11 +54,13 @@ registerFrameworkClassDeps([
|
|
|
46
54
|
[TasksDbContext, [DbContextOptions]],
|
|
47
55
|
[TimerStep, [RuntimeDbContext, WorkflowEngine]],
|
|
48
56
|
[ToolStep, [RuntimeDbContext, WorkflowEngine, WorkflowTools]],
|
|
49
|
-
[WorkflowDefinitions, [DefinitionsDbContext, WorkflowTools, CodeProcessCatalog, WorkflowClock]],
|
|
50
|
-
[WorkflowEngine, [RuntimeDbContext, IWorkflowDefinitions, GraphCache,
|
|
57
|
+
[WorkflowDefinitions, [DefinitionsDbContext, WorkflowTools, CodeProcessCatalog, WorkflowClock, WorkflowEvents, WorkflowLanguages, ServiceProvider]],
|
|
58
|
+
[WorkflowEngine, [RuntimeDbContext, IWorkflowDefinitions, GraphCache, WorkflowEvents, WorkflowClock]],
|
|
59
|
+
[WorkflowEvents, [EventBus, ServiceProvider, "optional:Logger"]],
|
|
60
|
+
[WorkflowLanguages, ["Config:workflow"]],
|
|
51
61
|
[WorkflowRetention, [ServiceProvider, "Config:workflow"]],
|
|
52
|
-
[WorkflowRuntime, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions, ServiceProvider]],
|
|
53
|
-
[WorkflowTasks, [TasksDbContext, IWorkflowRuntime, WorkflowClock]],
|
|
62
|
+
[WorkflowRuntime, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions, ServiceProvider, WorkflowLanguages]],
|
|
63
|
+
[WorkflowTasks, [TasksDbContext, IWorkflowRuntime, WorkflowClock, WorkflowEvents]],
|
|
54
64
|
[WorkflowTools, [ServiceProvider]],
|
|
55
65
|
[WorkflowWorker, [ServiceProvider, "Config:workflow"]],
|
|
56
66
|
]);
|
package/core/workflow/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Business processes without a UI (WORKFLOW-001): the composite `WorkflowModule`
|
|
2
|
+
* Business processes without a UI (WORKFLOW-001, WORKFLOW-002): the composite `WorkflowModule`
|
|
3
3
|
* with the Definitions and Runtime parts, their services, HTTP, events and
|
|
4
4
|
* extension points. The document format is `bazis/library/workflow`.
|
|
5
5
|
*/
|
|
@@ -14,6 +14,8 @@ export { WorkflowDefinitionsModule } from "./definitions_module/WorkflowDefiniti
|
|
|
14
14
|
export { WorkflowRuntimeModule } from "./runtime_module/WorkflowRuntime.module";
|
|
15
15
|
export { WorkflowTasksModule } from "./tasks_module/WorkflowTasks.module";
|
|
16
16
|
export { IWorkflowTasks, type TaskDetails, type TaskListQuery } from "./tasks_module/services/IWorkflowTasks.service";
|
|
17
|
+
export { WORKFLOW_TASK_COMPLETED, type WorkflowTaskCompleted } from "./tasks_module/events/Tasks.events";
|
|
18
|
+
export { WORKFLOW_PROCESS_CHANGED, type ProcessChange, type WorkflowProcessChanged } from "./definitions_module/events/Definitions.events";
|
|
17
19
|
export type { TaskStatus } from "./tasks_module/model/WfTask.model";
|
|
18
20
|
export {
|
|
19
21
|
IWorkflowDefinitions,
|
|
@@ -21,38 +23,50 @@ export {
|
|
|
21
23
|
type ProcessDetails,
|
|
22
24
|
type ProcessListQuery,
|
|
23
25
|
type ProcessSummary,
|
|
26
|
+
type SavedDraft,
|
|
27
|
+
type SaveDraftOptions,
|
|
24
28
|
type UpdateProcess,
|
|
29
|
+
type ValidateOptions,
|
|
25
30
|
type VersionDetails,
|
|
26
31
|
type VersionSummary,
|
|
27
32
|
} from "./definitions_module/services/IWorkflowDefinitions.service";
|
|
28
33
|
export {
|
|
29
34
|
IWorkflowRuntime,
|
|
35
|
+
type CancelInstance,
|
|
30
36
|
type EmitOptions,
|
|
31
37
|
type EmitResult,
|
|
32
38
|
type HistoryEntry,
|
|
33
39
|
type InstanceDetails,
|
|
34
40
|
type InstanceListQuery,
|
|
35
41
|
type InstanceSummary,
|
|
42
|
+
type RetryInstance,
|
|
36
43
|
type StartInstance,
|
|
37
44
|
type TokenDetails,
|
|
38
45
|
} from "./runtime_module/services/IWorkflowRuntime.service";
|
|
39
46
|
export {
|
|
47
|
+
WORKFLOW_INSTANCE_CANCELLED,
|
|
40
48
|
WORKFLOW_INSTANCE_FINISHED,
|
|
49
|
+
WORKFLOW_INSTANCE_RETRIED,
|
|
50
|
+
WORKFLOW_INSTANCE_STARTED,
|
|
41
51
|
WORKFLOW_INSTANCES_DELETED,
|
|
42
52
|
WORKFLOW_TASK_REQUESTED,
|
|
43
53
|
WORKFLOW_TOKEN_CLOSED,
|
|
54
|
+
type WorkflowInstanceCancelled,
|
|
44
55
|
type WorkflowInstanceFinished,
|
|
56
|
+
type WorkflowInstanceRetried,
|
|
57
|
+
type WorkflowInstanceStarted,
|
|
45
58
|
type WorkflowInstancesDeleted,
|
|
46
59
|
type WorkflowTaskRequested,
|
|
47
60
|
type WorkflowTokenClosed,
|
|
48
61
|
} from "./runtime_module/events/Runtime.events";
|
|
49
|
-
export {
|
|
62
|
+
export { workflowConfig, type WorkflowSettings } from "./contracts/Workflow.config";
|
|
50
63
|
export type { HistoryType } from "./runtime_module/model/WfHistory.model";
|
|
51
64
|
export type { InstanceStatus, StartedVia } from "./runtime_module/model/WfInstance.model";
|
|
52
65
|
export type { TokenStatus } from "./runtime_module/model/WfToken.model";
|
|
53
66
|
export { WorkflowInputValidator, DefaultWorkflowInputValidator, type WorkflowInputContext } from "./contracts/WorkflowInputValidator";
|
|
67
|
+
export { WorkflowExtensions, DefaultWorkflowExtensions, type ExtensionTarget, type WorkflowPortContext } from "./contracts/WorkflowExtensions";
|
|
54
68
|
export { WorkflowError, type WorkflowErrorCode } from "./contracts/WorkflowError";
|
|
55
|
-
export type { WorkflowActor, WorkflowPage, WorkflowPaging } from "./contracts/types";
|
|
69
|
+
export type { WorkflowActor, WorkflowAuditOptions, WorkflowPage, WorkflowPaging, WorkflowRequest, WorkflowToolMetadata } from "./contracts/types";
|
|
56
70
|
|
|
57
71
|
// Augment the declaring file, not the "../di" re-export: re-export targets
|
|
58
72
|
// depend on program file order and silently stop merging in some projects.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ServiceProvider } from "../../di";
|
|
2
|
+
import { EventBus, type EventToken, type Logger } from "../../kernel";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Publishes the events of the workflow services after their action is saved, in
|
|
6
|
+
* the scope of the action. A failing handler is logged: the action stands.
|
|
7
|
+
*/
|
|
8
|
+
export class WorkflowEvents {
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly bus: EventBus,
|
|
11
|
+
/** Event handlers run in this scope. */
|
|
12
|
+
private readonly scope: ServiceProvider,
|
|
13
|
+
private readonly logger?: Logger,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
async publish<T>(event: EventToken<T>, payload: T): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
await this.bus.publishScoped(this.scope, event, payload);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
this.logger?.error("workflow event handler failed", { event: event.name, error: error instanceof Error ? error.message : String(error) });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ConfigView } from "../../kernel";
|
|
2
|
+
import type { WorkflowSettings } from "../contracts/Workflow.config";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The language of a call (WORKFLOW-002 §6): over HTTP from `Accept-Language`
|
|
6
|
+
* among `workflow.languages`, otherwise `workflow.language`; in events, schedules
|
|
7
|
+
* and the background always `workflow.language`.
|
|
8
|
+
*/
|
|
9
|
+
export class WorkflowLanguages {
|
|
10
|
+
constructor(private readonly config: ConfigView<WorkflowSettings>) {}
|
|
11
|
+
|
|
12
|
+
get default(): string {
|
|
13
|
+
return this.config.get("language");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The best language of an `Accept-Language` header: by weight, then by order; `ru-RU` also finds `ru`. */
|
|
17
|
+
of(header: string | undefined): string {
|
|
18
|
+
if (header === undefined) return this.default;
|
|
19
|
+
const known = new Set([this.default, ...this.config.get("languages").split(",").map((item) => item.trim())]);
|
|
20
|
+
const wanted = header.split(",")
|
|
21
|
+
.map((part, index) => {
|
|
22
|
+
const [tag = "", ...parameters] = part.trim().split(";");
|
|
23
|
+
const weight = parameters.map((item) => /^\s*q=([\d.]+)\s*$/.exec(item)?.[1]).find((item) => item !== undefined);
|
|
24
|
+
return { tag: tag.trim(), weight: weight === undefined ? 1 : Number(weight), index };
|
|
25
|
+
})
|
|
26
|
+
.filter(({ tag, weight }) => tag !== "" && weight > 0)
|
|
27
|
+
.sort((a, b) => b.weight - a.weight || a.index - b.index);
|
|
28
|
+
for (const { tag } of wanted) {
|
|
29
|
+
if (known.has(tag)) return tag;
|
|
30
|
+
const base = tag.split("-")[0]!;
|
|
31
|
+
if (known.has(base)) return base;
|
|
32
|
+
}
|
|
33
|
+
return this.default;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** JSON with sorted keys: the same document gives the same hash whatever the key order. */
|
|
2
|
+
export function canonicalJson(value: unknown): string {
|
|
3
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
4
|
+
if (typeof value === "object" && value !== null) {
|
|
5
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson((value as Record<string, unknown>)[key])}`).join(",")}}`;
|
|
6
|
+
}
|
|
7
|
+
return JSON.stringify(value) ?? "null";
|
|
8
|
+
}
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { IAccess, type AccessObject, type SubjectAccess } from "../../access";
|
|
2
2
|
import { ForbiddenError, type HttpContext } from "../../http";
|
|
3
|
-
import
|
|
3
|
+
import { frameworkBadRequest } from "../../http/Errors/HttpError";
|
|
4
|
+
import { ListQueryError, parseListQuery, type ListQuery, type ListQueryOptions, type ListQueryProblem } from "../../../library/jsonapi";
|
|
5
|
+
import { textOf, type LocalizedText } from "../../../library/workflow";
|
|
6
|
+
import type { WorkflowActor, WorkflowPaging, WorkflowRequest } from "../contracts/types";
|
|
4
7
|
import { WorkflowError } from "../contracts/WorkflowError";
|
|
8
|
+
import { WorkflowLanguages } from "./WorkflowLanguages";
|
|
5
9
|
|
|
6
10
|
/** Who calls a route: their rights and how they are recorded as an actor. */
|
|
7
11
|
export interface WorkflowCaller {
|
|
8
12
|
readonly access: SubjectAccess;
|
|
9
13
|
readonly actor: WorkflowActor;
|
|
14
|
+
readonly request: WorkflowRequest;
|
|
15
|
+
/** The language of the request: `Accept-Language` among `workflow.languages`. */
|
|
16
|
+
readonly language: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A record with `titleText`: its `title` in the language of the request. `title` itself stays for editing. */
|
|
20
|
+
export function withTitleText<T extends { readonly title: LocalizedText }>(item: T, language: string, fallback: string): T & { readonly titleText: string } {
|
|
21
|
+
return { ...item, titleText: textOf(item.title, language, fallback) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The default language, for {@link withTitleText}. */
|
|
25
|
+
export function defaultLanguage(ctx: HttpContext): string {
|
|
26
|
+
return ctx.services.resolve(WorkflowLanguages).default;
|
|
10
27
|
}
|
|
11
28
|
|
|
12
29
|
/**
|
|
@@ -20,7 +37,7 @@ export async function callerOf(ctx: HttpContext, permission?: string, object?: A
|
|
|
20
37
|
if (permission !== undefined && !await access.can(permission, object)) throw new ForbiddenError();
|
|
21
38
|
const sub = access.subject.sub;
|
|
22
39
|
const [id = sub.slice(sub.lastIndexOf(":") + 1)] = await access.ownerIds();
|
|
23
|
-
return { access, actor: { kind: "user", id } };
|
|
40
|
+
return { access, actor: { kind: "user", id }, request: { ip: ctx.clientIp ?? null, userAgent: ctx.header("user-agent") ?? null }, language: ctx.services.resolve(WorkflowLanguages).of(ctx.header("accept-language")) };
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
/** A route parameter as a string. */
|
|
@@ -28,6 +45,11 @@ export function param(ctx: HttpContext, name: string): string {
|
|
|
28
45
|
return String(ctx.params[name] ?? "");
|
|
29
46
|
}
|
|
30
47
|
|
|
48
|
+
/** A title or a description from a body; the service checks it. */
|
|
49
|
+
export function optionalText(value: unknown): LocalizedText | null | undefined {
|
|
50
|
+
return value as LocalizedText | null | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
/** The JSON body as an object; anything else is `invalid_input`. */
|
|
32
54
|
export async function body(ctx: HttpContext): Promise<Record<string, unknown>> {
|
|
33
55
|
const text = await ctx.text();
|
|
@@ -64,7 +86,33 @@ export function optionalInteger(value: unknown, name: string): number | undefine
|
|
|
64
86
|
return number as number;
|
|
65
87
|
}
|
|
66
88
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The list query of a route: `filter[...]`, `sort`, `page[...]`; anything
|
|
91
|
+
* `options` does not list is 400. Each filter here names one service option, so
|
|
92
|
+
* OR groups and a field filtered twice are refused rather than half applied.
|
|
93
|
+
*/
|
|
94
|
+
export function listQuery(ctx: HttpContext, options: Pick<ListQueryOptions, "filter" | "sort">): ListQuery {
|
|
95
|
+
try {
|
|
96
|
+
const list = parseListQuery(ctx.url.searchParams, { ...options, page: { defaultSize: 50, maxSize: 200 } });
|
|
97
|
+
const problems: ListQueryProblem[] = list.or.length > 0 ? [{ parameter: "filter[or]", message: "OR groups are not supported here" }] : [];
|
|
98
|
+
for (const [index, { field }] of list.filters.entries()) {
|
|
99
|
+
if (list.filters.findIndex((rule) => rule.field === field) !== index) problems.push({ parameter: `filter[${field}]`, message: "the field is filtered twice" });
|
|
100
|
+
}
|
|
101
|
+
if (problems.length > 0) throw new ListQueryError(problems);
|
|
102
|
+
return list;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error instanceof ListQueryError) throw frameworkBadRequest("invalidListQuery", undefined, error.problems);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The page and order of a list query, for a list operation. */
|
|
110
|
+
export function pagingOfList(list: ListQuery): WorkflowPaging {
|
|
111
|
+
return { limit: list.page.limit, offset: list.page.offset, ...(list.sort.length === 0 ? {} : { sort: list.sort }) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The value of `filter[field]`. */
|
|
115
|
+
export function filterOf(list: ListQuery, field: string): string | undefined {
|
|
116
|
+
const value = list.filters.find((rule) => rule.field === field)?.value;
|
|
117
|
+
return Array.isArray(value) ? value.join(",") : value as string | undefined;
|
|
70
118
|
}
|
|
@@ -5,7 +5,6 @@ import { WorkflowDefinitionsModule } from "../definitions_module/WorkflowDefinit
|
|
|
5
5
|
import { runtimePermissions } from "./access/Runtime.permissions";
|
|
6
6
|
import { WorkflowRetention } from "./background/WorkflowRetention";
|
|
7
7
|
import { WorkflowWorker } from "./background/WorkflowWorker";
|
|
8
|
-
import { workflowRuntimeConfig } from "./config/Runtime.config";
|
|
9
8
|
import { EventsController } from "./http/Events.controller";
|
|
10
9
|
import { InstancesController } from "./http/Instances.controller";
|
|
11
10
|
import { GraphCache } from "./internal/GraphCache";
|
|
@@ -25,7 +24,6 @@ import { WorkflowRuntime } from "./services/WorkflowRuntime.service";
|
|
|
25
24
|
@Module({
|
|
26
25
|
imports: [AccessModule, WorkflowDefinitionsModule],
|
|
27
26
|
permissions: runtimePermissions,
|
|
28
|
-
config: [workflowRuntimeConfig],
|
|
29
27
|
ormBazis: { context: RuntimeDbContext, entities: [...RUNTIME_ENTITIES], ownedStore: runtimeStore, registerRepositories: false },
|
|
30
28
|
providers: [
|
|
31
29
|
singleton(GraphCache),
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import { definePermissions } from "../../../access";
|
|
2
|
+
import { ProcessFolderLookup } from "../../definitions_module/access/ProcessFolder.lookup";
|
|
2
3
|
|
|
3
4
|
const process = { ref: "workflow.process", column: "processKey" };
|
|
5
|
+
const folder = { ref: "workflow.folder", lookup: ProcessFolderLookup };
|
|
4
6
|
|
|
5
7
|
/** The rights of running processes (WORKFLOW-002 §6.2). */
|
|
6
8
|
export const runtimePermissions = definePermissions({
|
|
7
9
|
"workflow.instances.start": {
|
|
8
10
|
title: { en: "Start processes", ru: "Запускать процессы" },
|
|
9
11
|
scope: "request",
|
|
10
|
-
on: { process },
|
|
12
|
+
on: { process, folder },
|
|
11
13
|
},
|
|
12
14
|
"workflow.instances.read": {
|
|
13
15
|
title: { en: "See instances", ru: "Видеть экземпляры" },
|
|
14
16
|
scope: { column: "partition" },
|
|
15
|
-
on: { process },
|
|
17
|
+
on: { process, folder },
|
|
16
18
|
owner: "startedById",
|
|
17
19
|
},
|
|
18
20
|
"workflow.instances.manage": {
|
|
19
21
|
title: { en: "Cancel and retry instances", ru: "Отменять и повторять экземпляры" },
|
|
20
22
|
scope: { column: "partition" },
|
|
21
|
-
on: { process },
|
|
23
|
+
on: { process, folder },
|
|
22
24
|
},
|
|
23
25
|
"workflow.events.emit": {
|
|
24
26
|
title: { en: "Send events to processes", ru: "Отправлять события процессам" },
|