bazis 0.111.2 → 0.112.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 CHANGED
@@ -210,7 +210,10 @@ async function runGenerate(cwd: string, args: ParsedGenerateArgs, codegen: CliRu
210
210
  if (result.registered) s.line(` ${s.strong("↗", PALETTE.export)} ${s.text(t.module.registered(host ?? "App.module.ts"))}`);
211
211
  else s.warning(t.module.notRegistered);
212
212
  // What the module needs from the application to run.
213
- if (args.generator === "module" && args.profile !== "empty") s.line(` ${s.strong("◆", PALETTE.service)} ${s.muted(t.module.needsDb)}`);
213
+ if (args.generator === "module" && args.profile !== "empty") {
214
+ s.line(` ${s.strong("◆", PALETTE.service)} ${s.muted(t.module.needsDb)}`);
215
+ s.line(` ${s.strong("◆", PALETTE.service)} ${s.muted(t.module.needsTable(`${args.name}.module.ts`))}`);
216
+ }
214
217
  if (args.generator === "module" && args.profile === "full") s.line(` ${s.strong("◆", PALETTE.service)} ${s.muted(t.module.needsCache)}`);
215
218
  s.line();
216
219
  if (result.dryRun) {
@@ -0,0 +1,8 @@
1
+ import { KernelError } from "../kernel/errors";
2
+
3
+ /**
4
+ * A gRPC controller that cannot be served as declared (an RPC without
5
+ * @GrpcMethod, a duplicate RPC). A configuration error of the code: the start
6
+ * prints it as one line and exits with code 1.
7
+ */
8
+ export class GrpcSetupError extends KernelError {}
@@ -1,4 +1,5 @@
1
1
  import type { ServiceDefinition } from "./serviceDefinition";
2
+ import { GrpcSetupError } from "./GrpcSetupError";
2
3
  import type { Class } from "../di";
3
4
  import type { GrpcControllerDefinition } from "./contracts";
4
5
  import type { RequestModelClass } from "../http/Binding/requestModelRegistry";
@@ -62,21 +63,21 @@ export function GrpcMethod(rpcName?: string, requestModel?: RequestModelClass) {
62
63
  export function grpcControllerDefinition(controller: Class<object>): GrpcControllerDefinition {
63
64
  const carrier = (controller as unknown as { [Symbol.metadata]?: Carrier })[Symbol.metadata];
64
65
  const meta = carrier?.[GRPC_META];
65
- if (!meta?.service) throw new TypeError(`${controller.name} must use @GrpcController().`);
66
+ if (!meta?.service) throw new GrpcSetupError(`${controller.name} must use @GrpcController().`);
66
67
  const methods = new Map<string, string | symbol>();
67
68
  for (const [methodName, rpcName] of meta.methods) {
68
69
  const keys = Object.keys(meta.service).filter((key) => {
69
70
  const rpc = meta.service![key]!;
70
71
  return key === rpcName || rpc.originalName === rpcName || rpc.path.split("/").at(-1) === rpcName;
71
72
  });
72
- if (keys.length !== 1) throw new TypeError(`${controller.name}.${String(methodName)}: unknown or ambiguous RPC ${rpcName}.`);
73
+ if (keys.length !== 1) throw new GrpcSetupError(`${controller.name}.${String(methodName)}: unknown or ambiguous RPC ${rpcName}.`);
73
74
  const key = keys[0]!;
74
- if (methods.has(key)) throw new TypeError(`${controller.name}: duplicate RPC ${rpcName}.`);
75
- if (typeof controller.prototype[methodName] !== "function") throw new TypeError(`Missing RPC handler ${String(methodName)}.`);
75
+ if (methods.has(key)) throw new GrpcSetupError(`${controller.name}: duplicate RPC ${rpcName}.`);
76
+ if (typeof controller.prototype[methodName] !== "function") throw new GrpcSetupError(`Missing RPC handler ${String(methodName)}.`);
76
77
  methods.set(key, methodName);
77
78
  }
78
79
  for (const key of Object.keys(meta.service)) {
79
- if (!methods.has(key)) throw new TypeError(`${controller.name}: RPC ${key} is missing @GrpcMethod().`);
80
+ if (!methods.has(key)) throw new GrpcSetupError(`${controller.name}: RPC ${key} is missing @GrpcMethod().`);
80
81
  }
81
82
  return Object.freeze({ controller, service: meta.service, methods, requestModels: new Map(meta.requestModels) });
82
83
  }
@@ -1,5 +1,6 @@
1
1
  export { GrpcController, GrpcMethod } from "./decorators";
2
2
  export { GrpcError } from "./GrpcError";
3
+ export { GrpcSetupError } from "./GrpcSetupError";
3
4
  export { grpcModule } from "./Grpc.module";
4
5
  export { GrpcServer } from "./GrpcServer";
5
6
  export { GrpcClient } from "./GrpcClient";
@@ -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.111.2" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.112.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");
@@ -1,6 +1,6 @@
1
1
  import { KernelError } from "../errors";
2
2
 
3
- export type ConfigIssueKind = "missing-secret" | "development-secret" | "number" | "boolean" | "string" | "enum" | "validator" | "conflict";
3
+ export type ConfigIssueKind = "missing-secret" | "empty-secret" | "development-secret" | "number" | "boolean" | "string" | "enum" | "validator" | "conflict";
4
4
 
5
5
  /** One invalid configuration key, with what can change it. */
6
6
  export interface ConfigIssue {
@@ -202,7 +202,10 @@ export function defineConfig(prefixOrSchema: string | ConfigSchema<Defaults>, ma
202
202
  const raw = selected?.raw ?? (isSecret(cell) || isEnum(cell) ? cell.default : cell);
203
203
  let value: ConfigValueType | undefined;
204
204
  if (type === "secret") {
205
- if (typeof raw !== "string" || !raw.trim()) issue(key, "missing-secret", `required non-empty secret is not set (${names.get(key)!.join(", ")})`);
205
+ // secret("") reads like an empty default but would mean "required":
206
+ // the declaration is ambiguous, so it is refused with the fix.
207
+ if (isSecret(cell) && cell.default !== undefined && !cell.default.trim()) issue(key, "empty-secret", 'secret("") is not a value: a secret cannot be empty. Write secret() for a required secret, or give a non-empty development value');
208
+ else if (typeof raw !== "string" || !raw.trim()) issue(key, "missing-secret", `required non-empty secret is not set (${names.get(key)!.join(", ")})`);
206
209
  // A secret default in the code is a development value; production must
207
210
  // get the secret from a source or declare its own value explicitly.
208
211
  // No "apiKey: …" in the hint: the console redaction would hide it.
@@ -84,6 +84,7 @@ function configProblem(issue: ConfigIssue, screen: Screen): Problem {
84
84
  const variables = issue.env.map((name) => screen.code(name)).join(" / ");
85
85
  const causes: Record<ConfigIssue["kind"], string> = {
86
86
  "missing-secret": t.config.issue.missingSecret,
87
+ "empty-secret": t.config.issue.emptySecret,
87
88
  "development-secret": t.config.issue.devSecret,
88
89
  number: t.config.issue.number,
89
90
  boolean: t.config.issue.boolean,
@@ -94,6 +95,7 @@ function configProblem(issue: ConfigIssue, screen: Screen): Problem {
94
95
  };
95
96
  const fixes: string[] = [];
96
97
  if (issue.kind === "missing-secret") fixes.push(`${t.config.fix.setVariable(variables)}, ${t.config.fix.orSecretFile}`);
98
+ else if (issue.kind === "empty-secret") fixes.push(t.config.fix.emptySecret);
97
99
  else if (issue.kind === "development-secret") fixes.push(t.config.fix.setVariable(variables), t.config.fix.orProduction(screen.code(issue.key.split(".").at(-1) ?? issue.key)));
98
100
  else fixes.push(t.config.fix.change(issue.source === undefined ? variables : screen.code(issue.source)));
99
101
  // The wrong value and the variable that brought it.
@@ -101,8 +101,11 @@ export class MigrationRunner {
101
101
 
102
102
  for (const id of toRollback) {
103
103
  const migration = this.migrations.find((m) => m.id === id);
104
- if (!migration?.down) {
105
- throw new Error(`Migration "${id}" has no down() method.`);
104
+ if (!migration) {
105
+ throw new OrmError(`Migration "${id}" is applied but is not in the migrations passed to rollbackVersioned(); pass the full list.`);
106
+ }
107
+ if (!migration.down) {
108
+ throw new OrmError(`Migration "${id}" has no down() method.`);
106
109
  }
107
110
  await this.provider.transaction(async (tx) => {
108
111
  await migration.down!(this.context(tx));
@@ -198,7 +198,7 @@ export async function transactionScope<TResult>(context: DbContext, provider: Da
198
198
  function assertRetryable(context: DbContext): void {
199
199
  const tracked = trackedEntryCount(context.changeTracker);
200
200
  if (tracked === 0) return;
201
- throw new OrmTransactionScopeError(`transactionScope({ retry }) needs ${contextName(context)} without tracked entities: every attempt starts from a clean context, and the ${tracked} entities tracked before the scope would lose their changes. Start the scope before loading anything, or create a new scope for it.`);
201
+ throw new OrmTransactionScopeError(`transactionScope({ retry }) needs ${contextName(context)} without tracked entities: every attempt starts from a clean context, and the ${tracked === 1 ? "1 entity" : `${tracked} entities`} tracked before the scope would lose ${tracked === 1 ? "its" : "their"} changes. Start the scope before loading anything, or create a new scope for it.`);
202
202
  }
203
203
 
204
204
  async function runScope<TResult>(context: DbContext, provider: DatabaseProvider, work: (transaction: OrmTransaction) => Promise<TResult>, options: OrmTransactionScopeOptions | undefined, retryContexts?: Set<DbContext>): Promise<TResult> {
@@ -111,6 +111,7 @@ const en = {
111
111
  invalid: (total: number) => `of ${total} sections invalid`,
112
112
  issue: {
113
113
  missingSecret: "required secret is not set",
114
+ emptySecret: 'secret("") is not a value: a secret cannot be empty',
114
115
  devSecret: "production would use the development default secret from the code",
115
116
  number: "expected a finite number",
116
117
  boolean: "expected a boolean (true, false, 1, 0)",
@@ -124,6 +125,7 @@ const en = {
124
125
  setVariable: (names: string) => `set ${names}`,
125
126
  orSecretFile: "or a file in a secretFilesSource directory",
126
127
  orProduction: (key: string) => `or declare ${key}: secret() with a value source in the production section`,
128
+ emptySecret: "write secret() for a required secret, or give a non-empty development value",
127
129
  change: (names: string) => `change ${names}`,
128
130
  },
129
131
  },
@@ -233,6 +235,7 @@ const en = {
233
235
  notRegistered: "not connected — codegen skips it until a module imports it",
234
236
  codegenSkipped: "codegen skipped (--no-codegen) — run bazis codegen",
235
237
  needsDb: "this module uses a database: connect one with @Infra({ db: ormBazisConnect(dbConfig) })",
238
+ needsTable: (moduleFile: string) => `its table is not created: add ensureCreated: true to ormBazis in ${moduleFile}, or write a migration`,
236
239
  needsCache: "this module caches: pass runApp(AppModule, { cache: memory() })",
237
240
  keptOnFailure: "scaffold files were kept — fix the error and run bazis codegen",
238
241
  },
@@ -273,6 +276,7 @@ const ru: Messages = {
273
276
  invalid: (total) => `из ${total} с ошибками`,
274
277
  issue: {
275
278
  missingSecret: "обязательный секрет не задан",
279
+ emptySecret: 'secret("") — не значение: секрет не может быть пустым',
276
280
  devSecret: "в production попадёт секрет для разработки из кода",
277
281
  number: "нужно конечное число",
278
282
  boolean: "нужно логическое значение (true, false, 1, 0)",
@@ -286,6 +290,7 @@ const ru: Messages = {
286
290
  setVariable: (names) => `задайте ${names}`,
287
291
  orSecretFile: "или файл в каталоге secretFilesSource",
288
292
  orProduction: (key) => `или объявите ${key}: secret() с источником значения в разделе production`,
293
+ emptySecret: "обязательный секрет пишется secret(), а для разработки нужно непустое значение",
289
294
  change: (names) => `исправьте ${names}`,
290
295
  },
291
296
  },
@@ -339,10 +344,10 @@ const ru: Messages = {
339
344
  typecheck: (count) => `проверка типов не прошла · ${count} ${formRu(count, NOUNS_RU.error)}`,
340
345
  tests: "тесты не прошли",
341
346
  command: "команда не выполнена",
342
- skipConfig: "не проверялось — конфигурация с ошибками",
343
- skipInfra: "не запускалось — инфраструктура недоступна",
344
- skipCodegen: "не запускалось — кодогенерация не удалась",
345
- skipEarlier: "не запускалось — предыдущий этап не удался",
347
+ skipConfig: "без проверки — конфигурация с ошибками",
348
+ skipInfra: "без запуска — инфраструктура недоступна",
349
+ skipCodegen: "без запуска — кодогенерация не удалась",
350
+ skipEarlier: "без запуска — предыдущий этап не удался",
346
351
  exitNothing: (code) => `код выхода ${code} · ничего не запущено`,
347
352
  exitWatch: (code, src) => `код выхода ${code} · слежу за ${src} — исправьте и сохраните для перезапуска`,
348
353
  exit: (code) => `код выхода ${code}`,
@@ -395,6 +400,7 @@ const ru: Messages = {
395
400
  notRegistered: "не подключён — кодогенерация пропускает его, пока модуль не импортирован",
396
401
  codegenSkipped: "кодогенерация пропущена (--no-codegen) — запустите bazis codegen",
397
402
  needsDb: "модулю нужна база данных: подключите её через @Infra({ db: ormBazisConnect(dbConfig) })",
403
+ needsTable: (moduleFile) => `таблицу шаблон не создаёт: добавьте ensureCreated: true в ormBazis в ${moduleFile} или напишите миграцию`,
398
404
  needsCache: "модуль кэширует: передайте runApp(AppModule, { cache: memory() })",
399
405
  keptOnFailure: "файлы модуля оставлены — исправьте ошибку и запустите bazis codegen",
400
406
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.111.2",
3
+ "version": "0.112.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",