@rebasepro/server-postgres 0.17.3 → 0.18.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/LICENSE +0 -1
- package/README.md +31 -0
- package/dist/backup-argv.d.ts +19 -0
- package/dist/branch-argv.d.ts +28 -0
- package/dist/branch-prune.d.ts +77 -0
- package/dist/cli-collections-path.d.ts +31 -0
- package/dist/cli-errors.d.ts +123 -0
- package/dist/cli-flags.d.ts +101 -0
- package/dist/cli-helpers.d.ts +94 -10
- package/dist/collection-index-C7LopMyX.js +247 -0
- package/dist/collection-index-C7LopMyX.js.map +1 -0
- package/dist/databasePoolManager.d.ts +27 -0
- package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-CagOzEPS.js} +6 -6
- package/dist/{ensure-collection-policies-UCqgv_8c.js.map → ensure-collection-policies-CagOzEPS.js.map} +1 -1
- package/dist/{ensure-collection-tables-DgVixhX3.js → ensure-collection-tables-DbAhrjnG.js} +614 -52
- package/dist/ensure-collection-tables-DbAhrjnG.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.es.js +19307 -3059
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-ClJoTboT.js} +53 -189
- package/dist/rls-enforcement-ClJoTboT.js.map +1 -0
- package/dist/schema/column-type-drift.d.ts +79 -0
- package/dist/schema/doctor.d.ts +42 -0
- package/dist/schema/ensure-collection-tables.d.ts +90 -0
- package/dist/schema/generate-next-step.d.ts +30 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
- package/dist/schema/generated-schema-staleness.d.ts +37 -0
- package/dist/schema/introspect-db-queries.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +63 -2
- package/dist/services/BranchService.d.ts +86 -1
- package/dist/services/collection-helpers.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +30 -2
- package/dist/services/row-pipeline.d.ts +12 -1
- package/dist/utils/pg-error-utils.d.ts +3 -1
- package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
- package/package.json +32 -27
- package/dist/backup-service-DCk7KhhL.js +0 -9044
- package/dist/backup-service-DCk7KhhL.js.map +0 -1
- package/dist/collection-index-BRUg10H5.js +0 -5690
- package/dist/collection-index-BRUg10H5.js.map +0 -1
- package/dist/connection-GOKU3Hu5.js +0 -277
- package/dist/connection-GOKU3Hu5.js.map +0 -1
- package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
- package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
- package/dist/src-DiB5RP2Z.js +0 -541
- package/dist/src-DiB5RP2Z.js.map +0 -1
- package/dist/websocket-BZ4H5wUz.js +0 -8198
- package/dist/websocket-BZ4H5wUz.js.map +0 -1
- package/src/PostgresAdapter.ts +0 -91
- package/src/PostgresBackendDriver.ts +0 -1857
- package/src/PostgresBootstrapper.ts +0 -1157
- package/src/auth/ensure-tables.ts +0 -896
- package/src/auth/schema-version.ts +0 -260
- package/src/auth/services.ts +0 -1610
- package/src/backup/backup-cli.ts +0 -520
- package/src/backup/backup-cron.ts +0 -212
- package/src/backup/backup-logic.ts +0 -93
- package/src/backup/backup-service.ts +0 -485
- package/src/backup/index.ts +0 -12
- package/src/backup/pg-tools.ts +0 -403
- package/src/backup/retention.ts +0 -86
- package/src/cli-errors.ts +0 -265
- package/src/cli-helpers.ts +0 -569
- package/src/cli-output.ts +0 -43
- package/src/cli.ts +0 -1339
- package/src/collections/PostgresCollectionRegistry.ts +0 -108
- package/src/collections/buildRegistry.ts +0 -68
- package/src/collections/validate-relations.ts +0 -402
- package/src/connection.ts +0 -333
- package/src/data-transformer.ts +0 -845
- package/src/databasePoolManager.ts +0 -95
- package/src/history/HistoryService.ts +0 -254
- package/src/history/ensure-history-table.ts +0 -53
- package/src/index.ts +0 -19
- package/src/interfaces.ts +0 -60
- package/src/module-dir.ts +0 -7
- package/src/schema/atlas-argv.ts +0 -94
- package/src/schema/auth-schema.ts +0 -298
- package/src/schema/auth-users-columns.ts +0 -131
- package/src/schema/carved-out-migration.ts +0 -404
- package/src/schema/classify-change.ts +0 -436
- package/src/schema/collection-index.ts +0 -427
- package/src/schema/collections-schema-version.ts +0 -103
- package/src/schema/destructive-sql.ts +0 -94
- package/src/schema/doctor-cli.ts +0 -66
- package/src/schema/doctor-policy-checks.ts +0 -105
- package/src/schema/doctor.ts +0 -837
- package/src/schema/drizzle-ddl.ts +0 -33
- package/src/schema/dynamic-tables.test.ts +0 -302
- package/src/schema/dynamic-tables.ts +0 -293
- package/src/schema/ensure-collection-policies.ts +0 -198
- package/src/schema/ensure-collection-tables.test.ts +0 -518
- package/src/schema/ensure-collection-tables.ts +0 -1267
- package/src/schema/generate-drizzle-schema-logic.ts +0 -957
- package/src/schema/generate-drizzle-schema.ts +0 -143
- package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
- package/src/schema/generate-postgres-ddl.ts +0 -180
- package/src/schema/generate-schema-commit.ts +0 -267
- package/src/schema/generated-schema-staleness.ts +0 -284
- package/src/schema/introspect-db-constraints.ts +0 -385
- package/src/schema/introspect-db-inference.ts +0 -255
- package/src/schema/introspect-db-logic.ts +0 -1387
- package/src/schema/introspect-db-naming.ts +0 -15
- package/src/schema/introspect-db-project.ts +0 -78
- package/src/schema/introspect-db-queries.ts +0 -326
- package/src/schema/introspect-db-structure.ts +0 -670
- package/src/schema/introspect-db-types.ts +0 -56
- package/src/schema/introspect-db.ts +0 -257
- package/src/schema/introspect-runtime.test.ts +0 -260
- package/src/schema/introspect-runtime.ts +0 -327
- package/src/schema/non-sql-collections.test.ts +0 -131
- package/src/schema/rls-bootstrap-sql.ts +0 -288
- package/src/schema/search-column.ts +0 -643
- package/src/schema/test-schema.ts +0 -11
- package/src/schema/vector-index.ts +0 -491
- package/src/security/anonymous-grants.test.ts +0 -73
- package/src/security/policy-drift.test.ts +0 -435
- package/src/security/policy-drift.ts +0 -473
- package/src/security/rls-enforcement.ts +0 -505
- package/src/services/BranchService.ts +0 -326
- package/src/services/FetchService.ts +0 -2115
- package/src/services/PersistService.ts +0 -557
- package/src/services/RelationService.ts +0 -978
- package/src/services/RelationWriteService.ts +0 -653
- package/src/services/cdc/CdcListener.ts +0 -103
- package/src/services/cdc/junction-tables.ts +0 -91
- package/src/services/cdc/trigger-cdc.ts +0 -173
- package/src/services/channel-bus/ChannelBus.ts +0 -44
- package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
- package/src/services/channel-bus/index.ts +0 -114
- package/src/services/channel-history.ts +0 -424
- package/src/services/channel-presence.ts +0 -185
- package/src/services/collection-helpers.ts +0 -420
- package/src/services/dataService.ts +0 -256
- package/src/services/index.ts +0 -14
- package/src/services/junction-writes.ts +0 -295
- package/src/services/nested-path.ts +0 -145
- package/src/services/pg-notify-listener.ts +0 -151
- package/src/services/realtimeService.ts +0 -2545
- package/src/services/row-pipeline.ts +0 -240
- package/src/services/write-denial.ts +0 -55
- package/src/types.ts +0 -4
- package/src/utils/connection-string.ts +0 -58
- package/src/utils/drizzle-conditions.ts +0 -2743
- package/src/utils/pg-array-null-patch.ts +0 -42
- package/src/utils/pg-error-utils.ts +0 -391
- package/src/websocket.ts +0 -855
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { getTableColumns } from "drizzle-orm";
|
|
2
|
-
import { PgArray, PgTable } from "drizzle-orm/pg-core";
|
|
3
|
-
import { logger } from "@rebasepro/server";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Patches all PgArray columns on the given tables to handle NULL values safely.
|
|
7
|
-
*
|
|
8
|
-
* Drizzle ORM's `PgArray.mapFromDriverValue` calls `value.map(...)` without
|
|
9
|
-
* guarding against `null`. When a PostgreSQL native array column (`text[]`,
|
|
10
|
-
* `integer[]`, etc.) contains NULL, the pg driver returns `null` in JavaScript,
|
|
11
|
-
* and `null.map(...)` throws `TypeError: value.map is not a function`.
|
|
12
|
-
*
|
|
13
|
-
* This function walks every column of every registered table and, for any
|
|
14
|
-
* `PgArray` column, wraps its `mapFromDriverValue` to return `null` when the
|
|
15
|
-
* database value is nullish.
|
|
16
|
-
*
|
|
17
|
-
* This is a workaround for a known Drizzle ORM issue. Should be removed once
|
|
18
|
-
* Drizzle handles nullable arrays natively.
|
|
19
|
-
*/
|
|
20
|
-
export function patchPgArrayNullSafety(tables: Record<string, unknown>): void {
|
|
21
|
-
let patchedCount = 0;
|
|
22
|
-
|
|
23
|
-
for (const tableOrRelation of Object.values(tables)) {
|
|
24
|
-
if (!(tableOrRelation instanceof PgTable)) continue;
|
|
25
|
-
|
|
26
|
-
const columns = getTableColumns(tableOrRelation);
|
|
27
|
-
for (const column of Object.values(columns)) {
|
|
28
|
-
if (column instanceof PgArray) {
|
|
29
|
-
const original = column.mapFromDriverValue.bind(column);
|
|
30
|
-
column.mapFromDriverValue = function (value: unknown) {
|
|
31
|
-
if (value == null) return null;
|
|
32
|
-
return original(value as string | unknown[]);
|
|
33
|
-
};
|
|
34
|
-
patchedCount++;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (patchedCount > 0) {
|
|
40
|
-
logger.debug(`[PgArray] Patched ${patchedCount} array column(s) for null-safety`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared PostgreSQL error extraction and user-friendly message formatting.
|
|
3
|
-
*
|
|
4
|
-
* Drizzle wraps native PG errors in a `.cause` chain. These utilities
|
|
5
|
-
* unwrap that chain to get the real PostgreSQL error (identified by a
|
|
6
|
-
* 5-character alphanumeric `code` such as `42P01`) and translate it into
|
|
7
|
-
* a message that is safe and helpful to show to end-users.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { logger } from "@rebasepro/server";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Shape of a deliberate client-facing error — `ApiError` from
|
|
14
|
-
* `@rebasepro/server`, or anything else carrying a 4xx `statusCode`.
|
|
15
|
-
*
|
|
16
|
-
* Matched structurally rather than with `instanceof`: `@rebasepro/server` can
|
|
17
|
-
* be loaded twice (published dist vs. workspace source), which breaks class
|
|
18
|
-
* identity — `PersistService` hedges against the same thing by also accepting
|
|
19
|
-
* `name === "ApiError"`.
|
|
20
|
-
*/
|
|
21
|
-
interface ClientFacingError extends Error {
|
|
22
|
-
statusCode?: number;
|
|
23
|
-
code?: string;
|
|
24
|
-
/** See `ApiError.expected` — routine outcomes log at debug, not warn. */
|
|
25
|
-
expected?: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Return the error when it is a deliberate 4xx, otherwise null.
|
|
30
|
-
*
|
|
31
|
-
* A thrown `ApiError` is a decision the server made about the request, not a
|
|
32
|
-
* database failure: its message and code are already written for the client.
|
|
33
|
-
*/
|
|
34
|
-
function asClientFacingError(error: unknown): ClientFacingError | null {
|
|
35
|
-
if (!(error instanceof Error)) return null;
|
|
36
|
-
const e = error as ClientFacingError;
|
|
37
|
-
if (typeof e.statusCode !== "number" || e.statusCode < 400 || e.statusCode >= 500) return null;
|
|
38
|
-
return e;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Shape of PostgreSQL errors with diagnostic metadata. */
|
|
42
|
-
export interface PostgresError extends Error {
|
|
43
|
-
code?: string;
|
|
44
|
-
detail?: string;
|
|
45
|
-
hint?: string;
|
|
46
|
-
constraint?: string;
|
|
47
|
-
column?: string;
|
|
48
|
-
table?: string;
|
|
49
|
-
dataType?: string;
|
|
50
|
-
cause?: unknown;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Extract the underlying PostgreSQL error from a Drizzle wrapper.
|
|
55
|
-
* Drizzle wraps PG errors in a `cause` property — this function
|
|
56
|
-
* recursively walks the chain until it finds an object with a PG
|
|
57
|
-
* error code (5-char alphanumeric, e.g. `42P01`).
|
|
58
|
-
*/
|
|
59
|
-
export function extractPgError(error: unknown): PostgresError | null {
|
|
60
|
-
if (!error || typeof error !== "object") return null;
|
|
61
|
-
if (!(error instanceof Error)) {
|
|
62
|
-
// Check non-Error objects for a cause chain (Drizzle sometimes wraps oddly)
|
|
63
|
-
if ("cause" in error && (error as Record<string, unknown>).cause && typeof (error as Record<string, unknown>).cause === "object") {
|
|
64
|
-
return extractPgError((error as Record<string, unknown>).cause);
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Check if the error itself has a PG error code
|
|
70
|
-
if ("code" in error && typeof (error as PostgresError).code === "string" && /^[0-9A-Z]{5}$/.test((error as PostgresError).code!)) {
|
|
71
|
-
return error as PostgresError;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Check the cause chain (Drizzle wraps PG errors)
|
|
75
|
-
if (error.cause && typeof error.cause === "object") {
|
|
76
|
-
return extractPgError(error.cause);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Whether the failure came back from Postgres rather than from building the
|
|
84
|
-
* query — which decides whether a fallback query is worth issuing.
|
|
85
|
-
*
|
|
86
|
-
* Reads here run inside a transaction (that is where `SET LOCAL ROLE` binds
|
|
87
|
-
* RLS). Once a statement raises, that transaction is aborted, and every later
|
|
88
|
-
* statement on it returns `25P02` — "current transaction is aborted, commands
|
|
89
|
-
* ignored until end of transaction block". So a retry after a database error
|
|
90
|
-
* cannot succeed, and it replaces a precise diagnosis ("invalid input syntax
|
|
91
|
-
* for type uuid") with a generic one. Rethrow instead.
|
|
92
|
-
*
|
|
93
|
-
* A query the driver could not even build — a missing reciprocal relation, say
|
|
94
|
-
* — never reached Postgres, leaves the transaction usable, and is exactly what
|
|
95
|
-
* the fallback paths exist for.
|
|
96
|
-
*/
|
|
97
|
-
export function reachedDatabase(error: unknown): boolean {
|
|
98
|
-
return extractPgError(error) !== null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Walk the error cause chain and return the deepest meaningful message.
|
|
103
|
-
*/
|
|
104
|
-
export function extractCauseMessage(error: unknown): string | null {
|
|
105
|
-
if (!error || typeof error !== "object") return null;
|
|
106
|
-
if (!(error instanceof Error)) return null;
|
|
107
|
-
|
|
108
|
-
if (error.cause && typeof error.cause === "object") {
|
|
109
|
-
const deeper = extractCauseMessage(error.cause);
|
|
110
|
-
if (deeper) return deeper;
|
|
111
|
-
// The cause itself has a message
|
|
112
|
-
if (error.cause instanceof Error && error.cause.message) {
|
|
113
|
-
return error.cause.message;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Codes that mean "this connection will never work as configured".
|
|
121
|
-
*
|
|
122
|
-
* A wrong password or a database that does not exist is a settled fact about
|
|
123
|
-
* the connection string, not a transient fault — retrying produces the same
|
|
124
|
-
* answer forever.
|
|
125
|
-
*/
|
|
126
|
-
const UNRECOVERABLE_CONNECT_CODES = new Set([
|
|
127
|
-
"28P01", // invalid_password
|
|
128
|
-
"28000", // invalid_authorization_specification
|
|
129
|
-
"3D000", // invalid_catalog_name — the database does not exist
|
|
130
|
-
"42501" // insufficient_privilege
|
|
131
|
-
]);
|
|
132
|
-
|
|
133
|
-
export interface ConnectFailure {
|
|
134
|
-
/** True when retrying cannot help: the connection string itself is wrong. */
|
|
135
|
-
fatal: boolean;
|
|
136
|
-
/** The deepest message available — the Postgres one where there is one. */
|
|
137
|
-
reason: string;
|
|
138
|
-
/** The `SQLSTATE`, when the failure came from Postgres rather than the socket. */
|
|
139
|
-
code?: string;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Describe a failed connection attempt in terms a developer can act on.
|
|
144
|
-
*
|
|
145
|
-
* The error a caller catches is Drizzle's wrapper: its message is
|
|
146
|
-
* `Failed query: SELECT 1` and its stack runs through drizzle internals, while
|
|
147
|
-
* the sentence that says what is actually wrong — "password authentication
|
|
148
|
-
* failed for user …", "database … does not exist" — sits in `.cause`. Logging
|
|
149
|
-
* the wrapper, as the bootstrapper used to, tells a developer with a typo in
|
|
150
|
-
* their `DATABASE_URL` nothing at all.
|
|
151
|
-
*/
|
|
152
|
-
export function classifyConnectFailure(error: unknown): ConnectFailure {
|
|
153
|
-
const pgError = extractPgError(error);
|
|
154
|
-
const reason =
|
|
155
|
-
pgError?.message ??
|
|
156
|
-
extractCauseMessage(error) ??
|
|
157
|
-
(error instanceof Error ? error.message : String(error));
|
|
158
|
-
return {
|
|
159
|
-
fatal: Boolean(pgError?.code && UNRECOVERABLE_CONNECT_CODES.has(pgError.code)),
|
|
160
|
-
reason,
|
|
161
|
-
code: pgError?.code
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Detect whether an error is specifically a role-switching permission failure
|
|
167
|
-
* (e.g. "permission denied to set role" or "must be member of role"),
|
|
168
|
-
* as opposed to a table-level permission denial.
|
|
169
|
-
*
|
|
170
|
-
* This is used by the backend driver to auto-disable role switching when the
|
|
171
|
-
* connection user lacks SET ROLE privileges, rather than surfacing a confusing
|
|
172
|
-
* error to the Studio SQL Editor user.
|
|
173
|
-
*/
|
|
174
|
-
export function isRoleSwitchingPermissionError(error: unknown): boolean {
|
|
175
|
-
const pgError = extractPgError(error);
|
|
176
|
-
if (!pgError || pgError.code !== "42501") return false;
|
|
177
|
-
const msg = pgError.message.toLowerCase();
|
|
178
|
-
return msg.includes("set role") || msg.includes("member of role");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Was this `42501` the *caller* being refused by a policy, rather than the
|
|
183
|
-
* server lacking a privilege?
|
|
184
|
-
*
|
|
185
|
-
* Both arrive as `insufficient_privilege`, and they are opposite kinds of
|
|
186
|
-
* problem. A row-level-security refusal is a working access-control system
|
|
187
|
-
* doing its job: the caller asked for something their policies do not permit,
|
|
188
|
-
* which is a 403 and nobody's bug. A missing `GRANT` is the deployment being
|
|
189
|
-
* wrong — the connection role cannot touch the table at all, no policy is
|
|
190
|
-
* involved, and nothing the caller changes about the request will help.
|
|
191
|
-
*
|
|
192
|
-
* Postgres distinguishes them in the message, so this does too:
|
|
193
|
-
*
|
|
194
|
-
* new row violates row-level security policy for table "notes" → the caller
|
|
195
|
-
* permission denied for table notes → the server
|
|
196
|
-
*
|
|
197
|
-
* Only writes reach this. A read that RLS excludes is not an error — the rows
|
|
198
|
-
* are filtered and the caller gets an empty page — so the erroring case is
|
|
199
|
-
* specifically an `INSERT`/`UPDATE` whose row fails a policy's `WITH CHECK`.
|
|
200
|
-
*
|
|
201
|
-
* Matched on the message because that is the only thing carrying the
|
|
202
|
-
* distinction; the SQLSTATE is identical either way. Narrow by design: anything
|
|
203
|
-
* not naming row-level security stays the server's problem, since reporting a
|
|
204
|
-
* genuine privilege misconfiguration as "forbidden" would send an operator
|
|
205
|
-
* hunting for a policy bug that does not exist.
|
|
206
|
-
*/
|
|
207
|
-
export function isRowLevelSecurityDenial(error: unknown): boolean {
|
|
208
|
-
const pgError = extractPgError(error);
|
|
209
|
-
if (!pgError || pgError.code !== "42501") return false;
|
|
210
|
-
return pgError.message.toLowerCase().includes("row-level security policy");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Translate a raw PostgreSQL error into a user-friendly message.
|
|
215
|
-
*
|
|
216
|
-
* @param pgError - The extracted PostgreSQL error (from {@link extractPgError})
|
|
217
|
-
* @param context - A human-readable context string (e.g. collection slug or path)
|
|
218
|
-
* @returns An object with a `message` safe for the client and the PG `code`.
|
|
219
|
-
*/
|
|
220
|
-
export function pgErrorToFriendlyMessage(pgError: PostgresError, context: string): { message: string; code: string } {
|
|
221
|
-
const detail = pgError.detail as string | undefined;
|
|
222
|
-
const hint = pgError.hint as string | undefined;
|
|
223
|
-
const constraint = pgError.constraint as string | undefined;
|
|
224
|
-
const column = pgError.column as string | undefined;
|
|
225
|
-
const table = pgError.table as string | undefined;
|
|
226
|
-
const dataType = pgError.dataType as string | undefined;
|
|
227
|
-
const pgMessage = pgError.message || "Unknown database error";
|
|
228
|
-
const code = pgError.code || "UNKNOWN";
|
|
229
|
-
|
|
230
|
-
const suffix = hint ? ` Hint: ${hint}` : "";
|
|
231
|
-
const tableRef = table ?? context;
|
|
232
|
-
|
|
233
|
-
switch (pgError.code) {
|
|
234
|
-
case "23503": // foreign_key_violation
|
|
235
|
-
return {
|
|
236
|
-
message: detail
|
|
237
|
-
? `Foreign key constraint violated: ${detail}${suffix}`
|
|
238
|
-
: `Cannot complete operation: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
239
|
-
code
|
|
240
|
-
};
|
|
241
|
-
case "23505": // unique_violation
|
|
242
|
-
return {
|
|
243
|
-
message: detail
|
|
244
|
-
? `Duplicate value: ${detail}${suffix}`
|
|
245
|
-
: `Cannot complete operation: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
246
|
-
code
|
|
247
|
-
};
|
|
248
|
-
case "23502": // not_null_violation
|
|
249
|
-
return {
|
|
250
|
-
message: `Missing required field: "${column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`,
|
|
251
|
-
code
|
|
252
|
-
};
|
|
253
|
-
case "23514": // check_violation
|
|
254
|
-
return {
|
|
255
|
-
message: `Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
256
|
-
code
|
|
257
|
-
};
|
|
258
|
-
case "22P02": // invalid_text_representation (e.g. invalid UUID, wrong enum value)
|
|
259
|
-
return {
|
|
260
|
-
message: `Invalid data format in "${context}": ${pgMessage}${suffix}`,
|
|
261
|
-
code
|
|
262
|
-
};
|
|
263
|
-
case "22001": // string_data_right_truncation (value too long)
|
|
264
|
-
return {
|
|
265
|
-
message: `Value too long for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`,
|
|
266
|
-
code
|
|
267
|
-
};
|
|
268
|
-
case "22003": // numeric_value_out_of_range
|
|
269
|
-
return {
|
|
270
|
-
message: `Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`,
|
|
271
|
-
code
|
|
272
|
-
};
|
|
273
|
-
case "42703": // undefined_column
|
|
274
|
-
return {
|
|
275
|
-
message: `Unknown column in "${tableRef}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`,
|
|
276
|
-
code
|
|
277
|
-
};
|
|
278
|
-
case "42P01": // undefined_table
|
|
279
|
-
return {
|
|
280
|
-
message: `Table not found for "${context}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`,
|
|
281
|
-
code
|
|
282
|
-
};
|
|
283
|
-
case "42501": // insufficient_privilege
|
|
284
|
-
// Two unrelated failures share this SQLSTATE, and the old message
|
|
285
|
-
// named both causes because it could not tell them apart — which
|
|
286
|
-
// meant it was half wrong whichever one had happened, and sent the
|
|
287
|
-
// reader to check the other. Postgres says which in its own message.
|
|
288
|
-
return pgMessage.toLowerCase().includes("row-level security policy")
|
|
289
|
-
? {
|
|
290
|
-
// The caller. Their policies do not permit this row; the
|
|
291
|
-
// deployment is working exactly as configured.
|
|
292
|
-
message: `Not permitted to write this row in "${tableRef}": it does not satisfy the row-level security policy.${suffix}`,
|
|
293
|
-
code
|
|
294
|
-
}
|
|
295
|
-
: {
|
|
296
|
-
// The deployment. No policy is involved — the connecting
|
|
297
|
-
// role cannot touch the table at all.
|
|
298
|
-
message: `Permission denied on "${tableRef}": the database role this server connects as lacks privileges on it.${suffix}`,
|
|
299
|
-
code
|
|
300
|
-
};
|
|
301
|
-
case "28000": // invalid_authorization_specification
|
|
302
|
-
return {
|
|
303
|
-
message: `Authorization failed for "${context}". Check your database credentials.${suffix}`,
|
|
304
|
-
code
|
|
305
|
-
};
|
|
306
|
-
default: {
|
|
307
|
-
// Unhandled PG code — still surface the actual database message
|
|
308
|
-
const parts = [`Database error in "${context}" [${code}]: ${pgMessage}`];
|
|
309
|
-
if (detail) parts.push(`Detail: ${detail}`);
|
|
310
|
-
if (column) parts.push(`Column: ${column}`);
|
|
311
|
-
if (dataType) parts.push(`Data type: ${dataType}`);
|
|
312
|
-
if (constraint) parts.push(`Constraint: ${constraint}`);
|
|
313
|
-
if (hint) parts.push(`Hint: ${hint}`);
|
|
314
|
-
return { message: parts.join(". "), code };
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Sanitize any error into a message safe and helpful for the client.
|
|
321
|
-
*
|
|
322
|
-
* A deliberate 4xx (`ApiError`) passes through untouched — the server already
|
|
323
|
-
* decided what the client should read. Otherwise the PG error is extracted
|
|
324
|
-
* from the Drizzle cause chain, falling back to a generic message that
|
|
325
|
-
* doesn't leak SQL.
|
|
326
|
-
*
|
|
327
|
-
* @param error - The raw caught error
|
|
328
|
-
* @param context - A human-readable context string (e.g. collection path)
|
|
329
|
-
* @returns An object with `message` (user-friendly) and optional `code`
|
|
330
|
-
* (the `ApiError` code, or the PG SQLSTATE).
|
|
331
|
-
*/
|
|
332
|
-
export function sanitizeErrorForClient(error: unknown, context: string): { message: string; code?: string } {
|
|
333
|
-
// ── A deliberate 4xx is not a database failure ──────────────────
|
|
334
|
-
// Its message and code are written for the client; replacing them with
|
|
335
|
-
// "Check server logs" would discard the only diagnosis the caller gets
|
|
336
|
-
// (e.g. the offending filter field and the collection's valid ones).
|
|
337
|
-
// Log level follows the same convention as the HTTP error handler in
|
|
338
|
-
// @rebasepro/server: routine outcomes at debug, everything else at warn.
|
|
339
|
-
const clientError = asClientFacingError(error);
|
|
340
|
-
if (clientError) {
|
|
341
|
-
const line = `[API ${clientError.statusCode} ${clientError.code ?? "BAD_REQUEST"}] in "${context}": ${clientError.message}`;
|
|
342
|
-
if (clientError.expected) {
|
|
343
|
-
logger.debug(line);
|
|
344
|
-
} else {
|
|
345
|
-
logger.warn(`⚠️ ${line}`);
|
|
346
|
-
}
|
|
347
|
-
return { message: clientError.message, ...(clientError.code && { code: clientError.code }) };
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// ── Always log the full, unsanitized error server-side ──────────
|
|
351
|
-
const pgError = extractPgError(error);
|
|
352
|
-
|
|
353
|
-
if (pgError) {
|
|
354
|
-
logger.error(`[PG ${pgError.code}] Error in "${context}"`, {
|
|
355
|
-
code: pgError.code,
|
|
356
|
-
message: pgError.message,
|
|
357
|
-
detail: pgError.detail,
|
|
358
|
-
hint: pgError.hint,
|
|
359
|
-
column: pgError.column,
|
|
360
|
-
table: pgError.table,
|
|
361
|
-
constraint: pgError.constraint,
|
|
362
|
-
dataType: pgError.dataType
|
|
363
|
-
// The outer Drizzle wrapper message used to be logged here "for
|
|
364
|
-
// full context": it is `Failed query: <sql>\nparams: <values>`, so
|
|
365
|
-
// it published the statement and every bound value (an email, a
|
|
366
|
-
// password hash) on every realtime data failure. The SQLSTATE,
|
|
367
|
-
// detail, table, column and constraint above are the diagnostic
|
|
368
|
-
// value; the wrapper added only the leak. `logger` strips the
|
|
369
|
-
// wrapper as well, but the field itself carried nothing else.
|
|
370
|
-
});
|
|
371
|
-
return pgErrorToFriendlyMessage(pgError, context);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// No PG error found — log the raw error as-is
|
|
375
|
-
logger.error(`Database error in "${context}" (no PG error extracted)`, {
|
|
376
|
-
error: error instanceof Error ? error.message : String(error),
|
|
377
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
378
|
-
cause: error instanceof Error && error.cause
|
|
379
|
-
? (error.cause instanceof Error ? error.cause.message : String(error.cause))
|
|
380
|
-
: undefined
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
// Try to get the deepest cause message
|
|
384
|
-
const causeMessage = extractCauseMessage(error);
|
|
385
|
-
if (causeMessage) {
|
|
386
|
-
return { message: `Database error in "${context}": ${causeMessage}` };
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Last resort — generic message, never leak raw SQL
|
|
390
|
-
return { message: `Could not load data for "${context}". Check server logs for details.` };
|
|
391
|
-
}
|