@rebasepro/server-postgres 0.17.3 → 0.18.1

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.
Files changed (204) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/BranchService-ByLfM_Tt.js +360 -0
  4. package/dist/BranchService-ByLfM_Tt.js.map +1 -0
  5. package/dist/__vite-browser-external-BnuHet1e.js +46 -0
  6. package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
  7. package/dist/arg-Dni7MzLB.js +87 -0
  8. package/dist/arg-Dni7MzLB.js.map +1 -0
  9. package/dist/auth-users-columns-D2LBFrMH.js +116 -0
  10. package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
  11. package/dist/backup-argv.d.ts +19 -0
  12. package/dist/backup-cli-oM9WQVlV.js +408 -0
  13. package/dist/backup-cli-oM9WQVlV.js.map +1 -0
  14. package/dist/backup-service-Br3tqwsy.js +715 -0
  15. package/dist/backup-service-Br3tqwsy.js.map +1 -0
  16. package/dist/branch-argv.d.ts +28 -0
  17. package/dist/branch-prune.d.ts +77 -0
  18. package/dist/cli-collections-path.d.ts +31 -0
  19. package/dist/cli-errors-DxN4yzdH.js +657 -0
  20. package/dist/cli-errors-DxN4yzdH.js.map +1 -0
  21. package/dist/cli-errors.d.ts +123 -0
  22. package/dist/cli-flags.d.ts +101 -0
  23. package/dist/cli-helpers.d.ts +94 -10
  24. package/dist/cli-output-CNdMql-L.js +47 -0
  25. package/dist/cli-output-CNdMql-L.js.map +1 -0
  26. package/dist/cli.js +2020 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/collection-index-BHgT_TKd.js +276 -0
  29. package/dist/collection-index-BHgT_TKd.js.map +1 -0
  30. package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
  31. package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
  32. package/dist/{connection-GOKU3Hu5.js → databasePoolManager-Bj5FbeAs.js} +112 -40
  33. package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
  34. package/dist/databasePoolManager.d.ts +27 -0
  35. package/dist/doctor-D7HWmGzs.js +974 -0
  36. package/dist/doctor-D7HWmGzs.js.map +1 -0
  37. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-2c2NV8zX.js} +7 -27
  38. package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
  39. package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
  40. package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
  41. package/dist/ensure-tables-CIojK-Rx.js +650 -0
  42. package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
  43. package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
  44. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
  45. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
  46. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
  47. package/dist/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js} +126 -984
  48. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
  49. package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
  50. package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
  51. package/dist/index.d.ts +12 -0
  52. package/dist/index.es.js +7423 -1133
  53. package/dist/index.es.js.map +1 -1
  54. package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
  55. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
  56. package/dist/policy-drift-kNJ6X2Uj.js +313 -0
  57. package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
  58. package/dist/{backup-service-DCk7KhhL.js → rls-bootstrap-sql-DLDnUjtx.js} +737 -2689
  59. package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
  60. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-DeYEjYLt.js} +65 -190
  61. package/dist/rls-enforcement-DeYEjYLt.js.map +1 -0
  62. package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
  63. package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
  64. package/dist/schema/column-type-drift.d.ts +79 -0
  65. package/dist/schema/doctor-cli.js +129 -0
  66. package/dist/schema/doctor-cli.js.map +1 -0
  67. package/dist/schema/doctor.d.ts +42 -0
  68. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  69. package/dist/schema/generate-drizzle-schema.d.ts +1 -1
  70. package/dist/schema/generate-drizzle-schema.js +24 -0
  71. package/dist/schema/generate-drizzle-schema.js.map +1 -0
  72. package/dist/schema/generate-next-step.d.ts +30 -0
  73. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  74. package/dist/schema/generate-postgres-ddl.d.ts +1 -1
  75. package/dist/schema/generate-postgres-ddl.js +138 -0
  76. package/dist/schema/generate-postgres-ddl.js.map +1 -0
  77. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  78. package/dist/schema/introspect-db-queries.d.ts +34 -0
  79. package/dist/schema/introspect-db.js +866 -0
  80. package/dist/schema/introspect-db.js.map +1 -0
  81. package/dist/security/rls-enforcement.d.ts +63 -2
  82. package/dist/services/BranchService.d.ts +86 -1
  83. package/dist/services/collection-helpers.d.ts +1 -1
  84. package/dist/services/realtimeService.d.ts +30 -2
  85. package/dist/services/row-pipeline.d.ts +12 -1
  86. package/dist/source-Br7L7GOI.js +410 -0
  87. package/dist/source-Br7L7GOI.js.map +1 -0
  88. package/dist/utils/pg-error-utils.d.ts +3 -1
  89. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  90. package/dist/vector-index-DkWxm2de.js +309 -0
  91. package/dist/vector-index-DkWxm2de.js.map +1 -0
  92. package/package.json +32 -27
  93. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  94. package/dist/collection-index-BRUg10H5.js +0 -5690
  95. package/dist/collection-index-BRUg10H5.js.map +0 -1
  96. package/dist/connection-GOKU3Hu5.js.map +0 -1
  97. package/dist/ensure-collection-policies-UCqgv_8c.js.map +0 -1
  98. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  99. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  100. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  101. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  102. package/dist/src-DiB5RP2Z.js +0 -541
  103. package/dist/src-DiB5RP2Z.js.map +0 -1
  104. package/dist/websocket-BZ4H5wUz.js +0 -8198
  105. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  106. package/src/PostgresAdapter.ts +0 -91
  107. package/src/PostgresBackendDriver.ts +0 -1857
  108. package/src/PostgresBootstrapper.ts +0 -1157
  109. package/src/auth/ensure-tables.ts +0 -896
  110. package/src/auth/schema-version.ts +0 -260
  111. package/src/auth/services.ts +0 -1610
  112. package/src/backup/backup-cli.ts +0 -520
  113. package/src/backup/backup-cron.ts +0 -212
  114. package/src/backup/backup-logic.ts +0 -93
  115. package/src/backup/backup-service.ts +0 -485
  116. package/src/backup/index.ts +0 -12
  117. package/src/backup/pg-tools.ts +0 -403
  118. package/src/backup/retention.ts +0 -86
  119. package/src/cli-errors.ts +0 -265
  120. package/src/cli-helpers.ts +0 -569
  121. package/src/cli-output.ts +0 -43
  122. package/src/cli.ts +0 -1339
  123. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  124. package/src/collections/buildRegistry.ts +0 -68
  125. package/src/collections/validate-relations.ts +0 -402
  126. package/src/connection.ts +0 -333
  127. package/src/data-transformer.ts +0 -845
  128. package/src/databasePoolManager.ts +0 -95
  129. package/src/history/HistoryService.ts +0 -254
  130. package/src/history/ensure-history-table.ts +0 -53
  131. package/src/index.ts +0 -19
  132. package/src/interfaces.ts +0 -60
  133. package/src/module-dir.ts +0 -7
  134. package/src/schema/atlas-argv.ts +0 -94
  135. package/src/schema/auth-schema.ts +0 -298
  136. package/src/schema/auth-users-columns.ts +0 -131
  137. package/src/schema/carved-out-migration.ts +0 -404
  138. package/src/schema/classify-change.ts +0 -436
  139. package/src/schema/collection-index.ts +0 -427
  140. package/src/schema/collections-schema-version.ts +0 -103
  141. package/src/schema/destructive-sql.ts +0 -94
  142. package/src/schema/doctor-cli.ts +0 -66
  143. package/src/schema/doctor-policy-checks.ts +0 -105
  144. package/src/schema/doctor.ts +0 -837
  145. package/src/schema/drizzle-ddl.ts +0 -33
  146. package/src/schema/dynamic-tables.test.ts +0 -302
  147. package/src/schema/dynamic-tables.ts +0 -293
  148. package/src/schema/ensure-collection-policies.ts +0 -198
  149. package/src/schema/ensure-collection-tables.test.ts +0 -518
  150. package/src/schema/ensure-collection-tables.ts +0 -1267
  151. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  152. package/src/schema/generate-drizzle-schema.ts +0 -143
  153. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  154. package/src/schema/generate-postgres-ddl.ts +0 -180
  155. package/src/schema/generate-schema-commit.ts +0 -267
  156. package/src/schema/generated-schema-staleness.ts +0 -284
  157. package/src/schema/introspect-db-constraints.ts +0 -385
  158. package/src/schema/introspect-db-inference.ts +0 -255
  159. package/src/schema/introspect-db-logic.ts +0 -1387
  160. package/src/schema/introspect-db-naming.ts +0 -15
  161. package/src/schema/introspect-db-project.ts +0 -78
  162. package/src/schema/introspect-db-queries.ts +0 -326
  163. package/src/schema/introspect-db-structure.ts +0 -670
  164. package/src/schema/introspect-db-types.ts +0 -56
  165. package/src/schema/introspect-db.ts +0 -257
  166. package/src/schema/introspect-runtime.test.ts +0 -260
  167. package/src/schema/introspect-runtime.ts +0 -327
  168. package/src/schema/non-sql-collections.test.ts +0 -131
  169. package/src/schema/rls-bootstrap-sql.ts +0 -288
  170. package/src/schema/search-column.ts +0 -643
  171. package/src/schema/test-schema.ts +0 -11
  172. package/src/schema/vector-index.ts +0 -491
  173. package/src/security/anonymous-grants.test.ts +0 -73
  174. package/src/security/policy-drift.test.ts +0 -435
  175. package/src/security/policy-drift.ts +0 -473
  176. package/src/security/rls-enforcement.ts +0 -505
  177. package/src/services/BranchService.ts +0 -326
  178. package/src/services/FetchService.ts +0 -2115
  179. package/src/services/PersistService.ts +0 -557
  180. package/src/services/RelationService.ts +0 -978
  181. package/src/services/RelationWriteService.ts +0 -653
  182. package/src/services/cdc/CdcListener.ts +0 -103
  183. package/src/services/cdc/junction-tables.ts +0 -91
  184. package/src/services/cdc/trigger-cdc.ts +0 -173
  185. package/src/services/channel-bus/ChannelBus.ts +0 -44
  186. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  187. package/src/services/channel-bus/index.ts +0 -114
  188. package/src/services/channel-history.ts +0 -424
  189. package/src/services/channel-presence.ts +0 -185
  190. package/src/services/collection-helpers.ts +0 -420
  191. package/src/services/dataService.ts +0 -256
  192. package/src/services/index.ts +0 -14
  193. package/src/services/junction-writes.ts +0 -295
  194. package/src/services/nested-path.ts +0 -145
  195. package/src/services/pg-notify-listener.ts +0 -151
  196. package/src/services/realtimeService.ts +0 -2545
  197. package/src/services/row-pipeline.ts +0 -240
  198. package/src/services/write-denial.ts +0 -55
  199. package/src/types.ts +0 -4
  200. package/src/utils/connection-string.ts +0 -58
  201. package/src/utils/drizzle-conditions.ts +0 -2743
  202. package/src/utils/pg-array-null-patch.ts +0 -42
  203. package/src/utils/pg-error-utils.ts +0 -391
  204. 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
- }