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.
Files changed (132) hide show
  1. package/README.md +7 -4
  2. package/cli/generateProject.ts +11 -18
  3. package/cli/main.ts +9 -1
  4. package/cli/parseCli.ts +9 -0
  5. package/cli/templates/module.ts +16 -19
  6. package/cli/templates/passport.ts +2 -2
  7. package/cli/upgrade/markColumns.ts +151 -0
  8. package/cli/upgrade/runUpgrade.ts +57 -0
  9. package/core/access/Access.module.ts +1 -1
  10. package/core/access/catalog/moduleFields.ts +41 -4
  11. package/core/access/catalog/permissions.ts +10 -0
  12. package/core/access/check/Access.ts +5 -5
  13. package/core/access/check/can.ts +2 -1
  14. package/core/access/index.ts +11 -1
  15. package/core/access/roles/roles.ts +4 -2
  16. package/core/access/testing/index.ts +26 -4
  17. package/core/agent/ToolRunner.ts +7 -1
  18. package/core/agent/defineTool.ts +109 -0
  19. package/core/agent/index.ts +8 -3
  20. package/core/agent/moduleContributions-v1.ts +3 -10
  21. package/core/agent/session/entities.ts +45 -45
  22. package/core/app/index.ts +1 -1
  23. package/core/app/messages.ts +3 -3
  24. package/core/app/runApp.ts +90 -26
  25. package/core/cache/ICache.ts +1 -1
  26. package/core/cache/README.md +4 -2
  27. package/core/cache/SPEC.md +16 -9
  28. package/core/cache/http/missingCacheModuleCheck.ts +5 -4
  29. package/core/cache/internal/buildCacheProviders.ts +4 -5
  30. package/core/cache/providers/cachedProviders.ts +1 -1
  31. package/core/di/index.ts +0 -4
  32. package/core/di/lowLevel.ts +12 -0
  33. package/core/di/module/index.ts +0 -3
  34. package/core/di/module/shortcuts.ts +31 -86
  35. package/core/generatedRuntime.ts +11 -0
  36. package/core/grpc/Grpc.module.ts +2 -1
  37. package/core/grpc/grpcClients.ts +2 -1
  38. package/core/http/Authorization/metadata.ts +1 -1
  39. package/core/http/Decorators/metadata.ts +3 -0
  40. package/core/http/Decorators/routes.ts +12 -0
  41. package/core/http/Errors/HttpError.ts +16 -2
  42. package/core/http/Http.config.ts +22 -0
  43. package/core/http/HttpContext/HttpContext.ts +1 -1
  44. package/core/http/HttpServer.ts +29 -9
  45. package/core/http/Middleware/errorHandler.ts +16 -1
  46. package/core/http/OpenApi/openApiDocument.ts +44 -5
  47. package/core/http/Results/normalize.ts +38 -1
  48. package/core/http/Results/results.ts +5 -2
  49. package/core/http/Routing/RouterBuilder.ts +27 -2
  50. package/core/http/httpModule.ts +10 -8
  51. package/core/http/index.ts +4 -0
  52. package/core/http/middlewareTokens.ts +12 -0
  53. package/core/http/options.ts +5 -1
  54. package/core/http-client/httpClients.ts +2 -1
  55. package/core/infra/Infra.ts +3 -3
  56. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  57. package/core/infra/connectors/codex/contracts.ts +3 -3
  58. package/core/infra/connectors/llm.ts +2 -1
  59. package/core/infra/connectors/postgres.ts +4 -2
  60. package/core/infra/connectors/redis.ts +2 -1
  61. package/core/infra/index.ts +1 -1
  62. package/core/kernel/KernelBuilder.ts +2 -1
  63. package/core/kernel/config/ConfigRegistry.ts +2 -1
  64. package/core/kernel/config/addConfigOptions.ts +2 -1
  65. package/core/kernel/config/defineConfig.ts +56 -20
  66. package/core/kernel/events/EventBus.ts +1 -0
  67. package/core/kernel/events/eventSubscribers.ts +1 -1
  68. package/core/kernel/events/onEvent.ts +2 -1
  69. package/core/kernel/index.ts +3 -0
  70. package/core/orm/buildOrmProviders.ts +4 -2
  71. package/core/orm/contextEntities.ts +36 -0
  72. package/core/orm/databaseConnector.ts +9 -3
  73. package/core/orm/databaseHttpError.ts +21 -0
  74. package/core/orm/encryptedWith.ts +1 -1
  75. package/core/orm/index.ts +12 -1
  76. package/core/orm/ormModule.ts +19 -8
  77. package/core/orm/ownedStoreContributions.ts +2 -1
  78. package/core/orm/registerRepositories.ts +2 -1
  79. package/core/scripts/di-generate.ts +113 -12
  80. package/core/testing/index.ts +51 -11
  81. package/core/tools/StandardTools.module.ts +1 -1
  82. package/core/tools/contracts/Tools.config.ts +13 -7
  83. package/core/tools/generated/deps.ts +3 -3
  84. package/core/tools/http/HttpRequest.tool.ts +1 -2
  85. package/core/tools/http/publicAddress.ts +2 -2
  86. package/core/tools/index.ts +1 -1
  87. package/core/tools/orm/OrmEntity.references.ts +7 -8
  88. package/core/tools/orm/ormEntityTools.ts +54 -28
  89. package/core/websocket/DESIGN.md +5 -3
  90. package/core/websocket/middleware.ts +1 -1
  91. package/core/websocket/websocketModule.ts +3 -3
  92. package/core/workflow/contracts/Workflow.config.ts +5 -5
  93. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +3 -4
  94. package/core/workflow/definitions_module/code/defineProcess.ts +213 -11
  95. package/core/workflow/definitions_module/code/inspectProcesses.ts +12 -2
  96. package/core/workflow/definitions_module/http/Processes.controller.ts +2 -2
  97. package/core/workflow/definitions_module/internal/PublishedVersions.ts +39 -0
  98. package/core/workflow/definitions_module/model/WfProcess.model.ts +24 -11
  99. package/core/workflow/definitions_module/model/WfVersion.model.ts +7 -1
  100. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +4 -1
  101. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +17 -7
  102. package/core/workflow/generated/deps.ts +2 -1
  103. package/core/workflow/index.ts +23 -1
  104. package/core/workflow/internal/WorkflowLanguages.ts +1 -1
  105. package/core/workflow/internal/http.ts +2 -2
  106. package/core/workflow/runtime_module/background/WorkflowWorker.ts +6 -2
  107. package/core/workflow/runtime_module/http/Instances.controller.ts +1 -1
  108. package/core/workflow/runtime_module/internal/WorkflowJobs.ts +43 -1
  109. package/core/workflow/runtime_module/model/WfHistory.model.ts +13 -6
  110. package/core/workflow/runtime_module/model/WfInstance.model.ts +30 -10
  111. package/core/workflow/runtime_module/model/WfJob.model.ts +22 -10
  112. package/core/workflow/runtime_module/model/WfSubscription.model.ts +8 -3
  113. package/core/workflow/runtime_module/model/WfToken.model.ts +16 -7
  114. package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +10 -6
  115. package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +5 -2
  116. package/core/workflow/tasks_module/model/WfTask.model.ts +25 -9
  117. package/core/workflow/testing/index.ts +46 -4
  118. package/index.ts +4 -3
  119. package/library/openapi/codegen.ts +2 -2
  120. package/library/orm/DbContext.ts +37 -1
  121. package/library/orm/Metadata/ModelBuilder.ts +23 -7
  122. package/library/orm/Metadata/decorators.ts +23 -10
  123. package/library/orm/Metadata/types.ts +2 -0
  124. package/library/orm/Transactions/TransactionScopeCoordinator.ts +3 -3
  125. package/library/orm/errors.ts +39 -1
  126. package/library/orm/index.ts +1 -0
  127. package/library/terminal/messages.ts +6 -6
  128. package/library/validation/README.md +1 -1
  129. package/library/validation/SPEC.md +1 -1
  130. package/library/workflow/index.ts +1 -0
  131. package/library/workflow/nodeConfigs.ts +130 -0
  132. package/package.json +2 -1
