@rebasepro/server-postgres 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/backup-argv.d.ts +19 -0
  4. package/dist/branch-argv.d.ts +28 -0
  5. package/dist/branch-prune.d.ts +77 -0
  6. package/dist/cli-collections-path.d.ts +31 -0
  7. package/dist/cli-errors.d.ts +123 -0
  8. package/dist/cli-flags.d.ts +101 -0
  9. package/dist/cli-helpers.d.ts +94 -10
  10. package/dist/collection-index-C7LopMyX.js +247 -0
  11. package/dist/collection-index-C7LopMyX.js.map +1 -0
  12. package/dist/databasePoolManager.d.ts +27 -0
  13. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-CagOzEPS.js} +6 -6
  14. package/dist/{ensure-collection-policies-UCqgv_8c.js.map → ensure-collection-policies-CagOzEPS.js.map} +1 -1
  15. package/dist/{ensure-collection-tables-DgVixhX3.js → ensure-collection-tables-DbAhrjnG.js} +614 -52
  16. package/dist/ensure-collection-tables-DbAhrjnG.js.map +1 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.es.js +19307 -3059
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-ClJoTboT.js} +53 -189
  21. package/dist/rls-enforcement-ClJoTboT.js.map +1 -0
  22. package/dist/schema/column-type-drift.d.ts +79 -0
  23. package/dist/schema/doctor.d.ts +42 -0
  24. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  25. package/dist/schema/generate-next-step.d.ts +30 -0
  26. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  27. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  28. package/dist/schema/introspect-db-queries.d.ts +34 -0
  29. package/dist/security/rls-enforcement.d.ts +63 -2
  30. package/dist/services/BranchService.d.ts +86 -1
  31. package/dist/services/collection-helpers.d.ts +1 -1
  32. package/dist/services/realtimeService.d.ts +30 -2
  33. package/dist/services/row-pipeline.d.ts +12 -1
  34. package/dist/utils/pg-error-utils.d.ts +3 -1
  35. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  36. package/package.json +32 -27
  37. package/dist/backup-service-DCk7KhhL.js +0 -9044
  38. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  39. package/dist/collection-index-BRUg10H5.js +0 -5690
  40. package/dist/collection-index-BRUg10H5.js.map +0 -1
  41. package/dist/connection-GOKU3Hu5.js +0 -277
  42. package/dist/connection-GOKU3Hu5.js.map +0 -1
  43. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  44. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  45. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  46. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  47. package/dist/src-DiB5RP2Z.js +0 -541
  48. package/dist/src-DiB5RP2Z.js.map +0 -1
  49. package/dist/websocket-BZ4H5wUz.js +0 -8198
  50. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  51. package/src/PostgresAdapter.ts +0 -91
  52. package/src/PostgresBackendDriver.ts +0 -1857
  53. package/src/PostgresBootstrapper.ts +0 -1157
  54. package/src/auth/ensure-tables.ts +0 -896
  55. package/src/auth/schema-version.ts +0 -260
  56. package/src/auth/services.ts +0 -1610
  57. package/src/backup/backup-cli.ts +0 -520
  58. package/src/backup/backup-cron.ts +0 -212
  59. package/src/backup/backup-logic.ts +0 -93
  60. package/src/backup/backup-service.ts +0 -485
  61. package/src/backup/index.ts +0 -12
  62. package/src/backup/pg-tools.ts +0 -403
  63. package/src/backup/retention.ts +0 -86
  64. package/src/cli-errors.ts +0 -265
  65. package/src/cli-helpers.ts +0 -569
  66. package/src/cli-output.ts +0 -43
  67. package/src/cli.ts +0 -1339
  68. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  69. package/src/collections/buildRegistry.ts +0 -68
  70. package/src/collections/validate-relations.ts +0 -402
  71. package/src/connection.ts +0 -333
  72. package/src/data-transformer.ts +0 -845
  73. package/src/databasePoolManager.ts +0 -95
  74. package/src/history/HistoryService.ts +0 -254
  75. package/src/history/ensure-history-table.ts +0 -53
  76. package/src/index.ts +0 -19
  77. package/src/interfaces.ts +0 -60
  78. package/src/module-dir.ts +0 -7
  79. package/src/schema/atlas-argv.ts +0 -94
  80. package/src/schema/auth-schema.ts +0 -298
  81. package/src/schema/auth-users-columns.ts +0 -131
  82. package/src/schema/carved-out-migration.ts +0 -404
  83. package/src/schema/classify-change.ts +0 -436
  84. package/src/schema/collection-index.ts +0 -427
  85. package/src/schema/collections-schema-version.ts +0 -103
  86. package/src/schema/destructive-sql.ts +0 -94
  87. package/src/schema/doctor-cli.ts +0 -66
  88. package/src/schema/doctor-policy-checks.ts +0 -105
  89. package/src/schema/doctor.ts +0 -837
  90. package/src/schema/drizzle-ddl.ts +0 -33
  91. package/src/schema/dynamic-tables.test.ts +0 -302
  92. package/src/schema/dynamic-tables.ts +0 -293
  93. package/src/schema/ensure-collection-policies.ts +0 -198
  94. package/src/schema/ensure-collection-tables.test.ts +0 -518
  95. package/src/schema/ensure-collection-tables.ts +0 -1267
  96. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  97. package/src/schema/generate-drizzle-schema.ts +0 -143
  98. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  99. package/src/schema/generate-postgres-ddl.ts +0 -180
  100. package/src/schema/generate-schema-commit.ts +0 -267
  101. package/src/schema/generated-schema-staleness.ts +0 -284
  102. package/src/schema/introspect-db-constraints.ts +0 -385
  103. package/src/schema/introspect-db-inference.ts +0 -255
  104. package/src/schema/introspect-db-logic.ts +0 -1387
  105. package/src/schema/introspect-db-naming.ts +0 -15
  106. package/src/schema/introspect-db-project.ts +0 -78
  107. package/src/schema/introspect-db-queries.ts +0 -326
  108. package/src/schema/introspect-db-structure.ts +0 -670
  109. package/src/schema/introspect-db-types.ts +0 -56
  110. package/src/schema/introspect-db.ts +0 -257
  111. package/src/schema/introspect-runtime.test.ts +0 -260
  112. package/src/schema/introspect-runtime.ts +0 -327
  113. package/src/schema/non-sql-collections.test.ts +0 -131
  114. package/src/schema/rls-bootstrap-sql.ts +0 -288
  115. package/src/schema/search-column.ts +0 -643
  116. package/src/schema/test-schema.ts +0 -11
  117. package/src/schema/vector-index.ts +0 -491
  118. package/src/security/anonymous-grants.test.ts +0 -73
  119. package/src/security/policy-drift.test.ts +0 -435
  120. package/src/security/policy-drift.ts +0 -473
  121. package/src/security/rls-enforcement.ts +0 -505
  122. package/src/services/BranchService.ts +0 -326
  123. package/src/services/FetchService.ts +0 -2115
  124. package/src/services/PersistService.ts +0 -557
  125. package/src/services/RelationService.ts +0 -978
  126. package/src/services/RelationWriteService.ts +0 -653
  127. package/src/services/cdc/CdcListener.ts +0 -103
  128. package/src/services/cdc/junction-tables.ts +0 -91
  129. package/src/services/cdc/trigger-cdc.ts +0 -173
  130. package/src/services/channel-bus/ChannelBus.ts +0 -44
  131. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  132. package/src/services/channel-bus/index.ts +0 -114
  133. package/src/services/channel-history.ts +0 -424
  134. package/src/services/channel-presence.ts +0 -185
  135. package/src/services/collection-helpers.ts +0 -420
  136. package/src/services/dataService.ts +0 -256
  137. package/src/services/index.ts +0 -14
  138. package/src/services/junction-writes.ts +0 -295
  139. package/src/services/nested-path.ts +0 -145
  140. package/src/services/pg-notify-listener.ts +0 -151
  141. package/src/services/realtimeService.ts +0 -2545
  142. package/src/services/row-pipeline.ts +0 -240
  143. package/src/services/write-denial.ts +0 -55
  144. package/src/types.ts +0 -4
  145. package/src/utils/connection-string.ts +0 -58
  146. package/src/utils/drizzle-conditions.ts +0 -2743
  147. package/src/utils/pg-array-null-patch.ts +0 -42
  148. package/src/utils/pg-error-utils.ts +0 -391
  149. 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
- }