@rebasepro/server-postgres 0.24.0 → 0.24.1-canary.g56af4a4

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 (162) hide show
  1. package/dist/{BranchService-BRt78gfa.js → BranchService-Bsn2wfaj.js} +3 -2
  2. package/dist/{BranchService-BRt78gfa.js.map → BranchService-Bsn2wfaj.js.map} +1 -1
  3. package/dist/PostgresBackendDriver.d.ts +83 -2
  4. package/dist/auth/ensure-tables.d.ts +12 -0
  5. package/dist/auth/services.d.ts +6 -0
  6. package/dist/backup/backup-cli.d.ts +14 -0
  7. package/dist/backup/backup-service.d.ts +2 -2
  8. package/dist/backup/pg-binary.d.ts +5 -0
  9. package/dist/{backup-cli-DW5p9_zv.js → backup-cli-aUfRv16h.js} +37 -29
  10. package/dist/backup-cli-aUfRv16h.js.map +1 -0
  11. package/dist/{backup-service-EwcDVG-8.js → backup-service-BTBB7RXn.js} +55 -21
  12. package/dist/backup-service-BTBB7RXn.js.map +1 -0
  13. package/dist/boot-ddl-DVpsFN6r.js +105 -0
  14. package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
  15. package/dist/cli-errors-CaI4ntyD.js +1919 -0
  16. package/dist/cli-errors-CaI4ntyD.js.map +1 -0
  17. package/dist/cli-errors.d.ts +51 -5
  18. package/dist/{cli-flags-BglvpjHv.js → cli-flags-B-Dawocl.js} +5 -2
  19. package/dist/cli-flags-B-Dawocl.js.map +1 -0
  20. package/dist/cli-helpers.d.ts +40 -0
  21. package/dist/cli-output.d.ts +16 -28
  22. package/dist/cli-push.d.ts +87 -0
  23. package/dist/cli.js +563 -182
  24. package/dist/cli.js.map +1 -1
  25. package/dist/{column-plan-helpers-1-LQD0yI.js → column-plan-helpers-drZIsSNf.js} +10 -4
  26. package/dist/{column-plan-helpers-1-LQD0yI.js.map → column-plan-helpers-drZIsSNf.js.map} +1 -1
  27. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-BPoNILnD.js} +19 -110
  28. package/dist/connection-BPoNILnD.js.map +1 -0
  29. package/dist/connection.d.ts +9 -1
  30. package/dist/databasePoolManager-BkEy2w18.js +119 -0
  31. package/dist/databasePoolManager-BkEy2w18.js.map +1 -0
  32. package/dist/{ensure-collection-tables-kkkHk8oo.js → deferred-index-build-DP1_SNFo.js} +484 -346
  33. package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
  34. package/dist/{doctor-C-sYWbmt.js → doctor-C4WiftHm.js} +63 -52
  35. package/dist/doctor-C4WiftHm.js.map +1 -0
  36. package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
  37. package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
  38. package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
  39. package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
  40. package/dist/{ensure-tables-BmI_tRxc.js → ensure-tables-CHsgyLqg.js} +172 -76
  41. package/dist/ensure-tables-CHsgyLqg.js.map +1 -0
  42. package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
  43. package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
  44. package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
  45. package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
  46. package/dist/{generate-drizzle-schema-logic-sSFDp6LR.js → generate-drizzle-schema-logic-B74WN37S.js} +5 -5
  47. package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
  48. package/dist/generate-files-CdEckX6c.js +199 -0
  49. package/dist/generate-files-CdEckX6c.js.map +1 -0
  50. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js → generate-postgres-ddl-logic-B64O2oZF.js} +3 -3
  51. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js.map → generate-postgres-ddl-logic-B64O2oZF.js.map} +1 -1
  52. package/dist/generated-sql.d.ts +8 -2
  53. package/dist/history/HistoryService.d.ts +38 -6
  54. package/dist/history/ensure-history-table.d.ts +42 -4
  55. package/dist/index.es.js +2922 -7475
  56. package/dist/index.es.js.map +1 -1
  57. package/dist/{introspect-db-logic-kCETE8TY.js → introspect-db-logic-C-Di8dA9.js} +44 -6
  58. package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
  59. package/dist/{introspect-db-queries-C_Q5VgQw.js → introspect-db-queries-D6kAqReI.js} +23 -1
  60. package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
  61. package/dist/pg-error-utils-vJezlXdI.js +388 -0
  62. package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
  63. package/dist/{plan-schema-DU9exq6C.js → plan-schema-B1YF_tYq.js} +151 -41
  64. package/dist/plan-schema-B1YF_tYq.js.map +1 -0
  65. package/dist/{policy-drift-B-J2hhm0.js → policy-drift-Dm8i-s1J.js} +23 -6
  66. package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
  67. package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
  68. package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
  69. package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
  70. package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
  71. package/dist/{render-ddl-Ds2t_d9V.js → render-ddl-CtC3y9Ol.js} +8 -4
  72. package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
  73. package/dist/{rls-bootstrap-sql-_KNnjanK.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1270
  74. package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
  75. package/dist/{rls-enforcement-CfXOJJaW.js → rls-enforcement-DvzqlaL1.js} +129 -17
  76. package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
  77. package/dist/schema/auth-schema.d.ts +2 -2
  78. package/dist/schema/boot-ddl.d.ts +47 -0
  79. package/dist/schema/collection-index.d.ts +45 -0
  80. package/dist/schema/column-plan-helpers.d.ts +5 -0
  81. package/dist/schema/deferred-index-build.d.ts +131 -0
  82. package/dist/schema/destructive-sql.d.ts +7 -0
  83. package/dist/schema/doctor-cli.js +110 -6
  84. package/dist/schema/doctor-cli.js.map +1 -1
  85. package/dist/schema/doctor-search-scale.d.ts +45 -0
  86. package/dist/schema/doctor.d.ts +20 -1
  87. package/dist/schema/ensure-collection-policies.d.ts +73 -35
  88. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  89. package/dist/schema/enum-labels-kept.d.ts +26 -0
  90. package/dist/schema/generate-drizzle-schema.js +1 -1
  91. package/dist/schema/generate-files.d.ts +44 -0
  92. package/dist/schema/generate-next-step.d.ts +0 -13
  93. package/dist/schema/generate-postgres-ddl.js +10 -81
  94. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  95. package/dist/schema/introspect-db-logic.d.ts +12 -0
  96. package/dist/schema/introspect-db-queries.d.ts +11 -1
  97. package/dist/schema/introspect-db-structure.d.ts +10 -0
  98. package/dist/schema/introspect-db.js +4 -5
  99. package/dist/schema/introspect-db.js.map +1 -1
  100. package/dist/schema/migration-post-process.d.ts +34 -0
  101. package/dist/schema/plan/diff-plan.d.ts +126 -0
  102. package/dist/schema/plan/types.d.ts +8 -0
  103. package/dist/schema/push-index-prebuild.d.ts +25 -0
  104. package/dist/schema/push-legacy-renames.d.ts +20 -0
  105. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  106. package/dist/security/rls-enforcement.d.ts +32 -4
  107. package/dist/services/FetchService.d.ts +58 -8
  108. package/dist/services/PersistService.d.ts +15 -0
  109. package/dist/services/RelationService.d.ts +88 -3
  110. package/dist/services/cdc/CdcListener.d.ts +17 -6
  111. package/dist/services/cdc/trigger-cdc.d.ts +151 -27
  112. package/dist/services/channel-history.d.ts +15 -4
  113. package/dist/services/collection-helpers.d.ts +15 -0
  114. package/dist/services/field-viewer.d.ts +15 -1
  115. package/dist/services/pg-notify-listener.d.ts +7 -0
  116. package/dist/services/realtimeService.d.ts +314 -14
  117. package/dist/services/row-pipeline.d.ts +16 -9
  118. package/dist/services/socket-liveness.d.ts +42 -2
  119. package/dist/services/sql-script.d.ts +207 -3
  120. package/dist/services/vector-iterative-scan.d.ts +40 -0
  121. package/dist/services/write-depth.d.ts +6 -0
  122. package/dist/services/write-transaction-scope.d.ts +9 -0
  123. package/dist/socket-liveness-D3UD5_BW.js +7570 -0
  124. package/dist/socket-liveness-D3UD5_BW.js.map +1 -0
  125. package/dist/socket-opt-ins.d.ts +64 -0
  126. package/dist/source-DBDbBTL4.js +811 -0
  127. package/dist/source-DBDbBTL4.js.map +1 -0
  128. package/dist/tsx-loader.d.ts +14 -0
  129. package/dist/utils/drizzle-conditions.d.ts +21 -1
  130. package/dist/utils/error-cause.d.ts +7 -0
  131. package/dist/utils/pg-error-utils.d.ts +2 -4
  132. package/dist/utils/result-rows.d.ts +10 -0
  133. package/dist/websocket.d.ts +13 -0
  134. package/package.json +7 -7
  135. package/dist/backup-cli-DW5p9_zv.js.map +0 -1
  136. package/dist/backup-service-EwcDVG-8.js.map +0 -1
  137. package/dist/cli-errors-DsA-K9uP.js +0 -893
  138. package/dist/cli-errors-DsA-K9uP.js.map +0 -1
  139. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  140. package/dist/cli-output-CNdMql-L.js +0 -47
  141. package/dist/cli-output-CNdMql-L.js.map +0 -1
  142. package/dist/collection-index-DdnsxiJ_.js +0 -286
  143. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  144. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  145. package/dist/doctor-C-sYWbmt.js.map +0 -1
  146. package/dist/ensure-collection-policies-B1ureSIV.js +0 -160
  147. package/dist/ensure-collection-policies-B1ureSIV.js.map +0 -1
  148. package/dist/ensure-collection-tables-kkkHk8oo.js.map +0 -1
  149. package/dist/ensure-tables-BmI_tRxc.js.map +0 -1
  150. package/dist/generate-drizzle-schema-93M0lUxK.js +0 -123
  151. package/dist/generate-drizzle-schema-93M0lUxK.js.map +0 -1
  152. package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +0 -1
  153. package/dist/introspect-db-logic-kCETE8TY.js.map +0 -1
  154. package/dist/introspect-db-queries-C_Q5VgQw.js.map +0 -1
  155. package/dist/plan-schema-DU9exq6C.js.map +0 -1
  156. package/dist/policy-drift-B-J2hhm0.js.map +0 -1
  157. package/dist/render-ddl-Ds2t_d9V.js.map +0 -1
  158. package/dist/rls-bootstrap-sql-_KNnjanK.js.map +0 -1
  159. package/dist/rls-enforcement-CfXOJJaW.js.map +0 -1
  160. package/dist/source-Br7L7GOI.js +0 -410
  161. package/dist/source-Br7L7GOI.js.map +0 -1
  162. package/dist/utils/sql-redaction.d.ts +0 -21
@@ -228,6 +228,21 @@ var UNIQUE_CONSTRAINTS_QUERY = `
228
228
  ORDER BY t.relname, ic.relname
229
229
  `;
230
230
  /**
231
+ * Every index on the schema's tables, by name. Only the name is needed: it is
232
+ * what tells a foreign key Rebase already indexed apart from one it would
233
+ * index on the first push — see `foreignKeyIndexSpec`.
234
+ */
235
+ var INDEXES_QUERY = `
236
+ SELECT t.relname AS table_name, ic.relname AS index_name
237
+ FROM pg_index i
238
+ JOIN pg_class t ON t.oid = i.indrelid
239
+ JOIN pg_class ic ON ic.oid = i.indexrelid
240
+ JOIN pg_namespace n ON n.oid = t.relnamespace
241
+ WHERE n.nspname = $1
242
+ AND NOT t.relispartition
243
+ ORDER BY t.relname, ic.relname
244
+ `;
245
+ /**
231
246
  * CHECK constraints, as the source text Postgres reproduces them from.
232
247
  *
233
248
  * `pg_get_constraintdef` output is normalized by the server — the parser in
@@ -249,6 +264,10 @@ var CHECK_CONSTRAINTS_QUERY = `
249
264
  /**
250
265
  * `COMMENT ON TABLE` / `COMMENT ON COLUMN`, which is documentation the author
251
266
  * already wrote and which nothing downstream has ever read.
267
+ *
268
+ * Only comments on relations: `pg_description` also holds the stamps boot
269
+ * writes on each policy (`rebase:<hash>`), and an OID is unique only within
270
+ * its own catalogue, so a policy's could name a table.
252
271
  */
253
272
  var COMMENTS_QUERY = `
254
273
  SELECT t.relname AS table_name,
@@ -259,6 +278,7 @@ var COMMENTS_QUERY = `
259
278
  JOIN pg_namespace n ON n.oid = t.relnamespace
260
279
  LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.objsubid
261
280
  WHERE n.nspname = $1
281
+ AND d.classoid = 'pg_catalog.pg_class'::regclass
262
282
  AND t.relkind IN ('r', 'p')
263
283
  AND (d.objsubid = 0 OR a.attname IS NOT NULL)
264
284
  ORDER BY t.relname, d.objsubid
@@ -294,6 +314,7 @@ async function readSchemaMetadata(client, schema) {
294
314
  const pks = await client.query(PRIMARY_KEYS_QUERY, [schema]);
295
315
  const fks = await client.query(FOREIGN_KEYS_QUERY, [schema]);
296
316
  const uniques = await client.query(UNIQUE_CONSTRAINTS_QUERY, [schema]);
317
+ const indexes = await client.query(INDEXES_QUERY, [schema]);
297
318
  const checks = await client.query(CHECK_CONSTRAINTS_QUERY, [schema]);
298
319
  const comments = await client.query(COMMENTS_QUERY, [schema]);
299
320
  const known = new Set(tables.rows.map((t) => t.table_name));
@@ -306,6 +327,7 @@ async function readSchemaMetadata(client, schema) {
306
327
  pks: scoped(pks.rows),
307
328
  fks: scoped(fks.rows).filter((fk) => known.has(fk.foreign_table_name)),
308
329
  uniques: scoped(uniques.rows),
330
+ indexes: scoped(indexes.rows),
309
331
  checks: scoped(checks.rows),
310
332
  comments: scoped(comments.rows),
311
333
  rowCounts: {}
@@ -314,4 +336,4 @@ async function readSchemaMetadata(client, schema) {
314
336
  //#endregion
315
337
  export { readSchemaMetadata as i, isUsableFileName as n, quoteRelation as r, countRowsUpTo as t };
316
338
 
317
- //# sourceMappingURL=introspect-db-queries-C_Q5VgQw.js.map
339
+ //# sourceMappingURL=introspect-db-queries-D6kAqReI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspect-db-queries-D6kAqReI.js","names":[],"sources":["../src/schema/introspect-db-queries.ts"],"sourcesContent":["/**\n * The catalog queries introspection runs, and the shape they produce.\n *\n * These live apart from {@link ./introspect-db.ts} (the CLI entry point) on\n * purpose: the fixture-capture script and the live e2e read a database through\n * this module too, so what the tests see is what the CLI sees. A query that\n * only exists inside the CLI's `main()` can only be tested by running the CLI.\n *\n * No side effects beyond `SELECT`. Introspection reads a database it does not\n * own — it must never `ANALYZE`, create a temp table, or otherwise write.\n */\nimport type {\n TableRow,\n TableColumn,\n EnumValue,\n PrimaryKeyRow,\n ForeignKeyRow,\n UniqueConstraintRow,\n IndexRow,\n CheckConstraintRow,\n CommentRow,\n SchemaMetadata\n} from \"./introspect-db-logic\";\n\n/** The subset of `pg.Client` this module needs, so tests can pass a fake. */\nexport interface QueryableClient {\n query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<{ rows: R[] }>;\n}\n\n/**\n * A Postgres identifier, quoted so that whatever is inside it stays inside it.\n *\n * Introspection reads table and schema names out of `pg_class` and puts them\n * straight back into SQL, and wrapping a name in double quotes is not the same\n * as escaping it: a table named `x\" ; DROP TABLE users; --` closes the quote\n * and then the statement. Postgres's own escape for a quote inside a quoted\n * identifier is to double it, which is what this does.\n *\n * \"The catalogue is trusted\" is true of your own database, and is exactly the\n * assumption this tool cannot make. Introspection exists to be pointed at\n * databases somebody else built, and it runs with the privileges of whoever\n * ran it — which is usually a superuser, because that is who has a connection\n * string to a database they are adopting.\n */\nexport function quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, \"\\\"\\\"\")}\"`;\n}\n\n/** `\"schema\".\"table\"`, both halves quoted. */\nexport function quoteRelation(schema: string, table: string): string {\n return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;\n}\n\n/**\n * Can this table's name be the name of the file describing it?\n *\n * Introspection writes `<collectionsDir>/<tableName>.ts`, and the table name\n * came from a database this tool did not create. Postgres will hold a table\n * called `../../../etc/cron.d/x`, and `path.join` resolves it — so without this\n * the generator writes a file wherever the name points, as whoever ran the\n * command.\n *\n * Deliberately narrow, and deliberately not a blocklist of `..` and `/`: the\n * question is \"is this a plain file name?\", and every answer that is not\n * obviously yes is a table better written by hand. NUL and the Windows\n * separator are in scope because a collections directory is checked out on\n * whatever machine the developer has.\n */\nexport function isUsableFileName(name: string): boolean {\n if (name.length === 0 || name.length > 200) return false;\n if (name === \".\" || name === \"..\") return false;\n // Path separators (both platforms'), the Windows drive separator, and any\n // control character — a NUL truncates the path at the OS boundary, so a\n // name that reads as harmless in a log can be something else on disk.\n // eslint-disable-next-line no-control-regex\n if (/[/\\\\:\\u0000-\\u001f]/.test(name)) return false;\n // A leading dot makes a hidden file, and a trailing one is invalid on\n // Windows; neither is a table anyone means to generate.\n return !name.startsWith(\".\") && !name.endsWith(\".\");\n}\n\n/**\n * Base tables, excluding partitions.\n *\n * `relispartition` is the reason this reads `pg_class` rather than\n * `information_schema.tables`, which reports every partition as a base table of\n * its own. Pagila partitions `payment` by month; introspected through\n * information_schema it yields `payment` plus 26 near-identical\n * `payment_p2022_*` collections, each with its own nav entry.\n *\n * `relkind = 'p'` (the partitioned parent) is included and `'r'` partitions are\n * dropped, which is the right way round: the parent is the queryable table.\n */\nexport const TABLES_QUERY = `\n SELECT c.relname AS table_name,\n c.relkind = 'p' AS is_partitioned\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = $1\n AND c.relkind IN ('r', 'p')\n AND NOT c.relispartition\n AND c.relname NOT LIKE 'drizzle_%'\n AND c.relname NOT LIKE 'rebase_%'\n ORDER BY c.relname\n`;\n\n/**\n * Columns, with the catalog facts that say a column is not the user's to edit.\n *\n * `is_generated`/`is_identity` are read for that reason: a generated column\n * rejects any write, so a form that offers it is offering a field that always\n * errors. `character_maximum_length` is the declared `varchar(n)` bound —\n * already enforced by the database, and free to surface as validation.\n */\nexport const COLUMNS_QUERY = `\n SELECT\n c.table_name,\n c.column_name,\n c.data_type,\n c.udt_name,\n c.is_nullable,\n c.column_default,\n c.ordinal_position,\n c.is_generated,\n c.generation_expression,\n c.is_identity,\n c.identity_generation,\n c.character_maximum_length,\n c.numeric_precision,\n c.numeric_scale,\n (SELECT a.atttypmod FROM pg_attribute a\n JOIN pg_class pc ON a.attrelid = pc.oid\n WHERE pc.relname = c.table_name\n AND a.attname = c.column_name\n AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod\n FROM information_schema.columns c\n WHERE c.table_schema = $1\n ORDER BY c.table_name, c.ordinal_position\n`;\n\nexport const ENUMS_QUERY = `\n SELECT t.typname AS enum_name,\n e.enumlabel AS enum_value,\n e.enumsortorder AS sort_order\n FROM pg_type t\n JOIN pg_enum e ON t.oid = e.enumtypid\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE n.nspname = $1\n ORDER BY t.typname, e.enumsortorder\n`;\n\n/**\n * Primary key columns, in key order.\n *\n * `ORDER BY k.ord` matters for composite keys: the position of a column inside\n * the key is what tells a two-column PK made of two foreign keys apart from an\n * ordinary composite key, and `= ANY(i.indkey)` (the shape this replaced)\n * returns catalog order instead.\n */\nexport const PRIMARY_KEYS_QUERY = `\n SELECT t.relname AS table_name,\n a.attname AS column_name\n FROM pg_index i\n JOIN pg_class t ON t.oid = i.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord)\n JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum\n WHERE i.indisprimary AND n.nspname = $1\n ORDER BY t.relname, k.ord\n`;\n\n/**\n * Foreign keys, one row per referencing column, with the delete rule.\n *\n * Read from `pg_constraint` rather than through\n * `information_schema.constraint_column_usage`, which does not preserve the\n * pairing on a composite foreign key: it reports the cross product of the\n * constraint's columns, so a two-column FK comes back as four rows, two of them\n * pairing the wrong columns. `unnest(...) WITH ORDINALITY` on both sides joined\n * on the ordinal keeps each referencing column with the column it references.\n *\n * `confdeltype` is carried because `ON DELETE CASCADE` is the schema author\n * saying, in the database, that the child cannot outlive the parent — the one\n * unambiguous declaration of ownership a schema contains.\n *\n * Constraints declared on a *partition* are attributed to the partition root,\n * because that is the table this run generates a collection for. Pagila declares\n * `payment`'s three foreign keys on each monthly partition and none on the\n * parent, so reading `pg_constraint` at face value gives the `payment`\n * collection no relations at all. `pick = 1` then keeps one row per logical key,\n * preferring the root's own constraint when it has one, so a schema that\n * declares the key on the parent *and* inherits it onto 26 partitions does not\n * yield 27 copies of the same relation.\n */\nexport const FOREIGN_KEYS_QUERY = `\n WITH fk AS (\n SELECT con.oid,\n con.conname,\n con.conrelid,\n con.confrelid,\n con.conkey,\n con.confkey,\n con.confdeltype,\n COALESCE(pg_partition_root(con.conrelid), con.conrelid) AS root_oid,\n row_number() OVER (\n PARTITION BY COALESCE(pg_partition_root(con.conrelid), con.conrelid),\n con.confrelid, con.conkey, con.confkey\n ORDER BY (con.conrelid = COALESCE(pg_partition_root(con.conrelid), con.conrelid)) DESC,\n con.oid\n ) AS pick\n FROM pg_constraint con\n JOIN pg_namespace n ON n.oid = con.connamespace\n WHERE con.contype = 'f' AND n.nspname = $1\n )\n SELECT root.relname AS table_name,\n sa.attname AS column_name,\n tgt.relname AS foreign_table_name,\n ta.attname AS foreign_column_name,\n fk.conname AS constraint_name,\n s.ord::int AS ordinal,\n CASE fk.confdeltype\n WHEN 'a' THEN 'NO ACTION'\n WHEN 'r' THEN 'RESTRICT'\n WHEN 'c' THEN 'CASCADE'\n WHEN 'n' THEN 'SET NULL'\n WHEN 'd' THEN 'SET DEFAULT'\n END AS delete_rule\n FROM fk\n JOIN pg_class root ON root.oid = fk.root_oid\n JOIN pg_class tgt ON tgt.oid = fk.confrelid\n CROSS JOIN LATERAL unnest(fk.conkey) WITH ORDINALITY AS s(attnum, ord)\n JOIN pg_attribute sa ON sa.attrelid = fk.conrelid AND sa.attnum = s.attnum\n CROSS JOIN LATERAL unnest(fk.confkey) WITH ORDINALITY AS f(attnum, ord)\n JOIN pg_attribute ta ON ta.attrelid = fk.confrelid AND ta.attnum = f.attnum\n WHERE fk.pick = 1 AND s.ord = f.ord\n ORDER BY root.relname, fk.conname, s.ord\n`;\n\n/**\n * Unique constraints and unique indexes, as ordered column lists.\n *\n * Indexes rather than constraints alone, because `CREATE UNIQUE INDEX` and\n * `ADD CONSTRAINT ... UNIQUE` produce the same guarantee and schemas use both.\n * Partial indexes (`indpred IS NOT NULL`) are excluded: they promise uniqueness\n * only over the rows matching their predicate, which is not the promise a\n * `validation: { unique: true }` field makes.\n */\nexport const UNIQUE_CONSTRAINTS_QUERY = `\n SELECT t.relname AS table_name,\n ic.relname AS constraint_name,\n -- ::text, because the driver has no parser for an array of \\`name\\`\n -- and hands back the raw \\`{a,b}\\` literal as a string. Every consumer\n -- here indexes and compares it as an array, and a string of that\n -- shape fails those silently rather than loudly.\n array_agg(a.attname::text ORDER BY k.ord) AS column_names\n FROM pg_index i\n JOIN pg_class t ON t.oid = i.indrelid\n JOIN pg_class ic ON ic.oid = i.indexrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord)\n JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum\n WHERE i.indisunique\n AND NOT i.indisprimary\n AND i.indpred IS NULL\n AND n.nspname = $1\n AND NOT t.relispartition\n GROUP BY t.relname, ic.relname\n ORDER BY t.relname, ic.relname\n`;\n\n/**\n * Every index on the schema's tables, by name. Only the name is needed: it is\n * what tells a foreign key Rebase already indexed apart from one it would\n * index on the first push — see `foreignKeyIndexSpec`.\n */\nexport const INDEXES_QUERY = `\n SELECT t.relname AS table_name, ic.relname AS index_name\n FROM pg_index i\n JOIN pg_class t ON t.oid = i.indrelid\n JOIN pg_class ic ON ic.oid = i.indexrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE n.nspname = $1\n AND NOT t.relispartition\n ORDER BY t.relname, ic.relname\n`;\n\n/**\n * CHECK constraints, as the source text Postgres reproduces them from.\n *\n * `pg_get_constraintdef` output is normalized by the server — the parser in\n * {@link ./introspect-db-constraints.ts} reads that normalized form, not\n * whatever the author typed.\n */\nexport const CHECK_CONSTRAINTS_QUERY = `\n SELECT DISTINCT\n root.relname AS table_name,\n con.conname AS constraint_name,\n pg_get_constraintdef(con.oid) AS definition\n FROM pg_constraint con\n JOIN pg_namespace n ON n.oid = con.connamespace\n JOIN pg_class root ON root.oid = COALESCE(pg_partition_root(con.conrelid), con.conrelid)\n WHERE con.contype = 'c'\n AND n.nspname = $1\n ORDER BY root.relname, con.conname\n`;\n\n/**\n * `COMMENT ON TABLE` / `COMMENT ON COLUMN`, which is documentation the author\n * already wrote and which nothing downstream has ever read.\n *\n * Only comments on relations: `pg_description` also holds the stamps boot\n * writes on each policy (`rebase:<hash>`), and an OID is unique only within\n * its own catalogue, so a policy's could name a table.\n */\nexport const COMMENTS_QUERY = `\n SELECT t.relname AS table_name,\n a.attname AS column_name,\n d.description AS comment\n FROM pg_description d\n JOIN pg_class t ON t.oid = d.objoid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.objsubid\n WHERE n.nspname = $1\n AND d.classoid = 'pg_catalog.pg_class'::regclass\n AND t.relkind IN ('r', 'p')\n AND (d.objsubid = 0 OR a.attname IS NOT NULL)\n ORDER BY t.relname, d.objsubid\n`;\n\n/**\n * Counts a table's rows, stopping once the answer can only be \"more than\n * `limit`\".\n *\n * Introspection asks this to tell a small reference table from a table that\n * merely looks like one (see `classifyTables`). `reltuples` would answer for\n * free but lies on a table that has never been analyzed — which is every table\n * in a database restored from a dump, i.e. exactly the case introspection meets\n * — and reports -1 there rather than an error, so the lie is silent.\n *\n * The subquery's LIMIT caps the work at `limit + 1` rows however large the\n * table is, so this stays cheap on a table with a billion rows.\n */\nexport async function countRowsUpTo(\n client: QueryableClient,\n schema: string,\n table: string,\n limit: number\n): Promise<number> {\n const { rows } = await client.query<{ count: string }>(\n `SELECT count(*)::text AS count FROM (SELECT 1 FROM ${quoteRelation(schema, table)} LIMIT ${limit + 1}) probe`\n );\n return Number(rows[0]?.count ?? 0);\n}\n\n/**\n * Reads everything the generator needs, in one pass.\n *\n * `rowCounts` is deliberately absent here: it costs a query per table and only\n * a handful of tables can possibly need it. The caller fills it in for the\n * candidates `lookupCandidates()` names.\n */\nexport async function readSchemaMetadata(\n client: QueryableClient,\n schema: string\n): Promise<SchemaMetadata> {\n // Sequential, not `Promise.all`. A `pg.Client` is a single connection with\n // one protocol stream: concurrent `query()` calls on one are queued and\n // deprecated (removed in pg@9), so the parallel version buys nothing and\n // warns while doing it.\n const tables = await client.query<TableRow>(TABLES_QUERY, [schema]);\n const columns = await client.query<TableColumn>(COLUMNS_QUERY, [schema]);\n const enumValues = await client.query<EnumValue>(ENUMS_QUERY, [schema]);\n const pks = await client.query<PrimaryKeyRow>(PRIMARY_KEYS_QUERY, [schema]);\n const fks = await client.query<ForeignKeyRow>(FOREIGN_KEYS_QUERY, [schema]);\n const uniques = await client.query<UniqueConstraintRow>(UNIQUE_CONSTRAINTS_QUERY, [schema]);\n const indexes = await client.query<IndexRow>(INDEXES_QUERY, [schema]);\n const checks = await client.query<CheckConstraintRow>(CHECK_CONSTRAINTS_QUERY, [schema]);\n const comments = await client.query<CommentRow>(COMMENTS_QUERY, [schema]);\n\n // A column list scoped to the schema still names tables this run excludes —\n // views, partitions, the drizzle bookkeeping tables. Narrow every row set to\n // the tables actually being introspected so downstream code never has to\n // ask whether a table it found in `columns` really exists.\n const known = new Set(tables.rows.map((t) => t.table_name));\n const scoped = <T extends { table_name: string }>(rows: T[]) => rows.filter((r) => known.has(r.table_name));\n\n return {\n schema,\n tables: tables.rows,\n columns: scoped(columns.rows),\n enumValues: enumValues.rows,\n pks: scoped(pks.rows),\n // A foreign key pointing *out* of the introspected set (into another\n // schema, or at a view) cannot be turned into a relation: there is no\n // collection on the other end to link to.\n fks: scoped(fks.rows).filter((fk) => known.has(fk.foreign_table_name)),\n uniques: scoped(uniques.rows),\n indexes: scoped(indexes.rows),\n checks: scoped(checks.rows),\n comments: scoped(comments.rows),\n rowCounts: {}\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA4CA,SAAgB,gBAAgB,MAAsB;CAClD,OAAO,IAAI,KAAK,QAAQ,MAAM,MAAM,EAAE;AAC1C;;AAGA,SAAgB,cAAc,QAAgB,OAAuB;CACjE,OAAO,GAAG,gBAAgB,MAAM,EAAE,GAAG,gBAAgB,KAAK;AAC9D;;;;;;;;;;;;;;;;AAiBA,SAAgB,iBAAiB,MAAuB;CACpD,IAAI,KAAK,WAAW,KAAK,KAAK,SAAS,KAAK,OAAO;CACnD,IAAI,SAAS,OAAO,SAAS,MAAM,OAAO;CAK1C,IAAI,sBAAsB,KAAK,IAAI,GAAG,OAAO;CAG7C,OAAO,CAAC,KAAK,WAAW,GAAG,KAAK,CAAC,KAAK,SAAS,GAAG;AACtD;;;;;;;;;;;;;AAcA,IAAa,eAAe;;;;;;;;;;;;;;;;;;;;AAqB5B,IAAa,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;AA0B7B,IAAa,cAAc;;;;;;;;;;;;;;;;;;AAmB3B,IAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmClC,IAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDlC,IAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BxC,IAAa,gBAAgB;;;;;;;;;;;;;;;;;AAkB7B,IAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;AAqBvC,IAAa,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4B9B,eAAsB,cAClB,QACA,QACA,OACA,OACe;CACf,MAAM,EAAE,SAAS,MAAM,OAAO,MAC1B,sDAAsD,cAAc,QAAQ,KAAK,EAAE,SAAS,QAAQ,EAAE,QAC1G;CACA,OAAO,OAAO,KAAK,EAAE,EAAE,SAAS,CAAC;AACrC;;;;;;;;AASA,eAAsB,mBAClB,QACA,QACuB;CAKvB,MAAM,SAAS,MAAM,OAAO,MAAgB,cAAc,CAAC,MAAM,CAAC;CAClE,MAAM,UAAU,MAAM,OAAO,MAAmB,eAAe,CAAC,MAAM,CAAC;CACvE,MAAM,aAAa,MAAM,OAAO,MAAiB,aAAa,CAAC,MAAM,CAAC;CACtE,MAAM,MAAM,MAAM,OAAO,MAAqB,oBAAoB,CAAC,MAAM,CAAC;CAC1E,MAAM,MAAM,MAAM,OAAO,MAAqB,oBAAoB,CAAC,MAAM,CAAC;CAC1E,MAAM,UAAU,MAAM,OAAO,MAA2B,0BAA0B,CAAC,MAAM,CAAC;CAC1F,MAAM,UAAU,MAAM,OAAO,MAAgB,eAAe,CAAC,MAAM,CAAC;CACpE,MAAM,SAAS,MAAM,OAAO,MAA0B,yBAAyB,CAAC,MAAM,CAAC;CACvF,MAAM,WAAW,MAAM,OAAO,MAAkB,gBAAgB,CAAC,MAAM,CAAC;CAMxE,MAAM,QAAQ,IAAI,IAAI,OAAO,KAAK,KAAK,MAAM,EAAE,UAAU,CAAC;CAC1D,MAAM,UAA4C,SAAc,KAAK,QAAQ,MAAM,MAAM,IAAI,EAAE,UAAU,CAAC;CAE1G,OAAO;EACH;EACA,QAAQ,OAAO;EACf,SAAS,OAAO,QAAQ,IAAI;EAC5B,YAAY,WAAW;EACvB,KAAK,OAAO,IAAI,IAAI;EAIpB,KAAK,OAAO,IAAI,IAAI,CAAC,CAAC,QAAQ,OAAO,MAAM,IAAI,GAAG,kBAAkB,CAAC;EACrE,SAAS,OAAO,QAAQ,IAAI;EAC5B,SAAS,OAAO,QAAQ,IAAI;EAC5B,QAAQ,OAAO,OAAO,IAAI;EAC1B,UAAU,OAAO,SAAS,IAAI;EAC9B,WAAW,CAAC;CAChB;AACJ"}
@@ -0,0 +1,388 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ import { K as extractCauseMessage } from "./cli-errors-CaI4ntyD.js";
4
+ import { declaredErrorAnswer, logger } from "@rebasepro/server";
5
+ import { fieldKeyForColumn, getTableName } from "@rebasepro/common";
6
+ //#region src/utils/pg-error-utils.ts
7
+ /**
8
+ * Shared PostgreSQL error extraction and user-friendly message formatting.
9
+ *
10
+ * Drizzle wraps native PG errors in a `.cause` chain. These utilities
11
+ * unwrap that chain to get the real PostgreSQL error (identified by a
12
+ * 5-character alphanumeric `code` such as `42P01`) and translate it into
13
+ * a message that is safe and helpful to show to end-users.
14
+ */
15
+ /**
16
+ * Return the error when it is a deliberate 4xx, otherwise null.
17
+ *
18
+ * A thrown `ApiError` is a decision the server made about the request, not a
19
+ * database failure: its message and code are already written for the client.
20
+ *
21
+ * So is a `RebaseApiError` — what a collection callback throws, since a
22
+ * collection file cannot import the server package. It spells its status
23
+ * `status`, and reading only `statusCode` here turned an `afterRead` that
24
+ * refused with a 403 into "Could not load data … Check server logs" on a
25
+ * subscription, where REST and the socket's own requests answer the 403.
26
+ * Recognised by the predicate those doors use, `declaredErrorAnswer`.
27
+ */
28
+ function asClientFacingError(error) {
29
+ const answer = declaredErrorAnswer(error);
30
+ if (answer) {
31
+ if (answer.status < 400 || answer.status >= 500) return null;
32
+ return {
33
+ statusCode: answer.status,
34
+ code: answer.code,
35
+ message: answer.message,
36
+ expected: answer.expected
37
+ };
38
+ }
39
+ if (!(error instanceof Error)) return null;
40
+ const e = error;
41
+ if (typeof e.statusCode !== "number" || e.statusCode < 400 || e.statusCode >= 500) return null;
42
+ return e;
43
+ }
44
+ /**
45
+ * Extract the underlying PostgreSQL error from a Drizzle wrapper.
46
+ * Drizzle wraps PG errors in a `cause` property — this function
47
+ * recursively walks the chain until it finds an object with a PG
48
+ * error code (5-char alphanumeric, e.g. `42P01`).
49
+ */
50
+ function extractPgError(error) {
51
+ if (!error || typeof error !== "object") return null;
52
+ if (!(error instanceof Error)) {
53
+ if ("cause" in error && error.cause && typeof error.cause === "object") return extractPgError(error.cause);
54
+ return null;
55
+ }
56
+ if ("code" in error && typeof error.code === "string" && /^[0-9A-Z]{5}$/.test(error.code)) return error;
57
+ if (error.cause && typeof error.cause === "object") return extractPgError(error.cause);
58
+ return null;
59
+ }
60
+ /**
61
+ * Whether the failure came back from Postgres rather than from building the
62
+ * query — which decides whether a fallback query is worth issuing.
63
+ *
64
+ * Reads here run inside a transaction (that is where `SET LOCAL ROLE` binds
65
+ * RLS). Once a statement raises, that transaction is aborted, and every later
66
+ * statement on it returns `25P02` — "current transaction is aborted, commands
67
+ * ignored until end of transaction block". So a retry after a database error
68
+ * cannot succeed, and it replaces a precise diagnosis ("invalid input syntax
69
+ * for type uuid") with a generic one. Rethrow instead.
70
+ *
71
+ * A query the driver could not even build — a missing reciprocal relation, say
72
+ * — never reached Postgres, leaves the transaction usable, and is exactly what
73
+ * the fallback paths exist for.
74
+ */
75
+ function reachedDatabase(error) {
76
+ return extractPgError(error) !== null;
77
+ }
78
+ /**
79
+ * Codes that mean "this connection will never work as configured".
80
+ *
81
+ * A wrong password or a database that does not exist is a settled fact about
82
+ * the connection string, not a transient fault — retrying produces the same
83
+ * answer forever.
84
+ */
85
+ var UNRECOVERABLE_CONNECT_CODES = /* @__PURE__ */ new Set([
86
+ "28P01",
87
+ "28000",
88
+ "3D000",
89
+ "42501"
90
+ ]);
91
+ /**
92
+ * Describe a failed connection attempt in terms a developer can act on.
93
+ *
94
+ * The error a caller catches is Drizzle's wrapper: its message is
95
+ * `Failed query: SELECT 1` and its stack runs through drizzle internals, while
96
+ * the sentence that says what is actually wrong — "password authentication
97
+ * failed for user …", "database … does not exist" — sits in `.cause`. Logging
98
+ * the wrapper, as the bootstrapper used to, tells a developer with a typo in
99
+ * their `DATABASE_URL` nothing at all.
100
+ */
101
+ function classifyConnectFailure(error) {
102
+ const pgError = extractPgError(error);
103
+ const reason = pgError?.message ?? extractCauseMessage(error) ?? (error instanceof Error ? error.message : String(error));
104
+ return {
105
+ fatal: Boolean(pgError?.code && UNRECOVERABLE_CONNECT_CODES.has(pgError.code)),
106
+ reason,
107
+ code: pgError?.code
108
+ };
109
+ }
110
+ /**
111
+ * Detect whether an error is specifically a role-switching permission failure
112
+ * (e.g. "permission denied to set role" or "must be member of role"),
113
+ * as opposed to a table-level permission denial.
114
+ *
115
+ * This is used by the backend driver to auto-disable role switching when the
116
+ * connection user lacks SET ROLE privileges, rather than surfacing a confusing
117
+ * error to the Studio SQL Editor user.
118
+ */
119
+ function isRoleSwitchingPermissionError(error) {
120
+ const pgError = extractPgError(error);
121
+ if (!pgError || pgError.code !== "42501") return false;
122
+ const msg = pgError.message.toLowerCase();
123
+ return msg.includes("set role") || msg.includes("member of role");
124
+ }
125
+ /**
126
+ * Was this `42501` the *caller* being refused by a policy, rather than the
127
+ * server lacking a privilege?
128
+ *
129
+ * Both arrive as `insufficient_privilege`, and they are opposite kinds of
130
+ * problem. A row-level-security refusal is a working access-control system
131
+ * doing its job: the caller asked for something their policies do not permit,
132
+ * which is a 403 and nobody's bug. A missing `GRANT` is the deployment being
133
+ * wrong — the connection role cannot touch the table at all, no policy is
134
+ * involved, and nothing the caller changes about the request will help.
135
+ *
136
+ * Postgres distinguishes them in the message, so this does too:
137
+ *
138
+ * new row violates row-level security policy for table "notes" → the caller
139
+ * permission denied for table notes → the server
140
+ *
141
+ * Only writes reach this. A read that RLS excludes is not an error — the rows
142
+ * are filtered and the caller gets an empty page — so the erroring case is
143
+ * specifically an `INSERT`/`UPDATE` whose row fails a policy's `WITH CHECK`.
144
+ *
145
+ * Matched on the message because that is the only thing carrying the
146
+ * distinction; the SQLSTATE is identical either way. Narrow by design: anything
147
+ * not naming row-level security stays the server's problem, since reporting a
148
+ * genuine privilege misconfiguration as "forbidden" would send an operator
149
+ * hunting for a policy bug that does not exist.
150
+ */
151
+ function isRowLevelSecurityDenial(error) {
152
+ const pgError = extractPgError(error);
153
+ if (!pgError || pgError.code !== "42501") return false;
154
+ return pgError.message.toLowerCase().includes("row-level security policy");
155
+ }
156
+ /**
157
+ * The physical column names a Postgres error is about.
158
+ *
159
+ * Postgres puts one in `column` for `23502` and nowhere else, so the rest are
160
+ * read out of the two places that do carry them:
161
+ *
162
+ * - `detail` — `Key (author_id)=(9) is not present in table "authors".` and
163
+ * `Key (email, tenant_id)=(…) already exists.` Only the parenthesised column
164
+ * list is read; the values after `=` are never touched.
165
+ * - `constraint` — `users_email_key`, `posts_author_id_fkey`. The table prefix
166
+ * and the constraint suffix are stripped, leaving the columns joined by `_`,
167
+ * which is ambiguous for multi-column constraints and is therefore only used
168
+ * when `detail` gave nothing.
169
+ *
170
+ * `22P02` carries neither, because an invalid literal fails during parse rather
171
+ * than against a constraint. Its message names the *type*, and an enum type is
172
+ * generated as `<table>_<column>` — which is the one case worth decoding,
173
+ * because a value outside an enum's labels is the most common 22P02 anyone
174
+ * hits and the least legible one.
175
+ */
176
+ function offendingColumns(pgError, collection) {
177
+ if (pgError.column) return [pgError.column];
178
+ const detail = pgError.detail;
179
+ if (detail) {
180
+ const keyed = /Key \(([^)]+)\)=/.exec(detail);
181
+ if (keyed) return keyed[1].split(",").map((part) => part.trim().replace(/^"|"$/g, "")).filter(Boolean);
182
+ }
183
+ if (pgError.code === "22P02") {
184
+ const enumType = /invalid input value for enum ([^:\s]+)/i.exec(pgError.message ?? "");
185
+ if (enumType && collection) {
186
+ const typeName = enumType[1].replace(/^.*\./, "").replace(/"/g, "");
187
+ const table = getTableName(collection);
188
+ if (typeName.startsWith(`${table}_`)) return [typeName.slice(table.length + 1)];
189
+ }
190
+ return [];
191
+ }
192
+ const constraint = pgError.constraint;
193
+ if (constraint && collection) {
194
+ const table = getTableName(collection);
195
+ const stripped = constraint.replace(new RegExp(`^${table}_`), "").replace(/_(pkey|key|fkey|unique|check|excl|idx)$/, "");
196
+ if (stripped && stripped !== constraint) {
197
+ const columns = /* @__PURE__ */ new Set();
198
+ for (const [key, prop] of Object.entries(collection.properties ?? {})) columns.add(prop?.columnName ?? key);
199
+ if (columns.has(stripped)) return [stripped];
200
+ const wire = fieldKeyForColumn(collection, stripped);
201
+ if (collection.properties && wire in collection.properties) return [stripped];
202
+ }
203
+ }
204
+ return [];
205
+ }
206
+ /**
207
+ * The fields a Postgres error is about, for `details.violations`.
208
+ *
209
+ * Only the four SQLSTATEs that describe *the caller's data*: a missing value, a
210
+ * duplicate, a bad literal, a foreign key pointing at nothing. Everything else
211
+ * — a dropped connection, a missing table, a privilege problem — is the
212
+ * deployment's, and naming a field for it would send the reader to the wrong
213
+ * place.
214
+ *
215
+ * Empty when the collection is unknown or the column cannot be recovered. An
216
+ * empty list is the honest answer; inventing a field is worse than saying
217
+ * nothing, because a form will mark the wrong input red.
218
+ */
219
+ function pgFieldViolations(pgError, collection) {
220
+ const code = pgError.code;
221
+ if (code !== "23502" && code !== "23505" && code !== "22P02" && code !== "23503") return [];
222
+ if (!collection) return [];
223
+ const columns = offendingColumns(pgError, collection);
224
+ if (columns.length === 0) return [];
225
+ return columns.map((column) => ({
226
+ field: fieldKeyForColumn(collection, column),
227
+ code
228
+ }));
229
+ }
230
+ /**
231
+ * Translate a raw PostgreSQL error into a user-friendly message.
232
+ *
233
+ * @param pgError - The extracted PostgreSQL error (from {@link extractPgError})
234
+ * @param context - A human-readable context string (e.g. collection slug or path)
235
+ * @returns An object with a `message` safe for the client and the PG `code`.
236
+ */
237
+ function pgErrorToFriendlyMessage(pgError, context, options = {}) {
238
+ const verbose = options.verbose ?? process.env.NODE_ENV !== "production";
239
+ const detail = verbose ? pgError.detail : void 0;
240
+ const hint = verbose ? pgError.hint : void 0;
241
+ const constraint = pgError.constraint;
242
+ const column = pgError.column;
243
+ const table = pgError.table;
244
+ const dataType = verbose ? pgError.dataType : void 0;
245
+ const rawMessage = pgError.message || "Unknown database error";
246
+ const pgMessage = verbose ? rawMessage : "";
247
+ const code = pgError.code || "UNKNOWN";
248
+ const violations = pgFieldViolations(pgError, options.collection);
249
+ const suffix = hint ? ` Hint: ${hint}` : "";
250
+ const tableRef = table ?? context;
251
+ /** `": <postgres said>"`, or nothing when the details are withheld. */
252
+ const said = pgMessage ? `: ${pgMessage}` : "";
253
+ switch (pgError.code) {
254
+ case "23503": return {
255
+ message: detail ? `Foreign key constraint violated: ${detail}${suffix}` : `Cannot complete operation: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
256
+ code,
257
+ violations
258
+ };
259
+ case "23001": return {
260
+ message: `Cannot complete operation: rows in "${table ?? "another table"}" still reference this row in "${context}", and their foreign key${constraint ? ` (${constraint})` : ""} does not allow it to be deleted or its key changed. Delete or reassign those rows first.${detail ? ` ${detail}` : ""}${suffix}`,
261
+ code,
262
+ violations
263
+ };
264
+ case "23505": return {
265
+ message: detail ? `Duplicate value: ${detail}${suffix}` : `Cannot complete operation: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
266
+ code,
267
+ violations
268
+ };
269
+ case "23502": return {
270
+ message: `Missing required field: "${violations[0]?.field ?? column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`,
271
+ code,
272
+ violations
273
+ };
274
+ case "23514": return {
275
+ message: `Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
276
+ code,
277
+ violations
278
+ };
279
+ case "22P02": return {
280
+ message: `Invalid data format in "${context}"${said}.${suffix}`,
281
+ code,
282
+ violations
283
+ };
284
+ case "22001": return {
285
+ message: `Value too long for column "${column ?? "unknown"}" in "${tableRef}"${said}.${suffix}`,
286
+ code,
287
+ violations
288
+ };
289
+ case "22003": return {
290
+ message: `Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}"${said}.${suffix}`,
291
+ code,
292
+ violations
293
+ };
294
+ case "42703": return {
295
+ message: `Unknown column in "${tableRef}"${said}. Check if your schema is up to date (run migrations).${suffix}`,
296
+ code,
297
+ violations
298
+ };
299
+ case "42P01": return {
300
+ message: `Table not found for "${context}"${said}. Check if your schema is up to date (run migrations).${suffix}`,
301
+ code,
302
+ violations
303
+ };
304
+ case "42501": return rawMessage.toLowerCase().includes("row-level security policy") ? {
305
+ message: `Not permitted to write this row in "${tableRef}": it does not satisfy the row-level security policy.${suffix}`,
306
+ code,
307
+ violations
308
+ } : {
309
+ message: `Permission denied on "${tableRef}": the database role this server connects as lacks privileges on it.${suffix}`,
310
+ code,
311
+ violations
312
+ };
313
+ case "28000": return {
314
+ message: `Authorization failed for "${context}". Check your database credentials.${suffix}`,
315
+ code,
316
+ violations
317
+ };
318
+ default: {
319
+ const parts = [`Database error in "${context}" [${code}]${said}`];
320
+ if (detail) parts.push(`Detail: ${detail}`);
321
+ if (column) parts.push(`Column: ${column}`);
322
+ if (dataType) parts.push(`Data type: ${dataType}`);
323
+ if (constraint) parts.push(`Constraint: ${constraint}`);
324
+ if (hint) parts.push(`Hint: ${hint}`);
325
+ return {
326
+ message: parts.join(". "),
327
+ code,
328
+ violations
329
+ };
330
+ }
331
+ }
332
+ }
333
+ /**
334
+ * Sanitize any error into a message safe and helpful for the client.
335
+ *
336
+ * A deliberate 4xx (`ApiError`) passes through untouched — the server already
337
+ * decided what the client should read. Otherwise the PG error is extracted
338
+ * from the Drizzle cause chain, falling back to a generic message that
339
+ * doesn't leak SQL.
340
+ *
341
+ * @param error - The raw caught error
342
+ * @param context - A human-readable context string (e.g. collection path)
343
+ * @returns An object with `message` (user-friendly) and optional `code`
344
+ * (the `ApiError` code, or the PG SQLSTATE).
345
+ */
346
+ function sanitizeErrorForClient(error, context, options = {}) {
347
+ const clientError = asClientFacingError(error);
348
+ if (clientError) {
349
+ const line = `[API ${clientError.statusCode} ${clientError.code ?? "BAD_REQUEST"}] in "${context}": ${clientError.message}`;
350
+ if (clientError.expected) logger.debug(line);
351
+ else logger.warn(`⚠️ ${line}`);
352
+ return {
353
+ message: clientError.message,
354
+ ...clientError.code && { code: clientError.code }
355
+ };
356
+ }
357
+ const pgError = extractPgError(error);
358
+ if (pgError) {
359
+ logger.error(`[PG ${pgError.code}] Error in "${context}"`, {
360
+ code: pgError.code,
361
+ message: pgError.message,
362
+ detail: pgError.detail,
363
+ hint: pgError.hint,
364
+ column: pgError.column,
365
+ table: pgError.table,
366
+ constraint: pgError.constraint,
367
+ dataType: pgError.dataType
368
+ });
369
+ const { message, code, violations } = pgErrorToFriendlyMessage(pgError, context, { collection: options.collection });
370
+ return {
371
+ message,
372
+ code,
373
+ ...violations.length > 0 && { violations }
374
+ };
375
+ }
376
+ logger.error(`Database error in "${context}" (no PG error extracted)`, {
377
+ error: error instanceof Error ? error.message : String(error),
378
+ stack: error instanceof Error ? error.stack : void 0,
379
+ cause: error instanceof Error && error.cause ? error.cause instanceof Error ? error.cause.message : String(error.cause) : void 0
380
+ });
381
+ const causeMessage = extractCauseMessage(error);
382
+ if (causeMessage) return { message: `Database error in "${context}": ${causeMessage}` };
383
+ return { message: `Could not load data for "${context}". Check server logs for details.` };
384
+ }
385
+ //#endregion
386
+ export { pgErrorToFriendlyMessage as a, isRowLevelSecurityDenial as i, extractPgError as n, reachedDatabase as o, isRoleSwitchingPermissionError as r, sanitizeErrorForClient as s, classifyConnectFailure as t };
387
+
388
+ //# sourceMappingURL=pg-error-utils-vJezlXdI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pg-error-utils-vJezlXdI.js","names":[],"sources":["../src/utils/pg-error-utils.ts"],"sourcesContent":["/**\n * Shared PostgreSQL error extraction and user-friendly message formatting.\n *\n * Drizzle wraps native PG errors in a `.cause` chain. These utilities\n * unwrap that chain to get the real PostgreSQL error (identified by a\n * 5-character alphanumeric `code` such as `42P01`) and translate it into\n * a message that is safe and helpful to show to end-users.\n */\n\nimport { declaredErrorAnswer, logger } from \"@rebasepro/server\";\nimport type { CollectionConfig } from \"@rebasepro/types\";\nimport { fieldKeyForColumn, getTableName } from \"@rebasepro/common\";\nimport { extractCauseMessage } from \"./error-cause\";\n\n/**\n * Shape of a deliberate client-facing error — `ApiError` from\n * `@rebasepro/server`, `RebaseApiError` from `@rebasepro/types`, or anything\n * else carrying a 4xx `statusCode`.\n *\n * Matched structurally rather than with `instanceof`: `@rebasepro/server` can\n * be loaded twice (published dist vs. workspace source), which breaks class\n * identity — `PersistService` hedges against the same thing by also accepting\n * `name === \"ApiError\"`.\n */\ninterface ClientFacingError extends Error {\n statusCode?: number;\n code?: string;\n /** See `ApiError.expected` — routine outcomes log at debug, not warn. */\n expected?: boolean;\n}\n\n/**\n * Return the error when it is a deliberate 4xx, otherwise null.\n *\n * A thrown `ApiError` is a decision the server made about the request, not a\n * database failure: its message and code are already written for the client.\n *\n * So is a `RebaseApiError` — what a collection callback throws, since a\n * collection file cannot import the server package. It spells its status\n * `status`, and reading only `statusCode` here turned an `afterRead` that\n * refused with a 403 into \"Could not load data … Check server logs\" on a\n * subscription, where REST and the socket's own requests answer the 403.\n * Recognised by the predicate those doors use, `declaredErrorAnswer`.\n */\nfunction asClientFacingError(error: unknown): Pick<ClientFacingError, \"statusCode\" | \"code\" | \"message\" | \"expected\"> | null {\n const answer = declaredErrorAnswer(error);\n if (answer) {\n if (answer.status < 400 || answer.status >= 500) return null;\n return { statusCode: answer.status, code: answer.code, message: answer.message, expected: answer.expected };\n }\n if (!(error instanceof Error)) return null;\n const e = error as ClientFacingError;\n if (typeof e.statusCode !== \"number\" || e.statusCode < 400 || e.statusCode >= 500) return null;\n return e;\n}\n\n/** Shape of PostgreSQL errors with diagnostic metadata. */\nexport interface PostgresError extends Error {\n code?: string;\n detail?: string;\n hint?: string;\n constraint?: string;\n column?: string;\n table?: string;\n dataType?: string;\n cause?: unknown;\n}\n\n/**\n * Extract the underlying PostgreSQL error from a Drizzle wrapper.\n * Drizzle wraps PG errors in a `cause` property — this function\n * recursively walks the chain until it finds an object with a PG\n * error code (5-char alphanumeric, e.g. `42P01`).\n */\nexport function extractPgError(error: unknown): PostgresError | null {\n if (!error || typeof error !== \"object\") return null;\n if (!(error instanceof Error)) {\n // Check non-Error objects for a cause chain (Drizzle sometimes wraps oddly)\n if (\"cause\" in error && (error as Record<string, unknown>).cause && typeof (error as Record<string, unknown>).cause === \"object\") {\n return extractPgError((error as Record<string, unknown>).cause);\n }\n return null;\n }\n\n // Check if the error itself has a PG error code\n if (\"code\" in error && typeof (error as PostgresError).code === \"string\" && /^[0-9A-Z]{5}$/.test((error as PostgresError).code!)) {\n return error as PostgresError;\n }\n\n // Check the cause chain (Drizzle wraps PG errors)\n if (error.cause && typeof error.cause === \"object\") {\n return extractPgError(error.cause);\n }\n\n return null;\n}\n\n/**\n * Whether the failure came back from Postgres rather than from building the\n * query — which decides whether a fallback query is worth issuing.\n *\n * Reads here run inside a transaction (that is where `SET LOCAL ROLE` binds\n * RLS). Once a statement raises, that transaction is aborted, and every later\n * statement on it returns `25P02` — \"current transaction is aborted, commands\n * ignored until end of transaction block\". So a retry after a database error\n * cannot succeed, and it replaces a precise diagnosis (\"invalid input syntax\n * for type uuid\") with a generic one. Rethrow instead.\n *\n * A query the driver could not even build — a missing reciprocal relation, say\n * — never reached Postgres, leaves the transaction usable, and is exactly what\n * the fallback paths exist for.\n */\nexport function reachedDatabase(error: unknown): boolean {\n return extractPgError(error) !== null;\n}\n\n// In its own module so the driver CLI can use it without loading the server\n// (this file imports `@rebasepro/server`).\nexport { extractCauseMessage };\n\n/**\n * Codes that mean \"this connection will never work as configured\".\n *\n * A wrong password or a database that does not exist is a settled fact about\n * the connection string, not a transient fault — retrying produces the same\n * answer forever.\n */\nconst UNRECOVERABLE_CONNECT_CODES = new Set([\n \"28P01\", // invalid_password\n \"28000\", // invalid_authorization_specification\n \"3D000\", // invalid_catalog_name — the database does not exist\n \"42501\" // insufficient_privilege\n]);\n\nexport interface ConnectFailure {\n /** True when retrying cannot help: the connection string itself is wrong. */\n fatal: boolean;\n /** The deepest message available — the Postgres one where there is one. */\n reason: string;\n /** The `SQLSTATE`, when the failure came from Postgres rather than the socket. */\n code?: string;\n}\n\n/**\n * Describe a failed connection attempt in terms a developer can act on.\n *\n * The error a caller catches is Drizzle's wrapper: its message is\n * `Failed query: SELECT 1` and its stack runs through drizzle internals, while\n * the sentence that says what is actually wrong — \"password authentication\n * failed for user …\", \"database … does not exist\" — sits in `.cause`. Logging\n * the wrapper, as the bootstrapper used to, tells a developer with a typo in\n * their `DATABASE_URL` nothing at all.\n */\nexport function classifyConnectFailure(error: unknown): ConnectFailure {\n const pgError = extractPgError(error);\n const reason =\n pgError?.message ??\n extractCauseMessage(error) ??\n (error instanceof Error ? error.message : String(error));\n return {\n fatal: Boolean(pgError?.code && UNRECOVERABLE_CONNECT_CODES.has(pgError.code)),\n reason,\n code: pgError?.code\n };\n}\n\n/**\n * Detect whether an error is specifically a role-switching permission failure\n * (e.g. \"permission denied to set role\" or \"must be member of role\"),\n * as opposed to a table-level permission denial.\n *\n * This is used by the backend driver to auto-disable role switching when the\n * connection user lacks SET ROLE privileges, rather than surfacing a confusing\n * error to the Studio SQL Editor user.\n */\nexport function isRoleSwitchingPermissionError(error: unknown): boolean {\n const pgError = extractPgError(error);\n if (!pgError || pgError.code !== \"42501\") return false;\n const msg = pgError.message.toLowerCase();\n return msg.includes(\"set role\") || msg.includes(\"member of role\");\n}\n\n/**\n * Was this `42501` the *caller* being refused by a policy, rather than the\n * server lacking a privilege?\n *\n * Both arrive as `insufficient_privilege`, and they are opposite kinds of\n * problem. A row-level-security refusal is a working access-control system\n * doing its job: the caller asked for something their policies do not permit,\n * which is a 403 and nobody's bug. A missing `GRANT` is the deployment being\n * wrong — the connection role cannot touch the table at all, no policy is\n * involved, and nothing the caller changes about the request will help.\n *\n * Postgres distinguishes them in the message, so this does too:\n *\n * new row violates row-level security policy for table \"notes\" → the caller\n * permission denied for table notes → the server\n *\n * Only writes reach this. A read that RLS excludes is not an error — the rows\n * are filtered and the caller gets an empty page — so the erroring case is\n * specifically an `INSERT`/`UPDATE` whose row fails a policy's `WITH CHECK`.\n *\n * Matched on the message because that is the only thing carrying the\n * distinction; the SQLSTATE is identical either way. Narrow by design: anything\n * not naming row-level security stays the server's problem, since reporting a\n * genuine privilege misconfiguration as \"forbidden\" would send an operator\n * hunting for a policy bug that does not exist.\n */\nexport function isRowLevelSecurityDenial(error: unknown): boolean {\n const pgError = extractPgError(error);\n if (!pgError || pgError.code !== \"42501\") return false;\n return pgError.message.toLowerCase().includes(\"row-level security policy\");\n}\n\n/**\n * One constraint the database refused, named in the caller's vocabulary.\n *\n * `field` is the **wire** name, derived from the physical column: `author_id`\n * on the table is `authorId` on the row and in the request body, and telling a\n * caller their `author_id` is missing sends them looking for a key their client\n * has never seen. `code` is the SQLSTATE, which is stable, documented by\n * Postgres and the same in every environment — unlike the message, which is\n * deliberately thinner in production.\n *\n * Deliberately no `value`. The column name is safe to publish: it is already in\n * the OpenAPI, the generated SDK and the caller's own request. The *value* is\n * not — `23505`'s detail reads `Key (email)=(a@b.c) already exists.`, which\n * answers \"is this person registered?\" for any address on a table whose rows\n * RLS is otherwise hiding completely.\n */\nexport interface PgFieldViolation {\n field: string;\n code: string;\n}\n\n/**\n * The physical column names a Postgres error is about.\n *\n * Postgres puts one in `column` for `23502` and nowhere else, so the rest are\n * read out of the two places that do carry them:\n *\n * - `detail` — `Key (author_id)=(9) is not present in table \"authors\".` and\n * `Key (email, tenant_id)=(…) already exists.` Only the parenthesised column\n * list is read; the values after `=` are never touched.\n * - `constraint` — `users_email_key`, `posts_author_id_fkey`. The table prefix\n * and the constraint suffix are stripped, leaving the columns joined by `_`,\n * which is ambiguous for multi-column constraints and is therefore only used\n * when `detail` gave nothing.\n *\n * `22P02` carries neither, because an invalid literal fails during parse rather\n * than against a constraint. Its message names the *type*, and an enum type is\n * generated as `<table>_<column>` — which is the one case worth decoding,\n * because a value outside an enum's labels is the most common 22P02 anyone\n * hits and the least legible one.\n */\nfunction offendingColumns(pgError: PostgresError, collection?: CollectionConfig): string[] {\n if (pgError.column) return [pgError.column];\n\n const detail = pgError.detail;\n if (detail) {\n const keyed = /Key \\(([^)]+)\\)=/.exec(detail);\n if (keyed) {\n return keyed[1].split(\",\").map(part => part.trim().replace(/^\"|\"$/g, \"\")).filter(Boolean);\n }\n }\n\n if (pgError.code === \"22P02\") {\n // `invalid input value for enum posts_status: \"archived\"`\n const enumType = /invalid input value for enum ([^:\\s]+)/i.exec(pgError.message ?? \"\");\n if (enumType && collection) {\n const typeName = enumType[1].replace(/^.*\\./, \"\").replace(/\"/g, \"\");\n const table = getTableName(collection);\n if (typeName.startsWith(`${table}_`)) return [typeName.slice(table.length + 1)];\n }\n return [];\n }\n\n const constraint = pgError.constraint;\n if (constraint && collection) {\n const table = getTableName(collection);\n const stripped = constraint\n .replace(new RegExp(`^${table}_`), \"\")\n .replace(/_(pkey|key|fkey|unique|check|excl|idx)$/, \"\");\n // Only when it names something the collection actually has: a\n // hand-written constraint name is not a column list, and guessing at one\n // would put a field in the response that does not exist.\n if (stripped && stripped !== constraint) {\n const columns = new Set<string>();\n for (const [key, prop] of Object.entries(collection.properties ?? {})) {\n columns.add((prop as { columnName?: string })?.columnName ?? key);\n }\n if (columns.has(stripped)) return [stripped];\n const wire = fieldKeyForColumn(collection, stripped);\n if (collection.properties && wire in collection.properties) return [stripped];\n }\n }\n\n return [];\n}\n\n/**\n * The fields a Postgres error is about, for `details.violations`.\n *\n * Only the four SQLSTATEs that describe *the caller's data*: a missing value, a\n * duplicate, a bad literal, a foreign key pointing at nothing. Everything else\n * — a dropped connection, a missing table, a privilege problem — is the\n * deployment's, and naming a field for it would send the reader to the wrong\n * place.\n *\n * Empty when the collection is unknown or the column cannot be recovered. An\n * empty list is the honest answer; inventing a field is worse than saying\n * nothing, because a form will mark the wrong input red.\n */\nexport function pgFieldViolations(\n pgError: PostgresError,\n collection?: CollectionConfig\n): PgFieldViolation[] {\n const code = pgError.code;\n if (code !== \"23502\" && code !== \"23505\" && code !== \"22P02\" && code !== \"23503\") return [];\n // Without the collection there is no mapping, only a guess. `toWireKey`\n // would get the default case right and silently invent a field for any\n // property carrying an explicit `columnName` — and a form pointed at an\n // input that does not exist is worse off than one shown the raw column.\n if (!collection) return [];\n const columns = offendingColumns(pgError, collection);\n if (columns.length === 0) return [];\n return columns.map(column => ({\n field: fieldKeyForColumn(collection, column),\n code\n }));\n}\n\n/**\n * Translate a raw PostgreSQL error into a user-friendly message.\n *\n * @param pgError - The extracted PostgreSQL error (from {@link extractPgError})\n * @param context - A human-readable context string (e.g. collection slug or path)\n * @returns An object with a `message` safe for the client and the PG `code`.\n */\nexport function pgErrorToFriendlyMessage(\n pgError: PostgresError,\n context: string,\n options: { verbose?: boolean; collection?: CollectionConfig } = {}\n): { message: string; code: string; violations: PgFieldViolation[] } {\n // Postgres's own `DETAIL` and `HINT`, and the raw driver message, are what\n // make these errors useful while you are building — and what makes them an\n // oracle once the server is answering strangers.\n //\n // The sharp one is `23505`: its detail reads `Key (email)=(a@b.c) already\n // exists.`, which answers \"is this person registered?\" for any address, on\n // a table whose rows RLS is otherwise hiding completely. The rest leak\n // physical column and constraint names, which are the map for the next\n // question. So in production the shape of the failure is served — what\n // rule was broken, and where — and the values are not.\n //\n // The full text is not lost: the caller logs the original error. This\n // decides what crosses the wire, not what is recorded.\n const verbose = options.verbose ?? process.env.NODE_ENV !== \"production\";\n\n const detail = verbose ? (pgError.detail as string | undefined) : undefined;\n const hint = verbose ? (pgError.hint as string | undefined) : undefined;\n const constraint = pgError.constraint as string | undefined;\n const column = pgError.column as string | undefined;\n const table = pgError.table as string | undefined;\n const dataType = verbose ? (pgError.dataType as string | undefined) : undefined;\n const rawMessage = pgError.message || \"Unknown database error\";\n const pgMessage = verbose ? rawMessage : \"\";\n const code = pgError.code || \"UNKNOWN\";\n\n // Which *field* the caller has to fix, derived from the column. Safe in\n // production — a column name is already published in the OpenAPI and the\n // generated SDK — where the value that broke the rule is not, and is never\n // included. See `pgFieldViolations`.\n const violations = pgFieldViolations(pgError, options.collection);\n\n const suffix = hint ? ` Hint: ${hint}` : \"\";\n const tableRef = table ?? context;\n\n /** `\": <postgres said>\"`, or nothing when the details are withheld. */\n const said = pgMessage ? `: ${pgMessage}` : \"\";\n\n switch (pgError.code) {\n case \"23503\": // foreign_key_violation\n return {\n message: detail\n ? `Foreign key constraint violated: ${detail}${suffix}`\n : `Cannot complete operation: a foreign key constraint${constraint ? ` (${constraint})` : \"\"} was violated in \"${context}\".${suffix}`,\n code,\n violations\n };\n case \"23001\": // restrict_violation\n // A row other rows still point at, under `ON DELETE RESTRICT` — the\n // default for a required link. `table` is the *referencing* table,\n // which is the one the caller has to clear or reassign first.\n return {\n message: `Cannot complete operation: rows in \"${table ?? \"another table\"}\" still reference this row in ` +\n `\"${context}\", and their foreign key${constraint ? ` (${constraint})` : \"\"} does not allow it to be ` +\n `deleted or its key changed. Delete or reassign those rows first.${detail ? ` ${detail}` : \"\"}${suffix}`,\n code,\n violations\n };\n case \"23505\": // unique_violation\n return {\n message: detail\n ? `Duplicate value: ${detail}${suffix}`\n : `Cannot complete operation: a unique constraint${constraint ? ` (${constraint})` : \"\"} was violated in \"${context}\".${suffix}`,\n code,\n violations\n };\n case \"23502\": {\n // not_null_violation. The message names the *field* when the\n // collection is known, because that is the key the caller sent and\n // the only one they can act on: told `author_id` is missing, they go\n // looking for a key their generated client has never mentioned.\n const named = violations[0]?.field ?? column ?? \"unknown\";\n return {\n message: `Missing required field: \"${named}\" in \"${tableRef}\" cannot be empty.${suffix}`,\n code,\n violations\n };\n }\n case \"23514\": // check_violation\n return {\n message: `Validation failed: a check constraint${constraint ? ` (${constraint})` : \"\"} was violated in \"${context}\".${suffix}`,\n code,\n violations\n };\n case \"22P02\": // invalid_text_representation (e.g. invalid UUID, wrong enum value)\n return {\n message: `Invalid data format in \"${context}\"${said}.${suffix}`,\n code,\n violations\n };\n case \"22001\": // string_data_right_truncation (value too long)\n return {\n message: `Value too long for column \"${column ?? \"unknown\"}\" in \"${tableRef}\"${said}.${suffix}`,\n code,\n violations\n };\n case \"22003\": // numeric_value_out_of_range\n return {\n message: `Numeric value out of range for column \"${column ?? \"unknown\"}\" in \"${tableRef}\"${said}.${suffix}`,\n code,\n violations\n };\n case \"42703\": // undefined_column\n return {\n message: `Unknown column in \"${tableRef}\"${said}. Check if your schema is up to date (run migrations).${suffix}`,\n code,\n violations\n };\n case \"42P01\": // undefined_table\n return {\n message: `Table not found for \"${context}\"${said}. Check if your schema is up to date (run migrations).${suffix}`,\n code,\n violations\n };\n case \"42501\": // insufficient_privilege\n // Two unrelated failures share this SQLSTATE, and the old message\n // named both causes because it could not tell them apart — which\n // meant it was half wrong whichever one had happened, and sent the\n // reader to check the other. Postgres says which in its own message.\n return rawMessage.toLowerCase().includes(\"row-level security policy\")\n ? {\n // The caller. Their policies do not permit this row; the\n // deployment is working exactly as configured.\n message: `Not permitted to write this row in \"${tableRef}\": it does not satisfy the row-level security policy.${suffix}`,\n code,\n violations\n }\n : {\n // The deployment. No policy is involved — the connecting\n // role cannot touch the table at all.\n message: `Permission denied on \"${tableRef}\": the database role this server connects as lacks privileges on it.${suffix}`,\n code,\n violations\n };\n case \"28000\": // invalid_authorization_specification\n return {\n message: `Authorization failed for \"${context}\". Check your database credentials.${suffix}`,\n code,\n violations\n };\n default: {\n // Unhandled PG code — still surface the actual database message\n const parts = [`Database error in \"${context}\" [${code}]${said}`];\n if (detail) parts.push(`Detail: ${detail}`);\n if (column) parts.push(`Column: ${column}`);\n if (dataType) parts.push(`Data type: ${dataType}`);\n if (constraint) parts.push(`Constraint: ${constraint}`);\n if (hint) parts.push(`Hint: ${hint}`);\n return { message: parts.join(\". \"), code, violations };\n }\n }\n}\n\n/**\n * Sanitize any error into a message safe and helpful for the client.\n *\n * A deliberate 4xx (`ApiError`) passes through untouched — the server already\n * decided what the client should read. Otherwise the PG error is extracted\n * from the Drizzle cause chain, falling back to a generic message that\n * doesn't leak SQL.\n *\n * @param error - The raw caught error\n * @param context - A human-readable context string (e.g. collection path)\n * @returns An object with `message` (user-friendly) and optional `code`\n * (the `ApiError` code, or the PG SQLSTATE).\n */\nexport function sanitizeErrorForClient(\n error: unknown,\n context: string,\n options: { collection?: CollectionConfig } = {}\n): { message: string; code?: string; violations?: PgFieldViolation[] } {\n // ── A deliberate 4xx is not a database failure ──────────────────\n // Its message and code are written for the client; replacing them with\n // \"Check server logs\" would discard the only diagnosis the caller gets\n // (e.g. the offending filter field and the collection's valid ones).\n // Log level follows the same convention as the HTTP error handler in\n // @rebasepro/server: routine outcomes at debug, everything else at warn.\n const clientError = asClientFacingError(error);\n if (clientError) {\n const line = `[API ${clientError.statusCode} ${clientError.code ?? \"BAD_REQUEST\"}] in \"${context}\": ${clientError.message}`;\n if (clientError.expected) {\n logger.debug(line);\n } else {\n logger.warn(`⚠️ ${line}`);\n }\n return { message: clientError.message, ...(clientError.code && { code: clientError.code }) };\n }\n\n // ── Always log the full, unsanitized error server-side ──────────\n const pgError = extractPgError(error);\n\n if (pgError) {\n logger.error(`[PG ${pgError.code}] Error in \"${context}\"`, {\n code: pgError.code,\n message: pgError.message,\n detail: pgError.detail,\n hint: pgError.hint,\n column: pgError.column,\n table: pgError.table,\n constraint: pgError.constraint,\n dataType: pgError.dataType\n // The outer Drizzle wrapper message used to be logged here \"for\n // full context\": it is `Failed query: <sql>\\nparams: <values>`, so\n // it published the statement and every bound value (an email, a\n // password hash) on every realtime data failure. The SQLSTATE,\n // detail, table, column and constraint above are the diagnostic\n // value; the wrapper added only the leak. `logger` strips the\n // wrapper as well, but the field itself carried nothing else.\n });\n const { message, code, violations } = pgErrorToFriendlyMessage(pgError, context, { collection: options.collection });\n return { message, code, ...(violations.length > 0 && { violations }) };\n }\n\n // No PG error found — log the raw error as-is\n logger.error(`Database error in \"${context}\" (no PG error extracted)`, {\n error: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n cause: error instanceof Error && error.cause\n ? (error.cause instanceof Error ? error.cause.message : String(error.cause))\n : undefined\n });\n\n // Try to get the deepest cause message\n const causeMessage = extractCauseMessage(error);\n if (causeMessage) {\n return { message: `Database error in \"${context}\": ${causeMessage}` };\n }\n\n // Last resort — generic message, never leak raw SQL\n return { message: `Could not load data for \"${context}\". Check server logs for details.` };\n}\n"],"mappings":"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"}