bazis 0.98.24 → 0.98.26

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.
@@ -1,4 +1,5 @@
1
- import { ForbiddenError } from "../Errors/HttpError";
1
+ import { JwtError } from "../../../library/jwt/errors";
2
+ import { ForbiddenError, UnauthorizedError } from "../Errors/HttpError";
2
3
  import type { HttpMiddleware } from "../Middleware/types";
3
4
  import type { RouteMiddlewareComposer } from "../options";
4
5
  import { resolveAuthorizeMeta } from "./metadata";
@@ -8,7 +9,8 @@ import { resolveAuthorizeMeta } from "./metadata";
8
9
  * the per-route composers. For routes that require authorization it returns
9
10
  * middleware that runs all checks in order: any that returns `false` → `403`;
10
11
  * a check may throw an `HttpError` itself (for example
11
- * `UnauthorizedError` → `401`).
12
+ * `UnauthorizedError` → `401`). A `JwtError` (missing, malformed, expired or
13
+ * forged token) escaping a check is a 401 as well, not a server error.
12
14
  */
13
15
  export function createAuthorizeComposer(): RouteMiddlewareComposer {
14
16
  return (controllerClass, methodName) => {
@@ -20,7 +22,18 @@ export function createAuthorizeComposer(): RouteMiddlewareComposer {
20
22
  const { checks } = meta.authorize;
21
23
  const middleware: HttpMiddleware = async (ctx, next) => {
22
24
  for (const check of checks) {
23
- if (!(await check(ctx))) {
25
+ let allowed: boolean;
26
+ try {
27
+ allowed = await check(ctx);
28
+ } catch (error) {
29
+ if (error instanceof JwtError) {
30
+ const unauthorized = new UnauthorizedError();
31
+ Object.defineProperty(unauthorized, "cause", { value: error, configurable: true, writable: true });
32
+ throw unauthorized;
33
+ }
34
+ throw error;
35
+ }
36
+ if (!allowed) {
24
37
  throw new ForbiddenError();
25
38
  }
26
39
  }
@@ -0,0 +1,11 @@
1
+ import type { HttpContext } from "../HttpContext/HttpContext";
2
+
3
+ /**
4
+ * The token of an `Authorization: Bearer <token>` header, or `undefined`
5
+ * without the header, with another scheme or with an empty token. The scheme
6
+ * is case-insensitive (RFC 7235); surrounding spaces are trimmed.
7
+ */
8
+ export function bearerToken(ctx: Pick<HttpContext, "header">): string | undefined {
9
+ const match = /^\s*bearer\s+(\S+)\s*$/i.exec(ctx.header("authorization") ?? "");
10
+ return match?.[1];
11
+ }
@@ -19,6 +19,13 @@ export interface ErrorHandlerOptions {
19
19
  onUnexpectedError?: (ctx: HttpContext, error: unknown) => void;
20
20
  /** Replaces the built-in logging of unexpected errors. */
21
21
  logError?: (error: unknown) => void;
22
+ /**
23
+ * Status for a non-{@link HttpError} failure, for example
24
+ * `databaseErrorStatus` from `bazis/core/orm`; `undefined` keeps 500. The
25
+ * body stays the standard reason phrase. A mapped 4xx is the client's
26
+ * mistake and is not logged or sent to `onUnexpectedError`.
27
+ */
28
+ status?: (error: unknown) => number | undefined;
22
29
  /**
23
30
  * Logger for unexpected errors when {@link logError} is absent: one `error`
24
31
  * line with the method, path, request id and the redacted error. The server
@@ -124,8 +131,8 @@ export function errorHandler(options: ErrorHandlerOptions = {}): HttpMiddleware
124
131
  );
125
132
  return;
126
133
  }
127
- safelyReportUnexpected(options, log, ctx, error);
128
- const [status, title] = unexpectedStatus(error);
134
+ const [status, title] = unexpectedStatus(error, options.status);
135
+ if (status >= 500) safelyReportUnexpected(options, log, ctx, error);
129
136
  ctx.response = options.exposeDetails
130
137
  ? safeJsonResponse(status, {
131
138
  error: title,
@@ -146,9 +153,17 @@ export function errorHandler(options: ErrorHandlerOptions = {}): HttpMiddleware
146
153
  * and stays 500, like everything else. Upstream details never reach the
147
154
  * client; the log keeps the full error.
148
155
  */
149
- function unexpectedStatus(error: unknown): readonly [number, string] {
156
+ const REASONS: Readonly<Record<number, string>> = {
157
+ 400: "Bad Request", 404: "Not Found", 409: "Conflict", 410: "Gone", 412: "Precondition Failed", 422: "Unprocessable Content",
158
+ 423: "Locked", 429: "Too Many Requests", 500: "Internal Server Error", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout",
159
+ };
160
+
161
+ function unexpectedStatus(error: unknown, mapper?: (error: unknown) => number | undefined): readonly [number, string] {
150
162
  if (error instanceof HttpClientError && error.code !== undefined) {
151
163
  return error.code === HttpErrorCode.Timeout ? [504, "Gateway Timeout"] : [502, "Bad Gateway"];
152
164
  }
165
+ let mapped: number | undefined;
166
+ try { mapped = mapper?.(error); } catch { mapped = undefined; }
167
+ if (mapped !== undefined && Number.isInteger(mapped) && mapped >= 400 && mapped <= 599) return [mapped, REASONS[mapped] ?? (mapped >= 500 ? "Server Error" : "Client Error")];
153
168
  return [500, "Internal Server Error"];
154
169
  }
@@ -34,6 +34,7 @@ export { PRINCIPAL_STATE_KEY, type RequestPrincipal } from "./HttpContext/princi
34
34
  // Authorization
35
35
  export { Authorize, AllowAnonymous } from "./Authorization/Authorize";
36
36
  export { createAuthorizeComposer } from "./Authorization/authorizeComposer";
37
+ export { bearerToken } from "./Authorization/bearerToken";
37
38
  export {
38
39
  resolveAuthorizeMeta,
39
40
  type AuthorizeCheck,
@@ -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.24" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.98.26" }, 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");
@@ -0,0 +1,35 @@
1
+ import {
2
+ CheckViolationError,
3
+ EntityNotFoundError,
4
+ ForeignKeyViolationError,
5
+ LockTimeoutError,
6
+ NotNullViolationError,
7
+ OrmTransactionScopeError,
8
+ OrmValidationError,
9
+ TransactionOutcomeUnknownError,
10
+ UniqueViolationError,
11
+ } from "../../library/orm";
12
+
13
+ /** SQLSTATEs a client may retry: serialization failure, deadlock, statement timeout. */
14
+ const RETRYABLE = new Set(["40001", "40P01", "57014"]);
15
+
16
+ /**
17
+ * HTTP status for a database error, for `http: { errorHandler: { status:
18
+ * databaseErrorStatus } }`: 409 for a unique or foreign key conflict, 422 for
19
+ * a check, not-null or entity validation failure, 404 for `first()` without a
20
+ * row, 503 when the database did not answer, a lock or the transaction timed
21
+ * out, or a retryable conflict happened. Anything else stays 500.
22
+ */
23
+ export function databaseErrorStatus(error: unknown): number | undefined {
24
+ if (error instanceof UniqueViolationError || error instanceof ForeignKeyViolationError) return 409;
25
+ if (error instanceof CheckViolationError || error instanceof NotNullViolationError || error instanceof OrmValidationError) return 422;
26
+ if (error instanceof EntityNotFoundError) return 404;
27
+ if (error instanceof LockTimeoutError || error instanceof TransactionOutcomeUnknownError) return 503;
28
+ if (error instanceof OrmTransactionScopeError && / timed out after \d+ ms/.test(error.message)) return 503;
29
+ if (typeof error === "object" && error !== null) {
30
+ const { errno, code } = error as { errno?: unknown; code?: unknown };
31
+ if (typeof errno === "string" && RETRYABLE.has(errno)) return 503;
32
+ if (typeof code === "string" && code.startsWith("ERR_POSTGRES_CONNECTION")) return 503;
33
+ }
34
+ return undefined;
35
+ }
package/core/orm/index.ts CHANGED
@@ -10,6 +10,7 @@ export { OrmConnectionLifecycle, OrmLifecycle, OrmProviderReadyLifecycle } from
10
10
  export { IRepository, repositoryFor } from "./repository";
11
11
  export { registerRepositories } from "./registerRepositories";
12
12
  export { paginate, type PageResult } from "./listQuery";
13
+ export { databaseErrorStatus } from "./databaseErrorStatus";
13
14
 
14
15
  import { registerModuleMetadataExpander, type BazisModuleRef } from "../di";
15
16
  import { OrmOwnedStoreAdmissionError, type DbContext } from "../../library/orm";
@@ -34,14 +34,19 @@ export class Rs256Algorithm implements SigningAlgorithm {
34
34
  if (keys === null || typeof keys !== "object" || Array.isArray(keys)) {
35
35
  throw new TypeError("RS256 key material must be an object");
36
36
  }
37
- this.keys = Object.freeze({ ...keys });
38
37
  for (const field of ["publicKeyPem", "privateKeyPem", "keyId"] as const) {
39
- const value = this.keys[field];
38
+ const value = keys[field];
40
39
  if (value === undefined && field !== "publicKeyPem") continue;
41
40
  if (typeof value !== "string" || value.length === 0) {
42
41
  throw new TypeError(`RS256 ${field} must be a non-empty string`);
43
42
  }
44
43
  }
44
+ // A wrong key format must fail here, at startup, not on the first sign-in.
45
+ this.keys = Object.freeze({
46
+ ...keys,
47
+ publicKeyPem: checkedPem("publicKeyPem", keys.publicKeyPem),
48
+ ...(keys.privateKeyPem === undefined ? {} : { privateKeyPem: checkedPem("privateKeyPem", keys.privateKeyPem) }),
49
+ });
45
50
  this.canSign = this.keys.privateKeyPem !== undefined;
46
51
  this.keyId = this.keys.keyId;
47
52
  }
@@ -78,13 +83,13 @@ export class Rs256Algorithm implements SigningAlgorithm {
78
83
  if (this.keys.privateKeyPem === undefined) {
79
84
  throw new Error("RS256 private key PEM is required for signing");
80
85
  }
81
- const key = await crypto.subtle.importKey(
86
+ const key = await importKey("private", () => crypto.subtle.importKey(
82
87
  "pkcs8",
83
- decodePemToBuffer(this.keys.privateKeyPem),
88
+ decodePemToBuffer(this.keys.privateKeyPem!),
84
89
  RSA_PARAMS,
85
90
  false,
86
91
  ["sign"],
87
- );
92
+ ));
88
93
  return this.cachedPrivate = this.requireStrongKey(key);
89
94
  }
90
95
 
@@ -92,13 +97,13 @@ export class Rs256Algorithm implements SigningAlgorithm {
92
97
  if (this.cachedPublic !== undefined) {
93
98
  return this.cachedPublic;
94
99
  }
95
- const key = await crypto.subtle.importKey(
100
+ const key = await importKey("public", () => crypto.subtle.importKey(
96
101
  "spki",
97
102
  decodePemToBuffer(this.keys.publicKeyPem),
98
103
  RSA_PARAMS,
99
104
  true,
100
105
  ["verify"],
101
- );
106
+ ));
102
107
  return this.cachedPublic = this.requireStrongKey(key);
103
108
  }
104
109
 
@@ -112,6 +117,39 @@ export class Rs256Algorithm implements SigningAlgorithm {
112
117
  }
113
118
  }
114
119
 
120
+ const PEM = /^-----BEGIN ([A-Z0-9 ]+)-----\n([\s\S]*?)\n?-----END \1-----$/;
121
+
122
+ /**
123
+ * Normalizes and checks one PEM string: literal `\n` (a key stored on one line
124
+ * in .env or a CI secret) and CRLF become line breaks; the label must be the
125
+ * one Web Crypto imports (PKCS#8 private, SPKI public), with a fix otherwise.
126
+ */
127
+ function checkedPem(field: "publicKeyPem" | "privateKeyPem", value: string): string {
128
+ // A backslash never occurs in base64 or PEM armor, so this cannot change a valid key.
129
+ const pem = value.replaceAll("\\n", "\n").replaceAll("\r\n", "\n").trim();
130
+ const expected = field === "publicKeyPem" ? "PUBLIC KEY" : "PRIVATE KEY";
131
+ const match = PEM.exec(pem);
132
+ const label = match?.[1] ?? /^-----BEGIN ([A-Z0-9 ]+)-----/.exec(pem)?.[1];
133
+ if (label === undefined) throw new TypeError(`RS256 ${field} is not a PEM: expected "-----BEGIN ${expected}-----"`);
134
+ if (field === "privateKeyPem" && label === "RSA PRIVATE KEY") throw new TypeError("RS256 privateKeyPem is a PKCS#1 key (BEGIN RSA PRIVATE KEY); convert it to PKCS#8: openssl pkcs8 -topk8 -nocrypt -in key.pem -out key-pkcs8.pem");
135
+ if (field === "privateKeyPem" && label === "ENCRYPTED PRIVATE KEY") throw new TypeError("RS256 privateKeyPem is encrypted; remove the passphrase first: openssl pkcs8 -in key.pem -out key-plain.pem");
136
+ if (field === "privateKeyPem" && label.endsWith("PUBLIC KEY")) throw new TypeError("RS256 privateKeyPem contains a PUBLIC KEY; pass the private key (BEGIN PRIVATE KEY)");
137
+ if (field === "publicKeyPem" && label === "RSA PUBLIC KEY") throw new TypeError("RS256 publicKeyPem is a PKCS#1 public key (BEGIN RSA PUBLIC KEY); convert it to SPKI: openssl rsa -RSAPublicKey_in -in public.pem -pubout -out public-spki.pem");
138
+ if (field === "publicKeyPem" && label.endsWith("PRIVATE KEY")) throw new TypeError("RS256 publicKeyPem contains a private key; give verifiers only the public key: openssl pkey -in key.pem -pubout -out public.pem");
139
+ if (label !== expected || match === undefined || match === null) throw new TypeError(`RS256 ${field} is not a PEM: expected "-----BEGIN ${expected}-----"`);
140
+ if (!/^[A-Za-z0-9+/=\s]+$/.test(match[2]!)) throw new TypeError(`RS256 ${field} has an invalid base64 body`);
141
+ return pem;
142
+ }
143
+
144
+ /** Web Crypto says only "Invalid keyData"; name the key that failed. */
145
+ async function importKey(which: "private" | "public", work: () => Promise<CryptoKey>): Promise<CryptoKey> {
146
+ try {
147
+ return await work();
148
+ } catch (error) {
149
+ throw new Error(`RS256 ${which} key could not be imported (${error instanceof Error ? error.message : String(error)}); check that it is an RSA ${which === "private" ? "PKCS#8 private" : "SPKI public"} key in PEM`, { cause: error });
150
+ }
151
+ }
152
+
115
153
  /** Convenience factory for {@link Rs256Algorithm}. */
116
154
  export function rs256(keys: Rs256KeyMaterial): Rs256Algorithm {
117
155
  return new Rs256Algorithm(keys);
@@ -749,7 +749,7 @@ export class PostgresProvider implements DatabaseProvider {
749
749
  // Both query() and execute() can write in autocommit mode. Once handed
750
750
  // to the driver, a missing reply is not proof that replay is safe.
751
751
  if (autocommit && nativeQuery !== undefined && !isConfirmedStatementRejection(error)) {
752
- const unknown = this.uncertainty(error);
752
+ const unknown = this.uncertainty(error, "statement");
753
753
  if (owner) {
754
754
  owner.unknownOutcome ??= unknown;
755
755
  owner.state = "quarantined";
@@ -1205,7 +1205,7 @@ export class PostgresProvider implements DatabaseProvider {
1205
1205
  } catch { return false; }
1206
1206
  }
1207
1207
 
1208
- private uncertainty(cause?: unknown, phase: "commit" | "cancellation" = "commit"): TransactionOutcomeUnknownError {
1208
+ private uncertainty(cause?: unknown, phase: "commit" | "cancellation" | "statement" = "commit"): TransactionOutcomeUnknownError {
1209
1209
  const error = new TransactionOutcomeUnknownError(cause, phase); this.uncertainErrors.add(error); return error;
1210
1210
  }
1211
1211
 
@@ -5,13 +5,22 @@ import type { TransactionCallback } from "./types";
5
5
  export class TransactionOutcomeUnknownError extends DbUpdateError {
6
6
  readonly code = "ORM_TRANSACTION_OUTCOME_UNKNOWN";
7
7
  readonly outcome = "unknown";
8
- constructor(override readonly cause?: unknown, readonly phase: "commit" | "cancellation" = "commit") {
8
+ /** `statement`: a statement sent outside a transaction got no answer (a timeout or a lost connection). */
9
+ constructor(override readonly cause?: unknown, readonly phase: "commit" | "cancellation" | "statement" = "commit") {
9
10
  super(phase === "commit"
10
11
  ? "Transaction outcome is unknown. Reconcile database state and use a new DbContext before saving again."
11
- : "Transaction cancellation is unconfirmed. Reconcile database state and use a new DbContext before saving again.");
12
+ : phase === "statement"
13
+ ? `The database did not answer a statement sent outside a transaction${statementReason(cause)}. If the statement changed data, its outcome is unknown: check the data and use a new DbContext before saving again.`
14
+ : "Transaction cancellation is unconfirmed. Reconcile database state and use a new DbContext before saving again.");
12
15
  }
13
16
  }
14
17
 
18
+ function statementReason(cause: unknown): string {
19
+ if (!(cause instanceof Error) || cause.message === "") return "";
20
+ const message = cause.message.replace(/\.$/, "");
21
+ return `: ${cause.name === "Error" || cause.name.startsWith("Orm") ? message : `${cause.name}: ${message}`}`;
22
+ }
23
+
15
24
  export function isUnknownTransactionOutcome(error: unknown): boolean {
16
25
  const pending = [error];
17
26
  const seen = new Set<unknown>();
@@ -1,5 +1,5 @@
1
1
  import { Validator } from "../../validation";
2
- import { DbUpdateError, isCommittedOutcome, OrmValidationError, UniqueViolationError } from "../errors";
2
+ import { DbUpdateError, isCommittedOutcome, OrmValidationError, translateDatabaseError } from "../errors";
3
3
  import { isDatabaseGenerated, type EntityModel } from "../Metadata/types";
4
4
  import { maxRowsPerInsert } from "../Providers/limits";
5
5
  import { decodeProperty } from "../Providers/propertyConversion";
@@ -170,7 +170,7 @@ export class SaveExecutor {
170
170
  // idempotent fallback also protects custom/top-level providers.
171
171
  this.restoreGeneratedKeys(generatedKeys);
172
172
  for (const token of tokens) this.restoreRollbackToken(token);
173
- throw UniqueViolationError.from(error);
173
+ throw translateDatabaseError(error);
174
174
  }
175
175
  throw error;
176
176
  }
@@ -1,6 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import type { DbContext } from "../DbContext";
3
- import { ConcurrentTransactionScopeError, differentProviderMessage, OrmDatabaseTimeError, OrmProviderIdentityMismatchError, OrmTransactionScopeError } from "../errors";
3
+ import { ConcurrentTransactionScopeError, differentProviderMessage, OrmDatabaseTimeError, OrmProviderIdentityMismatchError, OrmTransactionScopeError, translateDatabaseError } from "../errors";
4
4
  import { baseProvider, withPostgresScopeOptions, createObservedProvider, postgresTransactionCapability, rootAuthority, withProviderDispatchObserver, withoutProviderDispatchObserver, type CancellableProviderDispatch } from "../Providers/ormTransactionRuntime";
5
5
  import type { DatabaseProvider } from "../Providers/types";
6
6
  import { OrmTransaction, type OrmDatabaseTimeV1, type OrmTransactionScopeOptions } from "./OrmTransaction";
@@ -86,7 +86,9 @@ export function observedProvider(context: object, provider: DatabaseProvider): D
86
86
  const authority = rootAuthority(provider);
87
87
  const fence = <T>(operation: () => Promise<T>, view = views.getStore()): Promise<T> => {
88
88
  if (uncertainContexts.has(token)) return Promise.reject(new TransactionOutcomeUnknownError());
89
- const guarded = () => operation().catch((error) => { if (postgresTransactionCapability(provider)?.isOutcomeUncertain?.(error)) uncertainContexts.add(token); throw error; });
89
+ // Known SQLSTATEs (unique, foreign key, check, not null, lock timeout)
90
+ // reach the context's caller as ORM errors; the outcome check sees the raw one.
91
+ const guarded = () => operation().catch((error) => { if (postgresTransactionCapability(provider)?.isOutcomeUncertain?.(error)) uncertainContexts.add(token); throw translateDatabaseError(error); });
90
92
  if (!view) return guarded();
91
93
  const frame = view.frame;
92
94
  if (frame.authority !== authority || !view.enrolled.has(token) || frame.state !== "active" || frame.activeChild !== undefined) {
@@ -192,6 +192,89 @@ export class UniqueViolationError extends DbUpdateError {
192
192
  }
193
193
  }
194
194
 
195
+ /** Reads a non-empty string field of a driver error. */
196
+ function driverField(cause: object, name: string): string | undefined {
197
+ const value = (cause as Record<string, unknown>)[name];
198
+ return typeof value === "string" && value !== "" ? value : undefined;
199
+ }
200
+
201
+ /** Copies the SQLSTATE so transaction-outcome checks see the same server rejection. */
202
+ function copyServerCode(target: object, cause: object): void {
203
+ for (const name of ["errno", "code"]) {
204
+ const descriptor = Object.getOwnPropertyDescriptor(cause, name);
205
+ if (descriptor && "value" in descriptor) Object.defineProperty(target, name, { value: descriptor.value, enumerable: true });
206
+ }
207
+ }
208
+
209
+ /** A foreign key constraint was violated (SQLSTATE 23503). */
210
+ export class ForeignKeyViolationError extends DbUpdateError {
211
+ readonly constraint: string | undefined;
212
+ readonly table: string | undefined;
213
+ constructor(cause: object) {
214
+ const constraint = driverField(cause, "constraint");
215
+ super(`Foreign key constraint${constraint === undefined ? "" : ` "${constraint}"`} violated: the row refers to a missing row, or a row that other rows refer to was deleted.`, { cause });
216
+ this.constraint = constraint;
217
+ this.table = driverField(cause, "table");
218
+ copyServerCode(this, cause);
219
+ }
220
+ }
221
+
222
+ /** A CHECK constraint was violated (SQLSTATE 23514). */
223
+ export class CheckViolationError extends DbUpdateError {
224
+ readonly constraint: string | undefined;
225
+ readonly table: string | undefined;
226
+ constructor(cause: object) {
227
+ const constraint = driverField(cause, "constraint");
228
+ super(constraint === undefined ? "Check constraint violated." : `Check constraint "${constraint}" violated.`, { cause });
229
+ this.constraint = constraint;
230
+ this.table = driverField(cause, "table");
231
+ copyServerCode(this, cause);
232
+ }
233
+ }
234
+
235
+ /** A NOT NULL column got NULL (SQLSTATE 23502). */
236
+ export class NotNullViolationError extends DbUpdateError {
237
+ readonly column: string | undefined;
238
+ readonly table: string | undefined;
239
+ constructor(cause: object) {
240
+ const column = driverField(cause, "column");
241
+ const table = driverField(cause, "table");
242
+ super(column === undefined ? "A NOT NULL column got NULL." : `Column "${column}"${table === undefined ? "" : ` of table "${table}"`} does not accept NULL.`, { cause });
243
+ this.column = column;
244
+ this.table = table;
245
+ copyServerCode(this, cause);
246
+ }
247
+ }
248
+
249
+ /** A lock was not granted within `lock_timeout` (SQLSTATE 55P03). */
250
+ export class LockTimeoutError extends OrmError {
251
+ constructor(cause: object) {
252
+ super("A lock was not granted within lockTimeoutMs: another transaction holds the row or table. Retry the operation, or shorten the transaction that holds the lock.", { cause });
253
+ copyServerCode(this, cause);
254
+ }
255
+ }
256
+
257
+ const TRANSLATED: Readonly<Record<string, new (cause: object) => OrmError>> = {
258
+ "23505": UniqueViolationError,
259
+ "23503": ForeignKeyViolationError,
260
+ "23514": CheckViolationError,
261
+ "23502": NotNullViolationError,
262
+ "55P03": LockTimeoutError,
263
+ };
264
+
265
+ /**
266
+ * A driver error with a known SQLSTATE as its ORM error (unique, foreign key,
267
+ * check, not-null violation, lock timeout); anything else unchanged. The
268
+ * original error is `cause`; messages carry constraint and column names only,
269
+ * never row values.
270
+ */
271
+ export function translateDatabaseError(error: unknown): unknown {
272
+ if (error === null || typeof error !== "object" || error instanceof OrmError) return error;
273
+ const errno = Object.getOwnPropertyDescriptor(error, "errno");
274
+ const Translated = errno && "value" in errno && typeof errno.value === "string" ? TRANSLATED[errno.value] : undefined;
275
+ return Translated === undefined ? error : new Translated(error);
276
+ }
277
+
195
278
  /** The entity failed validation before saving. */
196
279
  export class OrmValidationError extends DbUpdateError {
197
280
  constructor(
@@ -151,6 +151,11 @@ export {
151
151
  OrmError,
152
152
  OrmValidationError,
153
153
  UniqueViolationError,
154
+ ForeignKeyViolationError,
155
+ CheckViolationError,
156
+ NotNullViolationError,
157
+ LockTimeoutError,
158
+ translateDatabaseError,
154
159
  PostCommitError,
155
160
  isCommittedOutcome,
156
161
  OrmTransactionScopeError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.98.24",
3
+ "version": "0.98.26",
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",