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.
@@ -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
+ }
@@ -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,
@@ -6,6 +6,7 @@ export { ModuleRegistrar } from "./ModuleRegistrar";
6
6
  export {
7
7
  registerModuleMetadataExpander,
8
8
  expandModuleMetadata,
9
+ deferModuleDeclarationError,
9
10
  type ModuleMetadataExpander,
10
11
  } from "./moduleExtensions";
11
12
  export {
@@ -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
- extra.push(...expander(metadata));
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
- (contributors[index] as ModuleOwnedProviderContributor)(metadata, context);
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
- (validators[index] as ModuleOwnedContributionValidator)(snapshot);
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
  }
@@ -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] = { service: parent?.service, methods: new Map(parent?.methods), requestModels: new Map(parent?.requestModels) };
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
- throw new TypeError("@GrpcController requires a non-empty gRPC service definition.");
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
- throw new TypeError(`Invalid gRPC service method: ${name}.`);
46
+ meta.problems.push(`invalid gRPC service method: ${name}.`);
47
+ return;
38
48
  }
39
49
  snapshot[name] = Object.freeze({ ...method });
40
50
  }
41
- ownMetadata(context.metadata).service = Object.freeze(snapshot);
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) {
@@ -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
- const { providers, exports, config } = buildInfraDefinitions(manifest);
139
- return { global: true, providers, exports, config };
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
- const { providers, exports, config } = buildInfraDefinitions(manifest);
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.112.0" }, capabilities: { experimentalApi: true },
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
- const entries = profileEntries(profiles);
426
- const defaultProfile = defaultProfileName(entries, options);
427
- ensureFallbacksValid(entries);
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,
@@ -1,4 +1,4 @@
1
- import { DiError } from "../di";
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 DiError || error instanceof KernelError) {
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 DiError || error instanceof KernelError) {
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
- throw new KernelError("configEnum requires non-empty homogeneous finite values and a declared default.");
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 Object.freeze({ __enum: true, values: Object.freeze([...values]), default: defaultValue });
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)) return Object.freeze({ ...cell, values: Object.freeze([...cell.values]) });
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
- const definition: ConfigDefinition<ConfigShape> = Object.freeze({
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
- constructor(private readonly runtime: DbContextRuntime) {}
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 = new SqlTranslator(model, this.runtime.provider.dialect, effectiveQueryFilters(model, this.runtime.context));
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 = new SqlTranslator(model, this.runtime.provider.dialect, effectiveQueryFilters(model, this.runtime.context)); const { sql, params } = translator.selectAll({ ...EMPTY_PLAN, 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); } }
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.112.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",