bazis 0.98.12 → 0.98.14

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.
@@ -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.98.12" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.98.14" }, 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");
@@ -65,7 +65,10 @@ export class OrmLifecycle implements HostedService {
65
65
  const database = new DatabaseFacade(this.options.provider, this.options.model);
66
66
  try {
67
67
  if (this.ensureCreated) {
68
- await database.ensureCreated();
68
+ const result = await database.ensureCreated();
69
+ for (const warning of result.warnings) {
70
+ console.warn(`[orm:schema] ${warning}`);
71
+ }
69
72
  }
70
73
  if (this.migrateOnStart) {
71
74
  const result = await database.migrate();
@@ -4,7 +4,7 @@ import type { DatabaseProvider, DbExecutor, ExecuteResult, ForeignKeyConstraint,
4
4
  import { Migrator, type MigrationResult } from "./Schema/Migrator";
5
5
  import { MigrationRunner, type Migration, type VersionedMigrationResult } from "./Schema/MigrationRunner";
6
6
  import { SchemaDiffer } from "./Schema/SchemaDiffer";
7
- import { SchemaAdmissionEngine } from "./Schema/SchemaAdmission";
7
+ import { SchemaAdmissionEngine, type EnsureCreatedResult } from "./Schema/SchemaAdmission";
8
8
  import { SchemaAdmissionError } from "./errors";
9
9
  import { physicalColumnTypes } from "./Schema/physicalColumnTypes";
10
10
  import { bindResolvedForeignKey } from "./Providers/resolvedForeignKey";
@@ -26,10 +26,9 @@ export class DatabaseFacade {
26
26
  * whole tables, then exact-verifies again before commit. Existing tables are
27
27
  * never altered or repaired.
28
28
  */
29
- async ensureCreated(): Promise<void> {
29
+ async ensureCreated(): Promise<EnsureCreatedResult> {
30
30
  if (this.provider.name === "postgres") {
31
- await new SchemaAdmissionEngine(this.provider, this.models).ensureCreated();
32
- return;
31
+ return new SchemaAdmissionEngine(this.provider, this.models).ensureCreated();
33
32
  }
34
33
  // The physical default contract belongs exclusively to exact PostgreSQL
35
34
  // admission. Other providers must reject it rather than silently ignore it.
@@ -45,6 +44,7 @@ export class DatabaseFacade {
45
44
  await this.provider.execute(indexSql, []);
46
45
  }
47
46
  }
47
+ return { warnings: [] };
48
48
  }
49
49
 
50
50
  /**
@@ -38,8 +38,10 @@ export class ModelBuilder {
38
38
  const indexes: IndexModel[] = [];
39
39
  const foreignKeys: ForeignKeyModel[] = [];
40
40
  let explicitKey: PropertyModel | undefined;
41
+ const sample = ModelBuilder.sampleOf(ctor);
41
42
 
42
43
  for (const rawProp of raw.properties.values()) {
44
+ ModelBuilder.assertTypeMatchesInitializer(ctor.name, rawProp, sample);
43
45
  const property = ModelBuilder.buildProperty(ctor.name, rawProp);
44
46
  properties.push(property);
45
47
  if (property.isKey) {
@@ -184,6 +186,43 @@ export class ModelBuilder {
184
186
  return model;
185
187
  }
186
188
 
189
+ /** A fresh instance shows the initial values; an entity whose constructor throws skips the check. */
190
+ private static sampleOf(ctor: new () => object): Record<string, unknown> | undefined {
191
+ try {
192
+ return new ctor() as Record<string, unknown>;
193
+ } catch {
194
+ return undefined;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * A column without an explicit type maps to `text` (a key to an integer
200
+ * identity). When the initial value says otherwise (`pages = 0`,
201
+ * `code = ""` on a key), the mapping would silently change the value's type
202
+ * or drop it, so the model refuses to build and names the type to set.
203
+ */
204
+ private static assertTypeMatchesInitializer(entityName: string, raw: RawProperty, sample: Record<string, unknown> | undefined): void {
205
+ if (sample === undefined || raw.type !== undefined || raw.convention !== undefined || raw.converter !== undefined) return;
206
+ const initial = sample[raw.propertyName];
207
+ if (initial === undefined || initial === null) return;
208
+ const kind = initial instanceof Date ? "Date" : Array.isArray(initial) ? "array" : typeof initial;
209
+ if (raw.isKey === true) {
210
+ if (kind === "number" || kind === "bigint") return;
211
+ throw new ModelBuildError(
212
+ `Entity "${entityName}": key "${raw.propertyName}" has no column type and maps to an integer identity, but its initial value is a ${kind}. `
213
+ + `For a text key add @Column({ type: "text" }) next to @Key(); for a UUID key use @UUID().`,
214
+ );
215
+ }
216
+ if (kind === "string") return;
217
+ const suggestion = kind === "number" || kind === "bigint" ? `@Column({ type: "integer" }) or @Column({ type: "real" })`
218
+ : kind === "boolean" ? `@Column({ type: "boolean" })`
219
+ : kind === "Date" ? `@Column({ type: "datetime" })`
220
+ : `@Column({ type: "json" })`;
221
+ throw new ModelBuildError(
222
+ `Entity "${entityName}": property "${raw.propertyName}" has no column type and maps to text, but its initial value is a ${kind}. Set the type: ${suggestion}.`,
223
+ );
224
+ }
225
+
187
226
  private static buildProperty(entityName: string, raw: RawProperty): PropertyModel {
188
227
  const isKey = raw.isKey === true;
189
228
  const resolved = resolveColumnType(raw.type, isKey);
@@ -2,7 +2,7 @@ import type { DatabaseProvider, SchemaAdmissionScope } from "../Providers/types"
2
2
  import { SchemaAdmissionError, SchemaMigrationRequiredError, SchemaVerificationError } from "../errors";
3
3
  import type { OrmModel } from "../Metadata/OrmModel";
4
4
  import { compileExpectedSchema, type OrmExpectedSchema } from "./ExpectedSchema";
5
- import { ExactSchemaVerifier } from "./ExactSchemaVerifier";
5
+ import { ExactSchemaVerifier, type SchemaVerificationResult } from "./ExactSchemaVerifier";
6
6
  import { classifySafeAdditive, renderSafeAdditivePostgres } from "./SafeAdditiveSchema";
7
7
 
8
8
  /** PostgreSQL-private trace boundary; deliberately absent from public capability types. */
@@ -10,10 +10,15 @@ type TraceableSchemaAdmissionScope = SchemaAdmissionScope & {
10
10
  readonly executeSchemaAdmission?: (sql: string, operation: string) => ReturnType<SchemaAdmissionScope["execute"]>;
11
11
  };
12
12
 
13
+ /** Result of `ensureCreated`: tolerated differences the application should know about. */
14
+ export interface EnsureCreatedResult {
15
+ readonly warnings: readonly string[];
16
+ }
17
+
13
18
  /** PostgreSQL-only create-missing-whole-tables and exact admission. */
14
19
  export class SchemaAdmissionEngine {
15
20
  constructor(private readonly provider: DatabaseProvider, private readonly models: OrmModel) {}
16
- async ensureCreated(): Promise<void> {
21
+ async ensureCreated(): Promise<EnsureCreatedResult> {
17
22
  let expected: OrmExpectedSchema;
18
23
  try {
19
24
  expected = compileExpectedSchema(this.models);
@@ -25,16 +30,18 @@ export class SchemaAdmissionEngine {
25
30
  if (capability.version !== 1 || !capability.distributedLock || !capability.transactionalDdl || !capability.exactIntrospection) {
26
31
  throw new SchemaAdmissionError("ORM_SCHEMA_ATOMICITY_UNAVAILABLE", "PostgreSQL schema admission cannot guarantee atomic exact verification.");
27
32
  }
33
+ let warnings: readonly string[] = [];
28
34
  try {
29
- await capability.withSchemaAdmission([...new Set(expected.tables.map((table) => table.schema))], async (scope) => this.admit(scope, expected));
35
+ await capability.withSchemaAdmission([...new Set(expected.tables.map((table) => table.schema))], async (scope) => { warnings = await this.admit(scope, expected); });
30
36
  } catch (error) {
31
37
  if (error instanceof SchemaAdmissionError) throw error;
32
38
  throw new SchemaAdmissionError("ORM_SCHEMA_ATOMICITY_UNAVAILABLE", "PostgreSQL schema admission could not complete safely.");
33
39
  }
40
+ return { warnings };
34
41
  }
35
- private async admit(scope: SchemaAdmissionScope, expected: OrmExpectedSchema): Promise<void> {
42
+ private async admit(scope: SchemaAdmissionScope, expected: OrmExpectedSchema): Promise<readonly string[]> {
36
43
  const before = await scope.introspectExpected(expected);
37
- const initial = new ExactSchemaVerifier().verify(expected, before, true);
44
+ const { verification: initial, warnings } = tolerateNameDrift(new ExactSchemaVerifier().verify(expected, before, true));
38
45
  const preflight = classifySafeAdditive(expected, before, initial);
39
46
  if (preflight.hardDifferences.length) {
40
47
  if (preflight.hardDifferences.some((difference) => difference.code === "catalog.unsupported")) throw new SchemaAdmissionError("ORM_SCHEMA_CATALOG_UNSUPPORTED", "PostgreSQL catalog contains a schema form unsupported by exact admission.");
@@ -49,8 +56,9 @@ export class SchemaAdmissionEngine {
49
56
  }
50
57
  catch (error) { throw this.mapDdlError(operation.kind, error); }
51
58
  }
52
- const final = new ExactSchemaVerifier().verify(expected, await scope.introspectExpected(expected));
59
+ const { verification: final } = tolerateNameDrift(new ExactSchemaVerifier().verify(expected, await scope.introspectExpected(expected)));
53
60
  if (!final.compatible) throw new SchemaVerificationError(final);
61
+ return warnings;
54
62
  }
55
63
  private mapDdlError(kind: string, error: unknown): SchemaAdmissionError {
56
64
  const sqlState = typeof error === "object" && error !== null
@@ -60,3 +68,24 @@ export class SchemaAdmissionEngine {
60
68
  return new SchemaAdmissionError(kind === "createSchema" || kind === "createTable" ? "ORM_SCHEMA_CREATE_FAILED" : "ORM_SCHEMA_ADDITIVE_DDL_FAILED", "PostgreSQL additive schema admission failed.");
61
69
  }
62
70
  }
71
+
72
+ /**
73
+ * A primary key that differs only by name (`tasks_pkey` from an older schema vs
74
+ * the model's `pk_tasks`) enforces the same thing, and queries never reference
75
+ * the name (`ON CONFLICT` names columns). It is reported, not refused; the same
76
+ * table passes `migrateOnStart`. Every other difference stays exact.
77
+ */
78
+ function tolerateNameDrift(verification: SchemaVerificationResult): { readonly verification: SchemaVerificationResult; readonly warnings: readonly string[] } {
79
+ const tolerated = verification.differences.filter((difference) => difference.code === "primaryKey.name");
80
+ if (tolerated.length === 0) return { verification, warnings: [] };
81
+ const differences = verification.differences.filter((difference) => difference.code !== "primaryKey.name");
82
+ const quote = (name: string) => `"${name.replaceAll('"', '""')}"`;
83
+ const warnings = tolerated.map((difference) => {
84
+ const table = `${quote(difference.schema)}.${quote(difference.table)}`;
85
+ const actual = difference.actual?.value ?? "";
86
+ const expected = difference.expected?.value ?? difference.objectName ?? "";
87
+ return `table ${table}: primary key is named ${quote(actual)}, the model expects ${quote(expected)}. `
88
+ + `It works as is; to align the name run: ALTER TABLE ${table} RENAME CONSTRAINT ${quote(actual)} TO ${quote(expected)};`;
89
+ });
90
+ return { verification: { compatible: differences.length === 0, differences }, warnings };
91
+ }
@@ -131,6 +131,7 @@ export type {
131
131
  IntrospectedCheck,
132
132
  } from "./Schema/introspection";
133
133
  export type { SchemaDifferenceCode, SafeSchemaDescriptor, SchemaDifference, SchemaVerificationResult } from "./Schema/ExactSchemaVerifier";
134
+ export type { EnsureCreatedResult } from "./Schema/SchemaAdmission";
134
135
  export { SchemaAdmissionError, SchemaMigrationRequiredError, SchemaVerificationError } from "./errors";
135
136
  export type { SchemaAdmissionErrorCode } from "./errors";
136
137
  export { compileCheck, renderCheck } from "./Schema/CheckExpression";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.98.12",
3
+ "version": "0.98.14",
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",