@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,403 +0,0 @@
1
- /**
2
- * Pure helpers for the backup/restore commands.
3
- *
4
- * Everything in this file is side-effect free so it can be unit-tested
5
- * without a live Postgres server, matching the constraint that CI must
6
- * not require a database.
7
- */
8
-
9
- import { forLibpq } from "../utils/connection-string";
10
-
11
- /**
12
- * A parsed backup destination. `--out` (and the scheduled-backup config)
13
- * accepts either a local filesystem path or an object-storage URL.
14
- */
15
- export type BackupDestination =
16
- | { kind: "local"; path: string }
17
- | { kind: "s3"; bucket: string; prefix: string }
18
- | { kind: "gcs"; bucket: string; prefix: string };
19
-
20
- /**
21
- * Extract the database name from a Postgres connection string.
22
- * Returns `null` when the URL has no database path (e.g. bare host).
23
- */
24
- export function parseDbNameFromUrl(connectionString: string): string | null {
25
- try {
26
- const parsed = new URL(connectionString);
27
- const name = parsed.pathname.replace(/^\//, "").trim();
28
- return name.length > 0 ? name : null;
29
- } catch {
30
- // Fall back to a permissive regex for non-URL DSNs.
31
- const match = connectionString.match(/\/([^/?]+)(\?|$)/);
32
- return match && match[1] ? match[1] : null;
33
- }
34
- }
35
-
36
- /**
37
- * Swap the database name in a connection string, preserving credentials,
38
- * host, port and query params. Used by `--target-db` / `--create-db` so a
39
- * restore can target a fresh database instead of clobbering the live one.
40
- */
41
- export function withDatabaseName(connectionString: string, dbName: string): string {
42
- try {
43
- const parsed = new URL(connectionString);
44
- parsed.pathname = `/${dbName}`;
45
- return parsed.toString();
46
- } catch {
47
- return connectionString.replace(/\/([^/?]+)(\?|$)/, `/${dbName}$2`);
48
- }
49
- }
50
-
51
- /**
52
- * Parse the major version out of `pg_dump --version` / `pg_restore --version`
53
- * output, e.g. `"pg_dump (PostgreSQL) 16.2"` → `16`. Handles the pre-10
54
- * `9.6.x` scheme (returns `9`) and beta strings like `"17beta1"`.
55
- */
56
- export function parsePgToolMajor(versionOutput: string): number | null {
57
- const match = versionOutput.match(/(\d+)(?:\.(\d+))?/);
58
- if (!match) return null;
59
- const major = Number(match[1]);
60
- if (!Number.isFinite(major)) return null;
61
- // Postgres 9.x used the first two numbers as the major (9.6, 9.4…).
62
- if (major === 9 && match[2] !== undefined) {
63
- return 9;
64
- }
65
- return major;
66
- }
67
-
68
- /**
69
- * Convert `SELECT current_setting('server_version_num')` (e.g. `160002`)
70
- * into a major version (`16`). Also accepts pre-10 encodings like `90603`
71
- * → `9`.
72
- */
73
- export function serverVersionNumToMajor(versionNum: number | string): number | null {
74
- const num = typeof versionNum === "string" ? Number(versionNum) : versionNum;
75
- if (!Number.isFinite(num) || num <= 0) return null;
76
- if (num < 100000) {
77
- // 9.x scheme: 90603 = 9.6.3
78
- return Math.floor(num / 10000);
79
- }
80
- return Math.floor(num / 10000);
81
- }
82
-
83
- export interface VersionCompatibility {
84
- compatible: boolean;
85
- reason?: string;
86
- }
87
-
88
- /**
89
- * pg_dump / pg_restore must be **at least** as new as the server they talk
90
- * to. A newer client against an older server is supported; an older client
91
- * against a newer server is not and produces corrupt or rejected output.
92
- */
93
- export function checkToolServerCompatibility(
94
- toolMajor: number | null,
95
- serverMajor: number | null
96
- ): VersionCompatibility {
97
- if (toolMajor === null) {
98
- return { compatible: false, reason: "Could not determine the client tool version." };
99
- }
100
- if (serverMajor === null) {
101
- return { compatible: false, reason: "Could not determine the Postgres server version." };
102
- }
103
- if (toolMajor < serverMajor) {
104
- return {
105
- compatible: false,
106
- reason:
107
- `Client tool is Postgres ${toolMajor} but the server is Postgres ${serverMajor}. ` +
108
- `pg_dump/pg_restore must be the same major version as the server or newer. ` +
109
- `Install Postgres ${serverMajor} client tools.`
110
- };
111
- }
112
- return { compatible: true };
113
- }
114
-
115
- /**
116
- * Build a deterministic, sortable backup file name:
117
- * `rebase-<db>-<YYYYMMDD>T<HHMMSS>Z.dump`
118
- *
119
- * The UTC timestamp is embedded so retention pruning can recover the
120
- * creation time from the object key alone, without extra metadata.
121
- */
122
- export function buildBackupFilename(dbName: string, date: Date = new Date()): string {
123
- const iso = date.toISOString(); // 2026-07-14T09:12:03.123Z
124
- const stamp = iso.replace(/\.\d+Z$/, "Z").replace(/[-:]/g, "");
125
- const safeDb = dbName.replace(/[^a-zA-Z0-9_-]/g, "_");
126
- return `rebase-${safeDb}-${stamp}.dump`;
127
- }
128
-
129
- /**
130
- * Recover the creation timestamp encoded in a backup file name by
131
- * {@link buildBackupFilename}. Returns `null` for names that don't match,
132
- * so foreign objects in a shared prefix are never pruned.
133
- */
134
- export function parseBackupTimestamp(fileName: string): Date | null {
135
- const base = fileName.split("/").pop() ?? fileName;
136
- const match = base.match(/-(\d{8})T(\d{6})Z\.dump$/);
137
- if (!match) return null;
138
- const [, ymd, hms] = match;
139
- const iso =
140
- `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}` +
141
- `T${hms.slice(0, 2)}:${hms.slice(2, 4)}:${hms.slice(4, 6)}Z`;
142
- const date = new Date(iso);
143
- return Number.isNaN(date.getTime()) ? null : date;
144
- }
145
-
146
- /**
147
- * Parse a destination string into a structured {@link BackupDestination}.
148
- * `s3://bucket/prefix` and `gs://bucket/prefix` map to object storage;
149
- * anything else is treated as a local path.
150
- */
151
- export function parseBackupDestination(out: string): BackupDestination {
152
- const s3 = out.match(/^s3:\/\/([^/]+)\/?(.*)$/);
153
- if (s3) {
154
- return { kind: "s3", bucket: s3[1], prefix: stripTrailingSlash(s3[2]) };
155
- }
156
- const gcs = out.match(/^gs:\/\/([^/]+)\/?(.*)$/);
157
- if (gcs) {
158
- return { kind: "gcs", bucket: gcs[1], prefix: stripTrailingSlash(gcs[2]) };
159
- }
160
- return { kind: "local", path: out };
161
- }
162
-
163
- function stripTrailingSlash(s: string): string {
164
- return s.replace(/\/+$/, "");
165
- }
166
-
167
- /**
168
- * Join a storage prefix and a file name without producing a leading or
169
- * doubled slash.
170
- */
171
- export function joinStorageKey(prefix: string, fileName: string): string {
172
- const clean = prefix.replace(/^\/+|\/+$/g, "");
173
- return clean.length > 0 ? `${clean}/${fileName}` : fileName;
174
- }
175
-
176
- /**
177
- * The identity `pg_dump` reads rows as, when row security is left on.
178
- *
179
- * Not optional, and that is the whole design. `pg_dump --enable-row-security`
180
- * on its own is the dangerous command in this file: it turns the "query would
181
- * be affected by row-level security policy" *error* into a dump that exits 0
182
- * and is silently missing every row the dumping role's policies exclude. A
183
- * backup that looks fine and restores most of your data is worse than one that
184
- * refused to run.
185
- *
186
- * So the flag is unreachable without a subject to evaluate the policies
187
- * against. Rebase's generated policies read `app.uid` and `app.user_roles`;
188
- * supplying an admin role satisfies the `admin_full_access` rule and the dump
189
- * sees everything that rule sees.
190
- */
191
- export interface RowSecurityIdentity {
192
- /** Written to `app.uid`. Any non-empty value — it is only an audit trail. */
193
- uid: string;
194
- /** Written to `app.user_roles`. Must include a role the policies admit. */
195
- roles: string[];
196
- }
197
-
198
- /**
199
- * `PGOPTIONS` carrying an identity, for a libpq tool that has no other way to
200
- * set a GUC.
201
- *
202
- * A backslash escape rather than quoting, which is what libpq's `-c` parsing
203
- * takes: a space inside a value ends the option otherwise, so a role list is
204
- * comma-joined and never spaced.
205
- */
206
- export function buildRowSecurityPgOptions(identity: RowSecurityIdentity): string {
207
- const escape = (value: string) => value.replace(/([\\ ])/g, "\\$1");
208
- return [
209
- `-c app.uid=${escape(identity.uid)}`,
210
- `-c app.user_id=${escape(identity.uid)}`,
211
- `-c app.user_roles=${escape(identity.roles.join(","))}`
212
- ].join(" ");
213
- }
214
-
215
- /**
216
- * Assemble the `pg_dump` argument vector. Uses the custom format (`-Fc`),
217
- * which is compressed and restorable selectively via `pg_restore`.
218
- */
219
- export function buildPgDumpArgs(opts: {
220
- connectionString: string;
221
- outFile: string;
222
- /** Extra schemas/tables to exclude, e.g. Atlas revision tables. */
223
- excludeSchemas?: string[];
224
- /** Number of parallel jobs (directory format only; ignored for -Fc). */
225
- noOwner?: boolean;
226
- /**
227
- * Dump with row security on, as this identity. Omit — which is the default
228
- * — and `pg_dump` errors rather than skipping rows it cannot see.
229
- */
230
- rowSecurity?: RowSecurityIdentity;
231
- }): string[] {
232
- const args = ["--format=custom", "--no-password", `--file=${opts.outFile}`];
233
- if (opts.noOwner) {
234
- args.push("--no-owner");
235
- }
236
- if (opts.rowSecurity) {
237
- args.push("--enable-row-security");
238
- }
239
- for (const schema of opts.excludeSchemas ?? []) {
240
- args.push(`--exclude-schema=${schema}`);
241
- }
242
- args.push(forLibpq(opts.connectionString));
243
- return args;
244
- }
245
-
246
- /**
247
- * Whether a `pg_dump` failure is the row-security one, and what to do about it.
248
- *
249
- * The error text names the table and nothing else, so the first read of it is
250
- * "why would a backup be affected by RLS at all?" — the answer being that the
251
- * dumping role is not the tables' owner and has no `BYPASSRLS`, which is the
252
- * normal state of the `postgres` user on Cloud SQL, RDS and every other managed
253
- * Postgres. Nothing about that is visible from the message.
254
- *
255
- * Returns `null` for any other failure, so the caller reports it unchanged.
256
- */
257
- export function diagnoseRowSecurityDumpFailure(error: unknown): string | null {
258
- const text = [
259
- (error as { stderr?: unknown })?.stderr,
260
- (error as { message?: unknown })?.message
261
- ].map(part => (typeof part === "string" ? part : "")).join("\n");
262
-
263
- if (!/row-level security policy/i.test(text)) return null;
264
-
265
- const table = text.match(/for table "([^"]+)"/)?.[1];
266
-
267
- return [
268
- `pg_dump cannot read ${table ? `"${table}"` : "one of the tables"} because row-level security applies to it.`,
269
- "",
270
- " The dumping role is neither the table's owner nor `BYPASSRLS`, which is the normal",
271
- " state of the `postgres` user on Cloud SQL, RDS and other managed Postgres — there is",
272
- " no superuser to hand out.",
273
- "",
274
- " Two ways out:",
275
- "",
276
- " • Grant the dumping role BYPASSRLS, or make it the owner, and run this again. The",
277
- " dump then contains every row, which is what a backup should mean.",
278
- "",
279
- " • Re-run with --enable-row-security to dump as an admin subject instead. Rebase",
280
- " sets `app.uid`/`app.user_roles` so the generated `admin_full_access` policy",
281
- " admits the dump. Read the warning it prints: the result contains exactly the",
282
- " rows those policies admit, and any table whose policies do not include an",
283
- " admin rule comes out short — with no error.",
284
- "",
285
- " Do not reach for a bare `pg_dump --enable-row-security` by hand. Without the",
286
- " settings above it succeeds and silently omits rows."
287
- ].join("\n");
288
- }
289
-
290
- /**
291
- * Assemble the `pg_restore` argument vector for a custom-format dump.
292
- */
293
- export function buildPgRestoreArgs(opts: {
294
- connectionString: string;
295
- inputFile: string;
296
- /** Drop objects before recreating them (destructive but idempotent). */
297
- clean?: boolean;
298
- /**
299
- * Abort on the first error instead of logging and continuing. Defaults
300
- * ON: a restore that silently skips failed GRANT/RLS statements (because
301
- * a role is missing) "succeeds" with RLS un-enforced — a security hole.
302
- * Fail loudly instead so the operator knows the restore is incomplete.
303
- */
304
- exitOnError?: boolean;
305
- noOwner?: boolean;
306
- }): string[] {
307
- const args = ["--format=custom", "--no-password", `--dbname=${forLibpq(opts.connectionString)}`];
308
- if (opts.clean) {
309
- args.push("--clean", "--if-exists");
310
- }
311
- if (opts.noOwner) {
312
- args.push("--no-owner");
313
- }
314
- // Default to --exit-on-error unless explicitly disabled.
315
- if (opts.exitOnError !== false) {
316
- args.push("--exit-on-error");
317
- }
318
- args.push(opts.inputFile);
319
- return args;
320
- }
321
-
322
- /**
323
- * Assemble the `pg_restore --list` argument vector. Reading a dump's table
324
- * of contents parses the whole archive without touching a database, so it is
325
- * a cheap integrity check that the file isn't truncated or corrupt.
326
- */
327
- export function buildPgRestoreListArgs(inputFile: string): string[] {
328
- return ["--list", inputFile];
329
- }
330
-
331
- /**
332
- * Assemble the `pg_dumpall --globals-only` argument vector. Roles (and other
333
- * cluster-wide objects) live outside any single database, so a per-database
334
- * `pg_dump` omits them. Without the `rebase_user` role the RLS GRANT
335
- * statements in the main dump fail on restore and RLS is silently lost — so
336
- * every backup captures the globals into a sidecar `.globals.sql`.
337
- *
338
- * `--no-role-passwords` keeps role secrets out of the artifact (backups may
339
- * be shipped off-box); roles are recreated password-less and re-secured by
340
- * the operator.
341
- */
342
- export function buildPgDumpallGlobalsArgs(opts: {
343
- connectionString: string;
344
- outFile: string;
345
- }): string[] {
346
- return [
347
- "--globals-only",
348
- "--no-role-passwords",
349
- "--no-password",
350
- `--file=${opts.outFile}`,
351
- `--dbname=${forLibpq(opts.connectionString)}`
352
- ];
353
- }
354
-
355
- /**
356
- * Derive the globals sidecar path/key for a given `.dump` file. Keeps the
357
- * two artifacts adjacent so listing, uploading and pruning can find one from
358
- * the other. A name that doesn't end in `.dump` is returned unchanged with a
359
- * `.globals.sql` suffix appended.
360
- */
361
- export function globalsFileForDump(dumpPath: string): string {
362
- return dumpPath.endsWith(".dump")
363
- ? dumpPath.slice(0, -".dump".length) + ".globals.sql"
364
- : dumpPath + ".globals.sql";
365
- }
366
-
367
- /**
368
- * Split a `pg_dumpall --globals-only` script into individual statements.
369
- * Used when replaying globals on restore so each `CREATE ROLE` / `GRANT`
370
- * can run independently and a benign "role already exists" on one doesn't
371
- * abort the rest. Drops `--` comment lines and blank statements.
372
- */
373
- export function splitGlobalsStatements(sql: string): string[] {
374
- const withoutComments = sql
375
- .split("\n")
376
- // `--` comments, and psql meta-commands. pg_dumpall 15+ wraps its output
377
- // in `\restrict <token>` / `\unrestrict <token>`, which are instructions
378
- // to psql, not SQL. This replay sends statements over a connection
379
- // instead, so they arrived at the server as `\restrict …` and came back
380
- // as `syntax error at or near "\"` — reported to the user as two skipped
381
- // globals per restore, which reads like roles failed to apply when
382
- // nothing did. They carry no state worth replaying: dropping them is
383
- // exactly what a SQL-level consumer should do.
384
- .filter((line) => {
385
- const trimmed = line.trim();
386
- return !trimmed.startsWith("--") && !trimmed.startsWith("\\");
387
- })
388
- .join("\n");
389
- return withoutComments
390
- .split(";")
391
- .map((s) => s.trim())
392
- .filter((s) => s.length > 0);
393
- }
394
-
395
- /**
396
- * Resolve the Postgres connection string the backup commands should use,
397
- * mirroring the precedence the branch command already relies on.
398
- */
399
- export function resolveConnectionString(
400
- env: Record<string, string | undefined>
401
- ): string | null {
402
- return env.DATABASE_URL || env.ADMIN_CONNECTION_STRING || null;
403
- }
@@ -1,86 +0,0 @@
1
- /**
2
- * Retention pruning for scheduled backups. Pure logic — no I/O — so it can
3
- * be exhaustively unit-tested without touching a storage backend.
4
- */
5
- import { parseBackupTimestamp } from "./pg-tools";
6
-
7
- export interface BackupObject {
8
- /** Storage key / file name. */
9
- key: string;
10
- /**
11
- * Creation time. Optional — when absent it is recovered from the
12
- * timestamp encoded in the key by `buildBackupFilename`.
13
- */
14
- createdAt?: Date;
15
- /**
16
- * Size on disk, when the lister knows it (local destinations always do).
17
- *
18
- * Reported so an empty artifact cannot masquerade as a backup in
19
- * `rebase db backups list`. Deliberately NOT consulted by
20
- * {@link selectBackupsToPrune}: retention decides by age, and a rule that
21
- * silently skipped small files would make a genuinely tiny dump immortal.
22
- * Visibility is the fix here; the artifacts themselves are now removed at
23
- * the point of failure by `discardPartialDump`.
24
- */
25
- sizeBytes?: number;
26
- }
27
-
28
- export interface RetentionOptions {
29
- /** Delete backups older than this many days. `0`/negative disables age pruning. */
30
- retentionDays?: number;
31
- /**
32
- * Always keep at least this many of the most recent backups, even if
33
- * they are older than `retentionDays`. Guards against wiping every
34
- * backup after a long outage. Default: 0 (no minimum).
35
- */
36
- keepMinimum?: number;
37
- /** Reference "now" — injectable for deterministic tests. */
38
- now?: Date;
39
- }
40
-
41
- /**
42
- * Decide which backups to delete under the given retention policy.
43
- *
44
- * Rules, applied in order:
45
- * 1. Objects whose key isn't a recognised rebase backup (no parseable
46
- * timestamp and no `createdAt`) are never selected — foreign files in a
47
- * shared bucket are left untouched.
48
- * 2. The `keepMinimum` newest backups are always retained.
49
- * 3. Of the remainder, any older than `retentionDays` are selected for
50
- * deletion.
51
- *
52
- * Returns the keys to delete, oldest first.
53
- */
54
- export function selectBackupsToPrune(
55
- backups: BackupObject[],
56
- options: RetentionOptions
57
- ): string[] {
58
- const retentionDays = options.retentionDays ?? 0;
59
- const keepMinimum = Math.max(0, options.keepMinimum ?? 0);
60
- const now = options.now ?? new Date();
61
-
62
- if (retentionDays <= 0) {
63
- return [];
64
- }
65
-
66
- // Resolve a timestamp for every object; drop the ones we can't date.
67
- const dated = backups
68
- .map((b) => {
69
- const createdAt = b.createdAt ?? parseBackupTimestamp(b.key);
70
- return createdAt ? { key: b.key, createdAt } : null;
71
- })
72
- .filter((b): b is { key: string; createdAt: Date } => b !== null);
73
-
74
- // Newest first so the first `keepMinimum` are the protected ones.
75
- dated.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
76
-
77
- const cutoff = now.getTime() - retentionDays * 24 * 60 * 60 * 1000;
78
-
79
- const toDelete = dated
80
- .slice(keepMinimum)
81
- .filter((b) => b.createdAt.getTime() < cutoff);
82
-
83
- // Return oldest first for predictable, readable logs.
84
- toDelete.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
85
- return toDelete.map((b) => b.key);
86
- }