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
|
@@ -1,30 +1,35 @@
|
|
|
1
1
|
import { contextOnlyController } from "../../../http/Routing/contextOnlyControllers";
|
|
2
|
-
import {
|
|
2
|
+
import { can } from "../../../access";
|
|
3
|
+
import { Authorize, Controller, Get, Post, type HttpContext } from "../../../http";
|
|
3
4
|
import type { WorkflowActor } from "../../contracts/types";
|
|
4
5
|
import { WorkflowError } from "../../contracts/WorkflowError";
|
|
5
|
-
import {
|
|
6
|
+
import { body, callerOf, optionalString, pagingQuery, param } from "../../internal/http";
|
|
6
7
|
import type { TaskStatus } from "../model/WfTask.model";
|
|
7
8
|
import { IWorkflowTasks, type TaskDetails } from "../services/IWorkflowTasks.service";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* The caller's tasks (WORKFLOW-
|
|
11
|
-
* `
|
|
12
|
-
* answers 404, as if it did not exist.
|
|
11
|
+
* The caller's tasks (WORKFLOW-002 §6.2): those addressed to any of the
|
|
12
|
+
* subject's `principals` in the task's partition or claimed by the caller.
|
|
13
|
+
* Another person's task answers 404, as if it did not exist.
|
|
13
14
|
*/
|
|
14
15
|
@Controller("workflow/tasks")
|
|
15
16
|
export class TasksController {
|
|
16
17
|
constructor(private readonly tasks: IWorkflowTasks) {}
|
|
17
18
|
|
|
18
19
|
@Get()
|
|
20
|
+
@Authorize(can("workflow.tasks.work"))
|
|
19
21
|
async list(ctx: HttpContext) {
|
|
20
22
|
const partition = optionalString(ctx.query("partition"), "partition");
|
|
21
|
-
const actor = await
|
|
23
|
+
const { access, actor } = await callerOf(ctx);
|
|
22
24
|
const status = ctx.query("status");
|
|
25
|
+
const statuses = status === undefined ? {} : { status: status.split(",") as TaskStatus[] };
|
|
23
26
|
const page = await this.tasks.list({
|
|
24
27
|
...pagingQuery(ctx),
|
|
25
|
-
|
|
28
|
+
...statuses,
|
|
29
|
+
// Addressees are per partition and counted now, not when the task was created.
|
|
30
|
+
addressees: await access.principals(partition === undefined ? await this.tasks.partitions(statuses) : [partition]),
|
|
26
31
|
actor,
|
|
27
|
-
|
|
32
|
+
access: await access.filter("workflow.tasks.work"),
|
|
28
33
|
partition,
|
|
29
34
|
processKey: optionalString(ctx.query("processKey"), "processKey"),
|
|
30
35
|
});
|
|
@@ -32,35 +37,39 @@ export class TasksController {
|
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
@Get(":id")
|
|
40
|
+
@Authorize(can("workflow.tasks.work"))
|
|
35
41
|
async get(ctx: HttpContext) {
|
|
36
42
|
return (await this.visible(ctx)).task;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
@Post(":id/claim")
|
|
46
|
+
@Authorize(can("workflow.tasks.work"))
|
|
40
47
|
async claim(ctx: HttpContext) {
|
|
41
48
|
const { task, actor } = await this.visible(ctx);
|
|
42
49
|
return await this.tasks.claim(task.id, actor);
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
@Post(":id/release")
|
|
53
|
+
@Authorize(can("workflow.tasks.work"))
|
|
46
54
|
async release(ctx: HttpContext) {
|
|
47
55
|
const { task, actor } = await this.visible(ctx);
|
|
48
56
|
return await this.tasks.release(task.id, actor);
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
@Post(":id/complete")
|
|
60
|
+
@Authorize(can("workflow.tasks.work"))
|
|
52
61
|
async complete(ctx: HttpContext) {
|
|
53
62
|
const { task, actor } = await this.visible(ctx);
|
|
54
63
|
const request = await body(ctx);
|
|
55
64
|
return await this.tasks.complete(task.id, request.output ?? {}, actor);
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
/** The task if the caller may see it: addressed to one of their
|
|
67
|
+
/** The task if the caller may see it: addressed to one of their principals or claimed by them. */
|
|
59
68
|
private async visible(ctx: HttpContext): Promise<{ task: TaskDetails; actor: WorkflowActor }> {
|
|
60
69
|
const task = await this.tasks.get(param(ctx, "id"));
|
|
61
|
-
const actor = await
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
70
|
+
const { access, actor } = await callerOf(ctx, "workflow.tasks.work", { scope: task.partition, process: task.processKey });
|
|
71
|
+
const addressees = await access.principals([task.partition]);
|
|
72
|
+
if (!addressees.some(({ principals }) => principals.includes(task.assignee)) && task.claimedBy !== actor.id) throw new WorkflowError("not_found", `Task ${task.id} does not exist`);
|
|
64
73
|
return { task, actor };
|
|
65
74
|
}
|
|
66
75
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FormSpec } from "../../../../library/workflow";
|
|
2
|
+
import type { AccessFilter, ScopePrincipals } from "../../../access";
|
|
2
3
|
import type { WorkflowActor, WorkflowPage, WorkflowPaging } from "../../contracts/types";
|
|
3
4
|
import type { TaskStatus } from "../model/WfTask.model";
|
|
4
5
|
|
|
@@ -23,10 +24,12 @@ export interface TaskDetails {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export interface TaskListQuery extends WorkflowPaging {
|
|
26
|
-
/** Tasks addressed to
|
|
27
|
-
readonly
|
|
27
|
+
/** Tasks addressed to these (`role:support`, `user:42`) in their partition, plus those the actor claimed. */
|
|
28
|
+
readonly addressees: readonly ScopePrincipals[];
|
|
28
29
|
/** Whose claimed tasks count as theirs. */
|
|
29
30
|
readonly actor?: WorkflowActor;
|
|
31
|
+
/** Which tasks the caller may see, from `IAccess`; without it the calling code has decided. */
|
|
32
|
+
readonly access?: AccessFilter;
|
|
30
33
|
/** `open` and `claimed` by default. */
|
|
31
34
|
readonly status?: readonly TaskStatus[];
|
|
32
35
|
readonly partition?: string;
|
|
@@ -40,8 +43,10 @@ export interface TaskListQuery extends WorkflowPaging {
|
|
|
40
43
|
* by another person), `wrong_status`, `invalid_input` with the form issues.
|
|
41
44
|
*/
|
|
42
45
|
export abstract class IWorkflowTasks {
|
|
43
|
-
/** The tasks of these
|
|
46
|
+
/** The tasks of these addressees, oldest first. */
|
|
44
47
|
abstract list(query: TaskListQuery): Promise<WorkflowPage<TaskDetails>>;
|
|
48
|
+
/** The partitions that have tasks in these statuses (`open` and `claimed` by default): where to ask for addressees. */
|
|
49
|
+
abstract partitions(query?: { readonly status?: readonly TaskStatus[] }): Promise<readonly (string | null)[]>;
|
|
45
50
|
abstract get(taskId: string): Promise<TaskDetails>;
|
|
46
51
|
/** Takes an open task: nobody else can complete it until it is released. */
|
|
47
52
|
abstract claim(taskId: string, actor: WorkflowActor): Promise<TaskDetails>;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { grantsToWhere, type ScopePrincipals } from "../../../access";
|
|
2
2
|
import { isUuid, pagingOf, type WorkflowActor, type WorkflowPage } from "../../contracts/types";
|
|
3
3
|
import type { WorkflowClock } from "../../contracts/WorkflowClock";
|
|
4
4
|
import { WorkflowError } from "../../contracts/WorkflowError";
|
|
5
|
-
import type { WorkflowInstancesDeleted, WorkflowTaskRequested, WorkflowTokenClosed } from "../../runtime_module/events/Runtime.events";
|
|
6
5
|
import type { IWorkflowRuntime } from "../../runtime_module/services/IWorkflowRuntime.service";
|
|
7
6
|
import type { TasksDbContext } from "../model/Tasks.dbContext";
|
|
8
7
|
import { WfTask, type TaskStatus } from "../model/WfTask.model";
|
|
@@ -21,17 +20,22 @@ export class WorkflowTasks extends IWorkflowTasks {
|
|
|
21
20
|
|
|
22
21
|
async list(query: TaskListQuery): Promise<WorkflowPage<TaskDetails>> {
|
|
23
22
|
const { limit, offset } = pagingOf(query);
|
|
24
|
-
const statuses =
|
|
25
|
-
|
|
26
|
-
const assignees = [...query.assignees];
|
|
23
|
+
const statuses = statusesOf(query.status);
|
|
24
|
+
const groups = groupByPrincipals(query.addressees);
|
|
27
25
|
const mine = query.actor?.id;
|
|
28
|
-
|
|
26
|
+
const where = query.access === undefined ? "all" : grantsToWhere(query.access);
|
|
27
|
+
if (where === "none" || (groups.length === 0 && mine === undefined)) return { items: [], total: 0 };
|
|
29
28
|
let source = this.db.tasks.asNoTracking().where((t) => t.status.in(statuses));
|
|
30
|
-
source =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
if (where !== "all") source = source.where(() => where);
|
|
30
|
+
source = source.where((t) => {
|
|
31
|
+
const theirs = groups.map(({ scopes, principals }) => {
|
|
32
|
+
const named = scopes.filter((scope): scope is string => scope !== null);
|
|
33
|
+
const partition = !scopes.includes(null) ? t.partition.in(named) : named.length === 0 ? t.partition.isNull() : t.partition.isNull().or(t.partition.in(named));
|
|
34
|
+
return t.assignee.in(principals).and(partition);
|
|
35
|
+
});
|
|
36
|
+
if (mine !== undefined) theirs.push(t.claimedById.eq(mine));
|
|
37
|
+
return theirs.reduce((left, right) => left.or(right));
|
|
38
|
+
});
|
|
35
39
|
if (query.partition !== undefined) source = source.where((t) => t.partition.eq(query.partition!));
|
|
36
40
|
if (query.processKey !== undefined) source = source.where((t) => t.processKey.eq(query.processKey!));
|
|
37
41
|
const total = await source.count();
|
|
@@ -39,6 +43,12 @@ export class WorkflowTasks extends IWorkflowTasks {
|
|
|
39
43
|
return { items: rows.map(detailsOf), total };
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
async partitions(query: { readonly status?: readonly TaskStatus[] } = {}): Promise<readonly (string | null)[]> {
|
|
47
|
+
const statuses = statusesOf(query.status);
|
|
48
|
+
const rows = await this.db.database.querySqlRaw(`SELECT DISTINCT "partition" FROM "bazis_wf_task_items" WHERE "status" IN (${statuses.map((_, index) => `{${index}}`).join(", ")})`, ...statuses);
|
|
49
|
+
return rows.map((row) => row.partition as string | null);
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
async get(taskId: string): Promise<TaskDetails> {
|
|
43
53
|
return detailsOf(await this.task(taskId, false));
|
|
44
54
|
}
|
|
@@ -91,37 +101,6 @@ export class WorkflowTasks extends IWorkflowTasks {
|
|
|
91
101
|
return await this.get(task.id);
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
/** A step started waiting for a person: its task. Repeated events of one token make one task. */
|
|
95
|
-
async requested(event: WorkflowTaskRequested): Promise<void> {
|
|
96
|
-
const assignee = "role" in event.assignee ? `role:${event.assignee.role}` : `user:${event.assignee.user}`;
|
|
97
|
-
this.db.tasks.add(Object.assign(new WfTask(), {
|
|
98
|
-
tokenId: event.tokenId,
|
|
99
|
-
instanceId: event.instanceId,
|
|
100
|
-
processKey: event.processKey,
|
|
101
|
-
nodeId: event.nodeId,
|
|
102
|
-
partition: event.partition,
|
|
103
|
-
title: event.title,
|
|
104
|
-
assignee,
|
|
105
|
-
form: event.form,
|
|
106
|
-
dueAt: event.dueAt,
|
|
107
|
-
}));
|
|
108
|
-
try {
|
|
109
|
-
await this.db.saveChanges();
|
|
110
|
-
} catch (error) {
|
|
111
|
-
if (!(error instanceof UniqueViolationError)) throw error;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** The step closed otherwise than by its task: the task is cancelled. */
|
|
116
|
-
async closed(event: WorkflowTokenClosed): Promise<void> {
|
|
117
|
-
await this.close(event.tokenId, "cancelled");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Retention deleted the instances: their tasks go too. */
|
|
121
|
-
async deleted(event: WorkflowInstancesDeleted): Promise<void> {
|
|
122
|
-
if (event.instanceIds.length > 0) await this.db.tasks.asNoTracking().where((t) => t.instanceId.in([...event.instanceIds])).executeDelete();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
104
|
private async close(tokenId: string, status: "cancelled"): Promise<void> {
|
|
126
105
|
await this.db.tasks.asNoTracking().where((t) => t.tokenId.eq(tokenId).and(t.status.in(["open", "claimed"]))).executeUpdate({ status });
|
|
127
106
|
}
|
|
@@ -142,6 +121,26 @@ export class WorkflowTasks extends IWorkflowTasks {
|
|
|
142
121
|
}
|
|
143
122
|
}
|
|
144
123
|
|
|
124
|
+
function statusesOf(status: readonly TaskStatus[] | undefined): TaskStatus[] {
|
|
125
|
+
const statuses = [...(status ?? ["open", "claimed"] as const)];
|
|
126
|
+
if (statuses.length === 0 || statuses.some((item) => !STATUSES.has(item))) throw new WorkflowError("invalid_input", "status must be open, claimed, completed or cancelled");
|
|
127
|
+
return statuses;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Partitions with the same addressees go together: usually all of them, so the query stays short. */
|
|
131
|
+
function groupByPrincipals(addressees: readonly ScopePrincipals[]): { scopes: (string | null)[]; principals: string[] }[] {
|
|
132
|
+
const groups = new Map<string, { scopes: (string | null)[]; principals: string[] }>();
|
|
133
|
+
for (const { scope, principals } of addressees) {
|
|
134
|
+
if (principals.length === 0) continue;
|
|
135
|
+
const list = [...new Set(principals)].sort();
|
|
136
|
+
const key = JSON.stringify(list);
|
|
137
|
+
const group = groups.get(key);
|
|
138
|
+
if (group === undefined) groups.set(key, { scopes: [scope], principals: list });
|
|
139
|
+
else group.scopes.push(scope);
|
|
140
|
+
}
|
|
141
|
+
return [...groups.values()];
|
|
142
|
+
}
|
|
143
|
+
|
|
145
144
|
function notFound(taskId: string): WorkflowError {
|
|
146
145
|
return new WorkflowError("not_found", `Task ${taskId} does not exist`);
|
|
147
146
|
}
|
package/index.ts
CHANGED
|
@@ -41,7 +41,7 @@ export * as validation from "./library/validation";
|
|
|
41
41
|
// Golden-path application API.
|
|
42
42
|
export {
|
|
43
43
|
GrpcController, GrpcMethod, GrpcError, GrpcStatus, grpcModule, grpcService, loadGrpcPackage,
|
|
44
|
-
GrpcClient,
|
|
44
|
+
GrpcClient, GrpcClientBase,
|
|
45
45
|
type GrpcClientOptions, type GrpcClientTlsOptions, type GrpcCallOptions, type GrpcResponse, type GrpcResponseStream,
|
|
46
46
|
type GrpcContext, type GrpcModuleOptions,
|
|
47
47
|
} from "./core/grpc";
|
|
@@ -78,7 +78,7 @@ function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
78
78
|
* Basic `auth`, `withCredentials`, idempotent retries with backoff, and
|
|
79
79
|
* pluggable correlation-header propagation. No dependency on the Bazis core.
|
|
80
80
|
*/
|
|
81
|
-
export class
|
|
81
|
+
export class HttpClientBase {
|
|
82
82
|
public readonly defaults: RequestConfig;
|
|
83
83
|
public readonly interceptors: {
|
|
84
84
|
readonly request: InterceptorManager<ResolvedRequestConfig>;
|
|
@@ -95,12 +95,12 @@ export class HttpClient {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/** Creates a new client whose defaults are this client's defaults merged with `config`. */
|
|
98
|
-
public create(config: RequestConfig = {}):
|
|
99
|
-
return new
|
|
98
|
+
public create(config: RequestConfig = {}): HttpClientBase {
|
|
99
|
+
return new HttpClientBase(this.mergeConfig(this.defaults, config));
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
public static create(config: RequestConfig = {}):
|
|
103
|
-
return new
|
|
102
|
+
public static create(config: RequestConfig = {}): HttpClientBase {
|
|
103
|
+
return new HttpClientBase(config);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
public request<T = unknown>(config: RequestConfig): Promise<HttpResponse<T>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HttpClientBase } from "./HttpClientBase";
|
|
2
2
|
import { HttpClientConfigError } from "./errors";
|
|
3
3
|
import type { RequestConfig } from "./types";
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ import type { RequestConfig } from "./types";
|
|
|
7
7
|
* An abstract class, so a service can take it by type in its constructor.
|
|
8
8
|
*/
|
|
9
9
|
export abstract class HttpClientFactory {
|
|
10
|
-
public abstract createClient(name?: string):
|
|
10
|
+
public abstract createClient(name?: string): HttpClientBase;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
class ConfiguredHttpClientFactory extends HttpClientFactory {
|
|
@@ -18,15 +18,15 @@ class ConfiguredHttpClientFactory extends HttpClientFactory {
|
|
|
18
18
|
super();
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
public createClient(name?: string):
|
|
21
|
+
public createClient(name?: string): HttpClientBase {
|
|
22
22
|
if (name === undefined) {
|
|
23
|
-
return new
|
|
23
|
+
return new HttpClientBase(this.defaults);
|
|
24
24
|
}
|
|
25
25
|
const config = this.named.get(name);
|
|
26
26
|
if (config === undefined) {
|
|
27
27
|
throw new HttpClientConfigError(name, [...this.named.keys()]);
|
|
28
28
|
}
|
|
29
|
-
return new
|
|
29
|
+
return new HttpClientBase(this.defaults).create(config);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* Bun, browsers and Node 18+.
|
|
5
5
|
*
|
|
6
6
|
* ```ts
|
|
7
|
-
* import {
|
|
7
|
+
* import { HttpClientBase } from "@/library/http-client";
|
|
8
8
|
*
|
|
9
|
-
* const api = new
|
|
9
|
+
* const api = new HttpClientBase({ baseUrl: "https://api.example.com", timeoutMs: 5000 });
|
|
10
10
|
* const { data } = await api.get<User[]>("/users", { params: { page: 2 } });
|
|
11
11
|
* ```
|
|
12
12
|
*/
|
|
13
|
-
export {
|
|
13
|
+
export { HttpClientBase } from "./HttpClientBase";
|
|
14
14
|
export { InspectableRedirectProtocol } from "./redirectProtocol";
|
|
15
15
|
export { HttpClientFactoryBuilder, HttpClientFactory } from "./HttpClientFactory";
|
|
16
16
|
export { HttpClientError, HttpClientConfigError, HttpErrorCode } from "./errors";
|
|
@@ -211,7 +211,7 @@ const en = {
|
|
|
211
211
|
outputs: "generated",
|
|
212
212
|
sqlFix: "inject IPostgres from bazis/core/infra instead of Bun's SQL",
|
|
213
213
|
redisFix: "inject IRedis from bazis/core/infra instead of Bun's RedisClient",
|
|
214
|
-
unknownDepFix: "register the class in a module
|
|
214
|
+
unknownDepFix: "register the class in a module or declare a token with createToken",
|
|
215
215
|
run: "run",
|
|
216
216
|
},
|
|
217
217
|
dev: {
|
|
@@ -388,7 +388,7 @@ const ru: Messages = {
|
|
|
388
388
|
outputs: "сгенерировано",
|
|
389
389
|
sqlFix: "внедряйте IPostgres из bazis/core/infra вместо SQL из Bun",
|
|
390
390
|
redisFix: "внедряйте IRedis из bazis/core/infra вместо RedisClient из Bun",
|
|
391
|
-
unknownDepFix: "зарегистрируйте класс в
|
|
391
|
+
unknownDepFix: "зарегистрируйте класс в модуле или объявите токен через createToken",
|
|
392
392
|
run: "запуск",
|
|
393
393
|
},
|
|
394
394
|
dev: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.130.0",
|
|
4
4
|
"description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexander Alenin",
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"module": "./index.ts",
|
|
29
29
|
"exports": {
|
|
30
30
|
".": "./index.ts",
|
|
31
|
+
"./core/access": "./core/access/index.ts",
|
|
32
|
+
"./core/access/testing": "./core/access/testing/index.ts",
|
|
31
33
|
"./core/agent": "./core/agent/index.ts",
|
|
32
34
|
"./core/app": "./core/app/index.ts",
|
|
33
35
|
"./core/background": "./core/background/index.ts",
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { singletonFactory, type ProviderDefinition, type Token } from "../di";
|
|
2
|
-
import { GrpcClient } from "./GrpcClient";
|
|
3
|
-
import type { GrpcClientOptions } from "./clientContracts";
|
|
4
|
-
import type { ServiceDefinition } from "./serviceDefinition";
|
|
5
|
-
|
|
6
|
-
/** Container-owned singleton: lazy network connection and automatic dispose(). */
|
|
7
|
-
export function grpcClientProvider(service: ServiceDefinition, options: GrpcClientOptions, token: Token<GrpcClient> = GrpcClient): ProviderDefinition<GrpcClient> {
|
|
8
|
-
return singletonFactory(token, [], () => new GrpcClient(service, options));
|
|
9
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { HttpContext } from "../../http";
|
|
2
|
-
import type { WorkflowActor } from "../contracts/types";
|
|
3
|
-
|
|
4
|
-
export type WorkflowAction =
|
|
5
|
-
| "processes.read"
|
|
6
|
-
| "processes.manage"
|
|
7
|
-
| "processes.publish"
|
|
8
|
-
| "instances.start"
|
|
9
|
-
| "instances.read"
|
|
10
|
-
| "instances.manage"
|
|
11
|
-
| "events.emit"
|
|
12
|
-
| "tasks.work";
|
|
13
|
-
|
|
14
|
-
/** What an action is about; the fields known for the route. */
|
|
15
|
-
export interface WorkflowAccessTarget {
|
|
16
|
-
readonly processKey?: string;
|
|
17
|
-
readonly partition?: string | null;
|
|
18
|
-
readonly instanceId?: string;
|
|
19
|
-
readonly taskId?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Rights and identity of the workflow HTTP (WORKFLOW-001 §6). The framework's
|
|
24
|
-
* controllers do not know how the application signs people in, so they ask this
|
|
25
|
-
* port. It is the DI token too: register the application's implementation in any
|
|
26
|
-
* module, `DI.singleton(DI.classProvider(WorkflowAccess, AppWorkflowAccess))`.
|
|
27
|
-
* Without one every workflow route answers 403. The services never ask it: the
|
|
28
|
-
* application's code that calls them decides itself.
|
|
29
|
-
*/
|
|
30
|
-
export abstract class WorkflowAccess {
|
|
31
|
-
/** Who makes the request; `undefined` answers 401. */
|
|
32
|
-
abstract actor(ctx: HttpContext): WorkflowActor | undefined | Promise<WorkflowActor | undefined>;
|
|
33
|
-
|
|
34
|
-
/** Whether the actor may do `action` on `target`; `false` answers 403. */
|
|
35
|
-
abstract can(ctx: HttpContext, action: WorkflowAction, target: WorkflowAccessTarget): boolean | Promise<boolean>;
|
|
36
|
-
|
|
37
|
-
/** Whom the user's tasks are addressed to: `user:<id>`, `role:<code>`. */
|
|
38
|
-
assignees(_ctx: HttpContext, actor: WorkflowActor): readonly string[] | Promise<readonly string[]> {
|
|
39
|
-
return [`user:${actor.id}`];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** The default: nothing is allowed until the application says otherwise. */
|
|
44
|
-
export class DenyWorkflowAccess extends WorkflowAccess {
|
|
45
|
-
actor(): undefined {
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
can(): boolean {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
}
|