bazis 0.96.3 → 0.96.5

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.
@@ -52,7 +52,7 @@ Instead there are ASP.NET-style conventions that codegen infers from the signatu
52
52
 
53
53
  | Method parameter | Binding |
54
54
  |---|---|
55
- | the name matches the route `:name` | the route value (converted by the constraint/type) |
55
+ | the name matches a route parameter: `:name` in the method or `@Controller` template, or a wildcard `*name` (a bare `*` is `rest`) | the route value (converted by the constraint/type) |
56
56
  | a DTO class from the application sources | the request body + `@Validator` validation (errors -> 400) |
57
57
  | `HttpContext` / `Request` / `ResponseBuilder` | context / raw request / response builder |
58
58
  | a primitive (`string`/`number`/`boolean` or inferred from the default) | a query parameter; `?` and a default -> optional |
@@ -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.96.3" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.96.5" }, 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");
@@ -29,8 +29,10 @@ empty or whitespace value. TypeScript forbids unsupported object types.
29
29
 
30
30
  A declaration stores no chosen environment, values or cache. `get/has/ensureValid`
31
31
  stay for standalone reading of the process env, but `ensureValid` no longer
32
- switches later reads. Host code gets `resolve`, a service injects
33
- `definition.token`, a connector gets `ConfigRegistry` as the create argument.
32
+ switches later reads. Host code gets `resolve`, a service declares
33
+ `ConfigView<T>` in its constructor (codegen binds it to the token of the one
34
+ `defineConfig<T>(...)` declaration; the type argument is required) or injects
35
+ `definition.token` explicitly, a connector gets `ConfigRegistry` as the create argument.
34
36
  All built-in connectors use this argument; a custom connector reads
35
37
  `configs?.get(config) ?? config`. A direct global get inside a service does not
36
38
  read the settings of its kernel.
@@ -162,6 +162,9 @@ let inferredDiClasses = new Set<ts.ClassDeclaration>();
162
162
  let explicitlyBoundDiClasses = new Set<ts.ClassDeclaration>();
163
163
  // Known runtime token names: createToken descriptions + class names usable as class-tokens.
164
164
  let tokenDescriptions = new Set<string>();
165
+ // defineConfig<T>("prefix") declarations by the name of T: a ConfigView<T>
166
+ // constructor parameter binds to the token of that declaration.
167
+ let configPrefixesByType = new Map<string, Set<string>>();
165
168
  let knownClassNames = new Set<string>();
166
169
  let warnings: string[] = [];
167
170
  let fatalErrors: string[] = [];
@@ -446,7 +449,7 @@ function collectDynamicImportDiagnostics(source: ts.SourceFile, target: string,
446
449
  function resetTargetAnalysis(files: Set<string>, generatedDir: string): void {
447
450
  activeGeneratedDir = generatedDir;
448
451
  classDeps = new Map(); inferredDiClasses = new Set(); explicitlyBoundDiClasses = new Set();
449
- tokenDescriptions = new Set(); knownClassNames = new Set(); warnings = []; fatalErrors = [];
452
+ tokenDescriptions = new Set(); configPrefixesByType = new Map(); knownClassNames = new Set(); warnings = []; fatalErrors = [];
450
453
  httpBindings = {}; httpControllerFiles = {}; openApiSchemas = {}; openApiOperations = {};
451
454
  grpcBindings = new Map();
452
455
  uiProfileResponseModels = new Map(); openApiSchemaModels = new Map(); requestModelClassNames = new Set(); listRequestClassNames = new Set();
@@ -476,6 +479,7 @@ async function generateTarget(name: string, reachable: Set<string>, production:
476
479
  }
477
480
  collectHttpBindings(source); collectOpenApiSchemas(source); collectUiProfileResponseModels(source);
478
481
  }
482
+ resolveConfigViewDeps();
479
483
  validateDepsAgainstKnownTokens();
480
484
  const requestModelImports = resolveRequestModelImports();
481
485
  const requestModelHydration = resolveRequestModelHydration(requestModelImports);
@@ -711,6 +715,13 @@ function collectTokenDescriptions(source: ts.SourceFile): void {
711
715
  if (node.expression.text === "createToken" || node.expression.text === "createOpenGenericTokenFamily") {
712
716
  tokenDescriptions.add(firstArg.text);
713
717
  }
718
+ if (node.expression.text === "defineConfig") {
719
+ // The same description defineConfig gives its token.
720
+ const prefix = firstArg.text || "default";
721
+ tokenDescriptions.add(`Config:${prefix}`);
722
+ const typeName = node.typeArguments?.[0] && getDependencyTypeName(node.typeArguments[0]);
723
+ if (typeName) configPrefixesByType.set(typeName, (configPrefixesByType.get(typeName) ?? new Set()).add(prefix));
724
+ }
714
725
  }
715
726
  }
