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.
|
|
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
|
|
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);
|
|
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);
|
|
94
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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])))
|
|
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
|
|
package/library/orm/errors.ts
CHANGED
|
@@ -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() {
|
|
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.
|
|
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",
|