bazis 0.128.0 → 0.130.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/access/Access.module.ts +36 -0
- package/core/access/catalog/AccessCatalog.ts +73 -0
- package/core/access/catalog/IAccessCatalog.service.ts +17 -0
- package/core/access/catalog/moduleFields.ts +153 -0
- package/core/access/catalog/permissions.ts +127 -0
- package/core/access/check/Access.ts +191 -0
- package/core/access/check/IAccess.service.ts +20 -0
- package/core/access/check/accessConfig.ts +18 -0
- package/core/access/check/can.ts +21 -0
- package/core/access/check/conditions.ts +132 -0
- package/core/access/check/filter.ts +132 -0
- package/core/access/contracts/AccessPolicy.ts +29 -0
- package/core/access/contracts/AttributeLookup.ts +17 -0
- package/core/access/contracts/References.ts +33 -0
- package/core/access/contracts/errors.ts +13 -0
- package/core/access/contracts/types.ts +66 -0
- package/core/access/generated/deps.ts +14 -0
- package/core/access/http/AccessCatalog.controller.ts +51 -0
- package/core/access/index.ts +35 -0
- package/core/access/roles/RolesAccessPolicy.ts +64 -0
- package/core/access/roles/roles.ts +62 -0
- package/core/access/testing/index.ts +54 -0
- package/core/background/BackgroundService.ts +1 -1
- package/core/di/README.md +5 -5
- package/core/di/errors/ClassDependenciesMismatchError.ts +1 -1
- package/core/di/internal/GraphValidator.ts +1 -1
- package/core/di/module/autoDeps.ts +11 -1
- package/core/di/module/createContainer.ts +1 -1
- package/core/di/module/providerSelection.ts +1 -1
- package/core/grpc/{GrpcClient.ts → GrpcClientBase.ts} +1 -1
- package/core/grpc/grpcClients.ts +79 -0
- package/core/grpc/index.ts +2 -2
- package/core/http/options.ts +1 -1
- package/core/http-client/httpClients.ts +82 -0
- package/core/http-client/index.ts +11 -91
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/scripts/di-generate-target.ts +9 -0
- package/core/scripts/di-generate.ts +177 -9
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +15 -11
- package/core/workflow/definitions_module/access/Definitions.permissions.ts +23 -0
- package/core/workflow/definitions_module/access/Process.references.ts +27 -0
- package/core/workflow/definitions_module/code/CodeProcessCatalog.ts +4 -3
- package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +2 -2
- package/core/workflow/definitions_module/http/Processes.controller.ts +23 -14
- package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +9 -0
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +4 -0
- package/core/workflow/generated/deps.ts +56 -0
- package/core/workflow/index.ts +1 -1
- package/core/workflow/internal/WorkflowTools.ts +2 -2
- package/core/workflow/internal/http.ts +18 -17
- package/core/workflow/runtime_module/WorkflowRuntime.module.ts +17 -34
- package/core/workflow/runtime_module/access/Runtime.permissions.ts +28 -0
- package/core/workflow/runtime_module/background/WorkflowRetention.ts +1 -1
- package/core/workflow/runtime_module/background/WorkflowWorker.ts +1 -10
- package/core/workflow/runtime_module/config/Runtime.config.ts +11 -1
- package/core/workflow/runtime_module/http/Events.controller.ts +5 -3
- package/core/workflow/runtime_module/http/Instances.controller.ts +25 -15
- package/core/workflow/runtime_module/internal/WorkflowEngine.ts +4 -3
- package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +3 -0
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +16 -5
- package/core/workflow/tasks_module/WorkflowTasks.module.ts +8 -10
- package/core/workflow/tasks_module/access/Tasks.permissions.ts +13 -0
- package/core/workflow/tasks_module/events/TaskEvents.ts +29 -8
- package/core/workflow/tasks_module/http/Tasks.controller.ts +21 -12
- package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +8 -3
- package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +41 -42
- package/core/workflow/testing/index.ts +1 -0
- package/index.ts +1 -1
- package/library/http-client/{HttpClient.ts → HttpClientBase.ts} +5 -5
- package/library/http-client/HttpClientFactory.ts +5 -5
- package/library/http-client/index.ts +3 -3
- package/library/terminal/messages.ts +2 -2
- package/package.json +3 -1
- package/core/grpc/grpcClientProvider.ts +0 -9
- package/core/workflow/access/WorkflowAccess.ts +0 -52
package/core/workflow/index.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* extension points. The document format is `bazis/library/workflow`.
|
|
5
5
|
*/
|
|
6
6
|
import type { CodeProcess } from "./definitions_module/code/defineProcess";
|
|
7
|
+
import "./generated/deps";
|
|
7
8
|
|
|
8
9
|
export { WorkflowModule } from "./Workflow.module";
|
|
9
10
|
export { defineProcess, type CodeProcess, type ProcessDefinition } from "./definitions_module/code/defineProcess";
|
|
@@ -49,7 +50,6 @@ export { workflowRuntimeConfig } from "./runtime_module/config/Runtime.config";
|
|
|
49
50
|
export type { HistoryType } from "./runtime_module/model/WfHistory.model";
|
|
50
51
|
export type { InstanceStatus, StartedVia } from "./runtime_module/model/WfInstance.model";
|
|
51
52
|
export type { TokenStatus } from "./runtime_module/model/WfToken.model";
|
|
52
|
-
export { WorkflowAccess, DenyWorkflowAccess, type WorkflowAccessTarget, type WorkflowAction } from "./access/WorkflowAccess";
|
|
53
53
|
export { WorkflowInputValidator, DefaultWorkflowInputValidator, type WorkflowInputContext } from "./contracts/WorkflowInputValidator";
|
|
54
54
|
export { WorkflowError, type WorkflowErrorCode } from "./contracts/WorkflowError";
|
|
55
55
|
export type { WorkflowActor, WorkflowPage, WorkflowPaging } from "./contracts/types";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServiceProvider } from "../../di";
|
|
2
2
|
import { ToolRunner, type AgentToolContract, type BoundToolRunner } from "../../agent";
|
|
3
3
|
|
|
4
4
|
/** The workflow caller id of TOOLS-001: processes run only Tools whose `callers` list it. */
|
|
@@ -11,7 +11,7 @@ export const WORKFLOW_CALLER = "workflow";
|
|
|
11
11
|
export class WorkflowTools {
|
|
12
12
|
private readonly runner: BoundToolRunner | undefined;
|
|
13
13
|
|
|
14
|
-
constructor(services:
|
|
14
|
+
constructor(services: ServiceProvider) {
|
|
15
15
|
this.runner = services.tryResolve(ToolRunner)?.as(WORKFLOW_CALLER);
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { IAccess, type AccessObject, type SubjectAccess } from "../../access";
|
|
2
|
+
import { ForbiddenError, type HttpContext } from "../../http";
|
|
3
3
|
import type { WorkflowActor } from "../contracts/types";
|
|
4
4
|
import { WorkflowError } from "../contracts/WorkflowError";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export async function authorize(ctx: HttpContext, action: WorkflowAction, target: WorkflowAccessTarget = {}): Promise<WorkflowActor> {
|
|
11
|
-
const access = ctx.services.tryResolve(WorkflowAccess);
|
|
12
|
-
if (access === undefined) throw new ForbiddenError("Workflow HTTP is closed: register a WorkflowAccess implementation");
|
|
13
|
-
const actor = await access.actor(ctx);
|
|
14
|
-
if (actor === undefined) throw new UnauthorizedError();
|
|
15
|
-
if (!await access.can(ctx, action, target)) throw new ForbiddenError();
|
|
16
|
-
return actor;
|
|
6
|
+
/** Who calls a route: their rights and how they are recorded as an actor. */
|
|
7
|
+
export interface WorkflowCaller {
|
|
8
|
+
readonly access: SubjectAccess;
|
|
9
|
+
readonly actor: WorkflowActor;
|
|
17
10
|
}
|
|
18
11
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The caller of a route; 401 without a subject. With `permission` and `object`
|
|
14
|
+
* also checks the object the route's `can` could not know: 403 without the
|
|
15
|
+
* right. The actor's id is the first owner id, so that `{ own: true }` means
|
|
16
|
+
* the instances the subject started.
|
|
17
|
+
*/
|
|
18
|
+
export async function callerOf(ctx: HttpContext, permission?: string, object?: AccessObject): Promise<WorkflowCaller> {
|
|
19
|
+
const access = await ctx.services.resolve(IAccess).for(ctx);
|
|
20
|
+
if (permission !== undefined && !await access.can(permission, object)) throw new ForbiddenError();
|
|
21
|
+
const sub = access.subject.sub;
|
|
22
|
+
const [id = sub.slice(sub.lastIndexOf(":") + 1)] = await access.ownerIds();
|
|
23
|
+
return { access, actor: { kind: "user", id } };
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/** A route parameter as a string. */
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { WorkflowClock } from "../contracts/WorkflowClock";
|
|
1
|
+
import { AccessModule } from "../../access";
|
|
2
|
+
import { Module, scoped, singleton } from "../../di";
|
|
4
3
|
import { DefaultWorkflowInputValidator, WorkflowInputValidator } from "../contracts/WorkflowInputValidator";
|
|
5
4
|
import { WorkflowDefinitionsModule } from "../definitions_module/WorkflowDefinitions.module";
|
|
6
|
-
import {
|
|
7
|
-
import { WorkflowTools } from "../internal/WorkflowTools";
|
|
5
|
+
import { runtimePermissions } from "./access/Runtime.permissions";
|
|
8
6
|
import { WorkflowRetention } from "./background/WorkflowRetention";
|
|
9
7
|
import { WorkflowWorker } from "./background/WorkflowWorker";
|
|
10
8
|
import { workflowRuntimeConfig } from "./config/Runtime.config";
|
|
@@ -21,40 +19,25 @@ import { WorkflowRuntime } from "./services/WorkflowRuntime.service";
|
|
|
21
19
|
|
|
22
20
|
/**
|
|
23
21
|
* Runs published versions: instances, tokens, jobs, history, the worker and
|
|
24
|
-
* retention ([passport](MODULE.md)).
|
|
25
|
-
*
|
|
22
|
+
* retention ([passport](MODULE.md)). The application replaces the input
|
|
23
|
+
* validator by registering its own `WorkflowInputValidator`.
|
|
26
24
|
*/
|
|
27
25
|
@Module({
|
|
28
|
-
imports: [WorkflowDefinitionsModule],
|
|
26
|
+
imports: [AccessModule, WorkflowDefinitionsModule],
|
|
27
|
+
permissions: runtimePermissions,
|
|
29
28
|
config: [workflowRuntimeConfig],
|
|
30
29
|
ormBazis: { context: RuntimeDbContext, entities: [...RUNTIME_ENTITIES], ownedStore: runtimeStore, registerRepositories: false },
|
|
31
30
|
providers: [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
resolver.tryResolve(LOGGER),
|
|
40
|
-
resolver.resolve(WorkflowClock),
|
|
41
|
-
))),
|
|
42
|
-
scoped(ToolStep, ToolStep, [RuntimeDbContext, WorkflowEngine, WorkflowTools] as const),
|
|
43
|
-
scoped(TimerStep, TimerStep, [RuntimeDbContext, WorkflowEngine] as const),
|
|
44
|
-
scoped(Schedules, Schedules, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions] as const),
|
|
45
|
-
// The application's input validator, if it registered one anywhere; the built-in one otherwise.
|
|
46
|
-
DI.scoped(DI.factoryProviderWithResolver(IWorkflowRuntime, [], (resolver) => new WorkflowRuntime(
|
|
47
|
-
resolver.resolve(RuntimeDbContext),
|
|
48
|
-
resolver.resolve(WorkflowEngine),
|
|
49
|
-
resolver.resolve(IWorkflowDefinitions),
|
|
50
|
-
resolver.resolve(SERVICE_PROVIDER).tryResolve(WorkflowInputValidator) ?? new DefaultWorkflowInputValidator(),
|
|
51
|
-
))),
|
|
31
|
+
singleton(GraphCache),
|
|
32
|
+
scoped(WorkflowEngine),
|
|
33
|
+
scoped(ToolStep),
|
|
34
|
+
scoped(TimerStep),
|
|
35
|
+
scoped(Schedules),
|
|
36
|
+
singleton(WorkflowInputValidator, DefaultWorkflowInputValidator),
|
|
37
|
+
scoped(IWorkflowRuntime, WorkflowRuntime),
|
|
52
38
|
],
|
|
53
|
-
controllers: [
|
|
54
|
-
background: [
|
|
55
|
-
|
|
56
|
-
DI.bindDeps(WorkflowRetention, SERVICE_PROVIDER, workflowRuntimeConfig.token),
|
|
57
|
-
],
|
|
58
|
-
exports: [IWorkflowRuntime],
|
|
39
|
+
controllers: [InstancesController, EventsController],
|
|
40
|
+
background: [WorkflowWorker, WorkflowRetention],
|
|
41
|
+
exports: [IWorkflowRuntime, WorkflowInputValidator],
|
|
59
42
|
})
|
|
60
43
|
export class WorkflowRuntimeModule {}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { definePermissions } from "../../../access";
|
|
2
|
+
|
|
3
|
+
const process = { ref: "workflow.process", column: "processKey" };
|
|
4
|
+
|
|
5
|
+
/** The rights of running processes (WORKFLOW-002 §6.2). */
|
|
6
|
+
export const runtimePermissions = definePermissions({
|
|
7
|
+
"workflow.instances.start": {
|
|
8
|
+
title: { en: "Start processes", ru: "Запускать процессы" },
|
|
9
|
+
scope: "request",
|
|
10
|
+
on: { process },
|
|
11
|
+
},
|
|
12
|
+
"workflow.instances.read": {
|
|
13
|
+
title: { en: "See instances", ru: "Видеть экземпляры" },
|
|
14
|
+
scope: { column: "partition" },
|
|
15
|
+
on: { process },
|
|
16
|
+
owner: "startedById",
|
|
17
|
+
},
|
|
18
|
+
"workflow.instances.manage": {
|
|
19
|
+
title: { en: "Cancel and retry instances", ru: "Отменять и повторять экземпляры" },
|
|
20
|
+
scope: { column: "partition" },
|
|
21
|
+
on: { process },
|
|
22
|
+
},
|
|
23
|
+
"workflow.events.emit": {
|
|
24
|
+
title: { en: "Send events to processes", ru: "Отправлять события процессам" },
|
|
25
|
+
scope: "request",
|
|
26
|
+
on: { event: { type: "string" } },
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -5,7 +5,7 @@ import { WorkflowClock } from "../../contracts/WorkflowClock";
|
|
|
5
5
|
import { IWorkflowDefinitions } from "../../definitions_module/services/IWorkflowDefinitions.service";
|
|
6
6
|
import { WORKFLOW_INSTANCES_DELETED } from "../events/Runtime.events";
|
|
7
7
|
import { RuntimeDbContext } from "../model/Runtime.dbContext";
|
|
8
|
-
import type { WorkflowSettings } from "
|
|
8
|
+
import type { WorkflowSettings } from "../config/Runtime.config";
|
|
9
9
|
|
|
10
10
|
const DAY_MS = 86_400_000;
|
|
11
11
|
|
|
@@ -2,7 +2,7 @@ import { BackgroundService, delay } from "../../../background";
|
|
|
2
2
|
import type { ServiceProvider } from "../../../di";
|
|
3
3
|
import { WorkflowClock } from "../../contracts/WorkflowClock";
|
|
4
4
|
import type { ConfigView } from "../../../kernel";
|
|
5
|
-
import type {
|
|
5
|
+
import type { WorkflowSettings } from "../config/Runtime.config";
|
|
6
6
|
import { WorkflowJobs } from "../internal/WorkflowJobs";
|
|
7
7
|
|
|
8
8
|
/** How often the scheduled starts are matched against the published versions. */
|
|
@@ -51,12 +51,3 @@ export class WorkflowWorker extends BackgroundService {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/** The resolved settings of {@link workflowRuntimeConfig}. */
|
|
55
|
-
export interface WorkflowSettings {
|
|
56
|
-
readonly "worker.enabled": boolean;
|
|
57
|
-
readonly "worker.intervalMs": number;
|
|
58
|
-
readonly "worker.batchSize": number;
|
|
59
|
-
readonly "retention.enabled": boolean;
|
|
60
|
-
readonly "retention.intervalMs": number;
|
|
61
|
-
readonly "retention.batchSize": number;
|
|
62
|
-
}
|
|
@@ -5,7 +5,17 @@ import { defineConfig } from "../../../kernel";
|
|
|
5
5
|
* application configuration: `BAZIS_WORKFLOW__WORKER__ENABLED=false` keeps an
|
|
6
6
|
* instance from running jobs, for example a web-only one.
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
/** The settings of the workflow background services, `workflow.*` in the configuration. */
|
|
9
|
+
export interface WorkflowSettings {
|
|
10
|
+
readonly "worker.enabled": boolean;
|
|
11
|
+
readonly "worker.intervalMs": number;
|
|
12
|
+
readonly "worker.batchSize": number;
|
|
13
|
+
readonly "retention.enabled": boolean;
|
|
14
|
+
readonly "retention.intervalMs": number;
|
|
15
|
+
readonly "retention.batchSize": number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const workflowRuntimeConfig = defineConfig<WorkflowSettings>("workflow", {
|
|
9
19
|
default: {
|
|
10
20
|
"worker.enabled": true,
|
|
11
21
|
"worker.intervalMs": 1000,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { contextOnlyController } from "../../../http/Routing/contextOnlyControllers";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { can } from "../../../access";
|
|
3
|
+
import { Authorize, Controller, Post, type HttpContext } from "../../../http";
|
|
4
|
+
import { body, callerOf, optionalString, param } from "../../internal/http";
|
|
4
5
|
import { IWorkflowRuntime } from "../services/IWorkflowRuntime.service";
|
|
5
6
|
|
|
6
7
|
/** Events from outside — a webhook, another service — over HTTP (WORKFLOW-001 §4). */
|
|
@@ -9,11 +10,12 @@ export class EventsController {
|
|
|
9
10
|
constructor(private readonly runtime: IWorkflowRuntime) {}
|
|
10
11
|
|
|
11
12
|
@Post(":eventKey")
|
|
13
|
+
@Authorize(can("workflow.events.emit"))
|
|
12
14
|
async emit(ctx: HttpContext) {
|
|
13
15
|
const eventKey = param(ctx, "eventKey");
|
|
14
16
|
const request = await body(ctx);
|
|
15
17
|
const partition = optionalString(request.partition, "partition") ?? null;
|
|
16
|
-
const actor = await
|
|
18
|
+
const { actor } = await callerOf(ctx, "workflow.events.emit", { scope: partition, event: eventKey });
|
|
17
19
|
const result = await this.runtime.emit(eventKey, request.payload ?? {}, {
|
|
18
20
|
correlation: optionalString(request.correlation, "correlation") ?? null,
|
|
19
21
|
partition,
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { contextOnlyController } from "../../../http/Routing/contextOnlyControllers";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { can, type AccessObject } from "../../../access";
|
|
3
|
+
import { Authorize, Controller, Created, Get, Post, type HttpContext } from "../../../http";
|
|
4
|
+
import { body, callerOf, optionalString, pagingQuery, param, requiredString } from "../../internal/http";
|
|
5
5
|
import type { InstanceStatus } from "../model/WfInstance.model";
|
|
6
|
-
import { IWorkflowRuntime } from "../services/IWorkflowRuntime.service";
|
|
6
|
+
import { IWorkflowRuntime, type InstanceSummary } from "../services/IWorkflowRuntime.service";
|
|
7
7
|
|
|
8
|
-
/** Instances over HTTP (WORKFLOW-001 §4); rights
|
|
8
|
+
/** Instances over HTTP (WORKFLOW-001 §4); rights are `workflow.instances.*` (WORKFLOW-002 §6.2). */
|
|
9
9
|
@Controller("workflow/instances")
|
|
10
10
|
export class InstancesController {
|
|
11
11
|
constructor(private readonly runtime: IWorkflowRuntime) {}
|
|
12
12
|
|
|
13
13
|
@Post()
|
|
14
|
+
@Authorize(can("workflow.instances.start"))
|
|
14
15
|
async start(ctx: HttpContext) {
|
|
15
16
|
const request = await body(ctx);
|
|
16
17
|
const processKey = requiredString(request.processKey, "processKey", 64);
|
|
17
18
|
const partition = optionalString(request.partition, "partition") ?? null;
|
|
18
|
-
const actor = await
|
|
19
|
+
const { actor } = await callerOf(ctx, "workflow.instances.start", { scope: partition, process: processKey });
|
|
19
20
|
const instance = await this.runtime.start(processKey, {
|
|
20
21
|
input: request.input,
|
|
21
22
|
partition,
|
|
@@ -26,14 +27,14 @@ export class InstancesController {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
@Get()
|
|
30
|
+
@Authorize(can("workflow.instances.read"))
|
|
29
31
|
async list(ctx: HttpContext) {
|
|
30
|
-
const
|
|
31
|
-
const processKey = optionalString(ctx.query("processKey"), "processKey");
|
|
32
|
-
await authorize(ctx, "instances.read", { partition: partition ?? null, ...(processKey === undefined ? {} : { processKey }) });
|
|
32
|
+
const { access } = await callerOf(ctx);
|
|
33
33
|
const page = await this.runtime.instances({
|
|
34
34
|
...pagingQuery(ctx),
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
access: await access.filter("workflow.instances.read"),
|
|
36
|
+
partition: optionalString(ctx.query("partition"), "partition"),
|
|
37
|
+
processKey: optionalString(ctx.query("processKey"), "processKey"),
|
|
37
38
|
status: optionalString(ctx.query("status"), "status") as InstanceStatus | undefined,
|
|
38
39
|
businessKey: optionalString(ctx.query("businessKey"), "businessKey"),
|
|
39
40
|
startedById: optionalString(ctx.query("startedById"), "startedById"),
|
|
@@ -42,33 +43,42 @@ export class InstancesController {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
@Get(":id")
|
|
46
|
+
@Authorize(can("workflow.instances.read"))
|
|
45
47
|
async get(ctx: HttpContext) {
|
|
46
48
|
const instance = await this.runtime.instance(param(ctx, "id"));
|
|
47
|
-
await
|
|
49
|
+
await callerOf(ctx, "workflow.instances.read", objectOf(instance));
|
|
48
50
|
return instance;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
@Get(":id/history")
|
|
54
|
+
@Authorize(can("workflow.instances.read"))
|
|
52
55
|
async history(ctx: HttpContext) {
|
|
53
56
|
const instance = await this.runtime.instance(param(ctx, "id"));
|
|
54
|
-
await
|
|
57
|
+
await callerOf(ctx, "workflow.instances.read", objectOf(instance));
|
|
55
58
|
return { data: await this.runtime.history(instance.id) };
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
@Post(":id/cancel")
|
|
62
|
+
@Authorize(can("workflow.instances.manage"))
|
|
59
63
|
async cancel(ctx: HttpContext) {
|
|
60
64
|
const instance = await this.runtime.instance(param(ctx, "id"));
|
|
61
65
|
const request = await body(ctx);
|
|
62
|
-
const actor = await
|
|
66
|
+
const { actor } = await callerOf(ctx, "workflow.instances.manage", objectOf(instance));
|
|
63
67
|
return await this.runtime.cancel(instance.id, { reason: optionalString(request.reason, "reason", 1000) ?? null, actor });
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
@Post(":id/retry")
|
|
71
|
+
@Authorize(can("workflow.instances.manage"))
|
|
67
72
|
async retry(ctx: HttpContext) {
|
|
68
73
|
const instance = await this.runtime.instance(param(ctx, "id"));
|
|
69
|
-
const actor = await
|
|
74
|
+
const { actor } = await callerOf(ctx, "workflow.instances.manage", objectOf(instance));
|
|
70
75
|
return await this.runtime.retry(instance.id, { actor });
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
contextOnlyController(InstancesController);
|
|
74
79
|
|
|
80
|
+
/** The instance as the object of a permission. */
|
|
81
|
+
function objectOf(instance: InstanceSummary): AccessObject {
|
|
82
|
+
return { scope: instance.partition, process: instance.processKey, startedById: instance.startedBy.id };
|
|
83
|
+
}
|
|
84
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServiceProvider } from "../../../di";
|
|
2
2
|
import { EventBus, type EventToken, type Logger } from "../../../kernel";
|
|
3
3
|
import { durationMs, evaluateCondition, nodeKindOf, readPath, type FormSpec, type InstanceData, type ProcessFlow, type ProcessNode } from "../../../../library/workflow";
|
|
4
4
|
import type { WorkflowActor } from "../../contracts/types";
|
|
@@ -52,9 +52,10 @@ export class WorkflowEngine {
|
|
|
52
52
|
private readonly definitions: IWorkflowDefinitions,
|
|
53
53
|
private readonly graphs: GraphCache,
|
|
54
54
|
private readonly events: EventBus,
|
|
55
|
-
|
|
56
|
-
private readonly
|
|
55
|
+
/** The scope of the engine: event handlers run in it. */
|
|
56
|
+
private readonly scope: ServiceProvider,
|
|
57
57
|
private readonly clock: WorkflowClock,
|
|
58
|
+
private readonly logger?: Logger,
|
|
58
59
|
) {}
|
|
59
60
|
|
|
60
61
|
/** The engine's time ({@link WorkflowClock}). */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AccessFilter } from "../../../access";
|
|
1
2
|
import type { FormIssue } from "../../../../library/workflow";
|
|
2
3
|
import type { WorkflowActor, WorkflowPage, WorkflowPaging } from "../../contracts/types";
|
|
3
4
|
import type { HistoryType } from "../model/WfHistory.model";
|
|
@@ -75,6 +76,8 @@ export interface EmitResult {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
export interface InstanceListQuery extends WorkflowPaging {
|
|
79
|
+
/** Which instances the caller may see, from `IAccess`; without it the calling code has decided. */
|
|
80
|
+
readonly access?: AccessFilter;
|
|
78
81
|
readonly partition?: string;
|
|
79
82
|
readonly status?: InstanceStatus;
|
|
80
83
|
readonly processKey?: string;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { grantsToWhere } from "../../../access";
|
|
1
2
|
import type { FormIssue, FormSpec } from "../../../../library/workflow";
|
|
2
3
|
import { isUuid, pagingOf, type WorkflowActor, type WorkflowPage } from "../../contracts/types";
|
|
3
4
|
import { WorkflowError } from "../../contracts/WorkflowError";
|
|
4
|
-
import type {
|
|
5
|
+
import type { ServiceProvider } from "../../../di";
|
|
6
|
+
import { WorkflowInputValidator } from "../../contracts/WorkflowInputValidator";
|
|
5
7
|
import type { IWorkflowDefinitions } from "../../definitions_module/services/IWorkflowDefinitions.service";
|
|
6
8
|
import type { WorkflowEngine } from "../internal/WorkflowEngine";
|
|
7
9
|
import type { RuntimeDbContext } from "../model/Runtime.dbContext";
|
|
@@ -27,7 +29,8 @@ export class WorkflowRuntime extends IWorkflowRuntime {
|
|
|
27
29
|
private readonly db: RuntimeDbContext,
|
|
28
30
|
private readonly engine: WorkflowEngine,
|
|
29
31
|
private readonly definitions: IWorkflowDefinitions,
|
|
30
|
-
|
|
32
|
+
/** The scope: the input validator is asked here, so that the application's own registration replaces the default. */
|
|
33
|
+
private readonly services: ServiceProvider,
|
|
31
34
|
) {
|
|
32
35
|
super();
|
|
33
36
|
}
|
|
@@ -39,7 +42,7 @@ export class WorkflowRuntime extends IWorkflowRuntime {
|
|
|
39
42
|
const version = await this.definitions.latest(processKey);
|
|
40
43
|
const start = Object.entries(version.document.nodes).find(([, node]) => node.kind === "manualStart");
|
|
41
44
|
if (!start) throw new WorkflowError("wrong_status", `Process ${processKey} has no manual start`);
|
|
42
|
-
const { value, issues } = await this.validator.validate(version.document.input, options.input, { purpose: "input", processKey, partition });
|
|
45
|
+
const { value, issues } = await this.validator().validate(version.document.input, options.input, { purpose: "input", processKey, partition });
|
|
43
46
|
if (issues.length > 0) throw new WorkflowError("invalid_input", "The process input is invalid", issues);
|
|
44
47
|
const id = await this.engine.begin(this.engine.cache(version), start[0], { input: value, partition, businessKey, startedVia: "manual", actor: options.actor });
|
|
45
48
|
return await this.instance(id);
|
|
@@ -81,7 +84,7 @@ export class WorkflowRuntime extends IWorkflowRuntime {
|
|
|
81
84
|
if (node?.kind !== "userTask" || !current || current.status !== "waiting" || step.instance.status !== "running") {
|
|
82
85
|
throw new WorkflowError("wrong_status", "The token does not wait for a task");
|
|
83
86
|
}
|
|
84
|
-
const { value, issues } = await this.validator.validate(node.config?.form as FormSpec | undefined, output, { purpose: "task", processKey: step.instance.processKey, partition: step.instance.partition });
|
|
87
|
+
const { value, issues } = await this.validator().validate(node.config?.form as FormSpec | undefined, output, { purpose: "task", processKey: step.instance.processKey, partition: step.instance.partition });
|
|
85
88
|
if (issues.length > 0) throw new WorkflowError("invalid_input", "The task form is invalid", issues);
|
|
86
89
|
await this.engine.complete(step, tokenId, value, actor);
|
|
87
90
|
return true;
|
|
@@ -117,7 +120,7 @@ export class WorkflowRuntime extends IWorkflowRuntime {
|
|
|
117
120
|
for (const [nodeId, node] of Object.entries(version.document.nodes)) {
|
|
118
121
|
if (node.kind !== "eventStart" || node.config?.event !== eventKey) continue;
|
|
119
122
|
// An event carries more than a process needs: fields the input does not declare are dropped, not refused.
|
|
120
|
-
const { value, issues: all } = await this.validator.validate(version.document.input, data, { purpose: "input", processKey: version.key, partition });
|
|
123
|
+
const { value, issues: all } = await this.validator().validate(version.document.input, data, { purpose: "input", processKey: version.key, partition });
|
|
121
124
|
const issues = all.filter((issue) => issue.code !== "unknown");
|
|
122
125
|
if (issues.length > 0) {
|
|
123
126
|
rejected.push({ processKey: version.key, issues });
|
|
@@ -132,7 +135,10 @@ export class WorkflowRuntime extends IWorkflowRuntime {
|
|
|
132
135
|
async instances(query: InstanceListQuery = {}): Promise<WorkflowPage<InstanceSummary>> {
|
|
133
136
|
const { limit, offset } = pagingOf(query);
|
|
134
137
|
if (query.status !== undefined && !STATUSES.has(query.status)) throw new WorkflowError("invalid_input", "status must be running, completed, cancelled or failed");
|
|
138
|
+
const where = query.access === undefined ? "all" : grantsToWhere(query.access);
|
|
139
|
+
if (where === "none") return { items: [], total: 0 };
|
|
135
140
|
let source = this.db.instances.asNoTracking();
|
|
141
|
+
if (where !== "all") source = source.where(() => where);
|
|
136
142
|
if (query.partition !== undefined) source = source.where((i) => i.partition.eq(query.partition!));
|
|
137
143
|
if (query.status !== undefined) source = source.where((i) => i.status.eq(query.status!));
|
|
138
144
|
if (query.processKey !== undefined) source = source.where((i) => i.processKey.eq(query.processKey!));
|
|
@@ -158,6 +164,11 @@ export class WorkflowRuntime extends IWorkflowRuntime {
|
|
|
158
164
|
const rows = await this.db.history.asNoTracking().where((h) => h.instanceId.eq(instanceId)).orderBy((h) => h.id).toList();
|
|
159
165
|
return rows.map(historyOf);
|
|
160
166
|
}
|
|
167
|
+
|
|
168
|
+
/** The application's `WorkflowInputValidator`, or the module's `DefaultWorkflowInputValidator`. */
|
|
169
|
+
private validator(): WorkflowInputValidator {
|
|
170
|
+
return this.services.resolve(WorkflowInputValidator);
|
|
171
|
+
}
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
function notFound(instanceId: string): WorkflowError {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { AccessModule } from "../../access";
|
|
2
|
+
import { Module, scoped } from "../../di";
|
|
3
3
|
import { WorkflowDefinitionsModule } from "../definitions_module/WorkflowDefinitions.module";
|
|
4
4
|
import { withEventSubscribers } from "../../kernel";
|
|
5
|
-
import { IWorkflowRuntime } from "../runtime_module/services/IWorkflowRuntime.service";
|
|
6
5
|
import { WorkflowRuntimeModule } from "../runtime_module/WorkflowRuntime.module";
|
|
6
|
+
import { tasksPermissions } from "./access/Tasks.permissions";
|
|
7
7
|
import { TaskEvents } from "./events/TaskEvents";
|
|
8
8
|
import { TasksController } from "./http/Tasks.controller";
|
|
9
9
|
import { TasksDbContext, tasksStore } from "./model/Tasks.dbContext";
|
|
@@ -13,19 +13,17 @@ import { WorkflowTasks } from "./services/WorkflowTasks.service";
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Tasks for people: the inbox of `userTask` steps ([passport](MODULE.md)).
|
|
16
|
-
* Dependencies are explicit: the framework's classes are not covered by the
|
|
17
|
-
* application's codegen.
|
|
18
16
|
*/
|
|
19
17
|
@Module({
|
|
20
|
-
imports: [WorkflowDefinitionsModule, WorkflowRuntimeModule],
|
|
18
|
+
imports: [AccessModule, WorkflowDefinitionsModule, WorkflowRuntimeModule],
|
|
19
|
+
permissions: tasksPermissions,
|
|
21
20
|
ormBazis: { context: TasksDbContext, entities: [WfTask], ownedStore: tasksStore, registerRepositories: false },
|
|
22
21
|
providers: [
|
|
23
|
-
scoped(
|
|
24
|
-
|
|
25
|
-
scoped(TaskEvents, TaskEvents, [WorkflowTasks] as const),
|
|
22
|
+
scoped(IWorkflowTasks, WorkflowTasks),
|
|
23
|
+
scoped(TaskEvents),
|
|
26
24
|
...withEventSubscribers(TaskEvents),
|
|
27
25
|
],
|
|
28
|
-
controllers: [
|
|
26
|
+
controllers: [TasksController],
|
|
29
27
|
exports: [IWorkflowTasks],
|
|
30
28
|
})
|
|
31
29
|
export class WorkflowTasksModule {}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { definePermissions } from "../../../access";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The right to work on tasks (WORKFLOW-002 §6.2). Which tasks are the
|
|
5
|
+
* subject's is decided by their addressees, not by an owner column.
|
|
6
|
+
*/
|
|
7
|
+
export const tasksPermissions = definePermissions({
|
|
8
|
+
"workflow.tasks.work": {
|
|
9
|
+
title: { en: "Work on tasks", ru: "Работать с задачами" },
|
|
10
|
+
scope: { column: "partition" },
|
|
11
|
+
on: { process: { ref: "workflow.process", column: "processKey" } },
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
import { OnEvent } from "../../../kernel";
|
|
2
|
+
import { UniqueViolationError } from "../../../orm";
|
|
2
3
|
import { WORKFLOW_INSTANCES_DELETED, WORKFLOW_TASK_REQUESTED, WORKFLOW_TOKEN_CLOSED, type WorkflowInstancesDeleted, type WorkflowTaskRequested, type WorkflowTokenClosed } from "../../runtime_module/events/Runtime.events";
|
|
3
|
-
import type {
|
|
4
|
+
import type { TasksDbContext } from "../model/Tasks.dbContext";
|
|
5
|
+
import { WfTask } from "../model/WfTask.model";
|
|
4
6
|
|
|
5
7
|
/** Keeps the inbox in line with the steps: a waiting user task gets a task, a closed one cancels it. */
|
|
6
8
|
export class TaskEvents {
|
|
7
|
-
constructor(private readonly
|
|
9
|
+
constructor(private readonly db: TasksDbContext) {}
|
|
8
10
|
|
|
11
|
+
/** A step started waiting for a person: its task. Repeated events of one token make one task. */
|
|
9
12
|
@OnEvent(WORKFLOW_TASK_REQUESTED)
|
|
10
|
-
requested(event: WorkflowTaskRequested): Promise<void> {
|
|
11
|
-
|
|
13
|
+
async requested(event: WorkflowTaskRequested): Promise<void> {
|
|
14
|
+
const assignee = "role" in event.assignee ? `role:${event.assignee.role}` : `user:${event.assignee.user}`;
|
|
15
|
+
this.db.tasks.add(Object.assign(new WfTask(), {
|
|
16
|
+
tokenId: event.tokenId,
|
|
17
|
+
instanceId: event.instanceId,
|
|
18
|
+
processKey: event.processKey,
|
|
19
|
+
nodeId: event.nodeId,
|
|
20
|
+
partition: event.partition,
|
|
21
|
+
title: event.title,
|
|
22
|
+
assignee,
|
|
23
|
+
form: event.form,
|
|
24
|
+
dueAt: event.dueAt,
|
|
25
|
+
}));
|
|
26
|
+
try {
|
|
27
|
+
await this.db.saveChanges();
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (!(error instanceof UniqueViolationError)) throw error;
|
|
30
|
+
}
|
|
12
31
|
}
|
|
13
32
|
|
|
33
|
+
/** The step closed otherwise than by its task: the task is cancelled. */
|
|
14
34
|
@OnEvent(WORKFLOW_TOKEN_CLOSED)
|
|
15
|
-
closed(event: WorkflowTokenClosed): Promise<void> {
|
|
16
|
-
|
|
35
|
+
async closed(event: WorkflowTokenClosed): Promise<void> {
|
|
36
|
+
await this.db.tasks.asNoTracking().where((t) => t.tokenId.eq(event.tokenId).and(t.status.in(["open", "claimed"]))).executeUpdate({ status: "cancelled" });
|
|
17
37
|
}
|
|
18
38
|
|
|
39
|
+
/** Retention deleted the instances: their tasks go too. */
|
|
19
40
|
@OnEvent(WORKFLOW_INSTANCES_DELETED)
|
|
20
|
-
deleted(event: WorkflowInstancesDeleted): Promise<void> {
|
|
21
|
-
|
|
41
|
+
async deleted(event: WorkflowInstancesDeleted): Promise<void> {
|
|
42
|
+
if (event.instanceIds.length > 0) await this.db.tasks.asNoTracking().where((t) => t.instanceId.in([...event.instanceIds])).executeDelete();
|
|
22
43
|
}
|
|
23
44
|
}
|