716
727
  if (ts.isClassDeclaration(node) && node.name) {
@@ -722,6 +733,39 @@ function collectTokenDescriptions(source: ts.SourceFile): void {
722
733
  ts.forEachChild(source, visit);
723
734
  }
724
735
 
736
+ /**
737
+ * Replaces each `ConfigView<T>` placeholder with the token of the one
738
+ * `defineConfig<T>(...)` declaration. Runs after every source was read, because
739
+ * a service may come before the file that declares its configuration.
740
+ */
741
+ function resolveConfigViewDeps(): void {
742
+ for (const [declaration, deps] of classDeps) {
743
+ let changed = false;
744
+ const resolved: CollectedDependency[] = [];
745
+ for (const [index, dep] of deps.entries()) {
746
+ const typeName = /^ConfigView<(.+)>$/.exec(dep.name)?.[1];
747
+ if (typeName === undefined) {
748
+ resolved.push(dep);
749
+ continue;
750
+ }
751
+ const prefixes = [...(configPrefixesByType.get(typeName) ?? [])];
752
+ if (prefixes.length !== 1) {
753
+ classDeps.delete(declaration);
754
+ if (needsInferredDiDeps(declaration)) {
755
+ fatalErrors.push(prefixes.length === 0
756
+ ? `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: {...} }).`
757
+ : `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, or pass the token explicitly: scoped(Service, Service, [config.token]).`);
758
+ }
759
+ changed = false;
760
+ break;
761
+ }
762
+ resolved.push({ name: `Config:${prefixes[0]}` });
763
+ changed = true;
764
+ }
765
+ if (changed && classDeps.has(declaration)) classDeps.set(declaration, resolved);
766
+ }
767
+ }
768
+
725
769
  function validateDepsAgainstKnownTokens(): void {
726
770
  for (const [declaration, deps] of classDeps) {
727
771
  for (let index = 0; index < deps.length; index += 1) {
@@ -910,6 +954,11 @@ function dependencyFromType(type: ts.Type): CollectedDependency | undefined {
910
954
  const name = declaration?.name?.text ?? type.aliasSymbol?.name ?? symbol?.name;
911
955
  if (!name || BUILTIN_DEPENDENCY_NAMES.has(name)) return undefined;
912
956
  const arguments_ = type.flags & ts.TypeFlags.Object ? checker.getTypeArguments(type as ts.TypeReference) : [];
957
+ if (name === "ConfigView") {
958
+ const configType = arguments_[0];
959
+ const configTypeName = configType && (configType.aliasSymbol?.name ?? configType.getSymbol()?.name);
960
+ return configTypeName ? { name: `ConfigView<${configTypeName}>` } : undefined;
961
+ }
913
962
  if (name === "Lazy" || name === "IRepository") {
914
963
  const inner = arguments_[0] && dependencyFromType(arguments_[0]);
915
964
  if (!inner) return undefined;
@@ -950,6 +999,15 @@ function readConstructorTypeNames(node: ts.ClassDeclaration): ConstructorDepende
950
999
  continue;
951
1000
  }
952
1001
 
1002
+ if (refName === "ConfigView" && paramType && ts.isTypeReferenceNode(paramType)) {
1003
+ const configType = paramType.typeArguments?.[0];
1004
+ const configTypeName = configType ? getDependencyTypeName(configType) : undefined;
1005
+ if (configTypeName) {
1006
+ deps.push({ name: `ConfigView<${configTypeName}>` });
1007
+ continue;
1008
+ }
1009
+ }
1010
+
953
1011
  if (refName === "IRepository" && paramType && ts.isTypeReferenceNode(paramType)) {
954
1012
  const entityType = paramType.typeArguments?.[0];
955
1013
  const entityName = entityType ? getDependencyTypeName(entityType) : undefined;
@@ -1009,22 +1067,31 @@ function hasDecorator(node: ts.HasDecorators, name: string): boolean {
1009
1067
  }
1010
1068
 
1011
1069
  /** Route parameter names from all route decorators of a method (":id(int)" -> "id"). */
1070
+ /**
1071
+ * Route parameter names of an action: the `@Controller` prefix and the method
1072
+ * template, parsed like `Routing/template.ts` (`:name`, `:name(int)`, `*name`,
1073
+ * and a bare `*` named `rest`).
1074
+ */
1012
1075
  function routeParamNames(method: ts.MethodDeclaration): Set<string> {
1013
1076
  const names = new Set<string>();
1014
- for (const decorator of ts.getDecorators(method) ?? []) {
1015
- const info = decoratorCall(decorator);
1016
- if (!info?.call || !ROUTE_DECORATORS.has(info.name)) {
1017
- continue;
1018
- }
1019
- const firstArg = info.call.arguments[0];
1020
- if (!firstArg || !ts.isStringLiteralLike(firstArg)) {
1021
- continue;
1077
+ const templates: string[] = [];
1078
+ const owner = method.parent;
1079
+ const sources: readonly [readonly ts.Decorator[], (name: string) => boolean][] = [
1080
+ [ts.isClassDeclaration(owner) ? ts.getDecorators(owner) ?? [] : [], (name) => name === "Controller"],
1081
+ [ts.getDecorators(method) ?? [], (name) => ROUTE_DECORATORS.has(name)],
1082
+ ];
1083
+ for (const [decorators, accepts] of sources) {
1084
+ for (const decorator of decorators) {
1085
+ const info = decoratorCall(decorator);
1086
+ const firstArg = info?.call?.arguments[0];
1087
+ if (info && accepts(info.name) && firstArg && ts.isStringLiteralLike(firstArg)) templates.push(firstArg.text);
1022
1088
  }
1023
- for (const segment of firstArg.text.split("/")) {
1024
- const match = /^:([A-Za-z_][A-Za-z0-9_]*)/.exec(segment);
1025
- if (match) {
1026
- names.add(match[1] as string);
1027
- }
1089
+ }
1090
+ for (const template of templates) {
1091
+ for (const segment of template.split("/")) {
1092
+ const param = /^:([A-Za-z_][A-Za-z0-9_]*)/.exec(segment);
1093
+ if (param) names.add(param[1] as string);
1094
+ else if (segment.startsWith("*")) names.add(segment.length > 1 ? segment.slice(1) : "rest");
1028
1095
  }
1029
1096
  }
1030
1097
  return names;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.96.3",
3
+ "version": "0.96.5",
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",