bazis 0.136.0 → 0.138.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/README.md +7 -4
- package/cli/generateProject.ts +11 -18
- package/cli/main.ts +9 -1
- package/cli/parseCli.ts +9 -0
- package/cli/templates/module.ts +16 -19
- package/cli/templates/passport.ts +2 -2
- 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 +41 -4
- 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/access/testing/index.ts +26 -4
- package/core/agent/ToolRunner.ts +7 -1
- package/core/agent/defineTool.ts +109 -0
- package/core/agent/index.ts +8 -3
- package/core/agent/moduleContributions-v1.ts +3 -10
- package/core/agent/session/entities.ts +45 -45
- package/core/app/index.ts +1 -1
- package/core/app/messages.ts +3 -3
- package/core/app/runApp.ts +90 -26
- package/core/cache/ICache.ts +1 -1
- package/core/cache/README.md +4 -2
- package/core/cache/SPEC.md +16 -9
- package/core/cache/http/missingCacheModuleCheck.ts +5 -4
- package/core/cache/internal/buildCacheProviders.ts +4 -5
- package/core/cache/providers/cachedProviders.ts +1 -1
- package/core/di/index.ts +0 -4
- package/core/di/lowLevel.ts +12 -0
- package/core/di/module/index.ts +0 -3
- package/core/di/module/shortcuts.ts +31 -86
- package/core/generatedRuntime.ts +11 -0
- package/core/grpc/Grpc.module.ts +2 -1
- package/core/grpc/grpcClients.ts +2 -1
- package/core/http/Authorization/metadata.ts +1 -1
- package/core/http/Decorators/metadata.ts +3 -0
- package/core/http/Decorators/routes.ts +12 -0
- package/core/http/Errors/HttpError.ts +16 -2
- package/core/http/Http.config.ts +22 -0
- package/core/http/HttpContext/HttpContext.ts +1 -1
- package/core/http/HttpServer.ts +29 -9
- package/core/http/Middleware/errorHandler.ts +16 -1
- package/core/http/OpenApi/openApiDocument.ts +44 -5
- 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/httpModule.ts +10 -8
- package/core/http/index.ts +4 -0
- package/core/http/middlewareTokens.ts +12 -0
- package/core/http/options.ts +5 -1
- package/core/http-client/httpClients.ts +2 -1
- package/core/infra/Infra.ts +3 -3
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/infra/connectors/codex/contracts.ts +3 -3
- package/core/infra/connectors/llm.ts +2 -1
- package/core/infra/connectors/postgres.ts +4 -2
- package/core/infra/connectors/redis.ts +2 -1
- package/core/infra/index.ts +1 -1
- package/core/kernel/KernelBuilder.ts +2 -1
- package/core/kernel/config/ConfigRegistry.ts +2 -1
- package/core/kernel/config/addConfigOptions.ts +2 -1
- package/core/kernel/config/defineConfig.ts +56 -20
- package/core/kernel/events/EventBus.ts +1 -0
- package/core/kernel/events/eventSubscribers.ts +1 -1
- package/core/kernel/events/onEvent.ts +2 -1
- package/core/kernel/index.ts +3 -0
- package/core/orm/buildOrmProviders.ts +4 -2
- package/core/orm/contextEntities.ts +36 -0
- package/core/orm/databaseConnector.ts +9 -3
- package/core/orm/databaseHttpError.ts +21 -0
- package/core/orm/encryptedWith.ts +1 -1
- package/core/orm/index.ts +12 -1
- package/core/orm/ormModule.ts +19 -8
- package/core/orm/ownedStoreContributions.ts +2 -1
- package/core/orm/registerRepositories.ts +2 -1
- package/core/scripts/di-generate.ts +113 -12
- package/core/testing/index.ts +51 -11
- 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/index.ts +1 -1
- package/core/tools/orm/OrmEntity.references.ts +7 -8
- package/core/tools/orm/ormEntityTools.ts +54 -28
- package/core/websocket/DESIGN.md +5 -3
- package/core/websocket/middleware.ts +1 -1
- package/core/websocket/websocketModule.ts +3 -3
- package/core/workflow/contracts/Workflow.config.ts +5 -5
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +3 -4
- package/core/workflow/definitions_module/code/defineProcess.ts +213 -11
- package/core/workflow/definitions_module/code/inspectProcesses.ts +12 -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 +23 -1
- 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/runtime_module/services/IWorkflowRuntime.service.ts +10 -6
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +5 -2
- package/core/workflow/tasks_module/model/WfTask.model.ts +25 -9
- package/core/workflow/testing/index.ts +46 -4
- package/index.ts +4 -3
- 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/library/terminal/messages.ts +6 -6
- package/library/validation/README.md +1 -1
- package/library/validation/SPEC.md +1 -1
- package/library/workflow/index.ts +1 -0
- package/library/workflow/nodeConfigs.ts +130 -0
- package/package.json +2 -1
|
@@ -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 { FormSpec, LocalizedText } from "../../../../library/workflow";
|
|
3
3
|
|
|
4
4
|
/** `open` waits for someone, `claimed` is taken by one person, `completed` and `cancelled` are done. */
|
|
@@ -17,55 +17,71 @@ export class WfTask {
|
|
|
17
17
|
|
|
18
18
|
/** The waiting token: one task per token. */
|
|
19
19
|
@Index({ unique: true })
|
|
20
|
+
@Nullable()
|
|
20
21
|
@Column({ type: "text" })
|
|
21
22
|
tokenId = "";
|
|
22
23
|
|
|
23
24
|
@Index()
|
|
25
|
+
@Nullable()
|
|
24
26
|
@Column({ type: "text" })
|
|
25
27
|
instanceId = "";
|
|
26
28
|
|
|
29
|
+
@Nullable()
|
|
27
30
|
@Column({ type: "text" })
|
|
28
31
|
processKey = "";
|
|
29
32
|
|
|
33
|
+
@Nullable()
|
|
30
34
|
@Column({ type: "text" })
|
|
31
35
|
nodeId = "";
|
|
32
36
|
|
|
33
|
-
@
|
|
37
|
+
@Nullable()
|
|
38
|
+
@Column({ type: "text" })
|
|
34
39
|
partition: string | null = null;
|
|
35
40
|
|
|
41
|
+
@Nullable()
|
|
36
42
|
@Column({ type: "json" })
|
|
37
43
|
title: LocalizedText = "";
|
|
38
44
|
|
|
39
45
|
/** The task of a trial run (WORKFLOW-002 §5.4, stage 5). */
|
|
46
|
+
@Nullable()
|
|
40
47
|
@Column({ type: "boolean" })
|
|
41
48
|
test = false;
|
|
42
49
|
|
|
43
50
|
/** `role:<code>` or `user:<id>`. */
|
|
51
|
+
@Nullable()
|
|
44
52
|
@Column({ type: "text" })
|
|
45
53
|
assignee = "";
|
|
46
54
|
|
|
47
|
-
@
|
|
55
|
+
@Nullable()
|
|
56
|
+
@Column({ type: "json" })
|
|
48
57
|
form: FormSpec | null = null;
|
|
49
58
|
|
|
50
|
-
@
|
|
59
|
+
@Nullable()
|
|
60
|
+
@Column({ type: "datetime" })
|
|
51
61
|
dueAt: Date | null = null;
|
|
52
62
|
|
|
63
|
+
@Nullable()
|
|
53
64
|
@Column({ type: "text" })
|
|
54
65
|
status: TaskStatus = "open";
|
|
55
66
|
|
|
56
|
-
@
|
|
67
|
+
@Nullable()
|
|
68
|
+
@Column({ type: "text" })
|
|
57
69
|
claimedById: string | null = null;
|
|
58
70
|
|
|
59
|
-
@
|
|
71
|
+
@Nullable()
|
|
72
|
+
@Column({ type: "datetime" })
|
|
60
73
|
claimedAt: Date | null = null;
|
|
61
74
|
|
|
62
|
-
@
|
|
75
|
+
@Nullable()
|
|
76
|
+
@Column({ type: "text" })
|
|
63
77
|
completedById: string | null = null;
|
|
64
78
|
|
|
65
|
-
@
|
|
79
|
+
@Nullable()
|
|
80
|
+
@Column({ type: "datetime" })
|
|
66
81
|
completedAt: Date | null = null;
|
|
67
82
|
|
|
68
|
-
@
|
|
83
|
+
@Nullable()
|
|
84
|
+
@Column({ type: "json" })
|
|
69
85
|
output: Record<string, unknown> | null = null;
|
|
70
86
|
|
|
71
87
|
@Column({ type: "createdAt" })
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { DI } from "../../di";
|
|
1
|
+
import { DI } from "../../di/lowLevel";
|
|
2
2
|
import "../generated/deps";
|
|
3
3
|
import { startTestApp, type TestApp, type TestAppOptions } from "../../testing";
|
|
4
|
+
import { postgres } from "../../../library/orm";
|
|
4
5
|
import { durationMs } from "../../../library/workflow";
|
|
5
6
|
import { WorkflowClock } from "../contracts/WorkflowClock";
|
|
7
|
+
import type { CodeProcess } from "../definitions_module/code/defineProcess";
|
|
6
8
|
import type { WorkflowActor } from "../contracts/types";
|
|
7
9
|
import { WorkflowJobs } from "../runtime_module/internal/WorkflowJobs";
|
|
8
10
|
import { IWorkflowRuntime, type EmitOptions, type EmitResult, type HistoryEntry, type InstanceDetails, type StartInstance } from "../runtime_module/services/IWorkflowRuntime.service";
|
|
@@ -57,7 +59,11 @@ export interface TestWorkflow {
|
|
|
57
59
|
readonly app: TestApp;
|
|
58
60
|
readonly clock: ManualWorkflowClock;
|
|
59
61
|
now(): Date;
|
|
60
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Starts the latest version of a process from its manual start; jobs wait for {@link drain}.
|
|
64
|
+
* Given the `defineProcess` result, the input is checked by its variables.
|
|
65
|
+
*/
|
|
66
|
+
start<Input>(process: CodeProcess<Input, unknown>, options?: Partial<StartInstance<Input>>): Promise<InstanceDetails>;
|
|
61
67
|
start(processKey: string, options?: Partial<StartInstance>): Promise<InstanceDetails>;
|
|
62
68
|
emit(eventKey: string, payload?: unknown, options?: EmitOptions): Promise<EmitResult>;
|
|
63
69
|
/** Completes the user task waiting at `nodeId` (through its task when the Tasks part is there). */
|
|
@@ -67,7 +73,11 @@ export interface TestWorkflow {
|
|
|
67
73
|
/** Moves the clock forward (ISO duration or milliseconds), then {@link drain}. */
|
|
68
74
|
advance(by: string | number): Promise<number>;
|
|
69
75
|
instance(instanceId: string): Promise<InstanceDetails>;
|
|
76
|
+
/** The instance started with this `businessKey`, the newest one when there are several. */
|
|
77
|
+
instanceByKey(businessKey: string): Promise<InstanceDetails>;
|
|
70
78
|
history(instanceId: string): Promise<readonly HistoryEntry[]>;
|
|
79
|
+
/** Calls a service of the application in a scope of its own, see `TestApp.use`. */
|
|
80
|
+
use: TestApp["use"];
|
|
71
81
|
stop(): Promise<void>;
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -77,7 +87,7 @@ export interface TestWorkflow {
|
|
|
77
87
|
* deterministic. Needs PostgreSQL, as the workflow module does.
|
|
78
88
|
*
|
|
79
89
|
* ```ts
|
|
80
|
-
* const wf = await testWorkflow(
|
|
90
|
+
* const wf = await testWorkflow(OrdersApp, { now: "2026-11-02T09:00:00Z", http: false });
|
|
81
91
|
* try {
|
|
82
92
|
* const order = await wf.start("order-approval", { input: { amount: 5000 } });
|
|
83
93
|
* await wf.drain();
|
|
@@ -127,7 +137,7 @@ export async function testWorkflow(root: Parameters<typeof startTestApp>[0], opt
|
|
|
127
137
|
app,
|
|
128
138
|
clock,
|
|
129
139
|
now: () => clock.now(),
|
|
130
|
-
start: (
|
|
140
|
+
start: (process: string | CodeProcess<unknown, unknown>, start: Partial<StartInstance> = {}) => inScope((scope) => scope.resolve(IWorkflowRuntime).start(typeof process === "string" ? process : process.key, { ...start, actor: start.actor ?? actor })),
|
|
131
141
|
emit: (eventKey, payload, emitOptions = {}) => inScope((scope) => scope.resolve(IWorkflowRuntime).emit(eventKey, payload, { ...emitOptions, actor: emitOptions.actor ?? actor })),
|
|
132
142
|
complete: (instanceId, nodeId, output = {}, by = actor) => inScope(async (scope) => {
|
|
133
143
|
const runtime = scope.resolve(IWorkflowRuntime);
|
|
@@ -151,11 +161,43 @@ export async function testWorkflow(root: Parameters<typeof startTestApp>[0], opt
|
|
|
151
161
|
return await drain();
|
|
152
162
|
},
|
|
153
163
|
instance: (instanceId) => inScope((scope) => scope.resolve(IWorkflowRuntime).instance(instanceId)),
|
|
164
|
+
instanceByKey: (businessKey) => inScope(async (scope) => {
|
|
165
|
+
const runtime = scope.resolve(IWorkflowRuntime);
|
|
166
|
+
const found = (await runtime.instances({ businessKey })).items[0];
|
|
167
|
+
if (found === undefined) throw new Error(`No instance was started with the business key ${JSON.stringify(businessKey)}`);
|
|
168
|
+
return await runtime.instance(found.id);
|
|
169
|
+
}),
|
|
170
|
+
use: app.use,
|
|
154
171
|
history: (instanceId) => inScope((scope) => scope.resolve(IWorkflowRuntime).history(instanceId)),
|
|
155
172
|
stop: () => app.stop(),
|
|
156
173
|
};
|
|
157
174
|
}
|
|
158
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Drops the tables of the workflow module, so the next start creates them anew:
|
|
178
|
+
* a clean state for a test database. Everything the module stored there is lost,
|
|
179
|
+
* so the name of the database must start with `test_` or end with `_test`, or
|
|
180
|
+
* `force` must say the loss is meant.
|
|
181
|
+
*
|
|
182
|
+
* ```ts
|
|
183
|
+
* beforeAll(() => dropWorkflowTables(process.env.TEST_DATABASE_URL!));
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export async function dropWorkflowTables(url: string, options: { readonly force?: boolean } = {}): Promise<void> {
|
|
187
|
+
const database = new URL(url).pathname.slice(1);
|
|
188
|
+
if (options.force !== true && !/^test_|_test$/.test(database)) {
|
|
189
|
+
throw new Error(`dropWorkflowTables: the database "${database}" does not look like a test one (test_… or …_test). Pass { force: true } to drop its workflow tables anyway.`);
|
|
190
|
+
}
|
|
191
|
+
const sql = postgres({ url });
|
|
192
|
+
try {
|
|
193
|
+
const tables = (await sql.query("SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename LIKE 'bazis\\_wf%'", [])) as { tablename: string }[];
|
|
194
|
+
if (tables.length > 0) await sql.execute(`DROP TABLE ${tables.map((table) => `"${table.tablename}"`).join(", ")} CASCADE`, []);
|
|
195
|
+
await sql.execute(`DO $$ BEGIN IF to_regclass('public.__bazis_orm_owned_stores_v1') IS NOT NULL THEN DELETE FROM "__bazis_orm_owned_stores_v1" WHERE store_key LIKE 'bazis.workflow.%'; END IF; END $$`, []);
|
|
196
|
+
} finally {
|
|
197
|
+
await sql.close();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
159
201
|
function toTime(value: Date | string, what: string): number {
|
|
160
202
|
const time = value instanceof Date ? value.getTime() : Date.parse(value);
|
|
161
203
|
if (!Number.isFinite(time)) throw new Error(`The workflow clock ${what} must be a date, not ${JSON.stringify(value)}`);
|
package/index.ts
CHANGED
|
@@ -46,8 +46,11 @@ export {
|
|
|
46
46
|
type GrpcContext, type GrpcModuleOptions,
|
|
47
47
|
} from "./core/grpc";
|
|
48
48
|
export {
|
|
49
|
+
App,
|
|
49
50
|
runApp,
|
|
50
|
-
type
|
|
51
|
+
type AppClass,
|
|
52
|
+
type AppOptions,
|
|
53
|
+
type AppSettings,
|
|
51
54
|
type RunAppUiAppInfo,
|
|
52
55
|
type RunAppUiOptions,
|
|
53
56
|
type RunAppUiSurfaceOptions,
|
|
@@ -67,8 +70,6 @@ export {
|
|
|
67
70
|
createToken,
|
|
68
71
|
scoped,
|
|
69
72
|
singleton,
|
|
70
|
-
singletonFactory,
|
|
71
|
-
singletonFactoryWithResolver,
|
|
72
73
|
singletonValue,
|
|
73
74
|
transient,
|
|
74
75
|
type ModuleConfig,
|
|
@@ -47,7 +47,7 @@ const ERROR_HELPER_STATUS: Readonly<Record<string, number>> = {
|
|
|
47
47
|
BadRequest: 400, Unauthorized: 401, Forbidden: 403, NotFound: 404, Conflict: 409,
|
|
48
48
|
};
|
|
49
49
|
const ERROR_CLASS_STATUS: Readonly<Record<string, number>> = {
|
|
50
|
-
BadRequestError: 400, UnauthorizedError: 401, ForbiddenError: 403, NotFoundError: 404,
|
|
50
|
+
BadRequestError: 400, UnauthorizedError: 401, ForbiddenError: 403, NotFoundError: 404, ConflictError: 409,
|
|
51
51
|
PayloadTooLargeError: 413, UnsupportedMediaTypeError: 415, TooManyRequestsError: 429,
|
|
52
52
|
};
|
|
53
53
|
const OPENAPI_EMPTY_TYPE_NAMES = new Set(["HttpResult", "Response", "Blob", "ReadableStream", "Uint8Array"]);
|
|
@@ -333,7 +333,7 @@ export function createOpenApiCodegenAnalyzer(input: OpenApiCodegenAnalyzerInput)
|
|
|
333
333
|
return schemaFromOptionalExpression(unwrapped.arguments[0]);
|
|
334
334
|
}
|
|
335
335
|
if (CREATED_RESULT_HELPERS.has(name)) {
|
|
336
|
-
return schemaFromOptionalExpression(unwrapped.arguments[
|
|
336
|
+
return schemaFromOptionalExpression(unwrapped.arguments[0]);
|
|
337
337
|
}
|
|
338
338
|
if (name === "StatusCode") {
|
|
339
339
|
return schemaFromStatusCodeCall(unwrapped);
|
package/library/orm/DbContext.ts
CHANGED
|
@@ -11,6 +11,9 @@ import { OrmTransaction, type OrmTransactionScopeOptions } from "./Transactions/
|
|
|
11
11
|
|
|
12
12
|
type EntityClass<T extends object> = new () => T;
|
|
13
13
|
|
|
14
|
+
/** While a context is constructed only to read its sets: {@link DbContext.entitiesOf}. */
|
|
15
|
+
let discovered: Set<EntityClass<object>> | undefined;
|
|
16
|
+
|
|
14
17
|
/**
|
|
15
18
|
* Base class of a database context (a scoped DI service). A subclass declares the sets:
|
|
16
19
|
*
|
|
@@ -32,10 +35,39 @@ export abstract class DbContext {
|
|
|
32
35
|
readonly #options: DbContextOptions;
|
|
33
36
|
readonly #runtime: DbContextRuntime;
|
|
34
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The entities a context declares with `this.set(...)`, in the order of its
|
|
40
|
+
* fields. The class is constructed once without a database and without its
|
|
41
|
+
* own constructor dependencies only to read the sets: a constructor that needs
|
|
42
|
+
* them fails here, and the entities are then listed by hand.
|
|
43
|
+
*/
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
static entitiesOf(context: new (options: DbContextOptions, ...dependencies: any[]) => DbContext): readonly EntityClass<object>[] {
|
|
46
|
+
const previous = discovered;
|
|
47
|
+
const found = new Set<EntityClass<object>>();
|
|
48
|
+
discovered = found;
|
|
49
|
+
try {
|
|
50
|
+
new context(undefined as unknown as DbContextOptions);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw new OrmError(`The entities of ${context.name} cannot be read from its sets: its constructor failed without its dependencies (${error instanceof Error ? error.message : String(error)}). List them: entities: [...].`);
|
|
53
|
+
} finally {
|
|
54
|
+
discovered = previous;
|
|
55
|
+
}
|
|
56
|
+
return [...found];
|
|
57
|
+
}
|
|
58
|
+
|
|
35
59
|
constructor(options: DbContextOptions) {
|
|
60
|
+
if (discovered !== undefined) {
|
|
61
|
+
// Only the sets are read: nothing of the context works, and it is thrown away.
|
|
62
|
+
this.#options = options;
|
|
63
|
+
this.changeTracker = undefined as unknown as ChangeTracker;
|
|
64
|
+
this.#runtime = undefined as unknown as DbContextRuntime;
|
|
65
|
+
this.database = undefined as unknown as DatabaseFacade;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
36
68
|
this.#options = options;
|
|
37
69
|
this.changeTracker = new ChangeTracker();
|
|
38
|
-
const provider = observedProvider(this, options.provider);
|
|
70
|
+
const provider = observedProvider(this, options.provider, options.model);
|
|
39
71
|
this.#runtime = { context: this, provider, models: options.model, tracker: this.changeTracker, runImmediateOperation: (operation) => monitorImmediateOperation(this, operation) };
|
|
40
72
|
this.database = new DatabaseFacade(provider, options.model);
|
|
41
73
|
}
|
|
@@ -47,6 +79,10 @@ export abstract class DbContext {
|
|
|
47
79
|
|
|
48
80
|
/** Public access to a `DbSet` by entity class (for `Repository<T>` and generic scenarios). */
|
|
49
81
|
setOf<T extends object>(entity: EntityClass<T>): DbSet<T> {
|
|
82
|
+
if (discovered !== undefined) {
|
|
83
|
+
discovered.add(entity);
|
|
84
|
+
return undefined as unknown as DbSet<T>;
|
|
85
|
+
}
|
|
50
86
|
return new DbSet<T>(this.#options.model.requireByCtor(entity), this.#runtime);
|
|
51
87
|
}
|
|
52
88
|
|
|
@@ -59,6 +59,7 @@ export class ModelBuilder {
|
|
|
59
59
|
columns: [property.columnName],
|
|
60
60
|
unique: rawProp.index.unique,
|
|
61
61
|
...(rawProp.index.where === undefined ? {} : { where: rawProp.index.where }),
|
|
62
|
+
...conflictOf(ctor.name, rawProp.index),
|
|
62
63
|
});
|
|
63
64
|
}
|
|
64
65
|
if (rawProp.fkTarget) {
|
|
@@ -179,7 +180,7 @@ export class ModelBuilder {
|
|
|
179
180
|
return property.columnName;
|
|
180
181
|
});
|
|
181
182
|
if (new Set(columns).size !== columns.length) throw new ModelBuildError(`Entity "${ctor.name}": index contains duplicate properties.`);
|
|
182
|
-
indexes.push({ name: declared.name ?? `ix_${(raw.table ?? defaultTableName(ctor.name)).toLowerCase()}_${columns.join("_")}`, columns, unique: declared.unique, ...(declared.where === undefined ? {} : { where: declared.where }) });
|
|
183
|
+
indexes.push({ name: declared.name ?? `ix_${(raw.table ?? defaultTableName(ctor.name)).toLowerCase()}_${columns.join("_")}`, columns, unique: declared.unique, ...(declared.where === undefined ? {} : { where: declared.where }), ...conflictOf(ctor.name, declared) });
|
|
183
184
|
}
|
|
184
185
|
const indexNames = new Set<string>();
|
|
185
186
|
for (const index of indexes) {
|
|
@@ -255,12 +256,19 @@ export class ModelBuilder {
|
|
|
255
256
|
`Entity "${entityName}": key "${raw.propertyName}" has keyGenerated=true but is not an integer identity key.`,
|
|
256
257
|
);
|
|
257
258
|
}
|
|
258
|
-
// The key and semantic timestamp fields are always NOT NULL.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
raw.
|
|
263
|
-
|
|
259
|
+
// The key and semantic timestamp fields are always NOT NULL. Every other
|
|
260
|
+
// column says it itself: silence would be read as either by different readers.
|
|
261
|
+
const always = isKey || resolved.impliedRequired === true;
|
|
262
|
+
if (raw.required === true && raw.nullable === true) {
|
|
263
|
+
throw new ModelBuildError(`Entity "${entityName}": property "${raw.propertyName}" is marked both @Required() and @Nullable(). Keep one.`);
|
|
264
|
+
}
|
|
265
|
+
if (always && raw.nullable === true) {
|
|
266
|
+
throw new ModelBuildError(`Entity "${entityName}": property "${raw.propertyName}" is ${isKey ? "a key" : "a creation or change timestamp"} and cannot be @Nullable().`);
|
|
267
|
+
}
|
|
268
|
+
if (!always && raw.required !== true && raw.nullable !== true) {
|
|
269
|
+
throw new ModelBuildError(`Entity "${entityName}": property "${raw.propertyName}" does not say whether it may be empty. Mark it @Required() (NOT NULL) or @Nullable().`);
|
|
270
|
+
}
|
|
271
|
+
const required = always || raw.required === true;
|
|
264
272
|
const databaseDefault = ModelBuilder.defaultFor(entityName, raw, storageType, isKey, generated, convention, required);
|
|
265
273
|
const digits = ModelBuilder.decimalDigits(entityName, raw, storageType);
|
|
266
274
|
return {
|
|
@@ -371,6 +379,14 @@ function isCanonicalRfc3339(value: string): boolean {
|
|
|
371
379
|
* refused: PostgreSQL stores it in its own time zone form, so the declared
|
|
372
380
|
* and the stored conditions could never be compared.
|
|
373
381
|
*/
|
|
382
|
+
/** The text of a unique index for a rejected value; on any other index it would never be used. */
|
|
383
|
+
function conflictOf(entity: string, index: { readonly unique: boolean; readonly conflict?: string }): { readonly conflict?: string } {
|
|
384
|
+
if (index.conflict === undefined) return {};
|
|
385
|
+
if (!index.unique) throw new ModelBuildError(`Entity "${entity}": conflict is the text of a unique index; this index is not unique.`);
|
|
386
|
+
if (typeof index.conflict !== "string" || index.conflict.trim() === "") throw new ModelBuildError(`Entity "${entity}": conflict of an index must be a non-empty text.`);
|
|
387
|
+
return { conflict: index.conflict };
|
|
388
|
+
}
|
|
389
|
+
|
|
374
390
|
function validateIndexCondition(entity: string, index: string, where: CheckAst, properties: readonly PropertyModel[]): void {
|
|
375
391
|
validateCheckAst(where, properties, `Entity "${entity}": index "${index}" condition`);
|
|
376
392
|
const byName = new Map(properties.map((property) => [property.propertyName, property]));
|
|
@@ -30,8 +30,6 @@ export interface ColumnOptions {
|
|
|
30
30
|
readonly name?: string;
|
|
31
31
|
/** Type: physical (`text`, `datetime`, …) or semantic (`uuid`, `createdAt`, `updatedAt`). */
|
|
32
32
|
readonly type?: ColumnOptionsType;
|
|
33
|
-
/** Allow NULL (default true; `@Required` sets false). */
|
|
34
|
-
readonly nullable?: boolean;
|
|
35
33
|
/** Closed physical PostgreSQL default. It is not an application initializer or SQL fragment. */
|
|
36
34
|
readonly default?: ColumnDefaultValue;
|
|
37
35
|
/**
|
|
@@ -81,6 +79,12 @@ export interface IndexOptions<T extends object = any> {
|
|
|
81
79
|
* The condition uses the same operators as `@Check`.
|
|
82
80
|
*/
|
|
83
81
|
readonly where?: CheckPredicate<T>;
|
|
82
|
+
/**
|
|
83
|
+
* For a unique index: what to tell the client when a value is taken. Without it
|
|
84
|
+
* the answer is the standard text of 409. The names of the properties go to the
|
|
85
|
+
* details either way.
|
|
86
|
+
*/
|
|
87
|
+
readonly conflict?: string;
|
|
84
88
|
}
|
|
85
89
|
export interface CompositeKeyOptions { readonly name?: string }
|
|
86
90
|
export type ReferentialAction = "noAction" | "restrict" | "cascade" | "setNull";
|
|
@@ -104,7 +108,7 @@ export interface RawProperty {
|
|
|
104
108
|
default?: ColumnDefaultValue;
|
|
105
109
|
precision?: number;
|
|
106
110
|
scale?: number;
|
|
107
|
-
index?: { unique: boolean; name?: string; where?: import("../Schema/CheckExpression").CheckAst };
|
|
111
|
+
index?: { unique: boolean; name?: string; where?: import("../Schema/CheckExpression").CheckAst; conflict?: string };
|
|
108
112
|
/** `@ForeignKey(() => Principal)` on a scalar column, for the DDL FK. */
|
|
109
113
|
fkTarget?: () => EntityClass;
|
|
110
114
|
/** `@HasConversion(...)`: conversion before/after the dialect. */
|
|
@@ -136,7 +140,7 @@ export interface RawEntity {
|
|
|
136
140
|
/** Soft-delete property name (see `@SoftDelete` / `@Entity({ softDelete })`). */
|
|
137
141
|
softDeleteProperty?: string;
|
|
138
142
|
keyDeclaration?: { properties: readonly string[]; name?: string; anchor: string; composite: boolean };
|
|
139
|
-
indexes?: Array<{ properties: readonly string[]; unique: boolean; name?: string; where?: import("../Schema/CheckExpression").CheckAst }>;
|
|
143
|
+
indexes?: Array<{ properties: readonly string[]; unique: boolean; name?: string; where?: import("../Schema/CheckExpression").CheckAst; conflict?: string }>;
|
|
140
144
|
foreignKeys?: Array<{ properties: readonly string[]; target: () => EntityClass; name?: string; onDelete?: ReferentialAction; onUpdate?: ReferentialAction }>;
|
|
141
145
|
checks?: Array<{ name: string; expression: import("../Schema/CheckExpression").CheckAst }>;
|
|
142
146
|
}
|
|
@@ -297,19 +301,28 @@ export function Column(options: ColumnOptions = {}) {
|
|
|
297
301
|
prop.default = options.default;
|
|
298
302
|
prop.precision = options.precision;
|
|
299
303
|
prop.scale = options.scale;
|
|
300
|
-
if (options.nullable !== undefined) {
|
|
301
|
-
prop.nullable = options.nullable;
|
|
302
|
-
}
|
|
303
304
|
};
|
|
304
305
|
}
|
|
305
306
|
|
|
306
|
-
/**
|
|
307
|
+
/**
|
|
308
|
+
* The column is NOT NULL. Every column says whether it may be empty, with this
|
|
309
|
+
* or with {@link Nullable}: a reader sees it on the column, and a column that
|
|
310
|
+
* says neither stops the start. Keys and the creation and change timestamps
|
|
311
|
+
* need no mark: they are always NOT NULL.
|
|
312
|
+
*/
|
|
307
313
|
export function Required() {
|
|
308
314
|
return (_value: undefined, context: ClassFieldDecoratorContext): void => {
|
|
309
315
|
ownProperty(context.metadata, fieldName(context)).required = true;
|
|
310
316
|
};
|
|
311
317
|
}
|
|
312
318
|
|
|
319
|
+
/** The column allows NULL: the other of the two marks, see {@link Required}. */
|
|
320
|
+
export function Nullable() {
|
|
321
|
+
return (_value: undefined, context: ClassFieldDecoratorContext): void => {
|
|
322
|
+
ownProperty(context.metadata, fieldName(context)).nullable = true;
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
313
326
|
/** Index on the property's column. */
|
|
314
327
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
315
328
|
export function Index<T extends object = any>(options?: IndexOptions<T>): (value: undefined, context: ClassFieldDecoratorContext) => void;
|
|
@@ -319,13 +332,13 @@ export function Index(first: IndexOptions | readonly [string, ...string[]] = {},
|
|
|
319
332
|
if (Array.isArray(first)) {
|
|
320
333
|
const where = options.where === undefined ? undefined : compileCheck(options.where);
|
|
321
334
|
return (_value: abstract new (...args: never[]) => unknown, context: ClassDecoratorContext): void => {
|
|
322
|
-
(ownRaw(context.metadata).indexes ??= []).push({ properties: [...first], unique: options.unique === true, name: options.name, ...(where === undefined ? {} : { where }) });
|
|
335
|
+
(ownRaw(context.metadata).indexes ??= []).push({ properties: [...first], unique: options.unique === true, name: options.name, ...(where === undefined ? {} : { where }), ...(options.conflict === undefined ? {} : { conflict: options.conflict }) });
|
|
323
336
|
};
|
|
324
337
|
}
|
|
325
338
|
const propertyOptions = first as IndexOptions;
|
|
326
339
|
const where = propertyOptions.where === undefined ? undefined : compileCheck(propertyOptions.where);
|
|
327
340
|
return (_value: undefined, context: ClassFieldDecoratorContext): void => {
|
|
328
|
-
ownProperty(context.metadata, fieldName(context)).index = { unique: propertyOptions.unique === true, name: propertyOptions.name, ...(where === undefined ? {} : { where }) };
|
|
341
|
+
ownProperty(context.metadata, fieldName(context)).index = { unique: propertyOptions.unique === true, name: propertyOptions.name, ...(where === undefined ? {} : { where }), ...(propertyOptions.conflict === undefined ? {} : { conflict: propertyOptions.conflict }) };
|
|
329
342
|
};
|
|
330
343
|
}
|
|
331
344
|
|
|
@@ -77,6 +77,8 @@ export interface IndexModel {
|
|
|
77
77
|
readonly unique: boolean;
|
|
78
78
|
/** Partial index condition over property names (`@Index({ where })`). */
|
|
79
79
|
readonly where?: CheckAst;
|
|
80
|
+
/** What to tell the client when a unique index rejects a value (`@Index({ conflict })`). */
|
|
81
|
+
readonly conflict?: string;
|
|
80
82
|
}
|
|
81
83
|
export interface CheckModel { readonly name: string; readonly expression: CheckAst }
|
|
82
84
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import type { DbContext } from "../DbContext";
|
|
3
|
-
import { ConcurrentTransactionScopeError, differentProviderMessage, OrmDatabaseTimeError, OrmProviderIdentityMismatchError, OrmTransactionScopeError, translateDatabaseError } from "../errors";
|
|
3
|
+
import { ConcurrentTransactionScopeError, differentProviderMessage, OrmDatabaseTimeError, OrmProviderIdentityMismatchError, OrmTransactionScopeError, describeViolation, translateDatabaseError, type ViolationModels } from "../errors";
|
|
4
4
|
import { baseProvider, withPostgresScopeOptions, createObservedProvider, postgresTransactionCapability, rootAuthority, withProviderDispatchObserver, withoutProviderDispatchObserver, type CancellableProviderDispatch } from "../Providers/ormTransactionRuntime";
|
|
5
5
|
import type { DatabaseProvider } from "../Providers/types";
|
|
6
6
|
import { OrmTransaction, type OrmDatabaseTimeV1, type OrmTransactionScopeOptions } from "./OrmTransaction";
|
|
@@ -84,7 +84,7 @@ export function contextToken(context: object): object {
|
|
|
84
84
|
return token;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export function observedProvider(context: object, provider: DatabaseProvider): DatabaseProvider {
|
|
87
|
+
export function observedProvider(context: object, provider: DatabaseProvider, models?: ViolationModels): DatabaseProvider {
|
|
88
88
|
contextProviders.set(context, provider);
|
|
89
89
|
const token = contextToken(context);
|
|
90
90
|
const authority = rootAuthority(provider);
|
|
@@ -92,7 +92,7 @@ export function observedProvider(context: object, provider: DatabaseProvider): D
|
|
|
92
92
|
if (uncertainContexts.has(token)) return Promise.reject(new TransactionOutcomeUnknownError());
|
|
93
93
|
// Known SQLSTATEs (unique, foreign key, check, not null, lock timeout)
|
|
94
94
|
// reach the context's caller as ORM errors; the outcome check sees the raw one.
|
|
95
|
-
const guarded = () => operation().catch((error) => { if (postgresTransactionCapability(provider)?.isOutcomeUncertain?.(error)) uncertainContexts.add(token);
|
|
95
|
+
const guarded = () => operation().catch((error) => { if (postgresTransactionCapability(provider)?.isOutcomeUncertain?.(error)) uncertainContexts.add(token); const translated = translateDatabaseError(error); throw models === undefined ? translated : describeViolation(translated, models); });
|
|
96
96
|
if (!view) return guarded();
|
|
97
97
|
const frame = view.frame;
|
|
98
98
|
if (frame.authority !== authority || !view.enrolled.has(token) || frame.state !== "active" || frame.activeChild !== undefined) {
|
package/library/orm/errors.ts
CHANGED
|
@@ -103,7 +103,7 @@ export class EntityNotMappedError extends OrmError {
|
|
|
103
103
|
? `No entity or dynamic model named "${name}" is registered in this DbContext. Register a dynamic table first: options.model.registerModel(buildDynamicModel({ name: "${name}", fields: [...] })).`
|
|
104
104
|
: kind === "plain"
|
|
105
105
|
? `A plain object is not an entity class. For a dynamic table add the row through its set: db.setByName("Table").add(row).`
|
|
106
|
-
: `Type "${name}" is not mapped in this DbContext.
|
|
106
|
+
: `Type "${name}" is not mapped in this DbContext. Declare a set for it in the context (this.set(${name})) or add it to entities of ormBazis, and annotate it with @Entity().`,
|
|
107
107
|
);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
@@ -168,6 +168,13 @@ export class OrmUndeclaredConflictTargetError extends OrmError {
|
|
|
168
168
|
export class UniqueViolationError extends DbUpdateError {
|
|
169
169
|
readonly constraint: string | undefined;
|
|
170
170
|
readonly table: string | undefined;
|
|
171
|
+
/**
|
|
172
|
+
* The properties of the index or key that rejected the value, when a context
|
|
173
|
+
* with the entity of the table made the call; otherwise empty.
|
|
174
|
+
*/
|
|
175
|
+
properties: readonly string[] = [];
|
|
176
|
+
/** `conflict` of the index: what to tell the client. */
|
|
177
|
+
conflict: string | undefined;
|
|
171
178
|
|
|
172
179
|
constructor(cause: object) {
|
|
173
180
|
const field = (name: string): string | undefined => {
|
|
@@ -284,3 +291,34 @@ export class OrmValidationError extends DbUpdateError {
|
|
|
284
291
|
super(`Validation failed for "${entityName}": ${errors.map((error) => error.message).join("; ")}`);
|
|
285
292
|
}
|
|
286
293
|
}
|
|
294
|
+
|
|
295
|
+
/** What a context knows of a table, to name the properties of a violated constraint. */
|
|
296
|
+
export interface ViolationModels {
|
|
297
|
+
readonly entities: readonly {
|
|
298
|
+
readonly tableName: string;
|
|
299
|
+
readonly keyName?: string;
|
|
300
|
+
readonly key: readonly { readonly propertyName: string }[];
|
|
301
|
+
readonly properties: readonly { readonly propertyName: string; readonly columnName: string }[];
|
|
302
|
+
readonly indexes: readonly { readonly name: string; readonly columns: readonly string[]; readonly unique: boolean; readonly conflict?: string }[];
|
|
303
|
+
}[];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Names the properties behind a unique violation by the model of the context. Returns the same error. */
|
|
307
|
+
export function describeViolation(error: unknown, models: ViolationModels): unknown {
|
|
308
|
+
if (!(error instanceof UniqueViolationError) || error.constraint === undefined || error.properties.length > 0) return error;
|
|
309
|
+
for (const entity of models.entities) {
|
|
310
|
+
if (error.table !== undefined && entity.tableName !== error.table) continue;
|
|
311
|
+
const index = entity.indexes.find((item) => item.unique && item.name === error.constraint);
|
|
312
|
+
if (index !== undefined) {
|
|
313
|
+
error.properties = index.columns.map((column) => entity.properties.find((property) => property.columnName === column)?.propertyName ?? column);
|
|
314
|
+
error.conflict = index.conflict;
|
|
315
|
+
return error;
|
|
316
|
+
}
|
|
317
|
+
// `pk_<table>` is the name this ORM gives; `<table>_pkey` is PostgreSQL's own, from an older schema.
|
|
318
|
+
if (error.constraint === (entity.keyName ?? `pk_${entity.tableName}`) || error.constraint === `${entity.tableName}_pkey`) {
|
|
319
|
+
error.properties = entity.key.map((property) => property.propertyName);
|
|
320
|
+
return error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return error;
|
|
324
|
+
}
|
package/library/orm/index.ts
CHANGED
|
@@ -148,7 +148,7 @@ const en = {
|
|
|
148
148
|
dns: "the host name does not resolve — check the address",
|
|
149
149
|
database: "the database does not exist — create it or fix the name",
|
|
150
150
|
deadline: "give connect() its own deadline and fail with a clear message",
|
|
151
|
-
raiseLimit: "or raise the limit:
|
|
151
|
+
raiseLimit: "or raise the limit: @App({ kernel: { startupTimeoutMs } })",
|
|
152
152
|
},
|
|
153
153
|
},
|
|
154
154
|
schema: { add: "+", none: "schema is up to date", warnings: "schema warnings" },
|
|
@@ -168,7 +168,7 @@ const en = {
|
|
|
168
168
|
ready: {
|
|
169
169
|
running: (app: string, ms: string) => `${app} is running · ready in ${ms}`,
|
|
170
170
|
local: "Local", network: "Network", health: "Health", docs: "OpenAPI",
|
|
171
|
-
notExposed: (host: string) => `not exposed — listening on ${host} (set
|
|
171
|
+
notExposed: (host: string) => `not exposed — listening on ${host} (set BAZIS_HTTP__HOST=0.0.0.0)`,
|
|
172
172
|
watching: (src: string) => `watching ${src} · restarts on change · logs below · Ctrl+C to stop`,
|
|
173
173
|
noWatch: "logs below · Ctrl+C to stop",
|
|
174
174
|
worker: (app: string, ms: string) => `${app} started · ready in ${ms}`,
|
|
@@ -240,7 +240,7 @@ const en = {
|
|
|
240
240
|
codegenSkipped: "codegen skipped (--no-codegen) — run bazis codegen",
|
|
241
241
|
needsDb: "this module uses a database: connect one with @Infra({ db: ormBazisConnect(dbConfig) })",
|
|
242
242
|
needsTable: (moduleFile: string) => `its table is not created: add ensureCreated: true to ormBazis in ${moduleFile}, or write a migration`,
|
|
243
|
-
needsCache: "this module caches:
|
|
243
|
+
needsCache: "this module caches: add cache: memory() to @App",
|
|
244
244
|
keptOnFailure: "scaffold files were kept — fix the error and run bazis codegen",
|
|
245
245
|
},
|
|
246
246
|
workflow: {
|
|
@@ -325,7 +325,7 @@ const ru: Messages = {
|
|
|
325
325
|
dns: "имя хоста не разрешается — проверьте адрес",
|
|
326
326
|
database: "базы данных нет — создайте её или исправьте имя",
|
|
327
327
|
deadline: "дайте connect() свой срок и понятную ошибку",
|
|
328
|
-
raiseLimit: "или увеличьте предел:
|
|
328
|
+
raiseLimit: "или увеличьте предел: @App({ kernel: { startupTimeoutMs } })",
|
|
329
329
|
},
|
|
330
330
|
},
|
|
331
331
|
schema: { add: "+", none: "схема актуальна", warnings: "предупреждения схемы" },
|
|
@@ -345,7 +345,7 @@ const ru: Messages = {
|
|
|
345
345
|
ready: {
|
|
346
346
|
running: (app, ms) => `${app} работает · готово за ${ms}`,
|
|
347
347
|
local: "Локально", network: "Сеть", health: "Здоровье", docs: "OpenAPI",
|
|
348
|
-
notExposed: (host) => `недоступно снаружи — слушает ${host} (задайте
|
|
348
|
+
notExposed: (host) => `недоступно снаружи — слушает ${host} (задайте BAZIS_HTTP__HOST=0.0.0.0)`,
|
|
349
349
|
watching: (src) => `слежу за ${src} · перезапуск при изменениях · журнал ниже · Ctrl+C — остановить`,
|
|
350
350
|
noWatch: "журнал ниже · Ctrl+C — остановить",
|
|
351
351
|
worker: (app, ms) => `${app} запущено · готово за ${ms}`,
|
|
@@ -417,7 +417,7 @@ const ru: Messages = {
|
|
|
417
417
|
codegenSkipped: "кодогенерация пропущена (--no-codegen) — запустите bazis codegen",
|
|
418
418
|
needsDb: "модулю нужна база данных: подключите её через @Infra({ db: ormBazisConnect(dbConfig) })",
|
|
419
419
|
needsTable: (moduleFile) => `таблицу шаблон не создаёт: добавьте ensureCreated: true в ormBazis в ${moduleFile} или напишите миграцию`,
|
|
420
|
-
needsCache: "модуль кэширует:
|
|
420
|
+
needsCache: "модуль кэширует: добавьте cache: memory() в @App",
|
|
421
421
|
keptOnFailure: "файлы модуля оставлены — исправьте ошибку и запустите bazis codegen",
|
|
422
422
|
},
|
|
423
423
|
workflow: {
|
|
@@ -48,7 +48,7 @@ More scenarios (complex conditions, async, nested objects) are in
|
|
|
48
48
|
| `RuleEngine.ts` | the engine: one pass over the plan, sync/async, nested, cycle protection |
|
|
49
49
|
| `RuleCompiler.ts` | compiles class rules into a plan + WeakMap cache (RegExp, enum Set) |
|
|
50
50
|
| `RuleSet.ts` | rules as data without a class: `Validator.ruleSet({ field: options })` |
|
|
51
|
-
| `MessageRegistry.ts` | message templates, placeholders, priority local > global > built-in; built-in texts are English, `RU_VALIDATION_MESSAGES` is a Russian set for `setDefaults` and
|
|
51
|
+
| `MessageRegistry.ts` | message templates, placeholders, priority local > global > built-in; built-in texts are English, `RU_VALIDATION_MESSAGES` is a Russian set for `setDefaults` and `@App({ language: "ru" })` |
|
|
52
52
|
| `metadata.ts` | stores rules through `context.metadata` / `Symbol.metadata` |
|
|
53
53
|
| `types/ValidatorOptions.ts` | all decorator options |
|
|
54
54
|
| `types/ValidationCode.ts` | error codes |
|
|
@@ -473,7 +473,7 @@ so a valid object pays no allocations for it.
|
|
|
473
473
|
Template priority: **the decorator's local `message` > global (`setDefaultMessages`) >
|
|
474
474
|
built-in**. A string returned from `custom` wins over all of them.
|
|
475
475
|
Built-in messages are English (`EN_VALIDATION_MESSAGES`); `RU_VALIDATION_MESSAGES` is a Russian set for `setDefaultMessages`.
|
|
476
|
-
An HTTP application picks the language with
|
|
476
|
+
An HTTP application picks the language with `@App({ language: "ru", messages })`: each
|
|
477
477
|
`ValidationError` built from a template carries `params`, and the HTTP error handler
|
|
478
478
|
re-renders it in the application language (a rule's own `message` has no `params` and
|
|
479
479
|
stays as is). Priority there: `messages` > `setDefaultMessages` > the language set > English.
|
|
@@ -20,6 +20,7 @@ export {
|
|
|
20
20
|
type ProcessNode,
|
|
21
21
|
type ProcessSettings,
|
|
22
22
|
} from "./ProcessDocument";
|
|
23
|
+
export type { BoundaryNodeKind, ConfiguredNodeKind, InputMapping, NodeConfigs, ProcessNodeOf, RetryPolicy, TimerConfig, TypedProcessFlow, TypedProcessNode } from "./nodeConfigs";
|
|
23
24
|
export { NODE_KINDS, nodeKindOf, ISO_DURATION, MAX_ITERATIONS, MAX_LOOP_PARALLEL, PROCESS_KEY, ISO_MOMENT, type JsonSchema, type NodeKindDefinition, type NodeRole } from "./nodeKinds";
|
|
24
25
|
export { PROCESS_LIMITS, validateProcess, hasErrors, type ProcessIssue, type ProcessIssueCode, type ValidateProcessOptions } from "./validateProcess";
|
|
25
26
|
export { DATA_PATH, DATA_PATHS, isPath, readPath, resolveInput, secretNames, type InstanceData } from "./paths";
|