@@ -30,7 +30,9 @@ export interface PostgresConfigShape {
30
30
  readonly maxLifetime?: number;
31
31
  }
32
32
 
33
- export type PostgresSslMode = "disable" | "allow" | "prefer" | "require" | "verify-ca" | "verify-full";
33
+ /** The TLS modes of a PostgreSQL connection, for `configEnum(POSTGRES_SSL_MODES, "disable")`. */
34
+ export const POSTGRES_SSL_MODES = ["disable", "allow", "prefer", "require", "verify-ca", "verify-full"] as const;
35
+ export type PostgresSslMode = typeof POSTGRES_SSL_MODES[number];
34
36
  export interface PostgresConnectorOptions {
35
37
  readonly token?: InjectionToken<SQL>;
36
38
  /** Maximum pool drain before closing connections. Default 1000ms; 0 closes immediately. */
@@ -47,7 +49,7 @@ export function postgresConnectionOptions(config: AppConfig<PostgresConfigShape>
47
49
  return value;
48
50
  };
49
51
  const tls = c.has("tls") ? c.get("tls") : undefined;
50
- if (tls !== undefined && !["disable", "allow", "prefer", "require", "verify-ca", "verify-full"].includes(String(tls))) throw new InfraError("postgres tls is invalid");
52
+ if (tls !== undefined && !(POSTGRES_SSL_MODES as readonly string[]).includes(String(tls))) throw new InfraError("postgres tls is invalid");
51
53
  return {
52
54
  hostname: requireValue(c.get("host"), "host", "postgres"),
53
55
  port: integer("port", 1, 65535),
@@ -7,7 +7,8 @@ import {
7
7
  connectionCache,
8
8
  type DistributedCacheStores,
9
9
  } from "../../cache";
10
- import { createToken, DI, type InjectionToken, type ProviderDefinition } from "../../di";
10
+ import { createToken, type InjectionToken, type ProviderDefinition } from "../../di";
11
+ import { DI } from "../../di/lowLevel";
11
12
  import type { AppConfig, Secret } from "../../kernel";
12
13
  import { RedisDistributedCacheBackend, type RedisDistributedCacheTuning } from "../cache/RedisDistributedCacheBackend";
13
14
  import { reader, requireValue, settingNames } from "../connectorConfig";
@@ -10,7 +10,7 @@ export { reader, requireValue, settingNames, type ConfigReader } from "./connect
10
10
  export { codexAppServerConnect, ICodex } from "./connectors/codex";
11
11
  export { CodexError, RU_CODEX_MESSAGES, type CodexErrorCode, CODEX_MODEL_ID, CODEX_REASONING_EFFORT, type CodexClient, type CodexConfigShape, type CodexLogin, type CodexLoginMethod, type CodexModel, type CodexStatus, type CodexRunInput, type CodexChatMessage, type CodexTool, type CodexToolCall, type CodexToolResult } from "./connectors/codex/contracts";
12
12
 
13
- export { postgres, IPostgres, postgresConnectionOptions, type PostgresConfigShape, type PostgresSslMode, type PostgresConnectorOptions } from "./connectors/postgres";
13
+ export { postgres, IPostgres, POSTGRES_SSL_MODES, postgresConnectionOptions, type PostgresConfigShape, type PostgresSslMode, type PostgresConnectorOptions } from "./connectors/postgres";
14
14
  export {
15
15
  redisConnect,
16
16
  IRedis,
@@ -1,4 +1,5 @@
1
- import { DI, Global, Module, collectModuleConfigs, createContainer, type BazisModule, type BazisModuleRef, type ProviderDefinition } from "../di";
1
+ import { Global, Module, collectModuleConfigs, createContainer, type BazisModule, type BazisModuleRef, type ProviderDefinition } from "../di";
2
+ import { DI } from "../di/lowLevel";
2
3
  import { isBazisModuleClass } from "../di/module/Module";
3
4
  import { ApplicationLifetime } from "./ApplicationLifetime";
4
5
  import { Configuration, loadConfiguration } from "./config/Configuration";
@@ -1,4 +1,5 @@
1
- import { DI, type ProviderDefinition } from "../../di";
1
+ import { type ProviderDefinition } from "../../di";
2
+ import { DI } from "../../di/lowLevel";
2
3
  import { KernelError } from "../errors";
3
4
  import type { EnvironmentName } from "../types";
4
5
  import type { Configuration } from "./Configuration";
@@ -1,5 +1,6 @@
1
1
  import type { InjectionToken, Options, ProviderDefinition, ServiceCollection, ServiceResolver } from "../../di";
2
- import { DI, OPTIONS_STARTUP_VALIDATOR, OptionsValidationError } from "../../di";
2
+ import { OPTIONS_STARTUP_VALIDATOR, OptionsValidationError } from "../../di";
3
+ import { DI } from "../../di/lowLevel";
3
4
  import { ConfigKeyMissingError } from "../errors";
4
5
  import { Configuration } from "./Configuration";
5
6
 
@@ -15,7 +15,7 @@ registerNamedDependencyHint((name) => {
15
15
  const match = /^Config:(.+)$/.exec(name);
16
16
  if (!match) return undefined;
17
17
  const declaration = match[1] === "default" ? "defineConfig({ … })" : `defineConfig("${match[1]}", …)`;
18
- return `The config ${declaration} is not declared in the application: add it to \`config\` of the module that uses it, or to runApp({ config }).`;
18
+ return `The config ${declaration} is not declared in the application: add it to \`config\` of the module that uses it, or to @App({ config }).`;
19
19
  });
20
20
 
21
21
  type Stand = "development" | "test" | "production";
@@ -38,14 +38,23 @@ export function configEnum<const V extends readonly [string | number, ...(string
38
38
  return declared;
39
39
  }
40
40
  const invalidEnums = new WeakMap<object, KernelError>();
41
- type Cell = Primitive | SecretSpec | ConfigEnum;
41
+ export interface ConfigList { readonly __list: true; readonly default: readonly string[] }
42
+ /**
43
+ * A list of strings: `readonly string[]` in the code, comma-separated in the
44
+ * environment (`BAZIS_TOOLS__HTTP__ALLOW=api.example.com,*.example.org`). Items
45
+ * are trimmed, empty ones are dropped.
46
+ */
47
+ export function configList(defaultValue: readonly string[] = []): ConfigList {
48
+ return Object.freeze({ __list: true, default: Object.freeze([...defaultValue]) });
49
+ }
50
+ type Cell = Primitive | SecretSpec | ConfigEnum | ConfigList;
42
51
  type Defaults = Record<string, Cell>;
43
- export type ConfigValueType = Primitive | Secret;
52
+ export type ConfigValueType = Primitive | Secret | readonly string[];
44
53
  export type ConfigShape = Record<string, ConfigValueType>;
45
- type Resolved<V> = V extends SecretSpec ? Secret : V extends ConfigEnum<infer E> ? E : V extends number ? number : V extends boolean ? boolean : string;
54
+ type Resolved<V> = V extends SecretSpec ? Secret : V extends ConfigList ? readonly string[] : V extends ConfigEnum<infer E> ? E : V extends number ? number : V extends boolean ? boolean : string;
46
55
  type ResolveAll<D extends Defaults> = { [K in keyof D]: Resolved<D[K]> };
47
- type WidenCell<V> = V extends SecretSpec ? SecretSpec : V extends ConfigEnum<infer E> ? E : V extends number ? number : V extends boolean ? boolean : string;
48
- type CellFor<T> = [T] extends [Secret] ? SecretSpec : [T] extends [string] ? (string extends T ? string : ConfigEnum<T & string>) : [T] extends [number] ? (number extends T ? number : ConfigEnum<T & number>) : [T] extends [boolean] ? (boolean extends T ? boolean : never) : never;
56
+ type WidenCell<V> = V extends SecretSpec ? SecretSpec : V extends ConfigList ? readonly string[] : V extends ConfigEnum<infer E> ? E : V extends number ? number : V extends boolean ? boolean : string;
57
+ type CellFor<T> = [T] extends [Secret] ? SecretSpec : [T] extends [readonly string[]] ? ConfigList : [T] extends [string] ? (string extends T ? string : ConfigEnum<T & string>) : [T] extends [number] ? (number extends T ? number : ConfigEnum<T & number>) : [T] extends [boolean] ? (boolean extends T ? boolean : never) : never;
49
58
  type Declaration<T extends object> = { [K in keyof T]: CellFor<T[K]> };
50
59
  type OverrideFor<T> = T extends Secret ? SecretSpec : T;
51
60
  interface Rules<T extends object> {
@@ -76,18 +85,28 @@ export interface AppConfig<T extends object> extends ValidatableConfig {
76
85
  }
77
86
  export interface ConfigInspection {
78
87
  readonly key: string;
79
- readonly type: "string" | "number" | "boolean" | "secret";
88
+ readonly type: "string" | "number" | "boolean" | "secret" | "list";
80
89
  readonly env: readonly string[];
81
90
  readonly source: string;
82
91
  readonly value: Primitive;
83
92
  }
84
- export interface ConfigView<T extends object> extends AppConfig<T> {
93
+ /** The settings of one section as a service reads them. */
94
+ export interface ResolvedConfigView<T extends object> extends AppConfig<T> {
85
95
  readonly environment: Stand;
86
96
  inspect(): readonly ConfigInspection[];
87
97
  }
88
- export interface ConfigDefinition<T extends object> extends AppConfig<T> {
89
- readonly token: InjectionToken<ConfigView<T>>;
90
- resolve(environment?: Stand, configuration?: Configuration): ConfigView<T>;
98
+ /**
99
+ * The settings a service takes in its constructor. `T` is the declaration
100
+ * itself, `ConfigView<typeof mailConfig>`, so the settings are described once,
101
+ * by their defaults; or the shape of the settings, `ConfigView<MailConfig>`, for a
102
+ * declaration with an explicit type.
103
+ */
104
+ export type ConfigView<T extends object> = ResolvedConfigView<T extends ConfigDefinition<infer Shape, string> ? Shape : T>;
105
+ export interface ConfigDefinition<T extends object, P extends string = string> extends AppConfig<T> {
106
+ /** The name of the section: `mail`; `default` for a declaration without one. */
107
+ readonly prefix: P;
108
+ readonly token: InjectionToken<ResolvedConfigView<T>>;
109
+ resolve(environment?: Stand, configuration?: Configuration): ResolvedConfigView<T>;
91
110
  }
92
111
  const definitions = new WeakSet<object>();
93
112
  export function isConfigDefinition(config: ValidatableConfig): config is ConfigDefinition<object> {
@@ -118,8 +137,11 @@ function environmentNameProblem(name: string, claimed: ReadonlySet<string>): str
118
137
  return undefined;
119
138
  }
120
139
 
121
- function isSecret(cell: Cell): cell is SecretSpec { return typeof cell === "object" && cell !== null && "__secret" in cell; }
122
- function isEnum(cell: Cell): cell is ConfigEnum { return typeof cell === "object" && cell !== null && "__enum" in cell; }
140
+ function isSecret(cell: unknown): cell is SecretSpec { return typeof cell === "object" && cell !== null && "__secret" in cell; }
141
+ function isEnum(cell: unknown): cell is ConfigEnum { return typeof cell === "object" && cell !== null && "__enum" in cell; }
142
+ function isList(cell: unknown): cell is ConfigList { return typeof cell === "object" && cell !== null && "__list" in cell; }
143
+ /** `a, b,,c` → `["a", "b", "c"]`. */
144
+ function listItems(raw: string): readonly string[] { return Object.freeze(raw.split(",").map((item) => item.trim()).filter((item) => item !== "")); }
123
145
  function copyCell(cell: Cell): Cell {
124
146
  if (isSecret(cell)) return secret(cell.default);
125
147
  if (isEnum(cell)) {
@@ -127,20 +149,27 @@ function copyCell(cell: Cell): Cell {
127
149
  if (problem !== undefined) throw problem;
128
150
  return Object.freeze({ ...cell, values: Object.freeze([...cell.values]) });
129
151
  }
152
+ if (isList(cell)) {
153
+ if (!Array.isArray(cell.default) || cell.default.some((item) => typeof item !== "string")) throw new KernelError("configList requires a list of strings.");
154
+ return configList(cell.default);
155
+ }
156
+ // A list in an environment section is written as it is read: `test: { hosts: ["a"] }`.
157
+ if (Array.isArray(cell)) return copyCell(configList(cell as readonly string[]));
130
158
  if (["string", "number", "boolean"].includes(typeof cell)) return cell;
131
- throw new KernelError("Configuration supports only primitive values, secret() and configEnum().");
159
+ throw new KernelError("Configuration supports only primitive values, secret(), configEnum() and configList().");
132
160
  }
133
161
  function safeValue(key: string, value: ConfigValueType): Primitive {
134
- if (value instanceof Secret || (typeof value === "string" && isSensitiveKey(key))) return "***";
135
- if (typeof value !== "string") return value;
162
+ if (value instanceof Secret || ((typeof value === "string" || Array.isArray(value)) && isSensitiveKey(key))) return "***";
163
+ if (Array.isArray(value)) return value.join(", ");
164
+ if (typeof value !== "string") return value as Primitive;
136
165
  try {
137
166
  const url = new URL(value);
138
167
  if (url.username || url.password) { url.username = "***"; url.password = "***"; return redactSensitiveText(url.toString()); }
139
168
  } catch { /* Most strings are not URLs. */ }
140
169
  return redactSensitiveText(value);
141
170
  }
142
- export function defineConfig<const D extends Defaults>(schema: ConfigSchema<D>): ConfigDefinition<ResolveAll<D>>;
143
- export function defineConfig<const D extends Defaults>(prefix: string, schema: ConfigSchema<D>): ConfigDefinition<ResolveAll<D>>;
171
+ export function defineConfig<const D extends Defaults>(schema: ConfigSchema<D>): ConfigDefinition<ResolveAll<D>, "default">;
172
+ export function defineConfig<const P extends string, const D extends Defaults>(prefix: P, schema: ConfigSchema<D>): ConfigDefinition<ResolveAll<D>, P>;
144
173
  export function defineConfig<T extends object>(schema: SchemaFor<T>): ConfigDefinition<T>;
145
174
  export function defineConfig<T extends object>(prefix: string, schema: SchemaFor<T>): ConfigDefinition<T>;
146
175
  export function defineConfig(prefixOrSchema: string | ConfigSchema<Defaults>, maybeSchema?: ConfigSchema<Defaults>): ConfigDefinition<ConfigShape> {
@@ -157,6 +186,7 @@ export function defineConfig(prefixOrSchema: string | ConfigSchema<Defaults>, ma
157
186
  // errors, instead of crashing the import of the file that declares it.
158
187
  const fail = (): never => { throw error; };
159
188
  definition = Object.freeze({
189
+ prefix: prefix || "default",
160
190
  token: Object.freeze(createToken<ConfigView<ConfigShape>>(`Config:${prefix || "default"}`)),
161
191
  resolve: fail,
162
192
  get: fail,
@@ -209,7 +239,7 @@ function createDefinition(prefix: string, input: ConfigSchema<Defaults>, declare
209
239
  const base = schema.default[key]!;
210
240
  const override = schema[environment]?.[key];
211
241
  const cell = override ?? base;
212
- const type = isSecret(base) ? "secret" : isEnum(base) ? typeof base.default : typeof base;
242
+ const type = isSecret(base) ? "secret" : isList(base) ? "list" : isEnum(base) ? typeof base.default : typeof base;
213
243
  const candidates: { raw: string; source: string; priority: number; name: string }[] = [];
214
244
  for (const name of names.get(key)!) {
215
245
  const sourceKey = name.slice(ENV_PREFIX.length).toLowerCase().replaceAll("__", ".");
@@ -226,7 +256,7 @@ function createDefinition(prefix: string, input: ConfigSchema<Defaults>, declare
226
256
  const variables = [...new Set(winners.map(candidate => candidate.name))];
227
257
  issue(key, "conflict", `conflicting environment aliases: ${variables.join(" and ")} are set to different values; set only one`);
228
258
  }
229
- const raw = selected?.raw ?? (isSecret(cell) || isEnum(cell) ? cell.default : cell);
259
+ const raw: unknown = selected?.raw ?? (isSecret(cell) || isEnum(cell) || isList(cell) ? cell.default : cell);
230
260
  let value: ConfigValueType | undefined;
231
261
  if (type === "secret") {
232
262
  // secret("") reads like an empty default but would mean "required":
@@ -238,6 +268,11 @@ function createDefinition(prefix: string, input: ConfigSchema<Defaults>, declare
238
268
  // No "apiKey: …" in the hint: the console redaction would hide it.
239
269
  else if (environment === "production" && selected === undefined && override === undefined) issue(key, "development-secret", `production would use the development default secret from the code; set ${names.get(key)!.join(" or ")}, or declare ${key} in the production section`);
240
270
  else value = new Secret(raw);
271
+ } else if (type === "list") {
272
+ // From the environment a comma-separated string, from the code a list.
273
+ if (typeof raw === "string") value = listItems(raw);
274
+ else if (Array.isArray(raw) && (raw as unknown[]).every((item) => typeof item === "string")) value = Object.freeze([...(raw as string[])]);
275
+ else issue(key, "string", "expected a list of strings");
241
276
  } else if (type === "number") {
242
277
  const parsed = Number(raw);
243
278
  if (raw === undefined || (typeof raw === "string" && !raw.trim()) || !Number.isFinite(parsed)) issue(key, "number", `expected a finite number${got(key, raw)}${source(key, selected)}`, from(selected));
@@ -273,6 +308,7 @@ function createDefinition(prefix: string, input: ConfigSchema<Defaults>, declare
273
308
  });
274
309
  };
275
310
  return Object.freeze({
311
+ prefix: prefix || "default",
276
312
  token: Object.freeze(createToken<ConfigView<ConfigShape>>(`Config:${prefix || "default"}`)),
277
313
  resolve,
278
314
  get: <K extends keyof ConfigShape>(key: K): ConfigShape[K] => resolve().get(key),
@@ -1,4 +1,5 @@
1
1
  import { createToken, type ServiceResolver } from "../../di";
2
+ import { DI } from "../../di/lowLevel";
2
3
  import type { EventHandler, EventToken } from "./EventToken";
3
4
  import { awaitAbortable } from "../internal/awaitAbortable";
4
5
 
@@ -1,5 +1,4 @@
1
1
  import {
2
- DI,
3
2
  Module,
4
3
  singleton,
5
4
  type Class,
@@ -7,6 +6,7 @@ import {
7
6
  type ProviderDefinition,
8
7
  type ServiceResolver,
9
8
  } from "../../di";
9
+ import { DI } from "../../di/lowLevel";
10
10
  import { EVENT_HANDLER, type EventSubscription } from "./EventBus";
11
11
  import type { EventHandler, EventToken } from "./EventToken";
12
12
 
@@ -1,4 +1,5 @@
1
- import { DI, type ProviderDefinition, type ServiceCollection } from "../../di";
1
+ import { type ProviderDefinition, type ServiceCollection } from "../../di";
2
+ import { DI } from "../../di/lowLevel";
2
3
  import { EVENT_HANDLER, type EventSubscription } from "./EventBus";
3
4
  import type { EventHandler, EventToken } from "./EventToken";
4
5
 
@@ -16,12 +16,15 @@ export { Secret } from "./config/Secret";
16
16
  export {
17
17
  defineConfig,
18
18
  configEnum,
19
+ configList,
19
20
  secret,
20
21
  type AppConfig,
21
22
  type ConfigDefinition,
22
23
  type ConfigView,
24
+ type ResolvedConfigView,
23
25
  type ConfigInspection,
24
26
  type ConfigEnum,
27
+ type ConfigList,
25
28
  type ConfigSchema,
26
29
  type ConfigShape,
27
30
  type ConfigValueType,
@@ -1,4 +1,6 @@
1
- import { DI, HOSTED_SERVICE, type InjectionToken, type ModuleOwnedContributionSnapshot, type ProviderDefinition, type ProviderDependencyList, type ServiceResolver } from "../di";
1
+ import { HOSTED_SERVICE, type InjectionToken, type ModuleOwnedContributionSnapshot, type ProviderDefinition, type ProviderDependencyList, type ServiceResolver } from "../di";
2
+ import { DI } from "../di/lowLevel";
3
+ import { contextEntities } from "./contextEntities";
2
4
  import { getGeneratedClassDeps } from "../di/module/autoDeps";
3
5
  import { resolveDependencyList } from "../di/internal/resolveDependencyList";
4
6
  import { ConfigRegistry, HEALTH_CHECK, StartupReport } from "../kernel";
@@ -27,7 +29,7 @@ function createContextOptions<TContext extends DbContext>(
27
29
  ): DbContextOptions {
28
30
  return bindKeys(new DbContextOptions({
29
31
  provider,
30
- entities: config.entities ?? [],
32
+ entities: contextEntities(config),
31
33
  validateOnSave: config.validateOnSave,
32
34
  executionStrategy: config.executionStrategy,
33
35
  }), serviceProvider);
@@ -0,0 +1,36 @@
1
+ import { DbContext } from "../../library/orm";
2
+
3
+ type EntityClass = new () => object;
4
+ type ContextClass = abstract new (...parameters: never[]) => DbContext;
5
+
6
+ /** The sets of a context class are read once: a class does not change. */
7
+ const sets = new WeakMap<ContextClass, readonly EntityClass[] | Error>();
8
+
9
+ function setsOf(context: ContextClass): readonly EntityClass[] | Error {
10
+ let known = sets.get(context);
11
+ if (known === undefined) {
12
+ try {
13
+ known = DbContext.entitiesOf(context as unknown as Parameters<typeof DbContext.entitiesOf>[0]);
14
+ } catch (error) {
15
+ known = error instanceof Error ? error : new Error(String(error));
16
+ }
17
+ sets.set(context, known);
18
+ }
19
+ return known;
20
+ }
21
+
22
+ /**
23
+ * The entities of a context: the listed ones in their order, then those of its
24
+ * sets (`this.set(...)`). A context whose constructor cannot run without its
25
+ * dependencies has only the listed ones, and with none listed that is an error.
26
+ */
27
+ export function contextEntities(config: { readonly context?: ContextClass; readonly entities?: readonly EntityClass[] }): readonly EntityClass[] {
28
+ const listed = config.entities ?? [];
29
+ if (!config.context) return listed;
30
+ const ofSets = setsOf(config.context);
31
+ if (ofSets instanceof Error) {
32
+ if (listed.length === 0) throw ofSets;
33
+ return listed;
34
+ }
35
+ return ofSets.length === 0 ? listed : [...new Set([...listed, ...ofSets])];
36
+ }
@@ -1,7 +1,10 @@
1
1
  import { errorMessage, InfraError, reader, requireValue, postgresConnectionOptions, settingNames, type InfraConnector, type PostgresConfigShape } from "../infra";
2
- import { postgresAddress, serverVersion } from "../infra/connectors/postgres";
2
+ import { POSTGRES_SSL_MODES, postgresAddress, serverVersion, type PostgresSslMode } from "../infra/connectors/postgres";
3
3
  import type { AppConfig, ConfigRegistry } from "../kernel";
4
4
  import { postgres, type DatabaseProvider, type PostgresServerTimeouts } from "../../library/orm";
5
+ import { singletonValue } from "../di";
6
+ import { HTTP_ERROR_MAPPER } from "../http/middlewareTokens";
7
+ import { databaseHttpError } from "./databaseHttpError";
5
8
  import { DATABASE_PROVIDER } from "./DATABASE_PROVIDER";
6
9
 
7
10
  /** Connect before the servers and ORM features: open the database before anyone asks for it. */
@@ -13,7 +16,7 @@ export interface PostgresOrmConfigShape extends PostgresConfigShape {
13
16
  readonly connectionTimeout?: number;
14
17
  readonly idleTimeout?: number;
15
18
  readonly maxLifetime?: number;
16
- readonly tls?: "disable" | "allow" | "prefer" | "require" | "verify-ca" | "verify-full";
19
+ readonly tls?: PostgresSslMode;
17
20
  readonly tlsCa?: string;
18
21
  readonly operationTimeoutMs?: number;
19
22
  readonly cancellationTimeoutMs?: number;
@@ -43,7 +46,7 @@ function buildPostgresProvider<T extends PostgresOrmConfigShape>(config: AppConf
43
46
  // defineConfig keys are always present, so an empty tlsCa means "no additional CA".
44
47
  const tls = optional("tls"), configuredCa = optional("tlsCa"), ca = configuredCa === "" ? undefined : configuredCa;
45
48
  if (tls !== undefined) {
46
- if (typeof tls !== "string" || !["disable", "allow", "prefer", "require", "verify-ca", "verify-full"].includes(tls)) throw new InfraError("Invalid PostgreSQL configuration field: tls.");
49
+ if (typeof tls !== "string" || !(POSTGRES_SSL_MODES as readonly string[]).includes(tls)) throw new InfraError("Invalid PostgreSQL configuration field: tls.");
47
50
  options.tls = tls;
48
51
  }
49
52
  if (ca !== undefined) {
@@ -96,6 +99,9 @@ export function ormBazisConnect(
96
99
  token: DATABASE_PROVIDER,
97
100
  config,
98
101
  phase: DATABASE_PHASE,
102
+ // Database errors answer by their meaning (409, 404, 422, 503) without a setting in the application.
103
+ providers: [singletonValue(HTTP_ERROR_MAPPER, databaseHttpError)],
104
+ exports: [HTTP_ERROR_MAPPER],
99
105
  create(configs) {
100
106
  const c = reader(config, configs);
101
107
  target = `PostgreSQL at ${String(c.get("host"))}:${String(c.get("port"))} (database "${String(c.get("database"))}", user "${String(c.get("username"))}")`;
@@ -0,0 +1,21 @@
1
+ import { ConflictError, HttpError } from "../http/Errors/HttpError";
2
+ import { englishHttpMessage, STATUS_MESSAGE_KEYS } from "../http/Errors/messages";
3
+ import { UniqueViolationError } from "../../library/orm";
4
+ import { databaseErrorStatus } from "./databaseErrorStatus";
5
+
6
+ /**
7
+ * A database error as the HTTP answer for it: the statuses of
8
+ * {@link databaseErrorStatus}, and for a unique violation the text of
9
+ * `@Index({ conflict })` with the properties of the index in the details:
10
+ * `{ error, details: { code: "unique", fields: ["name"] } }`. The ORM registers
11
+ * it for every application with HTTP; the message never carries values of the row.
12
+ */
13
+ export function databaseHttpError(error: unknown): HttpError | undefined {
14
+ if (error instanceof UniqueViolationError) {
15
+ return new ConflictError(error.conflict, { code: "unique", fields: [...error.properties] });
16
+ }
17
+ const status = databaseErrorStatus(error);
18
+ if (status === undefined) return undefined;
19
+ const key = STATUS_MESSAGE_KEYS[status] ?? (status >= 500 ? "serverError" : "clientError");
20
+ return new HttpError(status, englishHttpMessage(key), undefined, { key });
21
+ }
@@ -60,7 +60,7 @@ export function bindEncryptedColumns(options: DbContextOptions, registry: Config
60
60
  try {
61
61
  view = registry.get(binding.config) as { get(key: string): unknown };
62
62
  } catch {
63
- throw new OrmError(`${where}: the config "${prefixOf(binding)}" is not declared in the application; add it to \`config\` of the module that owns ${entity.name} or to runApp({ config }).`);
63
+ throw new OrmError(`${where}: the config "${prefixOf(binding)}" is not declared in the application; add it to \`config\` of the module that owns ${entity.name} or to @App({ config }).`);
64
64
  }
65
65
  const value = view.get(binding.field);
66
66
  const secret = value instanceof Secret ? value.reveal() : value;
package/core/orm/index.ts CHANGED
@@ -11,12 +11,14 @@ export { IRepository, repositoryFor } from "./repository";
11
11
  export { registerRepositories } from "./registerRepositories";
12
12
  export { paginate, type PageResult } from "./listQuery";
13
13
  export { databaseErrorStatus } from "./databaseErrorStatus";
14
+ export { databaseHttpError } from "./databaseHttpError";
14
15
  export { encryptedWith } from "./encryptedWith";
15
16
 
16
17
  import { registerModuleMetadataExpander, type BazisModuleRef } from "../di";
17
18
  import { registerModuleDescriber } from "../kernel/report/moduleTree";
18
19
  import { OrmOwnedStoreAdmissionError, type DbContext } from "../../library/orm";
19
20
  import { registerRepositoryEncapsulationHook } from "./encapsulationHook";
21
+ import { contextEntities } from "./contextEntities";
20
22
  import { ormModule, type OrmModuleConfig } from "./ormModule";
21
23
  import { readOwnedStoreRegistration, revalidateOwnedStoreRegistration } from "./ownedStoreContributions";
22
24
 
@@ -62,13 +64,22 @@ function declaredOwnedStore(config: OrmModuleConfig<DbContext>): unknown {
62
64
  return descriptor.value;
63
65
  }
64
66
 
67
+ /** For the startup screen only: a context whose sets cannot be read shows what is listed. */
68
+ function describedEntities(config: OrmModuleConfig<DbContext>): readonly unknown[] {
69
+ try {
70
+ return contextEntities(config);
71
+ } catch {
72
+ return config.entities ?? [];
73
+ }
74
+ }
75
+
65
76
  // The module tree of the startup screen: `Task, Comment · TaskDbContext`.
66
77
  registerModuleDescriber((metadata) => {
67
78
  const orm = (metadata as { ormBazis?: OrmModuleConfig<DbContext> | readonly OrmModuleConfig<DbContext>[] }).ormBazis;
68
79
  if (orm === undefined) return [];
69
80
  return (Array.isArray(orm) ? orm : [orm]).map((config: OrmModuleConfig<DbContext>) => ({
70
81
  kind: "table" as const,
71
- text: `${(config.entities ?? []).map((entity) => (entity as { name?: string }).name ?? "entity").join(", ") || "no entities"} · ${(config.context as { name?: string } | undefined)?.name ?? "DbContext"}`,
82
+ text: `${describedEntities(config).map((entity) => (entity as { name?: string }).name ?? "entity").join(", ") || "no entities"} · ${(config.context as { name?: string } | undefined)?.name ?? "DbContext"}`,
72
83
  }));
73
84
  });
74
85
 
@@ -1,4 +1,5 @@
1
- import { DI, HOSTED_SERVICE, singletonValue, type BazisModuleRef, type ProviderDefinition } from "../di";
1
+ import { HOSTED_SERVICE, singletonValue, type BazisModuleRef, type ProviderDefinition } from "../di";
2
+ import { DI } from "../di/lowLevel";
2
3
  import {
3
4
  OrmError,
4
5
  type DatabaseProvider,
@@ -7,6 +8,9 @@ import {
7
8
  type DbContextOptionsConfig,
8
9
  type Migration,
9
10
  } from "../../library/orm";
11
+ import { HTTP_ERROR_MAPPER } from "../http/middlewareTokens";
12
+ import { databaseHttpError } from "./databaseHttpError";
13
+ import { contextEntities } from "./contextEntities";
10
14
  import { DATABASE_PROVIDER } from "./DATABASE_PROVIDER";
11
15
  import { IRepository } from "./repository";
12
16
  import { registerRepositories } from "./registerRepositories";
@@ -40,7 +44,12 @@ type ContextClass<TContext extends DbContext> = new (options: DbContextOptions,
40
44
  export interface OrmModuleConfig<TContext extends DbContext> {
41
45
  /** Context class (a DbContext subclass). Omitted in the "connection" mode. */
42
46
  readonly context?: ContextClass<TContext>;
43
- /** Entities mapped by this context. */
47
+ /**
48
+ * Entities of the context besides those of its sets: by default the context
49
+ * maps what it declares with `this.set(...)`. List here only an entity without
50
+ * a set, or every entity of a context whose constructor cannot run without its
51
+ * dependencies.
52
+ */
44
53
  readonly entities?: readonly EntityClass[];
45
54
  /**
46
55
  * Database provider value (`postgres(...)`). Without it the context connects
@@ -86,7 +95,7 @@ function defineFeatureOrmModule<TContext extends DbContext>(
86
95
  providers,
87
96
  exports: withRepositories ? [config.context, IRepository] : [config.context],
88
97
  configure: withRepositories
89
- ? (di) => registerRepositories(di, config.context, config.entities ?? [])
98
+ ? (di) => registerRepositories(di, config.context, contextEntities(config))
90
99
  : undefined,
91
100
  };
92
101
  }
@@ -94,6 +103,8 @@ function defineFeatureOrmModule<TContext extends DbContext>(
94
103
  function defineConnectionModule(provider: DatabaseProvider, healthCheck: boolean): BazisModuleRef {
95
104
  const providers: ProviderDefinition[] = [
96
105
  singletonValue(DATABASE_PROVIDER, provider),
106
+ // Database errors answer by their meaning (409, 404, 422, 503) without a setting in the application.
107
+ singletonValue(HTTP_ERROR_MAPPER, databaseHttpError),
97
108
  DI.singleton(
98
109
  DI.factoryProvider(
99
110
  HOSTED_SERVICE,
@@ -112,7 +123,7 @@ function defineConnectionModule(provider: DatabaseProvider, healthCheck: boolean
112
123
  /** Single ORM registration entry point (see {@link OrmModuleConfig}). */
113
124
  export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TContext>): BazisModuleRef {
114
125
  if (config.ownedStore !== undefined) {
115
- if (!config.context || !config.entities || config.entities.length === 0) {
126
+ if (!config.context || contextEntities(config).length === 0) {
116
127
  throw new OrmError("ORM owned store requires a context and entities.");
117
128
  }
118
129
  if (config.provider) {
@@ -124,7 +135,7 @@ export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TC
124
135
  // Provider factories outlive this call. Keep their context/entity view
125
136
  // registration-local so a caller cannot mutate the descriptor afterwards
126
137
  // and silently change the runtime graph behind its owned receipt.
127
- const buildConfig = Object.freeze({ ...config, context: config.context, entities: Object.freeze([...config.entities]) }) as OrmBuildConfig<TContext>;
138
+ const buildConfig = Object.freeze({ ...config, context: config.context, entities: Object.freeze([...contextEntities(config)]) }) as OrmBuildConfig<TContext>;
128
139
  const module = defineFeatureOrmModule(buildConfig, buildOrmModuleProviders(buildConfig, DATABASE_PROVIDER));
129
140
  attachOwnedStoreRegistration(module, buildConfig.context, buildConfig.entities ?? [], config.ownedStore);
130
141
  return module;
@@ -148,7 +159,7 @@ export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TC
148
159
  // may replace them with a container-local snapshot.
149
160
  const standaloneOptions = new DbContextOptions({
150
161
  provider: config.provider,
151
- entities: config.entities ?? [],
162
+ entities: contextEntities(config),
152
163
  validateOnSave: config.validateOnSave,
153
164
  executionStrategy: config.executionStrategy,
154
165
  });
@@ -158,7 +169,7 @@ export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TC
158
169
  providers.push(buildOrmHealthCheck(config.provider));
159
170
  }
160
171
  const module = defineFeatureOrmModule(buildConfig, providers);
161
- attachOrmGraphContribution(module, buildConfig.context, buildConfig.entities ?? [], config, registration);
172
+ attachOrmGraphContribution(module, buildConfig.context, contextEntities(buildConfig), config, registration);
162
173
  return module;
163
174
  }
164
175
 
@@ -174,7 +185,7 @@ export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TC
174
185
  const module = defineFeatureOrmModule(buildConfig, providers);
175
186
  // `registration` is deliberately per ormModule call, not a token or public
176
187
  // identity. It links this factory to its container-local graph snapshot.
177
- attachOrmGraphContribution(module, buildConfig.context, buildConfig.entities ?? [], config, registration);
188
+ attachOrmGraphContribution(module, buildConfig.context, contextEntities(buildConfig), config, registration);
178
189
  return module;
179
190
  }
180
191
 
@@ -6,6 +6,7 @@ import {
6
6
  type BazisModuleMetadata,
7
7
  } from "../di";
8
8
  import { OrmModel, type DbContext, type OrmOwnedStoreDefinitionV1 } from "../../library/orm";
9
+ import { contextEntities } from "./contextEntities";
9
10
  import { compileExpectedSchema, type OrmExpectedSchema } from "../../library/orm/Schema/ExpectedSchema";
10
11
  import { isDefinedOrmOwnedStoreV1 } from "../../library/orm/Schema/OrmOwnedStore";
11
12
  import { defineOrmOwnedStoreV1 } from "../../library/orm/Schema/OrmOwnedStore";
@@ -159,7 +160,7 @@ export function ownedStoreRegistrations(snapshot: ModuleOwnedContributionSnapsho
159
160
  // view. Direct ordinary modules intentionally read their caller config at
160
161
  // build time, then both graph preflight and runtime factories use this copy.
161
162
  const source = entry.config as { readonly entities?: readonly EntityClass[]; readonly migrations?: readonly unknown[] } | undefined;
162
- const entities = source === undefined ? entry.entities : Object.freeze([...(source.entities ?? [])]);
163
+ const entities = source === undefined ? entry.entities : Object.freeze([...contextEntities({ context: entry.context, entities: source.entities })]);
163
164
  // Migration instances remain caller-owned; only the sequence membership and
164
165
  // order are part of this container's effective config snapshot.
165
166
  const migrations = source?.migrations;
@@ -1,4 +1,5 @@
1
- import { DI, type DiRegistrar } from "../di";
1
+ import { type DiRegistrar } from "../di";
2
+ import { DI } from "../di/lowLevel";
2
3
  import { Repository, type DbContext, type DbContextOptions } from "../../library/orm";
3
4
  import { repositoryFor } from "./repository";
4
5