bazis 0.112.0 → 0.114.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/di/errors/configurationError.ts +21 -0
- package/core/di/errors/index.ts +1 -0
- package/core/di/index.ts +3 -0
- package/core/di/module/index.ts +1 -0
- package/core/di/module/moduleExtensions.ts +24 -1
- package/core/di/module/moduleOwnedProviderContributors.ts +13 -3
- package/core/grpc/decorators.ts +31 -10
- package/core/infra/Infra.ts +27 -3
- package/core/infra/connectorIdentity.ts +25 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/infra/connectors/llmRouter.ts +11 -4
- package/core/kernel/Bazis.ts +3 -3
- package/core/kernel/config/defineConfig.ts +31 -6
- package/core/kernel/report/startupProblems.ts +2 -2
- package/library/orm/Query/EntityQuery.ts +1 -1
- package/library/orm/Query/IncludeLoader.ts +12 -3
- package/package.json +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DiError } from "./DiError";
|
|
2
|
+
|
|
3
|
+
const CONFIGURATION_ERROR = Symbol.for("bazis.configurationError");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Marks an error found in the application's declarations — a module, a
|
|
7
|
+
* connector, a config — as a configuration error: the start prints it as one
|
|
8
|
+
* `[bazis] configuration error` line instead of a stack. The error keeps its
|
|
9
|
+
* class; the same error is returned.
|
|
10
|
+
*/
|
|
11
|
+
export function markConfigurationError<T>(error: T): T {
|
|
12
|
+
if (error instanceof Error && !Object.prototype.hasOwnProperty.call(error, CONFIGURATION_ERROR)) {
|
|
13
|
+
Object.defineProperty(error, CONFIGURATION_ERROR, { value: true });
|
|
14
|
+
}
|
|
15
|
+
return error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A DI error, or an error marked with {@link markConfigurationError}. */
|
|
19
|
+
export function isConfigurationError(error: unknown): error is Error {
|
|
20
|
+
return error instanceof DiError || (error instanceof Error && (error as unknown as Record<symbol, unknown>)[CONFIGURATION_ERROR] === true);
|
|
21
|
+
}
|
package/core/di/errors/index.ts
CHANGED
|
@@ -12,3 +12,4 @@ export { ProviderNotFoundError } from "./ProviderNotFoundError";
|
|
|
12
12
|
export { ScopeDisposedError } from "./ScopeDisposedError";
|
|
13
13
|
export { ScopedServiceFromRootError } from "./ScopedServiceFromRootError";
|
|
14
14
|
export { ServiceValidationError } from "./ServiceValidationError";
|
|
15
|
+
export { isConfigurationError, markConfigurationError } from "./configurationError";
|
package/core/di/index.ts
CHANGED
|
@@ -10,6 +10,8 @@ export {
|
|
|
10
10
|
CircularDependencyError,
|
|
11
11
|
ClassDependenciesMismatchError,
|
|
12
12
|
DiError,
|
|
13
|
+
isConfigurationError,
|
|
14
|
+
markConfigurationError,
|
|
13
15
|
InvalidProviderError,
|
|
14
16
|
ModuleEncapsulationError,
|
|
15
17
|
ModuleOwnedProviderConflictError,
|
|
@@ -35,6 +37,7 @@ export {
|
|
|
35
37
|
registerModuleOwnedContributionValidator,
|
|
36
38
|
registerModuleOwnedProviderContributor,
|
|
37
39
|
expandModuleMetadata,
|
|
40
|
+
deferModuleDeclarationError,
|
|
38
41
|
createModuleOwnedMetadataChannel,
|
|
39
42
|
createModuleOwnedProviderChannel,
|
|
40
43
|
scoped,
|
package/core/di/module/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { markConfigurationError } from "../errors/configurationError";
|
|
1
2
|
import type { BazisModuleMetadata, BazisModuleRef } from "./types";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -21,15 +22,37 @@ export function registerModuleMetadataExpander(expander: ModuleMetadataExpander)
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
const declarationErrors = new WeakMap<object, unknown>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Keeps a problem found while a module was declared (a bad `@Infra` manifest,
|
|
29
|
+
* …) instead of throwing it at import time. The application reports it as a
|
|
30
|
+
* configuration error when it builds this module.
|
|
31
|
+
*/
|
|
32
|
+
export function deferModuleDeclarationError(moduleRef: object, error: unknown): void {
|
|
33
|
+
if (!declarationErrors.has(moduleRef)) {
|
|
34
|
+
declarationErrors.set(moduleRef, markConfigurationError(error));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
/** Collects extra module refs contributed by registered expanders for a module. */
|
|
25
39
|
export function expandModuleMetadata(metadata: BazisModuleMetadata): readonly BazisModuleRef[] {
|
|
40
|
+
if (declarationErrors.has(metadata)) {
|
|
41
|
+
throw declarationErrors.get(metadata);
|
|
42
|
+
}
|
|
26
43
|
if (expanders.length === 0) {
|
|
27
44
|
return [];
|
|
28
45
|
}
|
|
29
46
|
const extra: BazisModuleRef[] = [];
|
|
30
47
|
for (let index = 0; index < expanders.length; index += 1) {
|
|
31
48
|
const expander = expanders[index] as ModuleMetadataExpander;
|
|
32
|
-
|
|
49
|
+
try {
|
|
50
|
+
extra.push(...expander(metadata));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// A bad declarative key (`ormBazis` options, …) is a configuration
|
|
53
|
+
// problem of the module, not an application crash.
|
|
54
|
+
throw markConfigurationError(error);
|
|
55
|
+
}
|
|
33
56
|
}
|
|
34
57
|
return extra;
|
|
35
58
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ModuleOwnedProviderConflictError } from "../errors";
|
|
1
|
+
import { markConfigurationError, ModuleOwnedProviderConflictError } from "../errors";
|
|
2
2
|
import type { ServiceCollection } from "../ServiceCollection";
|
|
3
3
|
import {
|
|
4
4
|
isClassProvider,
|
|
@@ -380,7 +380,11 @@ export function applyModuleOwnedProviderContributors(
|
|
|
380
380
|
};
|
|
381
381
|
|
|
382
382
|
for (let index = 0; index < contributors.length; index += 1) {
|
|
383
|
-
|
|
383
|
+
try {
|
|
384
|
+
(contributors[index] as ModuleOwnedProviderContributor)(metadata, context);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
throw markConfigurationError(error);
|
|
387
|
+
}
|
|
384
388
|
}
|
|
385
389
|
}
|
|
386
390
|
|
|
@@ -404,6 +408,12 @@ export function validateModuleOwnedContributions(
|
|
|
404
408
|
): readonly ModuleOwnedMetadataContribution<TPayload>[] => store.getMetadata(channel),
|
|
405
409
|
});
|
|
406
410
|
for (let index = 0; index < validators.length; index += 1) {
|
|
407
|
-
|
|
411
|
+
try {
|
|
412
|
+
(validators[index] as ModuleOwnedContributionValidator)(snapshot);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
// Conflicting owned stores or connector tokens across modules are
|
|
415
|
+
// declaration problems: report them as configuration errors.
|
|
416
|
+
throw markConfigurationError(error);
|
|
417
|
+
}
|
|
408
418
|
}
|
|
409
419
|
}
|
package/core/grpc/decorators.ts
CHANGED
|
@@ -11,6 +11,8 @@ interface ControllerMetadata {
|
|
|
11
11
|
service?: ServiceDefinition;
|
|
12
12
|
methods: Map<string | symbol, string>;
|
|
13
13
|
requestModels: Map<string | symbol, RequestModelClass>;
|
|
14
|
+
/** Declaration problems, reported by grpcControllerDefinition at start. */
|
|
15
|
+
problems: string[];
|
|
14
16
|
}
|
|
15
17
|
interface Carrier { [GRPC_META]?: ControllerMetadata }
|
|
16
18
|
|
|
@@ -18,7 +20,12 @@ function ownMetadata(metadata: object): ControllerMetadata {
|
|
|
18
20
|
const carrier = metadata as Carrier;
|
|
19
21
|
if (!Object.hasOwn(carrier, GRPC_META)) {
|
|
20
22
|
const parent = carrier[GRPC_META];
|
|
21
|
-
carrier[GRPC_META] = {
|
|
23
|
+
carrier[GRPC_META] = {
|
|
24
|
+
service: parent?.service,
|
|
25
|
+
methods: new Map(parent?.methods),
|
|
26
|
+
requestModels: new Map(parent?.requestModels),
|
|
27
|
+
problems: [...(parent?.problems ?? [])],
|
|
28
|
+
};
|
|
22
29
|
}
|
|
23
30
|
return carrier[GRPC_META]!;
|
|
24
31
|
}
|
|
@@ -26,32 +33,44 @@ function ownMetadata(metadata: object): ControllerMetadata {
|
|
|
26
33
|
/** Bind a controller to a generated codec definition or grpcService(). */
|
|
27
34
|
export function GrpcController(service: ServiceDefinition) {
|
|
28
35
|
return (_value: Class<object>, context: ClassDecoratorContext): void => {
|
|
36
|
+
const meta = ownMetadata(context.metadata);
|
|
29
37
|
if (!service || typeof service !== "object" || Object.keys(service).length === 0) {
|
|
30
|
-
|
|
38
|
+
meta.problems.push("@GrpcController requires a non-empty gRPC service definition.");
|
|
39
|
+
return;
|
|
31
40
|
}
|
|
32
41
|
const snapshot: Record<string, ServiceDefinition[string]> = Object.create(null);
|
|
33
42
|
for (const [name, method] of Object.entries(service)) {
|
|
34
43
|
if (!method || typeof method.path !== "string" || !/^\/[^/]+\/[^/]+$/.test(method.path)
|
|
35
44
|
|| typeof method.requestStream !== "boolean" || typeof method.responseStream !== "boolean"
|
|
36
45
|
|| typeof method.requestDeserialize !== "function" || typeof method.responseSerialize !== "function") {
|
|
37
|
-
|
|
46
|
+
meta.problems.push(`invalid gRPC service method: ${name}.`);
|
|
47
|
+
return;
|
|
38
48
|
}
|
|
39
49
|
snapshot[name] = Object.freeze({ ...method });
|
|
40
50
|
}
|
|
41
|
-
|
|
51
|
+
meta.service = Object.freeze(snapshot);
|
|
42
52
|
};
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
/** Request/response values for unary; AsyncIterable values for streaming sides. */
|
|
46
56
|
export function GrpcMethod(rpcName?: string, requestModel?: RequestModelClass) {
|
|
47
|
-
if (requestModel !== undefined && (typeof requestModel !== "function" || !requestModel.prototype)) {
|
|
48
|
-
throw new TypeError("gRPC requestModel must be a DTO class.");
|
|
49
|
-
}
|
|
50
57
|
return (_value: unknown, context: ClassMethodDecoratorContext): void => {
|
|
51
|
-
if (context.static || context.private) throw new TypeError("@GrpcMethod requires a public instance method.");
|
|
52
|
-
const name = rpcName ?? (typeof context.name === "string" ? context.name : "");
|
|
53
|
-
if (typeof name !== "string" || name.trim().length === 0) throw new TypeError("gRPC method name must not be empty.");
|
|
54
58
|
const meta = ownMetadata(context.metadata);
|
|
59
|
+
// Problems are reported when the controller is registered, not at import.
|
|
60
|
+
const method = String(context.name);
|
|
61
|
+
if (requestModel !== undefined && (typeof requestModel !== "function" || !requestModel.prototype)) {
|
|
62
|
+
meta.problems.push(`${method}: gRPC requestModel must be a DTO class.`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (context.static || context.private) {
|
|
66
|
+
meta.problems.push(`${method}: @GrpcMethod requires a public instance method.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const name = rpcName ?? (typeof context.name === "string" ? context.name : "");
|
|
70
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
71
|
+
meta.problems.push(`${method}: gRPC method name must not be empty.`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
55
74
|
meta.methods.set(context.name, name);
|
|
56
75
|
// A redecorated override must not silently retain the base method's DTO.
|
|
57
76
|
if (requestModel === undefined) meta.requestModels.delete(context.name);
|
|
@@ -63,6 +82,8 @@ export function GrpcMethod(rpcName?: string, requestModel?: RequestModelClass) {
|
|
|
63
82
|
export function grpcControllerDefinition(controller: Class<object>): GrpcControllerDefinition {
|
|
64
83
|
const carrier = (controller as unknown as { [Symbol.metadata]?: Carrier })[Symbol.metadata];
|
|
65
84
|
const meta = carrier?.[GRPC_META];
|
|
85
|
+
const problem = meta?.problems[0];
|
|
86
|
+
if (problem !== undefined) throw new GrpcSetupError(`${controller.name}: ${problem}`);
|
|
66
87
|
if (!meta?.service) throw new GrpcSetupError(`${controller.name} must use @GrpcController().`);
|
|
67
88
|
const methods = new Map<string, string | symbol>();
|
|
68
89
|
for (const [methodName, rpcName] of meta.methods) {
|
package/core/infra/Infra.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Module,
|
|
5
5
|
createToken,
|
|
6
6
|
createModuleOwnedMetadataChannel,
|
|
7
|
+
deferModuleDeclarationError,
|
|
7
8
|
registerModuleOwnedProviderContributor,
|
|
8
9
|
registerModuleOwnedContributionValidator,
|
|
9
10
|
type ModuleConfig,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
import { ConfigRegistry, HEALTH_CHECK, StartupReport } from "../kernel";
|
|
15
16
|
import { InfraError, type InfraConnector } from "./InfraConnector";
|
|
16
17
|
import { InfraLifecycle } from "./InfraLifecycle";
|
|
18
|
+
import { connectorDeclarationError } from "./connectorIdentity";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Infrastructure manifest: a readable "logical name → connector" map.
|
|
@@ -57,6 +59,8 @@ function buildInfraDefinitions(manifest: InfraManifest): {
|
|
|
57
59
|
const seenTokens = new Map<InfraConnector["token"], string>();
|
|
58
60
|
|
|
59
61
|
for (const [name, connector] of Object.entries(manifest)) {
|
|
62
|
+
const invalid = connectorDeclarationError(connector);
|
|
63
|
+
if (invalid !== undefined) throw invalid.error;
|
|
60
64
|
if (!name.trim() || name !== name.trim()) throw new InfraError("Infra instance names must be non-empty and trimmed.");
|
|
61
65
|
if (connector.phase !== undefined && !Number.isSafeInteger(connector.phase)) {
|
|
62
66
|
throw new InfraError(`Infra instance "${name}": phase must be a finite safe integer.`);
|
|
@@ -135,8 +139,15 @@ function buildInfraDefinitions(manifest: InfraManifest): {
|
|
|
135
139
|
* `@Infra`, but without a class. Handy for `imports` or `runApp({ infra })`.
|
|
136
140
|
*/
|
|
137
141
|
export function infraModule(manifest: InfraManifest): BazisModuleRef {
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
try {
|
|
143
|
+
const { providers, exports, config } = buildInfraDefinitions(manifest);
|
|
144
|
+
return { global: true, providers, exports, config };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// Reported when the application is built, not while the file is imported.
|
|
147
|
+
const moduleRef: BazisModuleRef = { global: true };
|
|
148
|
+
deferModuleDeclarationError(moduleRef, error);
|
|
149
|
+
return moduleRef;
|
|
150
|
+
}
|
|
140
151
|
}
|
|
141
152
|
|
|
142
153
|
/**
|
|
@@ -157,8 +168,21 @@ export function infraModule(manifest: InfraManifest): BazisModuleRef {
|
|
|
157
168
|
* ```
|
|
158
169
|
*/
|
|
159
170
|
export function Infra(manifest: InfraManifest) {
|
|
160
|
-
|
|
171
|
+
let definitions: ReturnType<typeof buildInfraDefinitions> | undefined;
|
|
172
|
+
let declarationError: unknown;
|
|
173
|
+
try {
|
|
174
|
+
definitions = buildInfraDefinitions(manifest);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
declarationError = error;
|
|
177
|
+
}
|
|
161
178
|
return (target: abstract new (...args: never) => unknown, _context: ClassDecoratorContext): void => {
|
|
179
|
+
if (definitions === undefined) {
|
|
180
|
+
// Reported when the application is built, not while the file is imported.
|
|
181
|
+
Module({ global: true })(target, _context);
|
|
182
|
+
deferModuleDeclarationError(target, declarationError);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const { providers, exports, config } = definitions;
|
|
162
186
|
Module({ global: true, providers, exports, config })(target, _context);
|
|
163
187
|
};
|
|
164
188
|
}
|
|
@@ -9,6 +9,31 @@ const lifecycles = new WeakMap<object, LifecycleBinding>();
|
|
|
9
9
|
const values = (c: InfraConnector) => [c.token, c.config, c.phase, c.create, c.connect, c.dispose, c.healthCheck, c.providers, c.exports] as const;
|
|
10
10
|
const unchanged = (c: InfraConnector, identity: ConnectorIdentity) => values(c).every((value, index) => Object.is(value, identity.fingerprint[index]));
|
|
11
11
|
|
|
12
|
+
const declarationErrors = new WeakMap<object, unknown>();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @internal A connector factory whose options are invalid returns a connector
|
|
16
|
+
* that carries the problem: `@Infra` reports it when the application is built
|
|
17
|
+
* instead of crashing the import of the manifest file.
|
|
18
|
+
*/
|
|
19
|
+
export function invalidConnector<T>(token: InfraConnector<T>["token"], error: unknown): InfraConnector<T> {
|
|
20
|
+
const connector: InfraConnector<T> = {
|
|
21
|
+
token,
|
|
22
|
+
create() {
|
|
23
|
+
throw error;
|
|
24
|
+
},
|
|
25
|
+
connect() {},
|
|
26
|
+
dispose() {},
|
|
27
|
+
};
|
|
28
|
+
declarationErrors.set(connector, error);
|
|
29
|
+
return connector;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @internal The declaration problem of a connector made by `invalidConnector`. */
|
|
33
|
+
export function connectorDeclarationError(connector: InfraConnector): { readonly error: unknown } | undefined {
|
|
34
|
+
return declarationErrors.has(connector) ? { error: declarationErrors.get(connector) } : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
12
37
|
/** @internal A factory identifies its original connector; copies inherit no identity. */
|
|
13
38
|
export function identifyConnector<T>(connector: InfraConnector<T>, role: string): InfraConnector<T> {
|
|
14
39
|
identities.set(connector, Object.freeze({ role, fingerprint: values(connector) }));
|
|
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
|
|
|
232
232
|
if (method === "account/updated") this.modelCache = undefined;
|
|
233
233
|
}, () => { this.clearLogin(); this.modelCache = undefined; });
|
|
234
234
|
const initialized = await client.request("initialize", {
|
|
235
|
-
clientInfo: { name: "bazis", title: "Bazis", version: "0.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.114.0" }, capabilities: { experimentalApi: true },
|
|
236
236
|
}, 15_000, signal);
|
|
237
237
|
if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
|
|
238
238
|
client.notify("initialized");
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type AgentModelResponse,
|
|
13
13
|
} from "../../agent";
|
|
14
14
|
import { InfraError, type InfraConnector } from "../InfraConnector";
|
|
15
|
-
import { identifyConnector } from "../connectorIdentity";
|
|
15
|
+
import { identifyConnector, invalidConnector } from "../connectorIdentity";
|
|
16
16
|
import { redactSensitiveText } from "../../../library/redaction";
|
|
17
17
|
import { awaitAbortable } from "../../kernel/internal/awaitAbortable";
|
|
18
18
|
import {
|
|
@@ -422,9 +422,16 @@ export function llmRouter(
|
|
|
422
422
|
profiles: Readonly<Record<string, LlmModelProfile>>,
|
|
423
423
|
options: LlmRouterOptions = {},
|
|
424
424
|
): InfraConnector<AgentModelProvider> {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
425
|
+
let entries: ReturnType<typeof profileEntries>;
|
|
426
|
+
let defaultProfile: string;
|
|
427
|
+
try {
|
|
428
|
+
entries = profileEntries(profiles);
|
|
429
|
+
defaultProfile = defaultProfileName(entries, options);
|
|
430
|
+
ensureFallbacksValid(entries);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
// Reported by `@Infra` when the application is built, not at import.
|
|
433
|
+
return invalidConnector(AGENT_MODEL_PROVIDER, error);
|
|
434
|
+
}
|
|
428
435
|
const configs = Object.freeze(Array.from(new Set(entries.map(([, profile]) => profile.config))));
|
|
429
436
|
|
|
430
437
|
// The framework LLM connector, like llmConnect: it never touches the schema,
|
package/core/kernel/Bazis.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isConfigurationError } from "../di";
|
|
2
2
|
import { Kernel } from "./Kernel";
|
|
3
3
|
import { KernelBuilder, type RootModuleInput } from "./KernelBuilder";
|
|
4
4
|
import { KernelError, ShutdownTimeoutError } from "./errors";
|
|
@@ -76,7 +76,7 @@ export class Bazis {
|
|
|
76
76
|
// deserve the same friendly output as build-time ones.
|
|
77
77
|
if (screen !== undefined) {
|
|
78
78
|
screen.failed(error, kernel);
|
|
79
|
-
} else if (error instanceof
|
|
79
|
+
} else if (error instanceof KernelError || isConfigurationError(error)) {
|
|
80
80
|
console.error(`[bazis] configuration error: ${redactSensitiveText(error.message)}`);
|
|
81
81
|
} else {
|
|
82
82
|
console.error(`[bazis] application failed: ${formatStartupFailure(error)}`);
|
|
@@ -103,7 +103,7 @@ export function reportBuildFailure(
|
|
|
103
103
|
process.exitCode = 1;
|
|
104
104
|
return 1;
|
|
105
105
|
}
|
|
106
|
-
if (error instanceof
|
|
106
|
+
if (error instanceof KernelError || isConfigurationError(error)) {
|
|
107
107
|
console.error(`[bazis] configuration error: ${redactSensitiveText(error.message)}`);
|
|
108
108
|
process.exitCode = 1;
|
|
109
109
|
return 1;
|
|
@@ -30,11 +30,14 @@ export interface ConfigEnum<T extends string | number = string | number> {
|
|
|
30
30
|
readonly default: T;
|
|
31
31
|
}
|
|
32
32
|
export function configEnum<const V extends readonly [string | number, ...(string | number)[]]>(values: V, defaultValue: V[number]): ConfigEnum<V[number]> {
|
|
33
|
+
const declared: ConfigEnum<V[number]> = Object.freeze({ __enum: true, values: Object.freeze([...values]), default: defaultValue });
|
|
33
34
|
if (!values.length || !values.includes(defaultValue) || values.some(value => typeof value !== typeof defaultValue || (typeof value === "number" && !Number.isFinite(value)))) {
|
|
34
|
-
|
|
35
|
+
// defineConfig reports it at start, with the other configuration errors.
|
|
36
|
+
invalidEnums.set(declared, new KernelError("configEnum requires non-empty homogeneous finite values and a declared default."));
|
|
35
37
|
}
|
|
36
|
-
return
|
|
38
|
+
return declared;
|
|
37
39
|
}
|
|
40
|
+
const invalidEnums = new WeakMap<object, KernelError>();
|
|
38
41
|
type Cell = Primitive | SecretSpec | ConfigEnum;
|
|
39
42
|
type Defaults = Record<string, Cell>;
|
|
40
43
|
export type ConfigValueType = Primitive | Secret;
|
|
@@ -119,7 +122,11 @@ function isSecret(cell: Cell): cell is SecretSpec { return typeof cell === "obje
|
|
|
119
122
|
function isEnum(cell: Cell): cell is ConfigEnum { return typeof cell === "object" && cell !== null && "__enum" in cell; }
|
|
120
123
|
function copyCell(cell: Cell): Cell {
|
|
121
124
|
if (isSecret(cell)) return secret(cell.default);
|
|
122
|
-
if (isEnum(cell))
|
|
125
|
+
if (isEnum(cell)) {
|
|
126
|
+
const problem = invalidEnums.get(cell);
|
|
127
|
+
if (problem !== undefined) throw problem;
|
|
128
|
+
return Object.freeze({ ...cell, values: Object.freeze([...cell.values]) });
|
|
129
|
+
}
|
|
123
130
|
if (["string", "number", "boolean"].includes(typeof cell)) return cell;
|
|
124
131
|
throw new KernelError("Configuration supports only primitive values, secret() and configEnum().");
|
|
125
132
|
}
|
|
@@ -141,6 +148,26 @@ export function defineConfig(prefixOrSchema: string | ConfigSchema<Defaults>, ma
|
|
|
141
148
|
// The file that declared the config, for "declared in" of a startup failure.
|
|
142
149
|
const declaredIn = callerFile(new Error().stack);
|
|
143
150
|
const input = typeof prefixOrSchema === "string" ? maybeSchema! : prefixOrSchema;
|
|
151
|
+
let definition: ConfigDefinition<ConfigShape>;
|
|
152
|
+
try {
|
|
153
|
+
definition = createDefinition(prefix, input, declaredIn);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (!(error instanceof KernelError)) throw error;
|
|
156
|
+
// A declaration problem is reported at start, with the other configuration
|
|
157
|
+
// errors, instead of crashing the import of the file that declares it.
|
|
158
|
+
const fail = (): never => { throw error; };
|
|
159
|
+
definition = Object.freeze({
|
|
160
|
+
token: Object.freeze(createToken<ConfigView<ConfigShape>>(`Config:${prefix || "default"}`)),
|
|
161
|
+
resolve: fail,
|
|
162
|
+
get: fail,
|
|
163
|
+
has: fail,
|
|
164
|
+
ensureValid: fail,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
definitions.add(definition);
|
|
168
|
+
return definition;
|
|
169
|
+
}
|
|
170
|
+
function createDefinition(prefix: string, input: ConfigSchema<Defaults>, declaredIn: string | undefined): ConfigDefinition<ConfigShape> {
|
|
144
171
|
const schema = Object.freeze({
|
|
145
172
|
default: Object.freeze(Object.fromEntries(Object.entries(input.default).map(([key, value]) => [key, copyCell(value)]))),
|
|
146
173
|
...Object.fromEntries((["development", "test", "production"] as const).map(stand => [stand, Object.freeze(Object.fromEntries(Object.entries(input[stand] ?? {}).map(([key, value]) => [key, copyCell(value as Cell)])))])),
|
|
@@ -245,13 +272,11 @@ export function defineConfig(prefixOrSchema: string | ConfigSchema<Defaults>, ma
|
|
|
245
272
|
inspect: () => rows,
|
|
246
273
|
});
|
|
247
274
|
};
|
|
248
|
-
|
|
275
|
+
return Object.freeze({
|
|
249
276
|
token: Object.freeze(createToken<ConfigView<ConfigShape>>(`Config:${prefix || "default"}`)),
|
|
250
277
|
resolve,
|
|
251
278
|
get: <K extends keyof ConfigShape>(key: K): ConfigShape[K] => resolve().get(key),
|
|
252
279
|
has: <K extends keyof ConfigShape>(key: K) => resolve().has(key),
|
|
253
280
|
ensureValid(environment?: Stand) { resolve(environment); },
|
|
254
281
|
});
|
|
255
|
-
definitions.add(definition);
|
|
256
|
-
return definition;
|
|
257
282
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DiError } from "../../di";
|
|
1
|
+
import { DiError, isConfigurationError } from "../../di";
|
|
2
2
|
import { ServiceValidationError } from "../../di/errors/ServiceValidationError";
|
|
3
3
|
import { redactSensitiveText } from "../../../library/redaction";
|
|
4
4
|
import type { Problem, Screen } from "../../../library/terminal";
|
|
@@ -68,7 +68,7 @@ export function diagnoseStartupFailure(error: unknown, screen: Screen, context:
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
const failed = context.failedServices?.[0];
|
|
71
|
-
const stage = error instanceof KernelError && failed === undefined ? "configuration" : stageOf(failed ?? "");
|
|
71
|
+
const stage = (error instanceof KernelError || isConfigurationError(error)) && failed === undefined ? "configuration" : stageOf(failed ?? "");
|
|
72
72
|
return { stage, title: t.failure.app, problems: [genericProblem(error, failed)] };
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -164,7 +164,7 @@ export class EntityQuery<T extends object, TResult = T> {
|
|
|
164
164
|
result[i] = entity;
|
|
165
165
|
}
|
|
166
166
|
if (this.plan.includes.length > 0) {
|
|
167
|
-
await new IncludeLoader(this.runtime).load(this.model, result as object[], this.plan.includes, this.plan.noTracking);
|
|
167
|
+
await new IncludeLoader(this.runtime, this.plan.ignoreQueryFilters).load(this.model, result as object[], this.plan.includes, this.plan.noTracking);
|
|
168
168
|
}
|
|
169
169
|
return result as unknown as TResult[];
|
|
170
170
|
}
|
|
@@ -19,7 +19,11 @@ interface Level {
|
|
|
19
19
|
* together in memory. This avoids the Cartesian product of collection JOINs.
|
|
20
20
|
*/
|
|
21
21
|
export class IncludeLoader {
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* `ignoreQueryFilters` comes from the main query: when it turned the filters
|
|
24
|
+
* off, included entities are read without them too (as in EF Core).
|
|
25
|
+
*/
|
|
26
|
+
constructor(private readonly runtime: DbContextRuntime, private readonly ignoreQueryFilters = false) {}
|
|
23
27
|
|
|
24
28
|
async load(rootModel: EntityModel, roots: object[], paths: readonly (readonly string[])[], noTracking: boolean): Promise<void> {
|
|
25
29
|
if (roots.length === 0) {
|
|
@@ -156,14 +160,19 @@ export class IncludeLoader {
|
|
|
156
160
|
return model.key[0];
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
private translator(model: EntityModel): SqlTranslator {
|
|
164
|
+
return new SqlTranslator(model, this.runtime.provider.dialect, this.ignoreQueryFilters ? [] : effectiveQueryFilters(model, this.runtime.context));
|
|
165
|
+
}
|
|
166
|
+
|
|
159
167
|
private async queryIn(model: EntityModel, property: string, values: readonly unknown[], noTracking: boolean): Promise<object[]> {
|
|
160
|
-
const translator =
|
|
168
|
+
const translator = this.translator(model);
|
|
161
169
|
const result: object[] = [];
|
|
162
170
|
const chunkSize = maxParametersPerInList(this.runtime.provider);
|
|
163
171
|
for (let start = 0; start < values.length; start += chunkSize) {
|
|
164
172
|
const chunk = values.slice(start, start + chunkSize);
|
|
165
173
|
const { sql, params } = translator.selectAll({
|
|
166
174
|
...EMPTY_PLAN,
|
|
175
|
+
ignoreQueryFilters: this.ignoreQueryFilters,
|
|
167
176
|
conditions: [{ kind: "in", property, values: chunk }],
|
|
168
177
|
noTracking,
|
|
169
178
|
});
|
|
@@ -181,7 +190,7 @@ export class IncludeLoader {
|
|
|
181
190
|
|
|
182
191
|
private async queryTuples(model: EntityModel, properties: readonly string[], values: readonly (readonly unknown[])[], noTracking: boolean): Promise<object[]> {
|
|
183
192
|
const result: object[] = []; const perTuple = properties.length; const chunkSize = Math.max(1, Math.floor(maxParametersPerInList(this.runtime.provider) / perTuple));
|
|
184
|
-
for (let start = 0; start < values.length; start += chunkSize) { const translator =
|
|
193
|
+
for (let start = 0; start < values.length; start += chunkSize) { const translator = this.translator(model); const { sql, params } = translator.selectAll({ ...EMPTY_PLAN, ignoreQueryFilters: this.ignoreQueryFilters, conditions: [{ kind: "tuples", properties, values: values.slice(start, start + chunkSize) }], noTracking }); for (let entity of (await this.runtime.provider.query(sql, params)).map((row) => materialize<object>(model, row, this.runtime.provider.dialect))) { if (!noTracking) entity = this.runtime.tracker.trackLoaded(entity, model); result.push(entity); } }
|
|
185
194
|
return result;
|
|
186
195
|
}
|
|
187
196
|
private tuple(model: EntityModel, values: readonly unknown[]): string | undefined {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.114.0",
|
|
4
4
|
"description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexander Alenin",
|