bazis 0.115.0 → 0.116.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/core/http/Results/normalize.ts +6 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/scripts/di-generate.ts +38 -0
- package/library/orm/DbContext.ts +16 -2
- package/library/orm/Providers/OwnedStoreCatalog.reader.ts +1 -1
- package/library/orm/Saving/SaveExecutor.ts +5 -0
- package/library/orm/Schema/OwnedStoreCatalog.ts +5 -1
- package/library/orm/Tracking/ChangeTracker.ts +8 -0
- package/package.json +1 -1
|
@@ -88,6 +88,12 @@ export function normalizeResult(result: unknown, ctx: HttpContext, defaults: Res
|
|
|
88
88
|
if (status === 204 || status === 304) {
|
|
89
89
|
payload = null;
|
|
90
90
|
}
|
|
91
|
+
// A known length survives HEAD: the server answers it with the headers of
|
|
92
|
+
// this response and no body, and Bun would otherwise send content-length: 0.
|
|
93
|
+
const length = typeof payload === "string" ? Buffer.byteLength(payload) : payload instanceof Uint8Array || payload instanceof ArrayBuffer ? payload.byteLength : undefined;
|
|
94
|
+
if (length !== undefined && !headers.has("content-length")) {
|
|
95
|
+
headers.set("content-length", String(length));
|
|
96
|
+
}
|
|
91
97
|
|
|
92
98
|
return new Response(payload, { status, headers });
|
|
93
99
|
}
|
|
@@ -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.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.116.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");
|
|
@@ -1173,6 +1173,32 @@ function routeParamNames(controller: ts.ClassDeclaration, method: ts.MethodDecla
|
|
|
1173
1173
|
return names;
|
|
1174
1174
|
}
|
|
1175
1175
|
|
|
1176
|
+
/** Route parameters with a constraint, `:id(int)` -> id: int, from the controller and method templates. */
|
|
1177
|
+
function routeParamConstraints(controller: ts.ClassDeclaration, method: ts.MethodDeclaration): Map<string, string> {
|
|
1178
|
+
const constraints = new Map<string, string>();
|
|
1179
|
+
for (const decorators of [ts.getDecorators(controller) ?? [], ts.getDecorators(method) ?? []]) {
|
|
1180
|
+
for (const decorator of decorators) {
|
|
1181
|
+
const firstArg = decoratorCall(decorator)?.call?.arguments[0];
|
|
1182
|
+
if (!firstArg || !ts.isStringLiteralLike(firstArg)) continue;
|
|
1183
|
+
for (const segment of firstArg.text.split("/")) {
|
|
1184
|
+
const param = /^:([A-Za-z_][A-Za-z0-9_]*)\(([A-Za-z]+)\)/.exec(segment);
|
|
1185
|
+
if (param) constraints.set(param[1] as string, param[2] as string);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return constraints;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* The router converts a constrained segment before binding, so the parameter
|
|
1194
|
+
* must be declared with the converted type; otherwise its TypeScript type lies.
|
|
1195
|
+
*/
|
|
1196
|
+
function constraintMismatch(paramName: string, declared: string | undefined, constraint: string | undefined): string | undefined {
|
|
1197
|
+
const expected = constraint === "int" || constraint === "number" ? "number" : constraint === "bool" ? "boolean" : constraint === "uuid" || constraint === "alpha" ? "string" : undefined;
|
|
1198
|
+
if (expected === undefined || declared === undefined || declared === expected) return undefined;
|
|
1199
|
+
return `route parameter "${paramName}" is constrained to (${constraint}), so it arrives as a ${expected}; declare it as ${expected}`;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1176
1202
|
/** Reuses the request hydration graph; no reflection metadata/runtime TS imports. */
|
|
1177
1203
|
function collectGrpcBindings(source: ts.SourceFile): void {
|
|
1178
1204
|
const visit = (node: ts.Node): void => {
|
|
@@ -1455,6 +1481,7 @@ function collectControllerBindings(node: ts.ClassDeclaration, controllerName: st
|
|
|
1455
1481
|
for (const { name, routes, member } of controllerActions(node)) {
|
|
1456
1482
|
collectOpenApiOperation(controllerName, member);
|
|
1457
1483
|
const routeParams = routeParamNames(node, routes);
|
|
1484
|
+
const constraints = routeParamConstraints(node, routes);
|
|
1458
1485
|
const specs: HttpBindingSpec[] = [];
|
|
1459
1486
|
let bodyCount = 0;
|
|
1460
1487
|
let failed: string | undefined;
|
|
@@ -1471,6 +1498,12 @@ function collectControllerBindings(node: ts.ClassDeclaration, controllerName: st
|
|
|
1471
1498
|
// An explicit source: FromQuery<…>, FromHeader<…>, FromBody<…>, FromRoute<…>.
|
|
1472
1499
|
const marker = typeNode ? sourceMarker(typeNode) : undefined;
|
|
1473
1500
|
if (marker !== undefined) {
|
|
1501
|
+
const routeName = marker.kind === "FromRoute" ? marker.name ?? paramName : undefined;
|
|
1502
|
+
const mismatch = routeName === undefined ? undefined : constraintMismatch(routeName, marker.inner?.getText().trim(), constraints.get(routeName));
|
|
1503
|
+
if (mismatch !== undefined) {
|
|
1504
|
+
failed = mismatch;
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1474
1507
|
const spec = markerBindingSpec(marker, paramName, optional, routeParams);
|
|
1475
1508
|
if (typeof spec === "string") {
|
|
1476
1509
|
failed = spec;
|
|
@@ -1494,6 +1527,11 @@ function collectControllerBindings(node: ts.ClassDeclaration, controllerName: st
|
|
|
1494
1527
|
continue;
|
|
1495
1528
|
}
|
|
1496
1529
|
if (routeParams.has(paramName)) {
|
|
1530
|
+
const mismatch = constraintMismatch(paramName, keywordType, constraints.get(paramName));
|
|
1531
|
+
if (mismatch !== undefined) {
|
|
1532
|
+
failed = mismatch;
|
|
1533
|
+
break;
|
|
1534
|
+
}
|
|
1497
1535
|
// Constraint conversion happens in the router; add type only for plain :params.
|
|
1498
1536
|
const type = keywordType !== undefined ? PRIMITIVE_QUERY_TYPES[keywordType] : undefined;
|
|
1499
1537
|
specs.push({ source: "route", name: paramName, type: type === "string" ? undefined : type, optional });
|
package/library/orm/DbContext.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { DatabaseFacade } from "./DatabaseFacade";
|
|
2
2
|
import type { DbContextOptions } from "./DbContextOptions";
|
|
3
|
-
import { EntityNotMappedError } from "./errors";
|
|
3
|
+
import { EntityNotMappedError, OrmError } from "./errors";
|
|
4
4
|
import { DbSet } from "./Query/DbSet";
|
|
5
5
|
import type { DbContextRuntime } from "./runtime";
|
|
6
|
-
import { SaveExecutor } from "./Saving/SaveExecutor";
|
|
6
|
+
import { hasPendingRollback, SaveExecutor } from "./Saving/SaveExecutor";
|
|
7
7
|
import { ChangeTracker } from "./Tracking/ChangeTracker";
|
|
8
8
|
import { EntityState } from "./Tracking/EntityState";
|
|
9
9
|
import { monitorImmediateOperation, monitorWholeOperation, observedProvider, transactionScope } from "./Transactions/TransactionScopeCoordinator";
|
|
@@ -88,6 +88,20 @@ export abstract class DbContext {
|
|
|
88
88
|
return this.changeTracker.stateOf(entity);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Stops tracking `entity`: its changes are no longer saved and the next
|
|
93
|
+
* read returns a fresh object (`Entry(e).State = Detached` in EF Core). Use
|
|
94
|
+
* it to drop the changes a rolled-back scope left in the context; an entity
|
|
95
|
+
* saved in a scope that is still open cannot be detached, because that
|
|
96
|
+
* scope's rollback would restore it.
|
|
97
|
+
*/
|
|
98
|
+
detach(entity: object): void {
|
|
99
|
+
if (hasPendingRollback(this.changeTracker, entity)) {
|
|
100
|
+
throw new OrmError(`${entity.constructor.name} was saved in a transaction scope that is still open; detach it after the scope ends.`);
|
|
101
|
+
}
|
|
102
|
+
this.changeTracker.detach(entity);
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
/**
|
|
92
106
|
* Applies all pending changes in one transaction and returns the number of
|
|
93
107
|
* processed entities. DetectChanges runs before saving, and (if enabled)
|
|
@@ -708,7 +708,7 @@ export class OwnedStoreCatalogReader {
|
|
|
708
708
|
const output: OwnedCatalogColumnV1[] = [], relationKinds = new Map(relations.map(relation => [relation.oid, relation.kind]));
|
|
709
709
|
for (let offset = 0; offset < relationOids.length; offset += 1024) {
|
|
710
710
|
const batch = relationOids.slice(offset, offset + 1024), parameter = this.encodePgOidArrayParameter(batch), issuedLimit = budget.remaining + 1;
|
|
711
|
-
const result = await this.source.query(`SELECT a.attrelid::pg_catalog.text AS relation_oid,a.attnum::pg_catalog.text AS attnum,a.attname AS column_name,a.attisdropped AS dropped,a.attislocal AS local,a.attinhcount::pg_catalog.text AS inheritance_count,pg_catalog.format_type(a.atttypid,a.atttypmod) AS physical_type,a.atttypid::pg_catalog.text AS type_oid,a.attnotnull AS not_null,d.oid::pg_catalog.text AS default_object_oid,pg_catalog.pg_get_expr(d.adbin,d.adrelid) AS default_expression,a.attidentity AS identity_code,a.attgenerated AS generated_code,a.attcollation::pg_catalog.text AS collation_oid,t.typcollation::pg_catalog.text AS type_default_collation_oid,a.attstorage AS storage_code,t.typstorage AS type_default_storage_code,a.attcompression AS compression_code,a.atthasdef AS has_default FROM pg_catalog.pg_attribute AS a LEFT JOIN pg_catalog.pg_type AS t ON t.oid=a.atttypid LEFT JOIN pg_catalog.pg_attrdef AS d ON d.adrelid=a.attrelid AND d.adnum=a.attnum WHERE a.attrelid=ANY($1::pg_catalog.oid[]) AND a.attnum>0 ORDER BY a.attrelid,a.attnum LIMIT ${issuedLimit}`, [parameter], operation);
|
|
711
|
+
const result = await this.source.query(`SELECT a.attrelid::pg_catalog.text AS relation_oid,a.attnum::pg_catalog.text AS attnum,a.attname AS column_name,a.attisdropped AS dropped,a.attislocal AS local,a.attinhcount::pg_catalog.text AS inheritance_count,pg_catalog.format_type(a.atttypid,a.atttypmod) AS physical_type,a.atttypid::pg_catalog.text AS type_oid,a.attnotnull AS not_null,d.oid::pg_catalog.text AS default_object_oid,pg_catalog.pg_get_expr(d.adbin,d.adrelid) AS default_expression,a.attidentity AS identity_code,a.attgenerated AS generated_code,a.attcollation::pg_catalog.text AS collation_oid,t.typcollation::pg_catalog.text AS type_default_collation_oid,a.attstorage AS storage_code,t.typstorage AS type_default_storage_code,a.attcompression AS compression_code,a.atthasdef AS has_default FROM pg_catalog.pg_attribute AS a LEFT JOIN pg_catalog.pg_type AS t ON t.oid=a.atttypid LEFT JOIN pg_catalog.pg_attrdef AS d ON d.adrelid=a.attrelid AND d.adnum=a.attnum WHERE a.attrelid=ANY($1::pg_catalog.oid[]) AND a.attnum>0 AND NOT a.attisdropped ORDER BY a.attrelid,a.attnum LIMIT ${issuedLimit}`, [parameter], operation);
|
|
712
712
|
const rows = selectOwnedStoreRows(result, [
|
|
713
713
|
"relation_oid", "attnum", "column_name", "dropped", "local", "inheritance_count", "physical_type", "type_oid", "not_null", "default_object_oid", "default_expression", "identity_code", "generated_code", "collation_oid", "type_default_collation_oid", "storage_code", "type_default_storage_code", "compression_code", "has_default"
|
|
714
714
|
], issuedLimit);
|
|
@@ -36,6 +36,11 @@ interface RollbackToken {
|
|
|
36
36
|
|
|
37
37
|
/** Per-tracker, per-entity save baselines. It is deliberately transient and never part of TrackedEntry. */
|
|
38
38
|
const rollbackTokens = new WeakMap<ChangeTracker, WeakMap<object, RollbackToken>>();
|
|
39
|
+
|
|
40
|
+
/** @internal The entity was saved in a transaction scope that has not ended yet. */
|
|
41
|
+
export function hasPendingRollback(tracker: ChangeTracker, entity: object): boolean {
|
|
42
|
+
return rollbackTokens.get(tracker)?.get(entity) !== undefined;
|
|
43
|
+
}
|
|
39
44
|
const uncertainTrackers = new WeakSet<ChangeTracker>();
|
|
40
45
|
|
|
41
46
|
export interface SaveOptions {
|
|
@@ -1128,13 +1128,17 @@ function verifyOwnedStoreCatalogModelsV1(snapshot: OwnedStoreCatalogSnapshotV1,
|
|
|
1128
1128
|
readonly default: CanonicalDefault;
|
|
1129
1129
|
readonly generation: "none" | "identityByDefault" | "uuidDefault";
|
|
1130
1130
|
}>();
|
|
1131
|
+
// A column added and dropped outside bazis stays in pg_attribute as a
|
|
1132
|
+
// dropped attribute: it holds no reachable data and cannot be removed
|
|
1133
|
+
// without rewriting the table. The reader skips such attributes, so live
|
|
1134
|
+
// columns may have gaps in attnum; their order is what counts.
|
|
1131
1135
|
const tableColumns = columns.get(relation.oid) ?? [];
|
|
1132
1136
|
if (snapshot.columns.some((column) => !owned.has(column.relationOid) && !declaredToastRelationOids.has(column.relationOid)) || tableColumns.length !== expectedTable.columns.length)
|
|
1133
1137
|
bad();
|
|
1134
1138
|
const orderedColumns = [...tableColumns].sort((a, b) => Number(a.attnum) - Number(b.attnum));
|
|
1135
1139
|
for (let index = 0; index < orderedColumns.length; index++) {
|
|
1136
1140
|
const column = orderedColumns[index]!, modelColumn = expectedTable.columns[index]!;
|
|
1137
|
-
if (column.name !== modelColumn.column || column.
|
|
1141
|
+
if (column.name !== modelColumn.column || column.dropped || !column.local || column.inheritanceCount !== "0" || column.typeOid === "0" || BigInt(column.typeOid) >= 12000n || !["integer", "real", "text", "boolean", "datetime", "json", "uuid"].includes(column.physicalType) || column.collationOid !== column.typeDefaultCollationOid || column.storageCode !== column.typeDefaultStorageCode || column.compressionCode !== "" || column.generation === "other")
|
|
1138
1142
|
bad();
|
|
1139
1143
|
const expectedGeneration = modelColumn.generation;
|
|
1140
1144
|
if (column.generation !== expectedGeneration || (column.generation === "none" && (column.identityCode !== "" || column.generatedCode !== "")) || (column.generation === "identityByDefault" && (column.identityCode !== "d" || column.generatedCode !== "")) || (column.generation === "uuidDefault" && (column.identityCode !== "" || column.generatedCode !== "")))
|
|
@@ -243,6 +243,14 @@ export class ChangeTracker {
|
|
|
243
243
|
this.entries.set(entry.entity, entry);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/** Stops tracking one entity: it becomes Detached and leaves the identity map. */
|
|
247
|
+
detach(entity: object): void {
|
|
248
|
+
const entry = this.entries.get(entity);
|
|
249
|
+
if (!entry) return;
|
|
250
|
+
this.unindexByKey(entry);
|
|
251
|
+
this.entries.delete(entity);
|
|
252
|
+
}
|
|
253
|
+
|
|
246
254
|
clear(): void {
|
|
247
255
|
this.entries.clear();
|
|
248
256
|
this.identity.clear();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.116.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",
|