bazis 0.98.18 → 0.98.19

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.18" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.98.19" }, 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 { types } from "node:util";
2
2
  import { OrmError, OrmTrackedMutationConflictError, OrmUndeclaredConflictTargetError, OrmUnsafeImmediateMutationError } from "../errors";
3
- import type { EntityModel, PropertyModel } from "../Metadata/types";
3
+ import { isDatabaseGenerated, type EntityModel, type PropertyModel } from "../Metadata/types";
4
4
  import type { DbContextRuntime } from "../runtime";
5
5
  import { hasTrackedEntriesForModel } from "../Tracking/ChangeTracker";
6
6
  import { isPostgresJsonScalarParameter } from "../Providers/PostgresDialect";
@@ -16,7 +16,7 @@ export type OrmUniqueKeySelectorV1<T extends object> = (entity: FieldSelector<T>
16
16
 
17
17
  export function executeImmediateUpdate<T extends object>(model: EntityModel, runtime: DbContextRuntime, plan: QueryPlan, values: OrmUpdateValuesV1<T>): Promise<OrmMutationResultV1> {
18
18
  return runtime.runImmediateOperation(async () => {
19
- guard(model, runtime); const admittedPlan = admitPlan(plan);
19
+ guard(model, runtime); const admittedPlan = admitPlan(plan, "executeUpdate");
20
20
  const conditions = effectiveConditions(model, admittedPlan); const assignments = encodeAssignments(runtime, admitUpdateValues(model, values));
21
21
  const compiled = new SqlTranslator(model, runtime.provider.dialect).immediateUpdate(assignments, conditions, (property, value) => encode(runtime, property, snapshot(value), true));
22
22
  Object.freeze(compiled.params);
@@ -28,7 +28,7 @@ export function executeImmediateUpdate<T extends object>(model: EntityModel, run
28
28
 
29
29
  export function executeImmediateDelete(model: EntityModel, runtime: DbContextRuntime, plan: QueryPlan): Promise<OrmMutationResultV1> {
30
30
  return runtime.runImmediateOperation(async () => {
31
- guard(model, runtime); const admittedPlan = admitPlan(plan); const conditions = effectiveConditions(model, admittedPlan);
31
+ guard(model, runtime); const admittedPlan = admitPlan(plan, "executeDelete"); const conditions = effectiveConditions(model, admittedPlan);
32
32
  const compiled = new SqlTranslator(model, runtime.provider.dialect).immediateDelete(conditions, (property, value) => encode(runtime, property, snapshot(value), true));
33
33
  Object.freeze(compiled.params);
34
34
  guard(model, runtime);
@@ -52,13 +52,14 @@ export function insertIfAbsent<T extends object>(model: EntityModel, runtime: Db
52
52
  }
53
53
 
54
54
  function guard(model: EntityModel, runtime: DbContextRuntime): void {
55
- if (hasTrackedEntriesForModel(runtime.tracker, model)) throw new OrmTrackedMutationConflictError();
55
+ if (hasTrackedEntriesForModel(runtime.tracker, model)) throw new OrmTrackedMutationConflictError(model.name);
56
56
  }
57
57
  interface AdmittedImmediatePlan { readonly explicitConditions: readonly Condition[]; readonly ignoreQueryFilters: boolean; }
58
- function admitPlan(plan: QueryPlan): AdmittedImmediatePlan {
58
+ function admitPlan(plan: QueryPlan, terminal: "executeUpdate" | "executeDelete"): AdmittedImmediatePlan {
59
59
  try {
60
60
  if (types.isProxy(plan) || typeof plan !== "object" || plan === null || Object.getPrototypeOf(plan) !== Object.prototype) unsafe();
61
61
  const fields = Object.getOwnPropertyDescriptors(plan); const names = Object.getOwnPropertyNames(plan); if (Object.getOwnPropertySymbols(plan).length !== 0) unsafe();
62
+ explainPlan(fields, terminal);
62
63
  const required = ["conditions", "orders", "noTracking", "includes", "ignoreQueryFilters", "projections"];
63
64
  const optional = ["limit", "requestedLimit", "invalidRequestedLimit", "offset", "rowLock", "skipLocked"];
64
65
  if (names.length !== required.length || required.some((key) => !Object.prototype.hasOwnProperty.call(fields, key)) || optional.some((key) => Object.prototype.hasOwnProperty.call(fields, key))) unsafe();
@@ -70,32 +71,69 @@ function admitPlan(plan: QueryPlan): AdmittedImmediatePlan {
70
71
  return Object.freeze({ explicitConditions: Object.freeze(explicitConditions), ignoreQueryFilters: fields.ignoreQueryFilters!.value });
71
72
  } catch (error) { if (error instanceof OrmUnsafeImmediateMutationError) throw error; unsafe(); }
72
73
  }
74
+ /** Names the query method that an immediate mutation does not accept; reads data descriptors only. */
75
+ function explainPlan(fields: Record<string, PropertyDescriptor>, terminal: string): void {
76
+ const valueOf = (key: string) => { const descriptor = fields[key]; return descriptor && "value" in descriptor ? descriptor.value as unknown : undefined; };
77
+ const length = (key: string) => { const value = valueOf(key); return Array.isArray(value) && !types.isProxy(value) ? Object.getOwnPropertyDescriptor(value, "length")?.value as unknown : undefined; };
78
+ if (valueOf("noTracking") !== true) unsafe(`call .asNoTracking() before ${terminal}(); immediate mutations bypass the change tracker`);
79
+ if (["limit", "requestedLimit", "invalidRequestedLimit", "offset"].some((key) => key in fields)) unsafe(`${terminal}() does not accept take() or skip(); narrow the rows with where(...)`);
80
+ if ("rowLock" in fields || "skipLocked" in fields) unsafe(`${terminal}() does not accept forUpdate()`);
81
+ for (const [key, method] of [["orders", "orderBy()"], ["includes", "include()"], ["projections", "select()"]] as const) {
82
+ const size = length(key); if (typeof size === "number" && size > 0) unsafe(`${terminal}() does not accept ${method}`);
83
+ }
84
+ if (length("conditions") === 0) unsafe(`add .where(...) before ${terminal}(); changing every row of a table is not allowed, use raw SQL for that`);
85
+ }
73
86
  function count(value: unknown): number { if (!Number.isSafeInteger(value) || (value as number) < 0) throw new OrmError("ORM provider returned an invalid immediate mutation count."); return value as number; }
74
- function unsafe(): never { throw new OrmUnsafeImmediateMutationError(); }
87
+ function unsafe(reason?: string): never { throw new OrmUnsafeImmediateMutationError(reason); }
88
+ function describeValue(value: unknown): string {
89
+ if (value === null) return "null";
90
+ if (Array.isArray(value)) return "array";
91
+ if (value instanceof Date) return "Date";
92
+ if (value instanceof Uint8Array) return "Uint8Array";
93
+ return typeof value;
94
+ }
95
+ function functionValue(name: string): never { unsafe(`"${name}" must be a value, got a function; expressions such as views + 1 are not supported, use raw SQL for them`); }
75
96
 
76
97
  interface AdmittedAssignment { readonly property: PropertyModel; readonly value: unknown; }
77
98
  function admitUpdateValues<T extends object>(model: EntityModel, values: OrmUpdateValuesV1<T>): readonly AdmittedAssignment[] {
78
- if (!plain(values)) unsafe();
79
- const descriptors = ownData(values); const keys = [...descriptors.keys()]; if (keys.length === 0) unsafe();
99
+ if (!plain(values)) unsafe('pass the new values as a plain object, for example { status: "archived" }');
100
+ const descriptors = ownData(values); const keys = [...descriptors.keys()]; if (keys.length === 0) unsafe("pass at least one property to set");
80
101
  const out: AdmittedAssignment[] = [];
81
102
  for (const key of keys) {
82
103
  const descriptor = descriptors.get(key)!;
83
- const property = model.propertyByName(key); if (!property || property.isKey || property.generation !== "none" || property.convention || descriptor.value === undefined || descriptor.value === null) unsafe();
104
+ const property = model.propertyByName(key);
105
+ if (!property) unsafe(`"${key}" is not a mapped property of ${model.name}`);
106
+ if (property.isKey) unsafe(`"${key}" is the primary key of ${model.name} and cannot be changed`);
107
+ if (property.generation !== "none" || property.convention) unsafe(`"${key}" is filled automatically and cannot be set`);
108
+ if (descriptor.value === undefined) unsafe(`"${key}" is undefined; omit it or pass a value`);
109
+ if (descriptor.value === null) {
110
+ if (property.required) unsafe(`"${key}" is required (NOT NULL) and cannot be set to null`);
111
+ out.push(Object.freeze({ property, value: null })); continue;
112
+ }
113
+ if (typeof descriptor.value === "function") functionValue(key);
84
114
  out.push(Object.freeze({ property, value: snapshot(descriptor.value) }));
85
115
  }
86
116
  return Object.freeze(out.map((value) => Object.freeze(value)));
87
117
  }
88
118
  function admitInsertValues<T extends object>(model: EntityModel, entity: T): readonly AdmittedAssignment[] {
89
- if (!objectInput(entity)) unsafe();
119
+ if (!objectInput(entity)) unsafe("pass an entity object to insertIfAbsent()");
90
120
  const descriptors = ownData(entity);
91
121
  const out: AdmittedAssignment[] = [];
92
122
  for (const property of model.properties) {
93
- const descriptor = descriptors.get(property.propertyName); if (!descriptor || descriptor.value === undefined) unsafe();
94
- if (descriptor.value === null) { if (property.required) unsafe(); out.push(Object.freeze({ property, value: null })); continue; }
123
+ const descriptor = descriptors.get(property.propertyName);
124
+ // An unset generated key is assigned as by saveChanges(); the caller's entity stays unchanged.
125
+ if (property.isKey && model.key.length === 1 && unsetKey(descriptor?.value)) {
126
+ if (isDatabaseGenerated(property.generation)) continue;
127
+ if (property.generation === "uuidV7") { out.push(Object.freeze({ property, value: Bun.randomUUIDv7() })); continue; }
128
+ }
129
+ if (!descriptor || descriptor.value === undefined) unsafe(`"${property.propertyName}" is missing; pass an entity with every mapped property of ${model.name}`);
130
+ if (descriptor.value === null) { if (property.required) unsafe(`"${property.propertyName}" is required (NOT NULL); got null`); out.push(Object.freeze({ property, value: null })); continue; }
131
+ if (typeof descriptor.value === "function") functionValue(property.propertyName);
95
132
  out.push(Object.freeze({ property, value: snapshot(descriptor.value) }));
96
133
  }
97
134
  return Object.freeze(out.map((value) => Object.freeze(value)));
98
135
  }
136
+ function unsetKey(value: unknown): boolean { return value === undefined || value === null || value === "" || value === 0; }
99
137
  function encodeAssignments(runtime: DbContextRuntime, assignments: readonly AdmittedAssignment[]): readonly { readonly property: PropertyModel; readonly value: SqlParam }[] {
100
138
  return Object.freeze(assignments.map(({ property, value }) => Object.freeze({ property, value: value === null ? null : encode(runtime, property, value) })));
101
139
  }
@@ -104,7 +142,11 @@ function encode(runtime: DbContextRuntime, property: PropertyModel, input: unkno
104
142
  const providerValue = property.converter ? property.converter.toProvider(input) : input;
105
143
  const safeProvider = storageValue(providerValue, property.type, allowNull);
106
144
  return snapshotEncodedSqlParam(runtime.provider.dialect.encode(safeProvider, property.type));
107
- } catch { unsafe(); }
145
+ } catch {
146
+ // Hostile inputs keep the generic message; describing them could run their traps.
147
+ if (typeof input === "object" && input !== null && types.isProxy(input)) unsafe();
148
+ unsafe(property.converter ? `"${property.propertyName}" could not be converted for its ${property.type} column` : `"${property.propertyName}" expects ${property.type}, got ${describeValue(input)}`);
149
+ }
108
150
  }
109
151
 
110
152
  function conflictTarget<T extends object>(model: EntityModel, options: { readonly conflictBy: OrmUniqueKeySelectorV1<T> }): readonly string[] {
@@ -120,11 +162,18 @@ function conflictTarget<T extends object>(model: EntityModel, options: { readonl
120
162
  if (!Number.isSafeInteger(length) || length < 1 || targetDescriptors.size !== length + 1) unsafe();
121
163
  const properties: string[] = [];
122
164
  for (let index = 0; index < length; index += 1) { const operand = targetDescriptors.get(String(index))?.value; if (typeof operand !== "object" || operand === null || types.isProxy(operand)) unsafe(); const property = issued.get(operand as Operand); if (!property) unsafe(); properties.push(property); }
123
- if (new Set(properties).size !== properties.length) throw new OrmUndeclaredConflictTargetError();
165
+ const repeated = properties.find((name, index) => properties.indexOf(name) !== index);
166
+ if (repeated !== undefined) throw new OrmUndeclaredConflictTargetError(`conflictBy lists "${repeated}" more than once`);
124
167
  const columns = properties.map((name) => model.propertyByName(name)?.columnName);
125
- if (columns.some((column) => column === undefined)) throw new OrmUndeclaredConflictTargetError();
168
+ const unknown = properties.find((_name, index) => columns[index] === undefined);
169
+ if (unknown !== undefined) throw new OrmUndeclaredConflictTargetError(`conflictBy uses "${unknown}", which is not a mapped property of ${model.name}`);
126
170
  const candidates = [model.key.map((property) => property.columnName), ...model.indexes.filter((index) => index.unique).map((index) => index.columns)];
127
- if (!candidates.some((candidate) => candidate.length === columns.length && candidate.every((column, index) => column === columns[index]))) throw new OrmUndeclaredConflictTargetError();
171
+ if (!candidates.some((candidate) => candidate.length === columns.length && candidate.every((column, index) => column === columns[index]))) {
172
+ // Candidates are physical columns; the message speaks in property names, in the declared order.
173
+ const propertyOf = (column: string) => model.properties.find((property) => property.columnName === column)?.propertyName ?? column;
174
+ const options = candidates.map((candidate) => `(${candidate.map(propertyOf).join(", ")})`).join(", ");
175
+ throw new OrmUndeclaredConflictTargetError(`conflictBy (${properties.join(", ")}) is not the primary key or a unique index of ${model.name}. Use one of: ${options}; or declare @Index({ unique: true }) on these properties`);
176
+ }
128
177
  return Object.freeze(columns as string[]);
129
178
  }
130
179
 
@@ -138,15 +138,19 @@ export class OrmDatabaseTimeError extends OrmError {
138
138
 
139
139
  /** Immediate DML is deliberately isolated from tracked state. */
140
140
  export class OrmTrackedMutationConflictError extends OrmError {
141
- constructor() { super("Immediate ORM mutation conflicts with tracked entities."); }
141
+ constructor(entityName?: string) {
142
+ super(entityName === undefined
143
+ ? "Immediate ORM mutation conflicts with tracked entities."
144
+ : `Immediate ORM mutation conflicts with tracked entities: this context tracks "${entityName}" entities (loaded with tracking or saved through saveChanges()), which the mutation would make stale. Load them with .asNoTracking(), or run the mutation in a separate DbContext.`);
145
+ }
142
146
  }
143
147
  /** Immediate DML accepts only the small, closed descriptor-safe input domain. */
144
148
  export class OrmUnsafeImmediateMutationError extends OrmError {
145
- constructor() { super("Immediate ORM mutation input is unsafe or unsupported."); }
149
+ constructor(reason?: string) { super(reason === undefined ? "Immediate ORM mutation input is unsafe or unsupported." : `Immediate ORM mutation input is unsafe or unsupported: ${reason}.`); }
146
150
  }
147
151
  /** INSERT ON CONFLICT may name only an exactly declared unique target. */
148
152
  export class OrmUndeclaredConflictTargetError extends OrmError {
149
- constructor() { super("Immediate ORM mutation conflict target is not a declared unique key."); }
153
+ constructor(detail?: string) { super(detail === undefined ? "Immediate ORM mutation conflict target is not a declared unique key." : `Immediate ORM mutation conflict target is not a declared unique key: ${detail}.`); }
150
154
  }
151
155
 
152
156
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.98.18",
3
+ "version": "0.98.19",
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",