bazis 0.136.0 → 0.137.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/main.ts +8 -0
- package/cli/parseCli.ts +9 -0
- package/cli/templates/module.ts +16 -19
- package/cli/templates/passport.ts +1 -1
- package/cli/upgrade/markColumns.ts +151 -0
- package/cli/upgrade/runUpgrade.ts +57 -0
- package/core/access/Access.module.ts +1 -1
- package/core/access/catalog/moduleFields.ts +16 -2
- package/core/access/catalog/permissions.ts +10 -0
- package/core/access/check/Access.ts +5 -5
- package/core/access/check/can.ts +2 -1
- package/core/access/index.ts +11 -1
- package/core/access/roles/roles.ts +4 -2
- package/core/agent/ToolRunner.ts +5 -0
- package/core/agent/session/entities.ts +45 -45
- package/core/http/Decorators/metadata.ts +3 -0
- package/core/http/Decorators/routes.ts +12 -0
- package/core/http/Errors/HttpError.ts +15 -1
- package/core/http/HttpServer.ts +19 -2
- package/core/http/Middleware/errorHandler.ts +16 -1
- package/core/http/OpenApi/openApiDocument.ts +43 -4
- package/core/http/Results/normalize.ts +38 -1
- package/core/http/Results/results.ts +5 -2
- package/core/http/Routing/RouterBuilder.ts +27 -2
- package/core/http/index.ts +3 -0
- package/core/http/middlewareTokens.ts +12 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/infra/connectors/postgres.ts +4 -2
- package/core/infra/index.ts +1 -1
- package/core/kernel/config/defineConfig.ts +55 -19
- package/core/kernel/index.ts +3 -0
- package/core/orm/buildOrmProviders.ts +2 -1
- package/core/orm/contextEntities.ts +36 -0
- package/core/orm/databaseConnector.ts +9 -3
- package/core/orm/databaseHttpError.ts +21 -0
- package/core/orm/index.ts +12 -1
- package/core/orm/ormModule.ts +17 -7
- package/core/orm/ownedStoreContributions.ts +2 -1
- package/core/scripts/di-generate.ts +61 -4
- package/core/tools/StandardTools.module.ts +1 -1
- package/core/tools/contracts/Tools.config.ts +13 -7
- package/core/tools/generated/deps.ts +3 -3
- package/core/tools/http/HttpRequest.tool.ts +1 -2
- package/core/tools/http/publicAddress.ts +2 -2
- package/core/tools/orm/OrmEntity.references.ts +7 -8
- package/core/workflow/contracts/Workflow.config.ts +5 -5
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +2 -2
- package/core/workflow/definitions_module/http/Processes.controller.ts +2 -2
- package/core/workflow/definitions_module/internal/PublishedVersions.ts +39 -0
- package/core/workflow/definitions_module/model/WfProcess.model.ts +24 -11
- package/core/workflow/definitions_module/model/WfVersion.model.ts +7 -1
- package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +4 -1
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +17 -7
- package/core/workflow/generated/deps.ts +2 -1
- package/core/workflow/index.ts +9 -0
- package/core/workflow/internal/WorkflowLanguages.ts +1 -1
- package/core/workflow/internal/http.ts +2 -2
- package/core/workflow/runtime_module/background/WorkflowWorker.ts +6 -2
- package/core/workflow/runtime_module/http/Instances.controller.ts +1 -1
- package/core/workflow/runtime_module/internal/WorkflowJobs.ts +43 -1
- package/core/workflow/runtime_module/model/WfHistory.model.ts +13 -6
- package/core/workflow/runtime_module/model/WfInstance.model.ts +30 -10
- package/core/workflow/runtime_module/model/WfJob.model.ts +22 -10
- package/core/workflow/runtime_module/model/WfSubscription.model.ts +8 -3
- package/core/workflow/runtime_module/model/WfToken.model.ts +16 -7
- package/core/workflow/tasks_module/model/WfTask.model.ts +25 -9
- package/library/openapi/codegen.ts +2 -2
- package/library/orm/DbContext.ts +37 -1
- package/library/orm/Metadata/ModelBuilder.ts +23 -7
- package/library/orm/Metadata/decorators.ts +23 -10
- package/library/orm/Metadata/types.ts +2 -0
- package/library/orm/Transactions/TransactionScopeCoordinator.ts +3 -3
- package/library/orm/errors.ts +39 -1
- package/library/orm/index.ts +1 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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:
|
|
122
|
-
function isEnum(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
|
|
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 (
|
|
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:
|
|
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),
|
package/core/kernel/index.ts
CHANGED
|
@@ -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,5 @@
|
|
|
1
1
|
import { DI, HOSTED_SERVICE, type InjectionToken, type ModuleOwnedContributionSnapshot, type ProviderDefinition, type ProviderDependencyList, type ServiceResolver } from "../di";
|
|
2
|
+
import { contextEntities } from "./contextEntities";
|
|
2
3
|
import { getGeneratedClassDeps } from "../di/module/autoDeps";
|
|
3
4
|
import { resolveDependencyList } from "../di/internal/resolveDependencyList";
|
|
4
5
|
import { ConfigRegistry, HEALTH_CHECK, StartupReport } from "../kernel";
|
|
@@ -27,7 +28,7 @@ function createContextOptions<TContext extends DbContext>(
|
|
|
27
28
|
): DbContextOptions {
|
|
28
29
|
return bindKeys(new DbContextOptions({
|
|
29
30
|
provider,
|
|
30
|
-
entities: config
|
|
31
|
+
entities: contextEntities(config),
|
|
31
32
|
validateOnSave: config.validateOnSave,
|
|
32
33
|
executionStrategy: config.executionStrategy,
|
|
33
34
|
}), 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?:
|
|
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" || !
|
|
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
|
+
}
|
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
|
|
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
|
|
package/core/orm/ormModule.ts
CHANGED
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
type DbContextOptionsConfig,
|
|
8
8
|
type Migration,
|
|
9
9
|
} from "../../library/orm";
|
|
10
|
+
import { HTTP_ERROR_MAPPER } from "../http/middlewareTokens";
|
|
11
|
+
import { databaseHttpError } from "./databaseHttpError";
|
|
12
|
+
import { contextEntities } from "./contextEntities";
|
|
10
13
|
import { DATABASE_PROVIDER } from "./DATABASE_PROVIDER";
|
|
11
14
|
import { IRepository } from "./repository";
|
|
12
15
|
import { registerRepositories } from "./registerRepositories";
|
|
@@ -40,7 +43,12 @@ type ContextClass<TContext extends DbContext> = new (options: DbContextOptions,
|
|
|
40
43
|
export interface OrmModuleConfig<TContext extends DbContext> {
|
|
41
44
|
/** Context class (a DbContext subclass). Omitted in the "connection" mode. */
|
|
42
45
|
readonly context?: ContextClass<TContext>;
|
|
43
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Entities of the context besides those of its sets: by default the context
|
|
48
|
+
* maps what it declares with `this.set(...)`. List here only an entity without
|
|
49
|
+
* a set, or every entity of a context whose constructor cannot run without its
|
|
50
|
+
* dependencies.
|
|
51
|
+
*/
|
|
44
52
|
readonly entities?: readonly EntityClass[];
|
|
45
53
|
/**
|
|
46
54
|
* Database provider value (`postgres(...)`). Without it the context connects
|
|
@@ -86,7 +94,7 @@ function defineFeatureOrmModule<TContext extends DbContext>(
|
|
|
86
94
|
providers,
|
|
87
95
|
exports: withRepositories ? [config.context, IRepository] : [config.context],
|
|
88
96
|
configure: withRepositories
|
|
89
|
-
? (di) => registerRepositories(di, config.context, config
|
|
97
|
+
? (di) => registerRepositories(di, config.context, contextEntities(config))
|
|
90
98
|
: undefined,
|
|
91
99
|
};
|
|
92
100
|
}
|
|
@@ -94,6 +102,8 @@ function defineFeatureOrmModule<TContext extends DbContext>(
|
|
|
94
102
|
function defineConnectionModule(provider: DatabaseProvider, healthCheck: boolean): BazisModuleRef {
|
|
95
103
|
const providers: ProviderDefinition[] = [
|
|
96
104
|
singletonValue(DATABASE_PROVIDER, provider),
|
|
105
|
+
// Database errors answer by their meaning (409, 404, 422, 503) without a setting in the application.
|
|
106
|
+
singletonValue(HTTP_ERROR_MAPPER, databaseHttpError),
|
|
97
107
|
DI.singleton(
|
|
98
108
|
DI.factoryProvider(
|
|
99
109
|
HOSTED_SERVICE,
|
|
@@ -112,7 +122,7 @@ function defineConnectionModule(provider: DatabaseProvider, healthCheck: boolean
|
|
|
112
122
|
/** Single ORM registration entry point (see {@link OrmModuleConfig}). */
|
|
113
123
|
export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TContext>): BazisModuleRef {
|
|
114
124
|
if (config.ownedStore !== undefined) {
|
|
115
|
-
if (!config.context ||
|
|
125
|
+
if (!config.context || contextEntities(config).length === 0) {
|
|
116
126
|
throw new OrmError("ORM owned store requires a context and entities.");
|
|
117
127
|
}
|
|
118
128
|
if (config.provider) {
|
|
@@ -124,7 +134,7 @@ export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TC
|
|
|
124
134
|
// Provider factories outlive this call. Keep their context/entity view
|
|
125
135
|
// registration-local so a caller cannot mutate the descriptor afterwards
|
|
126
136
|
// and silently change the runtime graph behind its owned receipt.
|
|
127
|
-
const buildConfig = Object.freeze({ ...config, context: config.context, entities: Object.freeze([...config
|
|
137
|
+
const buildConfig = Object.freeze({ ...config, context: config.context, entities: Object.freeze([...contextEntities(config)]) }) as OrmBuildConfig<TContext>;
|
|
128
138
|
const module = defineFeatureOrmModule(buildConfig, buildOrmModuleProviders(buildConfig, DATABASE_PROVIDER));
|
|
129
139
|
attachOwnedStoreRegistration(module, buildConfig.context, buildConfig.entities ?? [], config.ownedStore);
|
|
130
140
|
return module;
|
|
@@ -148,7 +158,7 @@ export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TC
|
|
|
148
158
|
// may replace them with a container-local snapshot.
|
|
149
159
|
const standaloneOptions = new DbContextOptions({
|
|
150
160
|
provider: config.provider,
|
|
151
|
-
entities: config
|
|
161
|
+
entities: contextEntities(config),
|
|
152
162
|
validateOnSave: config.validateOnSave,
|
|
153
163
|
executionStrategy: config.executionStrategy,
|
|
154
164
|
});
|
|
@@ -158,7 +168,7 @@ export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TC
|
|
|
158
168
|
providers.push(buildOrmHealthCheck(config.provider));
|
|
159
169
|
}
|
|
160
170
|
const module = defineFeatureOrmModule(buildConfig, providers);
|
|
161
|
-
attachOrmGraphContribution(module, buildConfig.context, buildConfig
|
|
171
|
+
attachOrmGraphContribution(module, buildConfig.context, contextEntities(buildConfig), config, registration);
|
|
162
172
|
return module;
|
|
163
173
|
}
|
|
164
174
|
|
|
@@ -174,7 +184,7 @@ export function ormModule<TContext extends DbContext>(config: OrmModuleConfig<TC
|
|
|
174
184
|
const module = defineFeatureOrmModule(buildConfig, providers);
|
|
175
185
|
// `registration` is deliberately per ormModule call, not a token or public
|
|
176
186
|
// identity. It links this factory to its container-local graph snapshot.
|
|
177
|
-
attachOrmGraphContribution(module, buildConfig.context, buildConfig
|
|
187
|
+
attachOrmGraphContribution(module, buildConfig.context, contextEntities(buildConfig), config, registration);
|
|
178
188
|
return module;
|
|
179
189
|
}
|
|
180
190
|
|
|
@@ -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;
|
|
@@ -189,6 +189,8 @@ interface ConstructorDependencies {
|
|
|
189
189
|
readonly unsupportedParameter?: { readonly index: number; readonly name: string; readonly type: string };
|
|
190
190
|
}
|
|
191
191
|
let classDeps = new Map<ts.ClassDeclaration, readonly CollectedDependency[]>();
|
|
192
|
+
/** `permissions` of the target's modules: the exported constant and its file. */
|
|
193
|
+
let permissionDeclarations = new Map<string, { readonly name: string; readonly file: string }>();
|
|
192
194
|
let inferredDiClasses = new Set<ts.ClassDeclaration>();
|
|
193
195
|
let explicitlyBoundDiClasses = new Set<ts.ClassDeclaration>();
|
|
194
196
|
// Known runtime token names: createToken descriptions + class names usable as class-tokens.
|
|
@@ -505,7 +507,7 @@ function collectDynamicImportDiagnostics(source: ts.SourceFile, target: string,
|
|
|
505
507
|
|
|
506
508
|
function resetTargetAnalysis(files: Set<string>, generatedDir: string): void {
|
|
507
509
|
activeGeneratedDir = generatedDir;
|
|
508
|
-
classDeps = new Map(); inferredDiClasses = new Set(); explicitlyBoundDiClasses = new Set();
|
|
510
|
+
classDeps = new Map(); inferredDiClasses = new Set(); explicitlyBoundDiClasses = new Set(); permissionDeclarations = new Map();
|
|
509
511
|
tokenDescriptions = new Set(); configPrefixesByType = new Map(); knownClassNames = new Set(); warnings = []; fatalErrors = [];
|
|
510
512
|
httpBindings = {}; httpControllerFiles = {}; openApiSchemas = {}; openApiOperations = {};
|
|
511
513
|
grpcBindings = new Map();
|
|
@@ -555,6 +557,8 @@ async function generateTarget(name: string, reachable: Set<string>, production:
|
|
|
555
557
|
stageWrite(`${generatedDir}/httpRequestModels.ts`, requestModels);
|
|
556
558
|
stageWrite(`${generatedDir}/httpListModels.ts`, listModels);
|
|
557
559
|
stageWrite(`${generatedDir}/openapi.ts`, openApi);
|
|
560
|
+
if (permissionDeclarations.size > 0) stageWrite(`${generatedDir}/permissions.ts`, renderPermissions());
|
|
561
|
+
else stageRemove(`${generatedDir}/permissions.ts`);
|
|
558
562
|
// Generated before 0.99.0, when agents were classes; nothing imports it now.
|
|
559
563
|
stageRemove(`${generatedDir}/agentCatalog.ts`);
|
|
560
564
|
stageWrite(`${generatedDir}/runtime.ts`, renderGeneratedRuntime(name));
|
|
@@ -708,6 +712,15 @@ function renderGeneratedRuntime(targetId: string): string {
|
|
|
708
712
|
}
|
|
709
713
|
|
|
710
714
|
/** Sources of this target as codegen saw them; read back by the generated runtime. */
|
|
715
|
+
/** Types only: the keys of the target's permissions for `can`, `access.can`, `access.filter` and `defineRoles`. */
|
|
716
|
+
function renderPermissions(): string {
|
|
717
|
+
const entries = [...permissionDeclarations.values()].sort((left, right) => left.file.localeCompare(right.file) || left.name.localeCompare(right.name));
|
|
718
|
+
const lines = ["// This file is auto-generated by `bazis codegen`.", "// Do not edit manually.", 'import type { PermissionsOf } from "bazis/core/access";'];
|
|
719
|
+
entries.forEach((entry, index) => lines.push(`import type { ${entry.name} as Permissions_${index} } from ${JSON.stringify(toModuleSpecifierFrom(activeGeneratedDir, entry.file))};`));
|
|
720
|
+
lines.push("", 'declare module "bazis/core/access" {', ` interface BazisPermissions extends ${entries.map((_, index) => `PermissionsOf<typeof Permissions_${index}>`).join(", ")} {}`, "}", "");
|
|
721
|
+
return lines.join("\n");
|
|
722
|
+
}
|
|
723
|
+
|
|
711
724
|
function renderSourceFingerprint(): string {
|
|
712
725
|
const files = sourceEntries.map((entry) => entry.filePath).sort();
|
|
713
726
|
const framework = JSON.parse(readFileSync(path.join(FRAMEWORK_DIR, "package.json"), "utf8")).version as string;
|
|
@@ -832,6 +845,8 @@ function collectTokenDescriptions(source: ts.SourceFile): void {
|
|
|
832
845
|
const visit = (node: ts.Node): void => {
|
|
833
846
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.arguments.length > 0) {
|
|
834
847
|
const firstArg = node.arguments[0];
|
|
848
|
+
// defineConfig({ default: … }) without a section: its token is Config:default.
|
|
849
|
+
if (node.expression.text === "defineConfig" && firstArg && !ts.isStringLiteralLike(firstArg)) tokenDescriptions.add("Config:default");
|
|
835
850
|
if (firstArg && ts.isStringLiteralLike(firstArg)) {
|
|
836
851
|
if (node.expression.text === "createToken" || node.expression.text === "createOpenGenericTokenFamily") {
|
|
837
852
|
tokenDescriptions.add(firstArg.text);
|
|
@@ -873,7 +888,9 @@ function resolveConfigViewDeps(): void {
|
|
|
873
888
|
if (prefixes.length !== 1) {
|
|
874
889
|
classDeps.delete(declaration);
|
|
875
890
|
if (needsInferredDiDeps(declaration)) {
|
|
876
|
-
fatalErrors.push(
|
|
891
|
+
fatalErrors.push(typeName.startsWith("typeof ")
|
|
892
|
+
? `BAZIS_DI_CONFIG_UNKNOWN: ${sourceLocation(declaration)}: constructor parameter ${index + 1} of "${declaration.name?.text}" has type "ConfigView<${typeName}>", but ${typeName.slice(7)} is not a declaration whose section codegen can read. Declare it as defineConfig("name", { default: {...} }) with a literal name and without a type argument.`
|
|
893
|
+
: prefixes.length === 0
|
|
877
894
|
? `BAZIS_DI_CONFIG_UNKNOWN: ${sourceLocation(declaration)}: constructor parameter ${index + 1} of "${declaration.name?.text}" has type "ConfigView<${typeName}>", but no defineConfig<${typeName}>(...) declaration was found. Declare the configuration with an explicit type argument, for example defineConfig<${typeName}>("name", { default: {...} }).`
|
|
878
895
|
: `BAZIS_DI_CONFIG_AMBIGUOUS: ${sourceLocation(declaration)}: constructor parameter ${index + 1} of "${declaration.name?.text}" has type "ConfigView<${typeName}>", which matches several declarations: ${prefixes.map((prefix) => `"${prefix}"`).join(", ")}. Give each declaration its own type.`);
|
|
879
896
|
}
|
|
@@ -987,6 +1004,9 @@ function collectDiClassRegistrations(source: ts.SourceFile): void {
|
|
|
987
1004
|
const classes = properties.get(name);
|
|
988
1005
|
if (classes && ts.isArrayLiteralExpression(classes)) for (const entry of classes.elements) record(entry);
|
|
989
1006
|
}
|
|
1007
|
+
// `references: { "orders.order": OrderReferences }`: the field registers the classes.
|
|
1008
|
+
const references = objectFor(properties.get("references"));
|
|
1009
|
+
if (references) for (const entry of propertiesOf(references).values()) record(entry);
|
|
990
1010
|
const imports = properties.get("imports");
|
|
991
1011
|
if (imports && ts.isArrayLiteralExpression(imports)) for (const entry of imports.elements) recordModule(entry);
|
|
992
1012
|
};
|
|
@@ -1093,12 +1113,33 @@ function collectProviderFormErrors(source: ts.SourceFile): void {
|
|
|
1093
1113
|
const target = call && callee(call);
|
|
1094
1114
|
return target?.name === "ormEntityTools" && target.file?.endsWith("/core/tools/orm/ormEntityTools.ts") === true;
|
|
1095
1115
|
};
|
|
1116
|
+
/**
|
|
1117
|
+
* `permissions: orderPermissions`: the exported constant goes to the generated
|
|
1118
|
+
* `permissions.ts`, which makes its keys the type of `can` and `defineRoles`.
|
|
1119
|
+
* A framework pack names its own in its sources.
|
|
1120
|
+
*/
|
|
1121
|
+
const collectPermissions = (module: string, initializer: ts.Expression): void => {
|
|
1122
|
+
let symbol = ts.isIdentifier(initializer) ? checker.getSymbolAtLocation(initializer) : undefined;
|
|
1123
|
+
if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) symbol = checker.getAliasedSymbol(symbol);
|
|
1124
|
+
const declaration = symbol?.valueDeclaration;
|
|
1125
|
+
const file = normalizeProjectPath((declaration ?? initializer).getSourceFile().fileName);
|
|
1126
|
+
// A framework pack names its permissions in its own sources.
|
|
1127
|
+
if (file.includes("/node_modules/") || isFrameworkPackPath(file)) return;
|
|
1128
|
+
const fileSymbol = declaration && checker.getSymbolAtLocation(declaration.getSourceFile());
|
|
1129
|
+
const exported = fileSymbol && symbol && checker.getExportsOfModule(fileSymbol).find((entry) => (entry.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(entry) : entry) === symbol);
|
|
1130
|
+
if (!declaration || !ts.isVariableDeclaration(declaration) || exported === undefined) {
|
|
1131
|
+
fatalErrors.push(`BAZIS_ACCESS_PERMISSIONS_FORM: ${sourceLocation(initializer)}: permissions of ${module}: "${text(initializer)}" — export the result of definePermissions(...) as a constant and name it here, so that its keys become the type of can(...) and defineRoles(...).`);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
permissionDeclarations.set(`${file}\0${exported.name}`, { name: exported.name, file });
|
|
1135
|
+
};
|
|
1096
1136
|
const checkModule = (module: string, argument: ts.Expression | undefined): void => {
|
|
1097
1137
|
const object = argument && resolved(argument, ts.isObjectLiteralExpression);
|
|
1098
1138
|
if (!object) return;
|
|
1099
1139
|
for (const property of object.properties) {
|
|
1100
1140
|
if (!ts.isPropertyAssignment(property)) continue;
|
|
1101
1141
|
const field = property.name.getText().replace(/["']/g, "");
|
|
1142
|
+
if (field === "permissions") collectPermissions(module, property.initializer);
|
|
1102
1143
|
if (field !== "providers" && !CLASS_LISTS.includes(field)) continue;
|
|
1103
1144
|
const list = resolved(property.initializer, ts.isArrayLiteralExpression);
|
|
1104
1145
|
if (!list) {
|
|
@@ -1182,6 +1223,13 @@ function readInheritedConstructorDeps(node: ts.ClassDeclaration): ConstructorDep
|
|
|
1182
1223
|
return { deps };
|
|
1183
1224
|
}
|
|
1184
1225
|
|
|
1226
|
+
/** The section of a `defineConfig("mail", …)` declaration by its type: the literal of its `prefix`. */
|
|
1227
|
+
function configPrefixOf(type: ts.Type): string | undefined {
|
|
1228
|
+
const prefix = type.getProperty("prefix");
|
|
1229
|
+
const literal = prefix && checker.getTypeOfSymbol(prefix);
|
|
1230
|
+
return literal?.isStringLiteral() ? literal.value : undefined;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1185
1233
|
function dependencyFromType(type: ts.Type): CollectedDependency | undefined {
|
|
1186
1234
|
if (type.flags & ts.TypeFlags.TypeParameter) return undefined;
|
|
1187
1235
|
const symbol = type.getSymbol();
|
|
@@ -1191,8 +1239,11 @@ function dependencyFromType(type: ts.Type): CollectedDependency | undefined {
|
|
|
1191
1239
|
const name = declaration?.name?.text ?? type.aliasSymbol?.name ?? symbol?.name;
|
|
1192
1240
|
if (!name || BUILTIN_DEPENDENCY_NAMES.has(name)) return undefined;
|
|
1193
1241
|
const arguments_ = type.flags & ts.TypeFlags.Object ? checker.getTypeArguments(type as ts.TypeReference) : [];
|
|
1194
|
-
if (name === "ConfigView") {
|
|
1195
|
-
|
|
1242
|
+
if (name === "ConfigView" || name === "ResolvedConfigView") {
|
|
1243
|
+
// ConfigView<typeof mailConfig>: the declaration carries the name of its section in its type.
|
|
1244
|
+
const declared = type.aliasTypeArguments?.[0] && configPrefixOf(type.aliasTypeArguments[0]);
|
|
1245
|
+
if (declared !== undefined) return { name: `Config:${declared}` };
|
|
1246
|
+
const configType = type.aliasTypeArguments?.[0] ?? arguments_[0];
|
|
1196
1247
|
const configTypeName = configType && (configType.aliasSymbol?.name ?? configType.getSymbol()?.name);
|
|
1197
1248
|
return configTypeName ? { name: `ConfigView<${configTypeName}>` } : undefined;
|
|
1198
1249
|
}
|
|
@@ -1244,6 +1295,12 @@ function readConstructorTypeNames(node: ts.ClassDeclaration): ConstructorDepende
|
|
|
1244
1295
|
|
|
1245
1296
|
if (refName === "ConfigView" && paramType && ts.isTypeReferenceNode(paramType)) {
|
|
1246
1297
|
const configType = paramType.typeArguments?.[0];
|
|
1298
|
+
if (configType && ts.isTypeQueryNode(configType)) {
|
|
1299
|
+
// ConfigView<typeof mailConfig>: the declaration carries the name of its section in its type.
|
|
1300
|
+
const declared = configPrefixOf(checker.getTypeFromTypeNode(configType));
|
|
1301
|
+
deps.push({ name: declared === undefined ? `ConfigView<${configType.getText()}>` : `Config:${declared}`, ...optionalFlag });
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1247
1304
|
const configTypeName = configType ? getDependencyTypeName(configType) : undefined;
|
|
1248
1305
|
if (configTypeName) {
|
|
1249
1306
|
deps.push({ name: `ConfigView<${configTypeName}>`, ...optionalFlag });
|
|
@@ -16,7 +16,7 @@ import { ConfigSecretStore, SecretStore } from "./secrets/SecretStore";
|
|
|
16
16
|
config: [toolsConfig],
|
|
17
17
|
references: { "orm.entity": OrmEntityReferences, "orm.field": OrmFieldReferences },
|
|
18
18
|
tools: [HttpRequestTool],
|
|
19
|
-
providers: [singleton(
|
|
19
|
+
providers: [singleton(SecretStore, ConfigSecretStore)],
|
|
20
20
|
exports: [SecretStore],
|
|
21
21
|
})
|
|
22
22
|
export class StandardToolsModule {}
|
|
@@ -1,29 +1,35 @@
|
|
|
1
|
-
import { defineConfig } from "../../kernel";
|
|
1
|
+
import { configList, defineConfig } from "../../kernel";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Settings of the standard Tools. `http.allow` is the
|
|
5
|
-
*
|
|
4
|
+
* Settings of the standard Tools. `http.allow` is the list of hosts `http.request`
|
|
5
|
+
* may reach, comma-separated in the environment: `api.example.com`, `*.example.com`,
|
|
6
|
+
* `localhost:8080`.
|
|
6
7
|
* Empty by default: requests go nowhere. `http.allowPrivateNetworks` lets it reach
|
|
7
8
|
* private and loopback addresses of a listed host: intranet services, tests.
|
|
8
9
|
* `http.callers` says who may call it: `workflow`, `agent`.
|
|
9
10
|
*/
|
|
10
11
|
export interface ToolsSettings {
|
|
11
|
-
readonly "http.allow": string;
|
|
12
|
-
readonly "http.callers": string;
|
|
12
|
+
readonly "http.allow": readonly string[];
|
|
13
|
+
readonly "http.callers": readonly string[];
|
|
13
14
|
readonly "http.allowPrivateNetworks": boolean;
|
|
14
15
|
readonly "http.maxResponseBytes": number;
|
|
15
16
|
readonly "http.timeoutMs": number;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/** A host, a subdomain pattern or an IPv6 address in brackets, with an optional port. */
|
|
20
|
+
const HOST = /^(\*\.)?[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?(:\d{1,5})?$|^\[[0-9A-Fa-f:.]+\](:\d{1,5})?$/;
|
|
21
|
+
|
|
18
22
|
export const toolsConfig = defineConfig<ToolsSettings>("tools", {
|
|
19
23
|
default: {
|
|
20
|
-
"http.allow":
|
|
21
|
-
"http.callers": "workflow",
|
|
24
|
+
"http.allow": configList(),
|
|
25
|
+
"http.callers": configList(["workflow"]),
|
|
22
26
|
"http.allowPrivateNetworks": false,
|
|
23
27
|
"http.maxResponseBytes": 1_048_576,
|
|
24
28
|
"http.timeoutMs": 30_000,
|
|
25
29
|
},
|
|
26
30
|
validate: {
|
|
31
|
+
"http.allow": (value) => value.find((item) => !HOST.test(item)) === undefined ? undefined : `must list hosts like api.example.com, *.example.com or localhost:8080; ${JSON.stringify(value.find((item) => !HOST.test(item)))} is not one`,
|
|
32
|
+
"http.callers": (value) => (value.every((item) => /^[a-z][a-z0-9_.-]{0,63}$/.test(item)) ? undefined : "must list caller ids like workflow, agent"),
|
|
27
33
|
"http.maxResponseBytes": (value) => (Number.isSafeInteger(value) && value >= 1 && value <= 104_857_600 ? undefined : "must be an integer 1–104857600"),
|
|
28
34
|
"http.timeoutMs": (value) => (Number.isSafeInteger(value) && value >= 1 && value <= 600_000 ? undefined : "must be an integer 1–600000"),
|
|
29
35
|
},
|