bazis 0.108.0 → 0.109.1
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/cache/ICache.ts +5 -0
- package/core/cache/distributed/IDistributedCache.ts +5 -0
- package/core/di/errors/NamedDependencyNotFoundError.ts +6 -5
- package/core/di/internal/GraphValidator.ts +3 -3
- package/core/di/module/encapsulation.ts +17 -3
- package/core/http/OpenApi/openApiDocument.ts +5 -4
- package/core/http-client/index.ts +14 -20
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/testing/index.ts +19 -6
- package/library/redaction/index.ts +15 -0
- package/package.json +1 -1
package/core/cache/ICache.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createToken, type InjectionToken } from "../di";
|
|
2
|
+
import { registerNamedDependencyHint } from "../di/errors/NamedDependencyNotFoundError";
|
|
2
3
|
import type { CacheEntry } from "./types/CacheEntry";
|
|
3
4
|
import type { CacheSetOptions } from "./types/CacheOptions";
|
|
4
5
|
|
|
@@ -65,6 +66,10 @@ export interface ICache<TValue = unknown> {
|
|
|
65
66
|
/** Default cache DI token. */
|
|
66
67
|
export const ICache = createToken<ICache>("ICache") as InjectionToken<ICache<unknown>>;
|
|
67
68
|
|
|
69
|
+
registerNamedDependencyHint((name) => name === "ICache" || name.startsWith("ICache<")
|
|
70
|
+
? "The cache is provided by a cache module: add cache: memory() to the runApp options (startTestApp takes the same options)."
|
|
71
|
+
: undefined);
|
|
72
|
+
|
|
68
73
|
/** Typed token for a specific value type. */
|
|
69
74
|
export function cacheToken<TValue>(name: string): InjectionToken<ICache<TValue>> {
|
|
70
75
|
return createToken<ICache<TValue>>(`ICache<${name}>`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createToken, type InjectionToken } from "../../di";
|
|
2
|
+
import { registerNamedDependencyHint } from "../../di/errors/NamedDependencyNotFoundError";
|
|
2
3
|
import type { CacheSetOptions } from "../types/CacheOptions";
|
|
3
4
|
|
|
4
5
|
/** Default named connection used when a decorator omits `connection`. */
|
|
@@ -47,3 +48,7 @@ export interface IDistributedCache<TValue = unknown> {
|
|
|
47
48
|
* `DISTRIBUTED_SERVICE_CACHE.resolve(name)`.
|
|
48
49
|
*/
|
|
49
50
|
export const IDistributedCache = createToken<IDistributedCache>("IDistributedCache") as InjectionToken<IDistributedCache<unknown>>;
|
|
51
|
+
|
|
52
|
+
registerNamedDependencyHint((name) => name === "IDistributedCache"
|
|
53
|
+
? "The distributed cache is provided by Redis: add redisConnect(redisConfig, { cache: \"distributed\" }) to the @Infra manifest."
|
|
54
|
+
: undefined);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { DiError } from "./DiError";
|
|
2
2
|
|
|
3
|
-
/** Returns a hint for a
|
|
3
|
+
/** Returns a hint for a dependency nobody registers (by its name), or `undefined`. */
|
|
4
4
|
export type NamedDependencyHint = (name: string) => string | undefined;
|
|
5
5
|
|
|
6
6
|
const hints: NamedDependencyHint[] = [];
|
|
7
7
|
|
|
8
|
-
/** Infrastructure extensions (ORM repositories, …) explain their own
|
|
8
|
+
/** Infrastructure extensions (ORM repositories, the cache, …) explain their own dependencies here. */
|
|
9
9
|
export function registerNamedDependencyHint(hint: NamedDependencyHint): void {
|
|
10
10
|
hints.push(hint);
|
|
11
11
|
}
|
|
@@ -15,9 +15,10 @@ function hintFor(name: string): string {
|
|
|
15
15
|
return hint ? ` ${hint}` : "";
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/** The graph-validation issue for a
|
|
19
|
-
export function
|
|
20
|
-
|
|
18
|
+
/** The graph-validation issue for a dependency nobody registers, naming the service that needs it and the fix when one is known. */
|
|
19
|
+
export function missingDependencyIssue(name: string, requester: string): string {
|
|
20
|
+
const hint = hintFor(name);
|
|
21
|
+
return hint === "" ? `Missing dependency "${name}" for "${requester}"` : `Missing dependency "${name}" for "${requester}".${hint}`;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export class NamedDependencyNotFoundError extends DiError {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ServiceValidationError } from "../errors";
|
|
2
|
-
import {
|
|
2
|
+
import { missingDependencyIssue } from "../errors/NamedDependencyNotFoundError";
|
|
3
3
|
import { isClassProvider, isLazyDependency, isNamedDependency, isOptionalDependency } from "../provider";
|
|
4
4
|
import type {
|
|
5
5
|
KeyedDependency,
|
|
@@ -99,7 +99,7 @@ export class GraphValidator {
|
|
|
99
99
|
const inner = isLazy || isOptional ? dependency.inner : dependency;
|
|
100
100
|
// An optional dependency on a name nothing registered is simply absent.
|
|
101
101
|
if (isNamedDependency(inner) && !this.context.hasName(inner.name)) {
|
|
102
|
-
if (!isOptional) issues.add(
|
|
102
|
+
if (!isOptional) issues.add(missingDependencyIssue(inner.name, serviceName(registration)));
|
|
103
103
|
continue;
|
|
104
104
|
}
|
|
105
105
|
const descriptor = this.context.describeDependency(inner);
|
|
@@ -109,7 +109,7 @@ export class GraphValidator {
|
|
|
109
109
|
}
|
|
110
110
|
if (!depRegistration) {
|
|
111
111
|
issues.add(
|
|
112
|
-
|
|
112
|
+
missingDependencyIssue(tokenToDebugName(descriptor.token), serviceName(registration)),
|
|
113
113
|
);
|
|
114
114
|
continue;
|
|
115
115
|
}
|
|
@@ -184,16 +184,30 @@ export function validateModuleEncapsulation(
|
|
|
184
184
|
// module, say) registered the token.
|
|
185
185
|
const blocking = consumer.imports.find((imported) => !imported.providedTokens.has(token)
|
|
186
186
|
&& !exportedTokens(imported).has(token) && visibleTokens(imported).has(token));
|
|
187
|
+
const owner = records.find((record) => record !== consumer && record.providedTokens.has(token));
|
|
187
188
|
if (blocking !== undefined) {
|
|
188
|
-
|
|
189
|
+
// The module that registers and exports the token can be imported
|
|
190
|
+
// directly; an unnamed inner module (`ormBazis`) cannot.
|
|
191
|
+
const direct = owner !== undefined && owner !== blocking && exportedTokens(owner).has(token) && !/^module#\d+$/.test(owner.name);
|
|
192
|
+
return direct
|
|
193
|
+
? `which its import "${blocking.name}" receives from "${owner.name}" but does not export. Add "${owner.name}" to the imports of "${consumer.name}", or add it to the exports of "${blocking.name}".`
|
|
194
|
+
: `which its import "${blocking.name}" receives from its own imports but does not export. Add it to the exports of "${blocking.name}".`;
|
|
189
195
|
}
|
|
190
|
-
const owner = records.find((record) => record !== consumer && record.providedTokens.has(token));
|
|
191
196
|
if (owner === undefined) return "which is provided by another module but not exported.";
|
|
192
197
|
return exportedTokens(owner).has(token)
|
|
193
198
|
? `which module "${owner.name}" exports, but "${consumer.name}" does not list "${owner.name}" in its imports. Add "${owner.name}" to the imports of "${consumer.name}".`
|
|
194
199
|
: `which module "${owner.name}" provides but does not export. Add it to the exports of "${owner.name}".`;
|
|
195
200
|
};
|
|
196
201
|
|
|
202
|
+
// Whether `record` imports `target`, directly or through other modules. Such a
|
|
203
|
+
// module cannot be the fix: importing it into `target` would be a cycle, and a
|
|
204
|
+
// root without exports (AppModule, the kernel root) passes on everything.
|
|
205
|
+
const importsModule = (record: ModuleGraphRecord, target: ModuleGraphRecord, path = new Set<ModuleGraphRecord>()): boolean => {
|
|
206
|
+
if (path.has(record)) return false;
|
|
207
|
+
path.add(record);
|
|
208
|
+
return record.imports.some((imported) => imported === target || importsModule(imported as ModuleGraphRecord, target, path));
|
|
209
|
+
};
|
|
210
|
+
|
|
197
211
|
// A closed token (`IRepository<Product>`) travels with its family export, so
|
|
198
212
|
// the fix is named in family terms and skips unnamed inner modules such as
|
|
199
213
|
// the one `ormBazis` creates.
|
|
@@ -203,7 +217,7 @@ export function validateModuleEncapsulation(
|
|
|
203
217
|
const exportFix = `${family} (or only ${closed})`;
|
|
204
218
|
const sees = (record: ModuleGraphRecord) => visibleFamilies(record).has(familyId) || visibleTokens(record).has(token);
|
|
205
219
|
const passes = (record: ModuleGraphRecord) => exportedFamilies(record).has(familyId) || exportedTokens(record).has(token);
|
|
206
|
-
const named = (record: ModuleGraphRecord) => !/^module#\d+$/.test(record.name);
|
|
220
|
+
const named = (record: ModuleGraphRecord) => !/^module#\d+$/.test(record.name) && !importsModule(record, consumer);
|
|
207
221
|
const blocking = consumer.imports.find((imported) => named(imported) && sees(imported) && !passes(imported));
|
|
208
222
|
if (blocking !== undefined) {
|
|
209
223
|
return `which its import "${blocking.name}" receives from its own imports but does not export. Add ${exportFix} to the exports of "${blocking.name}".`;
|
|
@@ -379,9 +379,9 @@ function successStatus(method: string, action: ActionMeta, generated: number | u
|
|
|
379
379
|
|
|
380
380
|
/**
|
|
381
381
|
* Errors the operation can answer with: 400 when the request carries values
|
|
382
|
-
* that are converted or validated (body, list query, query
|
|
383
|
-
* parameters), 401/403 under @Authorize, and the
|
|
384
|
-
* returns or throws itself.
|
|
382
|
+
* that are converted or validated (body, list query, query and header values
|
|
383
|
+
* and models, typed route parameters), 401/403 under @Authorize, and the
|
|
384
|
+
* errors the method body returns or throws itself.
|
|
385
385
|
*/
|
|
386
386
|
function errorStatuses(
|
|
387
387
|
bindings: readonly ParameterBinding[],
|
|
@@ -390,7 +390,8 @@ function errorStatuses(
|
|
|
390
390
|
): number[] {
|
|
391
391
|
const statuses = new Set(generated ?? []);
|
|
392
392
|
const validated = bindings.some((binding) =>
|
|
393
|
-
binding.source === "body" || binding.source === "list" || binding.source === "query"
|
|
393
|
+
binding.source === "body" || binding.source === "list" || binding.source === "query" || binding.source === "header"
|
|
394
|
+
|| binding.source === "queryModel" || binding.source === "headerModel"
|
|
394
395
|
|| (binding.source === "route" && binding.type !== undefined && binding.type !== "string"));
|
|
395
396
|
if (validated) statuses.add(400);
|
|
396
397
|
if (authorized) {
|
|
@@ -2,17 +2,20 @@
|
|
|
2
2
|
* Core DI/correlation integration for the pure `@/library/http-client`.
|
|
3
3
|
*
|
|
4
4
|
* The HTTP engine lives in the library (framework-agnostic). This module only
|
|
5
|
-
* wires it into the runtime:
|
|
6
|
-
* correlation propagation. All library
|
|
5
|
+
* wires it into the runtime: a module factory that registers the clients for
|
|
6
|
+
* injection by type, and kernel-based correlation propagation. All library
|
|
7
|
+
* symbols are re-exported for convenience.
|
|
7
8
|
*
|
|
8
9
|
* ```ts
|
|
9
|
-
* import {
|
|
10
|
+
* import { HttpClient, httpClientModule } from "@/core/http-client";
|
|
10
11
|
*
|
|
11
|
-
* @Module({ imports: [httpClientModule({ default: { baseUrl: "https://api" } })] })
|
|
12
|
-
* class
|
|
12
|
+
* @Module({ imports: [httpClientModule({ default: { baseUrl: "https://api" } })], providers: [scoped(WeatherService)] })
|
|
13
|
+
* class WeatherModule {}
|
|
14
|
+
*
|
|
15
|
+
* class WeatherService { constructor(private readonly http: HttpClient) {} }
|
|
13
16
|
* ```
|
|
14
17
|
*/
|
|
15
|
-
import { DI, Module,
|
|
18
|
+
import { DI, Module, type BazisModule, type ProviderDefinition } from "../di";
|
|
16
19
|
import { getOutboundCorrelationHeaders } from "../kernel";
|
|
17
20
|
import {
|
|
18
21
|
HttpClient,
|
|
@@ -24,15 +27,10 @@ import {
|
|
|
24
27
|
|
|
25
28
|
export * from "../../library/http-client";
|
|
26
29
|
|
|
27
|
-
/** Named-clients factory token. */
|
|
28
|
-
export const HTTP_CLIENT_FACTORY = createToken<HttpClientFactory>("IHttpClientFactory");
|
|
29
|
-
/** Default injectable {@link HttpClient} token. */
|
|
30
|
-
export const HTTP_CLIENT = createToken<HttpClient>("HttpClient");
|
|
31
|
-
|
|
32
30
|
export interface HttpClientModuleConfig {
|
|
33
|
-
/** Named clients
|
|
31
|
+
/** Named clients: `HttpClientFactory.createClient(name)`. */
|
|
34
32
|
readonly clients?: Readonly<Record<string, RequestConfig>>;
|
|
35
|
-
/** Defaults for the injectable
|
|
33
|
+
/** Defaults for the injectable `HttpClient` and `createClient()`. */
|
|
36
34
|
readonly default?: RequestConfig;
|
|
37
35
|
/** Correlation header source. Default: the kernel async request context. */
|
|
38
36
|
readonly correlationHeaders?: () => HeaderBag;
|
|
@@ -45,9 +43,8 @@ export interface HttpClientModuleConfig {
|
|
|
45
43
|
/**
|
|
46
44
|
* Registers the outbound HTTP client in DI with correlation propagation wired
|
|
47
45
|
* to the kernel request context. A service takes it by type:
|
|
48
|
-
* - `HttpClient`
|
|
49
|
-
* - `HttpClientFactory`
|
|
50
|
-
* (fail-fast on unknown names).
|
|
46
|
+
* - `HttpClient` — the default client;
|
|
47
|
+
* - `HttpClientFactory` — named clients (fail-fast on unknown names).
|
|
51
48
|
*/
|
|
52
49
|
export function httpClientModule(config: HttpClientModuleConfig = {}): BazisModule {
|
|
53
50
|
const correlationHeaders = config.correlationHeaders ?? getOutboundCorrelationHeaders;
|
|
@@ -85,13 +82,10 @@ export function httpClientModule(config: HttpClientModuleConfig = {}): BazisModu
|
|
|
85
82
|
const factory = builder.build();
|
|
86
83
|
|
|
87
84
|
const client = factory.createClient();
|
|
88
|
-
// One factory and one default client,
|
|
89
|
-
// and under the original tokens.
|
|
85
|
+
// One factory and one default client, injected by type.
|
|
90
86
|
const providers: ProviderDefinition[] = [
|
|
91
87
|
DI.singleton(DI.valueProvider(HttpClientFactory, factory)),
|
|
92
|
-
DI.singleton(DI.valueProvider(HTTP_CLIENT_FACTORY, factory)),
|
|
93
88
|
DI.singleton(DI.valueProvider(HttpClient, client)),
|
|
94
|
-
DI.singleton(DI.valueProvider(HTTP_CLIENT, client)),
|
|
95
89
|
];
|
|
96
90
|
|
|
97
91
|
@Module({ providers })
|
|
@@ -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.109.1" }, 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");
|
package/core/testing/index.ts
CHANGED
|
@@ -1,23 +1,36 @@
|
|
|
1
|
-
import { HOSTED_SERVICE,
|
|
1
|
+
import { HOSTED_SERVICE, type BazisModuleRef, type DiContainer, type ProviderDefinition } from "../di";
|
|
2
2
|
import { loadBazisGeneratedRuntime } from "../generatedRuntime";
|
|
3
3
|
import { HttpServer, type HttpModuleOptions } from "../http";
|
|
4
4
|
import { KernelBuilder, type Kernel } from "../kernel";
|
|
5
5
|
import { composeApp } from "../app/runApp";
|
|
6
6
|
import type { RunAppOptions } from "../app";
|
|
7
7
|
|
|
8
|
+
export interface TestContainerOptions {
|
|
9
|
+
/** Replacements for registered providers: `singleton(ITaskStore, FakeStore)`. */
|
|
10
|
+
readonly overrides?: readonly ProviderDefinition[];
|
|
11
|
+
/** Adjust the kernel before it is built: config sources, a logger. */
|
|
12
|
+
readonly configure?: (builder: KernelBuilder) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
/**
|
|
9
|
-
* A container for module tests
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* A container for module tests: the module graph with the kernel services an
|
|
17
|
+
* application has — `Logger`, `EventBus`, `ConfigView` of declared configs,
|
|
18
|
+
* `Environment` (`test`) — but nothing started: no hosted services, no HTTP.
|
|
19
|
+
* Loads the project's generated code first (without it constructor
|
|
20
|
+
* dependencies are missing and a test passes for the wrong reason).
|
|
12
21
|
*
|
|
13
22
|
* ```ts
|
|
14
23
|
* const container = await createTestContainer(TaskModule, { overrides: [singleton(ITaskStore, FakeStore)] });
|
|
15
24
|
* const tasks = container.createScope().resolve(TaskService);
|
|
16
25
|
* ```
|
|
17
26
|
*/
|
|
18
|
-
export async function createTestContainer(root: BazisModuleRef, options:
|
|
27
|
+
export async function createTestContainer(root: BazisModuleRef, options: TestContainerOptions = {}): Promise<DiContainer> {
|
|
19
28
|
await loadBazisGeneratedRuntime();
|
|
20
|
-
|
|
29
|
+
const builder = new KernelBuilder(root).useOptions({ startupReport: false, environment: "test", signals: [] });
|
|
30
|
+
options.configure?.(builder);
|
|
31
|
+
if (options.overrides !== undefined) builder.useOverrides(options.overrides);
|
|
32
|
+
const kernel: Kernel = await builder.build();
|
|
33
|
+
return kernel.container;
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
export interface TestAppOptions extends Omit<RunAppOptions, "http"> {
|
|
@@ -84,10 +84,25 @@ function redactValue(
|
|
|
84
84
|
if (depth >= options.maxDepth) {
|
|
85
85
|
return options.replacement;
|
|
86
86
|
}
|
|
87
|
+
// `seen` holds the objects on the current path only: an object reached
|
|
88
|
+
// again through another branch is a shared reference, not a cycle.
|
|
87
89
|
if (seen.has(value)) {
|
|
88
90
|
return CIRCULAR;
|
|
89
91
|
}
|
|
90
92
|
seen.add(value);
|
|
93
|
+
try {
|
|
94
|
+
return redactObject(value, options, seen, depth);
|
|
95
|
+
} finally {
|
|
96
|
+
seen.delete(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function redactObject(
|
|
101
|
+
value: object,
|
|
102
|
+
options: NormalizedRedactionOptions,
|
|
103
|
+
seen: WeakSet<object>,
|
|
104
|
+
depth: number,
|
|
105
|
+
): unknown {
|
|
91
106
|
if (value instanceof Error) {
|
|
92
107
|
return redactError(value, options, seen, depth);
|
|
93
108
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.109.1",
|
|
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",
|