bazis 0.136.0 → 0.137.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/main.ts +8 -0
- package/cli/parseCli.ts +9 -0
- package/cli/templates/module.ts +16 -19
- package/cli/templates/passport.ts +1 -1
- package/cli/upgrade/markColumns.ts +151 -0
- package/cli/upgrade/runUpgrade.ts +57 -0
- package/core/access/Access.module.ts +1 -1
- package/core/access/catalog/moduleFields.ts +16 -2
- package/core/access/catalog/permissions.ts +10 -0
- package/core/access/check/Access.ts +5 -5
- package/core/access/check/can.ts +2 -1
- package/core/access/index.ts +11 -1
- package/core/access/roles/roles.ts +4 -2
- package/core/agent/ToolRunner.ts +5 -0
- package/core/agent/session/entities.ts +45 -45
- package/core/http/Decorators/metadata.ts +3 -0
- package/core/http/Decorators/routes.ts +12 -0
- package/core/http/Errors/HttpError.ts +15 -1
- package/core/http/HttpServer.ts +19 -2
- package/core/http/Middleware/errorHandler.ts +16 -1
- package/core/http/OpenApi/openApiDocument.ts +43 -4
- package/core/http/Results/normalize.ts +38 -1
- package/core/http/Results/results.ts +5 -2
- package/core/http/Routing/RouterBuilder.ts +27 -2
- package/core/http/index.ts +3 -0
- package/core/http/middlewareTokens.ts +12 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/infra/connectors/postgres.ts +4 -2
- package/core/infra/index.ts +1 -1
- package/core/kernel/config/defineConfig.ts +55 -19
- package/core/kernel/index.ts +3 -0
- package/core/orm/buildOrmProviders.ts +2 -1
- package/core/orm/contextEntities.ts +36 -0
- package/core/orm/databaseConnector.ts +9 -3
- package/core/orm/databaseHttpError.ts +21 -0
- package/core/orm/index.ts +12 -1
- package/core/orm/ormModule.ts +17 -7
- package/core/orm/ownedStoreContributions.ts +2 -1
- package/core/scripts/di-generate.ts +61 -4
- package/core/tools/StandardTools.module.ts +1 -1
- package/core/tools/contracts/Tools.config.ts +13 -7
- package/core/tools/generated/deps.ts +3 -3
- package/core/tools/http/HttpRequest.tool.ts +1 -2
- package/core/tools/http/publicAddress.ts +2 -2
- package/core/tools/orm/OrmEntity.references.ts +7 -8
- package/core/workflow/contracts/Workflow.config.ts +5 -5
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +2 -2
- package/core/workflow/definitions_module/http/Processes.controller.ts +2 -2
- package/core/workflow/definitions_module/internal/PublishedVersions.ts +39 -0
- package/core/workflow/definitions_module/model/WfProcess.model.ts +24 -11
- package/core/workflow/definitions_module/model/WfVersion.model.ts +7 -1
- package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +4 -1
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +17 -7
- package/core/workflow/generated/deps.ts +2 -1
- package/core/workflow/index.ts +9 -0
- package/core/workflow/internal/WorkflowLanguages.ts +1 -1
- package/core/workflow/internal/http.ts +2 -2
- package/core/workflow/runtime_module/background/WorkflowWorker.ts +6 -2
- package/core/workflow/runtime_module/http/Instances.controller.ts +1 -1
- package/core/workflow/runtime_module/internal/WorkflowJobs.ts +43 -1
- package/core/workflow/runtime_module/model/WfHistory.model.ts +13 -6
- package/core/workflow/runtime_module/model/WfInstance.model.ts +30 -10
- package/core/workflow/runtime_module/model/WfJob.model.ts +22 -10
- package/core/workflow/runtime_module/model/WfSubscription.model.ts +8 -3
- package/core/workflow/runtime_module/model/WfToken.model.ts +16 -7
- package/core/workflow/tasks_module/model/WfTask.model.ts +25 -9
- package/library/openapi/codegen.ts +2 -2
- package/library/orm/DbContext.ts +37 -1
- package/library/orm/Metadata/ModelBuilder.ts +23 -7
- package/library/orm/Metadata/decorators.ts +23 -10
- package/library/orm/Metadata/types.ts +2 -0
- package/library/orm/Transactions/TransactionScopeCoordinator.ts +3 -3
- package/library/orm/errors.ts +39 -1
- package/library/orm/index.ts +1 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// This file is auto-generated by `bun run di:generate` (framework.packs of bazis.config.json).
|
|
2
2
|
// Do not edit manually.
|
|
3
3
|
import { registerFrameworkClassDeps } from "../../di/module/autoDeps";
|
|
4
|
-
import {
|
|
4
|
+
import { ToolRunner } from "../../agent/ToolRunner";
|
|
5
5
|
import { Configuration } from "../../kernel/config/Configuration";
|
|
6
6
|
import { HttpRequestTool } from "../http/HttpRequest.tool";
|
|
7
7
|
import { OrmEntityReferences, OrmFieldReferences } from "../orm/OrmEntity.references";
|
|
@@ -10,6 +10,6 @@ import { ConfigSecretStore } from "../secrets/SecretStore";
|
|
|
10
10
|
registerFrameworkClassDeps([
|
|
11
11
|
[ConfigSecretStore, [Configuration]],
|
|
12
12
|
[HttpRequestTool, ["Config:tools"]],
|
|
13
|
-
[OrmEntityReferences, [
|
|
14
|
-
[OrmFieldReferences, [
|
|
13
|
+
[OrmEntityReferences, [ToolRunner]],
|
|
14
|
+
[OrmFieldReferences, [ToolRunner]],
|
|
15
15
|
]);
|
|
@@ -82,8 +82,7 @@ export class HttpRequestTool {
|
|
|
82
82
|
constructor(private readonly config: ConfigView<ToolsSettings>) {}
|
|
83
83
|
|
|
84
84
|
async execute(raw: unknown, context: AgentToolExecutionContext): Promise<unknown> {
|
|
85
|
-
|
|
86
|
-
if (context.caller !== undefined && !callers.includes(context.caller)) throw new ToolError(`tools.http.callers does not list ${context.caller}`, { reason: "caller_not_allowed" });
|
|
85
|
+
if (context.caller !== undefined && !this.config.get("http.callers").includes(context.caller)) throw new ToolError(`tools.http.callers does not list ${context.caller}`, { reason: "caller_not_allowed" });
|
|
87
86
|
const request = this.requestOf(raw);
|
|
88
87
|
if (context.dryRun === true && !SAFE_METHODS.has(request.method)) return { dryRun: true };
|
|
89
88
|
const signal = AbortSignal.any([context.signal, AbortSignal.timeout(request.timeoutMs)]);
|
|
@@ -82,9 +82,9 @@ export function isPublicAddress(address: string): boolean {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/** Whether `tools.http.allow` lets this host and port through: `api.example.com`, `*.example.com`, `localhost:8080`. */
|
|
85
|
-
export function hostAllowed(allow: string, hostname: string, port: string): boolean {
|
|
85
|
+
export function hostAllowed(allow: readonly string[], hostname: string, port: string): boolean {
|
|
86
86
|
const host = hostname.toLowerCase().replace(/\.$/, "");
|
|
87
|
-
return allow.
|
|
87
|
+
return allow.map((item) => item.toLowerCase()).some((item) => {
|
|
88
88
|
// An IPv6 address goes in brackets, as in a URL: [::1]:8080.
|
|
89
89
|
const match = /^\[(.+)\](?::(\d+))?$/.exec(item) ?? /^(.*?)(?::(\d+))?$/.exec(item)!;
|
|
90
90
|
const [name, wanted] = [match[1]!, match[2]];
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import type { ReferenceContext, ReferenceItem, ReferencePage } from "../../access";
|
|
2
2
|
import { References } from "../../access";
|
|
3
|
-
import {
|
|
4
|
-
import type { ServiceProvider } from "../../di";
|
|
3
|
+
import type { ToolRunner } from "../../agent";
|
|
5
4
|
import { ormEntityToolInfo, type OrmEntityToolInfo } from "./ormEntityTools";
|
|
6
5
|
|
|
7
6
|
/** What `ormEntityTools` opened in this application: only that is referenced. */
|
|
8
|
-
function opened(
|
|
7
|
+
function opened(tools: ToolRunner): readonly OrmEntityToolInfo[] {
|
|
9
8
|
const entities = new Map<string, Set<string>>();
|
|
10
|
-
for (const tool of
|
|
9
|
+
for (const tool of tools.listTools()) {
|
|
11
10
|
const info = ormEntityToolInfo(tool.target);
|
|
12
11
|
if (info === undefined) continue;
|
|
13
12
|
const fields = entities.get(info.entity) ?? new Set<string>();
|
|
@@ -24,12 +23,12 @@ function page(ids: readonly string[], text: string, { limit, offset }: Reference
|
|
|
24
23
|
|
|
25
24
|
/** Reference kind `orm.entity`: an entity open to processes, `Order`. */
|
|
26
25
|
export class OrmEntityReferences extends References {
|
|
27
|
-
constructor(private readonly
|
|
26
|
+
constructor(private readonly tools: ToolRunner) {
|
|
28
27
|
super();
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
private ids(): readonly string[] {
|
|
32
|
-
return opened(this.
|
|
31
|
+
return opened(this.tools).map((item) => item.entity);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
async exists(ids: readonly string[]): Promise<ReadonlySet<string>> {
|
|
@@ -48,12 +47,12 @@ export class OrmEntityReferences extends References {
|
|
|
48
47
|
|
|
49
48
|
/** Reference kind `orm.field`: a field open to processes, `Order.status`, or `status` within `Order`. */
|
|
50
49
|
export class OrmFieldReferences extends References {
|
|
51
|
-
constructor(private readonly
|
|
50
|
+
constructor(private readonly tools: ToolRunner) {
|
|
52
51
|
super();
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
private ids(): readonly string[] {
|
|
56
|
-
return opened(this.
|
|
55
|
+
return opened(this.tools).flatMap((item) => item.fields.map((field) => `${item.entity}.${field}`));
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
async exists(ids: readonly string[], context: ReferenceContext): Promise<ReadonlySet<string>> {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { defineConfig } from "../../kernel";
|
|
1
|
+
import { configList, defineConfig } from "../../kernel";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Settings of the workflow pack, from the application configuration:
|
|
5
5
|
* `BAZIS_WORKFLOW__WORKER__ENABLED=false` keeps an instance from running jobs,
|
|
6
6
|
* for example a web-only one (WORKFLOW-001 §4). `language` is the default
|
|
7
|
-
* language of texts, `languages` the
|
|
7
|
+
* language of texts, `languages` the list a request may ask for (comma-separated in the environment)
|
|
8
8
|
* with `Accept-Language` (WORKFLOW-002 §6).
|
|
9
9
|
*/
|
|
10
10
|
export interface WorkflowSettings {
|
|
11
11
|
readonly language: string;
|
|
12
|
-
readonly languages: string;
|
|
12
|
+
readonly languages: readonly string[];
|
|
13
13
|
readonly "worker.enabled": boolean;
|
|
14
14
|
readonly "worker.intervalMs": number;
|
|
15
15
|
readonly "worker.batchSize": number;
|
|
@@ -23,7 +23,7 @@ const LANGUAGE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
|
23
23
|
export const workflowConfig = defineConfig<WorkflowSettings>("workflow", {
|
|
24
24
|
default: {
|
|
25
25
|
language: "en",
|
|
26
|
-
languages: "en",
|
|
26
|
+
languages: configList(["en"]),
|
|
27
27
|
"worker.enabled": true,
|
|
28
28
|
"worker.intervalMs": 1000,
|
|
29
29
|
"worker.batchSize": 10,
|
|
@@ -33,7 +33,7 @@ export const workflowConfig = defineConfig<WorkflowSettings>("workflow", {
|
|
|
33
33
|
},
|
|
34
34
|
validate: {
|
|
35
35
|
language: (value) => (LANGUAGE.test(value) ? undefined : "must be a language: en, ru, pt-BR"),
|
|
36
|
-
languages: (value) => (value.
|
|
36
|
+
languages: (value) => (value.every((item) => LANGUAGE.test(item)) ? undefined : "must be a list of languages: en, ru, pt-BR"),
|
|
37
37
|
"worker.intervalMs": (value) => positive(value, 3_600_000),
|
|
38
38
|
"worker.batchSize": (value) => positive(value, 1000),
|
|
39
39
|
"retention.intervalMs": (value) => positive(value, 86_400_000),
|
|
@@ -17,6 +17,7 @@ import { CatalogImport } from "./code/CatalogImport";
|
|
|
17
17
|
import { CodeProcessCatalog } from "./code/CodeProcessCatalog";
|
|
18
18
|
import { CodeProcessPublisher } from "./code/CodeProcessPublisher";
|
|
19
19
|
import { ProcessesController } from "./http/Processes.controller";
|
|
20
|
+
import { PublishedVersions } from "./internal/PublishedVersions";
|
|
20
21
|
import { DefinitionsDbContext, definitionsStore } from "./model/Definitions.dbContext";
|
|
21
22
|
import { WfProcess } from "./model/WfProcess.model";
|
|
22
23
|
import { WfVersion } from "./model/WfVersion.model";
|
|
@@ -39,6 +40,7 @@ import { WorkflowDefinitions } from "./services/WorkflowDefinitions.service";
|
|
|
39
40
|
singleton(WorkflowClock, SystemWorkflowClock),
|
|
40
41
|
singleton(WorkflowTools),
|
|
41
42
|
singleton(CodeProcessCatalog),
|
|
43
|
+
singleton(PublishedVersions),
|
|
42
44
|
singleton(WorkflowLanguages),
|
|
43
45
|
singleton(WorkflowExtensions, DefaultWorkflowExtensions),
|
|
44
46
|
singleton(WorkflowPartitions, DefaultWorkflowPartitions),
|
|
@@ -46,8 +48,6 @@ import { WorkflowDefinitions } from "./services/WorkflowDefinitions.service";
|
|
|
46
48
|
scoped(WorkflowReferences),
|
|
47
49
|
scoped(CatalogImport),
|
|
48
50
|
scoped(IWorkflowDefinitions, WorkflowDefinitions),
|
|
49
|
-
scoped(ProcessReferences),
|
|
50
|
-
scoped(FolderReferences),
|
|
51
51
|
scoped(ProcessFolderLookup),
|
|
52
52
|
scoped(ProcessPartitionLookup),
|
|
53
53
|
singleton(HOSTED_SERVICE, CodeProcessPublisher),
|
|
@@ -45,7 +45,7 @@ export class ProcessesController {
|
|
|
45
45
|
owner: optionalString(request.owner, "owner") ?? null,
|
|
46
46
|
retentionDays: optionalInteger(request.retentionDays, "retentionDays") ?? null,
|
|
47
47
|
}, actor, { request: from });
|
|
48
|
-
return Created(
|
|
48
|
+
return Created(withTitleText(process, language, defaultLanguage(ctx)), `${ctx.path}/${process.key}`);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
@Get("processes/:key")
|
|
@@ -105,7 +105,7 @@ export class ProcessesController {
|
|
|
105
105
|
const key = param(ctx, "key");
|
|
106
106
|
const { actor, request, language } = await callerOf(ctx, "workflow.processes.publish", await this.objectOf(key));
|
|
107
107
|
const version = await this.definitions.publish(key, actor, { request, language });
|
|
108
|
-
return Created(`${ctx.path.replace(/\/publish$/, "")}/versions/${version.version}
|
|
108
|
+
return Created(version, `${ctx.path.replace(/\/publish$/, "")}/versions/${version.version}`);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
@Get("processes/:key/versions")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { VersionDetails } from "../services/IWorkflowDefinitions.service";
|
|
2
|
+
|
|
3
|
+
/** A process with a published version, as a list query reads it: no documents. */
|
|
4
|
+
export interface PublishedHead {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly key: string;
|
|
7
|
+
readonly latestVersion: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The latest published versions, kept between calls. Versions are immutable, so
|
|
12
|
+
* what was read once is read no more: an event or a firing of a schedule costs
|
|
13
|
+
* one query of the heads, and documents are loaded only for versions published
|
|
14
|
+
* since. One per application instance; another instance sees a publication with
|
|
15
|
+
* its next query of the heads.
|
|
16
|
+
*/
|
|
17
|
+
export class PublishedVersions {
|
|
18
|
+
private readonly byProcess = new Map<string, VersionDetails>();
|
|
19
|
+
|
|
20
|
+
/** The latest version of each head; `load` reads the documents this cache does not hold yet. */
|
|
21
|
+
async resolve(heads: readonly PublishedHead[], load: (missing: readonly PublishedHead[]) => Promise<readonly VersionDetails[]>): Promise<readonly VersionDetails[]> {
|
|
22
|
+
const current = new Set(heads.map((head) => head.id));
|
|
23
|
+
for (const id of this.byProcess.keys()) if (!current.has(id)) this.byProcess.delete(id);
|
|
24
|
+
const missing = heads.filter((head) => this.byProcess.get(head.id)?.version !== head.latestVersion);
|
|
25
|
+
if (missing.length > 0) {
|
|
26
|
+
const loaded = new Map((await load(missing)).map((version) => [version.key, version]));
|
|
27
|
+
for (const head of missing) {
|
|
28
|
+
const version = loaded.get(head.key);
|
|
29
|
+
// A version another instance has just replaced is read on the next call.
|
|
30
|
+
if (version?.version === head.latestVersion) this.byProcess.set(head.id, version);
|
|
31
|
+
else this.byProcess.delete(head.id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return heads.flatMap((head) => {
|
|
35
|
+
const version = this.byProcess.get(head.id);
|
|
36
|
+
return version === undefined ? [] : [version];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Column, Entity, Index, UUID } from "../../../orm";
|
|
1
|
+
import { Column, Entity, Index, UUID, Nullable } from "../../../orm";
|
|
2
2
|
import type { LocalizedText } from "../../../../library/workflow";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -12,50 +12,63 @@ export class WfProcess {
|
|
|
12
12
|
id = "";
|
|
13
13
|
|
|
14
14
|
@Index({ unique: true })
|
|
15
|
+
@Nullable()
|
|
15
16
|
@Column({ type: "text" })
|
|
16
17
|
key = "";
|
|
17
18
|
|
|
19
|
+
@Nullable()
|
|
18
20
|
@Column({ type: "json" })
|
|
19
21
|
title: LocalizedText = "";
|
|
20
22
|
|
|
21
23
|
/** Every text of `title`, the default language first: what a list searches and sorts by. */
|
|
24
|
+
@Nullable()
|
|
22
25
|
@Column({ type: "text" })
|
|
23
26
|
titleText = "";
|
|
24
27
|
|
|
25
|
-
@
|
|
28
|
+
@Nullable()
|
|
29
|
+
@Column({ type: "json" })
|
|
26
30
|
description: LocalizedText | null = null;
|
|
27
31
|
|
|
28
32
|
/** A path, `Sales/Orders`: there is no folder entity. */
|
|
29
33
|
@Index()
|
|
30
|
-
@
|
|
34
|
+
@Nullable()
|
|
35
|
+
@Column({ type: "text" })
|
|
31
36
|
folder: string | null = null;
|
|
32
37
|
|
|
33
38
|
/** Whose process it is: an opaque id of the application. */
|
|
34
|
-
@
|
|
39
|
+
@Nullable()
|
|
40
|
+
@Column({ type: "text" })
|
|
35
41
|
owner: string | null = null;
|
|
36
42
|
|
|
37
43
|
/** The draft document; kept with validation errors too. */
|
|
38
|
-
@
|
|
44
|
+
@Nullable()
|
|
45
|
+
@Column({ type: "json" })
|
|
39
46
|
draft: unknown = null;
|
|
40
47
|
|
|
41
|
-
@
|
|
48
|
+
@Nullable()
|
|
49
|
+
@Column({ type: "datetime" })
|
|
42
50
|
draftUpdatedAt: Date | null = null;
|
|
43
51
|
|
|
44
|
-
@
|
|
52
|
+
@Nullable()
|
|
53
|
+
@Column({ type: "text" })
|
|
45
54
|
draftUpdatedByKind: string | null = null;
|
|
46
55
|
|
|
47
|
-
@
|
|
56
|
+
@Nullable()
|
|
57
|
+
@Column({ type: "text" })
|
|
48
58
|
draftUpdatedById: string | null = null;
|
|
49
59
|
|
|
50
|
-
@
|
|
60
|
+
@Nullable()
|
|
61
|
+
@Column({ type: "integer" })
|
|
51
62
|
latestVersion: number | null = null;
|
|
52
63
|
|
|
53
64
|
/** Days to keep completed and cancelled instances; `null` keeps them. */
|
|
54
|
-
@
|
|
65
|
+
@Nullable()
|
|
66
|
+
@Column({ type: "integer" })
|
|
55
67
|
retentionDays: number | null = null;
|
|
56
68
|
|
|
57
69
|
/** An archived process is not started. */
|
|
58
|
-
@
|
|
70
|
+
@Nullable()
|
|
71
|
+
@Column({ type: "datetime" })
|
|
59
72
|
archivedAt: Date | null = null;
|
|
60
73
|
|
|
61
74
|
@Column({ type: "createdAt" })
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Column, Entity, ForeignKey, Index, UUID } from "../../../orm";
|
|
1
|
+
import { Column, Entity, ForeignKey, Index, UUID, Nullable } from "../../../orm";
|
|
2
2
|
import { WfProcess } from "./WfProcess.model";
|
|
3
3
|
|
|
4
4
|
/** A published version of a process: an immutable snapshot of its document. */
|
|
@@ -10,25 +10,31 @@ export class WfVersion {
|
|
|
10
10
|
id = "";
|
|
11
11
|
|
|
12
12
|
/** A reference: the foreign key makes the column a native uuid. A non-key `type: "uuid"` would generate a value. */
|
|
13
|
+
@Nullable()
|
|
13
14
|
@Column({ type: "text" })
|
|
14
15
|
processId = "";
|
|
15
16
|
|
|
17
|
+
@Nullable()
|
|
16
18
|
@Column({ type: "integer" })
|
|
17
19
|
version = 0;
|
|
18
20
|
|
|
21
|
+
@Nullable()
|
|
19
22
|
@Column({ type: "json" })
|
|
20
23
|
document: unknown = null;
|
|
21
24
|
|
|
22
25
|
/** sha-256 of the document: publishing the same document again adds no version. */
|
|
26
|
+
@Nullable()
|
|
23
27
|
@Column({ type: "text" })
|
|
24
28
|
documentHash = "";
|
|
25
29
|
|
|
26
30
|
@Column({ type: "createdAt" })
|
|
27
31
|
publishedAt = new Date(0);
|
|
28
32
|
|
|
33
|
+
@Nullable()
|
|
29
34
|
@Column({ type: "text" })
|
|
30
35
|
publishedByKind = "";
|
|
31
36
|
|
|
37
|
+
@Nullable()
|
|
32
38
|
@Column({ type: "text" })
|
|
33
39
|
publishedById = "";
|
|
34
40
|
}
|
|
@@ -146,7 +146,10 @@ export abstract class IWorkflowDefinitions {
|
|
|
146
146
|
abstract version(key: string, version: number): Promise<VersionDetails>;
|
|
147
147
|
/** The latest published version of a process that is not archived. */
|
|
148
148
|
abstract latest(key: string): Promise<VersionDetails>;
|
|
149
|
-
/**
|
|
149
|
+
/**
|
|
150
|
+
* The latest version of every process that is not archived: where event and scheduled
|
|
151
|
+
* starts come from. Documents are kept between calls and must not be changed.
|
|
152
|
+
*/
|
|
150
153
|
abstract published(): Promise<readonly VersionDetails[]>;
|
|
151
154
|
/** A version by id, for the engine; `undefined` if there is none. */
|
|
152
155
|
abstract versionById(id: string): Promise<VersionDetails | undefined>;
|
|
@@ -19,6 +19,7 @@ import type { CodeProcessCatalog } from "../code/CodeProcessCatalog";
|
|
|
19
19
|
import type { OwnedCodeProcess } from "../code/codeProcesses";
|
|
20
20
|
import { WORKFLOW_PROCESS_CHANGED, type ProcessChange } from "../events/Definitions.events";
|
|
21
21
|
import type { DefinitionsDbContext } from "../model/Definitions.dbContext";
|
|
22
|
+
import type { PublishedVersions } from "../internal/PublishedVersions";
|
|
22
23
|
import { WfProcess } from "../model/WfProcess.model";
|
|
23
24
|
import { WfVersion } from "../model/WfVersion.model";
|
|
24
25
|
import {
|
|
@@ -52,6 +53,7 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
52
53
|
private readonly events: WorkflowEvents,
|
|
53
54
|
private readonly languages: WorkflowLanguages,
|
|
54
55
|
private readonly references: WorkflowReferences,
|
|
56
|
+
private readonly publishedVersions: PublishedVersions,
|
|
55
57
|
/** The scope: the port is asked here, so that the application's own registration replaces the default. */
|
|
56
58
|
private readonly services: ServiceProvider,
|
|
57
59
|
) {
|
|
@@ -351,13 +353,21 @@ export class WorkflowDefinitions extends IWorkflowDefinitions {
|
|
|
351
353
|
}
|
|
352
354
|
|
|
353
355
|
async published(): Promise<readonly VersionDetails[]> {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
356
|
+
// Without the drafts and documents: this runs on every event and every firing of a schedule.
|
|
357
|
+
const rows = await this.db.processes.asNoTracking()
|
|
358
|
+
.where((p) => p.archivedAt.isNull().and(p.latestVersion.isNotNull()))
|
|
359
|
+
.select((p) => ({ id: p.id, key: p.key, latestVersion: p.latestVersion }))
|
|
360
|
+
.toList();
|
|
361
|
+
const heads = rows.map((row) => ({ id: row.id, key: row.key, latestVersion: row.latestVersion! }));
|
|
362
|
+
return await this.publishedVersions.resolve(heads, async (missing) => {
|
|
363
|
+
const versions = await this.db.versions.asNoTracking()
|
|
364
|
+
.where((v) => v.processId.in(missing.map((head) => head.id)).and(v.version.in([...new Set(missing.map((head) => head.latestVersion))])))
|
|
365
|
+
.toList();
|
|
366
|
+
const byId = new Map(missing.map((head) => [head.id, head]));
|
|
367
|
+
return versions
|
|
368
|
+
.filter((version) => byId.get(version.processId)?.latestVersion === version.version)
|
|
369
|
+
.map((version) => versionOf(byId.get(version.processId)!.key, version));
|
|
370
|
+
});
|
|
361
371
|
}
|
|
362
372
|
|
|
363
373
|
async versionById(id: string): Promise<VersionDetails | undefined> {
|
|
@@ -14,6 +14,7 @@ import { CatalogImport } from "../definitions_module/code/CatalogImport";
|
|
|
14
14
|
import { CodeProcessCatalog } from "../definitions_module/code/CodeProcessCatalog";
|
|
15
15
|
import { CodeProcessPublisher } from "../definitions_module/code/CodeProcessPublisher";
|
|
16
16
|
import { ProcessesController } from "../definitions_module/http/Processes.controller";
|
|
17
|
+
import { PublishedVersions } from "../definitions_module/internal/PublishedVersions";
|
|
17
18
|
import { DefinitionsDbContext } from "../definitions_module/model/Definitions.dbContext";
|
|
18
19
|
import { IWorkflowDefinitions } from "../definitions_module/services/IWorkflowDefinitions.service";
|
|
19
20
|
import { WorkflowDefinitions } from "../definitions_module/services/WorkflowDefinitions.service";
|
|
@@ -66,7 +67,7 @@ registerFrameworkClassDeps([
|
|
|
66
67
|
[TasksDbContext, [DbContextOptions]],
|
|
67
68
|
[TimerStep, [RuntimeDbContext, WorkflowEngine]],
|
|
68
69
|
[ToolStep, [RuntimeDbContext, WorkflowEngine, WorkflowTools, WorkflowReferences, WorkflowLanguages, ServiceProvider]],
|
|
69
|
-
[WorkflowDefinitions, [DefinitionsDbContext, WorkflowTools, CodeProcessCatalog, WorkflowClock, WorkflowEvents, WorkflowLanguages, WorkflowReferences, ServiceProvider]],
|
|
70
|
+
[WorkflowDefinitions, [DefinitionsDbContext, WorkflowTools, CodeProcessCatalog, WorkflowClock, WorkflowEvents, WorkflowLanguages, WorkflowReferences, PublishedVersions, ServiceProvider]],
|
|
70
71
|
[WorkflowEngine, [RuntimeDbContext, IWorkflowDefinitions, GraphCache, WorkflowEvents, WorkflowClock]],
|
|
71
72
|
[WorkflowEvents, [EventBus, ServiceProvider, "optional:Logger"]],
|
|
72
73
|
[WorkflowLanguages, ["Config:workflow"]],
|
package/core/workflow/index.ts
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
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
|
*/
|
|
6
|
+
import type { PermissionsOf } from "../access";
|
|
7
|
+
import type { definitionsPermissions } from "./definitions_module/access/Definitions.permissions";
|
|
6
8
|
import type { CodeProcess } from "./definitions_module/code/defineProcess";
|
|
9
|
+
import type { runtimePermissions } from "./runtime_module/access/Runtime.permissions";
|
|
10
|
+
import type { tasksPermissions } from "./tasks_module/access/Tasks.permissions";
|
|
7
11
|
import "./generated/deps";
|
|
8
12
|
|
|
9
13
|
export { WorkflowModule } from "./Workflow.module";
|
|
@@ -73,6 +77,11 @@ export { WorkflowPartitions, DefaultWorkflowPartitions, type ProcessPartition }
|
|
|
73
77
|
export { WorkflowError, type WorkflowErrorCode } from "./contracts/WorkflowError";
|
|
74
78
|
export type { WorkflowActor, WorkflowAuditOptions, WorkflowPage, WorkflowPaging, WorkflowRequest, WorkflowToolMetadata } from "./contracts/types";
|
|
75
79
|
|
|
80
|
+
// The pack's own permissions: an application that imports the pack may name them in `can` and `defineRoles`.
|
|
81
|
+
declare module "../access/index" {
|
|
82
|
+
interface BazisPermissions extends PermissionsOf<typeof definitionsPermissions>, PermissionsOf<typeof runtimePermissions>, PermissionsOf<typeof tasksPermissions> {}
|
|
83
|
+
}
|
|
84
|
+
|
|
76
85
|
// Augment the declaring file, not the "../di" re-export: re-export targets
|
|
77
86
|
// depend on program file order and silently stop merging in some projects.
|
|
78
87
|
declare module "../di/module/types/BazisModule" {
|
|
@@ -16,7 +16,7 @@ export class WorkflowLanguages {
|
|
|
16
16
|
/** The best language of an `Accept-Language` header: by weight, then by order; `ru-RU` also finds `ru`. */
|
|
17
17
|
of(header: string | undefined): string {
|
|
18
18
|
if (header === undefined) return this.default;
|
|
19
|
-
const known = new Set([this.default, ...this.config.get("languages")
|
|
19
|
+
const known = new Set([this.default, ...this.config.get("languages")]);
|
|
20
20
|
const wanted = header.split(",")
|
|
21
21
|
.map((part, index) => {
|
|
22
22
|
const [tag = "", ...parameters] = part.trim().split(";");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IAccess, type AccessObject, type SubjectAccess } from "../../access";
|
|
1
|
+
import { IAccess, type AccessObject, type SubjectAccess, type PermissionKey } from "../../access";
|
|
2
2
|
import { ForbiddenError, type HttpContext } from "../../http";
|
|
3
3
|
import { frameworkBadRequest } from "../../http/Errors/HttpError";
|
|
4
4
|
import { ListQueryError, parseListQuery, type ListQuery, type ListQueryOptions, type ListQueryProblem } from "../../../library/jsonapi";
|
|
@@ -32,7 +32,7 @@ export function defaultLanguage(ctx: HttpContext): string {
|
|
|
32
32
|
* right. The actor's id is the first owner id, so that `{ own: true }` means
|
|
33
33
|
* the instances the subject started.
|
|
34
34
|
*/
|
|
35
|
-
export async function callerOf(ctx: HttpContext, permission?:
|
|
35
|
+
export async function callerOf(ctx: HttpContext, permission?: PermissionKey, object?: AccessObject): Promise<WorkflowCaller> {
|
|
36
36
|
const access = await ctx.services.resolve(IAccess).for(ctx);
|
|
37
37
|
if (permission !== undefined && !await access.can(permission, object)) throw new ForbiddenError();
|
|
38
38
|
const sub = access.subject.sub;
|
|
@@ -38,8 +38,12 @@ export class WorkflowWorker extends BackgroundService {
|
|
|
38
38
|
try {
|
|
39
39
|
const jobs = await this.jobs.claim(batchSize);
|
|
40
40
|
claimed = jobs.length;
|
|
41
|
-
for (const job of jobs) {
|
|
42
|
-
if (signal.aborted)
|
|
41
|
+
for (const [index, job] of jobs.entries()) {
|
|
42
|
+
if (signal.aborted) {
|
|
43
|
+
// What was claimed and will not run is free for another worker at once, not in a lease.
|
|
44
|
+
await this.jobs.release(jobs.slice(index));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
43
47
|
await this.jobs.run(job, signal);
|
|
44
48
|
}
|
|
45
49
|
} catch (error) {
|
|
@@ -32,7 +32,7 @@ export class InstancesController {
|
|
|
32
32
|
language,
|
|
33
33
|
...(request.test === true ? { test: true } : {}),
|
|
34
34
|
});
|
|
35
|
-
return Created(`${ctx.path}/${instance.id}
|
|
35
|
+
return Created(instance, `${ctx.path}/${instance.id}`);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/** A start per element, for an action over many records (WORKFLOW-002 §5.1). */
|
|
@@ -26,7 +26,10 @@ export class WorkflowJobs {
|
|
|
26
26
|
private readonly report: JobFailureReport,
|
|
27
27
|
) {}
|
|
28
28
|
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Takes up to `batchSize` due jobs with `FOR UPDATE SKIP LOCKED` and a lease, so
|
|
31
|
+
* instances never take one job twice. {@link run} renews the lease of each as it starts.
|
|
32
|
+
*/
|
|
30
33
|
async claim(batchSize: number): Promise<WfJob[]> {
|
|
31
34
|
const scope = this.services.createScope();
|
|
32
35
|
try {
|
|
@@ -51,7 +54,46 @@ export class WorkflowJobs {
|
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The lease of a claimed job starts when the job does: a batch runs one job after
|
|
59
|
+
* another, and a job that waited for the ones before it longer than a lease may
|
|
60
|
+
* have been taken by another worker. `false` then: the job is not this worker's.
|
|
61
|
+
*/
|
|
62
|
+
private async begin(job: WfJob): Promise<boolean> {
|
|
63
|
+
const scope = this.services.createScope();
|
|
64
|
+
try {
|
|
65
|
+
const db = scope.resolve(RuntimeDbContext);
|
|
66
|
+
const lockedUntil = new Date(scope.resolve(WorkflowClock).now().getTime() + LEASE_MS);
|
|
67
|
+
const held = job.lockedUntil;
|
|
68
|
+
const { affectedRows } = await db.jobs.asNoTracking()
|
|
69
|
+
.where((j) => j.id.eq(job.id).and(held === null ? j.lockedUntil.isNull() : j.lockedUntil.eq(held)))
|
|
70
|
+
.executeUpdate({ lockedUntil });
|
|
71
|
+
if (affectedRows === 0) return false;
|
|
72
|
+
job.lockedUntil = lockedUntil;
|
|
73
|
+
return true;
|
|
74
|
+
} finally {
|
|
75
|
+
await scope.dispose();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Gives back claimed jobs that will not run, a stopping worker's: they are free at once, and the claim does not count as an attempt. */
|
|
80
|
+
async release(jobs: readonly WfJob[]): Promise<void> {
|
|
81
|
+
if (jobs.length === 0) return;
|
|
82
|
+
const scope = this.services.createScope();
|
|
83
|
+
try {
|
|
84
|
+
const db = scope.resolve(RuntimeDbContext);
|
|
85
|
+
for (const job of jobs) {
|
|
86
|
+
const held = job.lockedUntil;
|
|
87
|
+
if (held === null) continue;
|
|
88
|
+
await db.jobs.asNoTracking().where((j) => j.id.eq(job.id).and(j.lockedUntil.eq(held))).executeUpdate({ lockedUntil: null, attempts: Math.max(job.attempts - 1, 0) });
|
|
89
|
+
}
|
|
90
|
+
} finally {
|
|
91
|
+
await scope.dispose();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
54
95
|
async run(job: WfJob, signal: AbortSignal): Promise<void> {
|
|
96
|
+
if (!await this.begin(job).catch((error: unknown) => { this.report("job could not begin", error, { job: job.id }); return false; })) return;
|
|
55
97
|
const scope = this.services.createScope();
|
|
56
98
|
try {
|
|
57
99
|
switch (job.kind) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Column, Entity, ForeignKey, Index, UUID } from "../../../orm";
|
|
1
|
+
import { Column, Entity, ForeignKey, Index, UUID, Nullable } from "../../../orm";
|
|
2
2
|
import { WfInstance } from "./WfInstance.model";
|
|
3
3
|
|
|
4
4
|
export type HistoryType = "started" | "queued" | "released" | "entered" | "left" | "tool" | "assigned" | "iteration" | "called" | "returned" | "skipped" | "timer" | "event" | "error" | "retried" | "cancelled" | "timedOut" | "handler" | "completed";
|
|
@@ -11,26 +11,33 @@ export class WfHistory {
|
|
|
11
11
|
@UUID({ version: "v7" })
|
|
12
12
|
id = "";
|
|
13
13
|
|
|
14
|
+
@Nullable()
|
|
14
15
|
@Column({ type: "text" })
|
|
15
16
|
instanceId = "";
|
|
16
17
|
|
|
17
|
-
@
|
|
18
|
+
@Nullable()
|
|
19
|
+
@Column({ type: "text" })
|
|
18
20
|
tokenId: string | null = null;
|
|
19
21
|
|
|
20
|
-
@
|
|
22
|
+
@Nullable()
|
|
23
|
+
@Column({ type: "text" })
|
|
21
24
|
nodeId: string | null = null;
|
|
22
25
|
|
|
26
|
+
@Nullable()
|
|
23
27
|
@Column({ type: "text" })
|
|
24
28
|
type: HistoryType = "entered";
|
|
25
29
|
|
|
26
|
-
@
|
|
30
|
+
@Nullable()
|
|
31
|
+
@Column({ type: "json" })
|
|
27
32
|
data: Record<string, unknown> | null = null;
|
|
28
33
|
|
|
29
34
|
/** Who did it: cancel, retry, task completion; `null` for engine steps. */
|
|
30
|
-
@
|
|
35
|
+
@Nullable()
|
|
36
|
+
@Column({ type: "text" })
|
|
31
37
|
actorKind: string | null = null;
|
|
32
38
|
|
|
33
|
-
@
|
|
39
|
+
@Nullable()
|
|
40
|
+
@Column({ type: "text" })
|
|
34
41
|
actorId: string | null = null;
|
|
35
42
|
|
|
36
43
|
@Column({ type: "createdAt" })
|