@rebasepro/server-postgres 0.25.0-canary.ge06257e → 0.25.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 (126) hide show
  1. package/dist/{BranchService-DNg4txyf.js → BranchService-ClklxyAQ.js} +283 -20
  2. package/dist/BranchService-ClklxyAQ.js.map +1 -0
  3. package/dist/backup/backup-cli.d.ts +70 -1
  4. package/dist/backup/backup-service.d.ts +79 -0
  5. package/dist/backup/pg-binary.d.ts +2 -2
  6. package/dist/backup/pg-tools.d.ts +60 -1
  7. package/dist/backup-cli-gijeXe0u.js +1961 -0
  8. package/dist/backup-cli-gijeXe0u.js.map +1 -0
  9. package/dist/{boot-ddl-Dnvpmm_T.js → boot-ddl-DYH95Qeg.js} +3 -3
  10. package/dist/{boot-ddl-Dnvpmm_T.js.map → boot-ddl-DYH95Qeg.js.map} +1 -1
  11. package/dist/branch-argv.d.ts +15 -0
  12. package/dist/branch-prune.d.ts +18 -0
  13. package/dist/{cli-console-C_ISGwJo.js → cli-console-CQrQ0lAY.js} +19 -13
  14. package/dist/cli-console-CQrQ0lAY.js.map +1 -0
  15. package/dist/cli-console.d.ts +4 -3
  16. package/dist/{cli-errors-mx4sSjB8.js → cli-errors-DiA0XFIS.js} +110 -14
  17. package/dist/{cli-errors-mx4sSjB8.js.map → cli-errors-DiA0XFIS.js.map} +1 -1
  18. package/dist/cli-errors.d.ts +26 -1
  19. package/dist/cli-flags.d.ts +51 -0
  20. package/dist/{cli-prompt-egtNKkZl.js → cli-prompt-DXG0yk-h.js} +39 -4
  21. package/dist/{cli-prompt-egtNKkZl.js.map → cli-prompt-DXG0yk-h.js.map} +1 -1
  22. package/dist/cli.js +120 -184
  23. package/dist/cli.js.map +1 -1
  24. package/dist/collections/validate-relations.d.ts +8 -2
  25. package/dist/{column-plan-helpers-D_V4oaZh.js → column-plan-helpers-IG6uFb1s.js} +2 -2
  26. package/dist/{column-plan-helpers-D_V4oaZh.js.map → column-plan-helpers-IG6uFb1s.js.map} +1 -1
  27. package/dist/{connection-C938nR3E.js → connection-czvI7TZL.js} +57 -10
  28. package/dist/connection-czvI7TZL.js.map +1 -0
  29. package/dist/{databasePoolManager-Yqdw14wY.js → databasePoolManager-3stMNFL-.js} +4 -6
  30. package/dist/databasePoolManager-3stMNFL-.js.map +1 -0
  31. package/dist/db-command-line.d.ts +14 -0
  32. package/dist/{rls-bootstrap-sql-Bd-hJk08.js → db-subcommands-BPoHCxJR.js} +455 -3
  33. package/dist/db-subcommands-BPoHCxJR.js.map +1 -0
  34. package/dist/db-subcommands.d.ts +11 -0
  35. package/dist/{deferred-index-build-Ca6OqKyR.js → deferred-index-build-BE0I6sYU.js} +8 -8
  36. package/dist/{deferred-index-build-Ca6OqKyR.js.map → deferred-index-build-BE0I6sYU.js.map} +1 -1
  37. package/dist/{doctor-CJyuhV1n.js → doctor-BT--1kq3.js} +107 -11
  38. package/dist/doctor-BT--1kq3.js.map +1 -0
  39. package/dist/{ensure-collection-policies-C1ux3V_z.js → ensure-collection-policies-BnGOURpG.js} +60 -11
  40. package/dist/ensure-collection-policies-BnGOURpG.js.map +1 -0
  41. package/dist/{ensure-collection-tables-Do-Bvb64.js → ensure-collection-tables-Cet4650m.js} +9 -9
  42. package/dist/{ensure-collection-tables-Do-Bvb64.js.map → ensure-collection-tables-Cet4650m.js.map} +1 -1
  43. package/dist/{ensure-tables-Dl_Eh9ow.js → ensure-tables-l_OgFQjN.js} +5 -5
  44. package/dist/{ensure-tables-Dl_Eh9ow.js.map → ensure-tables-l_OgFQjN.js.map} +1 -1
  45. package/dist/{enum-labels-kept-D9eTIWCz.js → enum-labels-kept-nheku1pB.js} +3 -3
  46. package/dist/{enum-labels-kept-D9eTIWCz.js.map → enum-labels-kept-nheku1pB.js.map} +1 -1
  47. package/dist/{generate-drizzle-schema-Bf8canaR.js → generate-drizzle-schema-DC0BOUpn.js} +3 -3
  48. package/dist/{generate-drizzle-schema-Bf8canaR.js.map → generate-drizzle-schema-DC0BOUpn.js.map} +1 -1
  49. package/dist/{generate-drizzle-schema-logic-BaT0H7Gq.js → generate-drizzle-schema-logic-CPzCRD-8.js} +4 -4
  50. package/dist/{generate-drizzle-schema-logic-BaT0H7Gq.js.map → generate-drizzle-schema-logic-CPzCRD-8.js.map} +1 -1
  51. package/dist/{generate-files-D50Pxw1G.js → generate-files-BP-4W_gm.js} +6 -6
  52. package/dist/{generate-files-D50Pxw1G.js.map → generate-files-BP-4W_gm.js.map} +1 -1
  53. package/dist/{generate-next-step-DCd7yiJ2.js → generate-next-step-DgK5BlBn.js} +22 -14
  54. package/dist/{generate-next-step-DCd7yiJ2.js.map → generate-next-step-DgK5BlBn.js.map} +1 -1
  55. package/dist/{generate-postgres-ddl-logic-B4k9djAV.js → generate-postgres-ddl-logic-DPl1oH_z.js} +4 -4
  56. package/dist/{generate-postgres-ddl-logic-B4k9djAV.js.map → generate-postgres-ddl-logic-DPl1oH_z.js.map} +1 -1
  57. package/dist/generated-schema-diff-eZCklRZQ.js +1025 -0
  58. package/dist/generated-schema-diff-eZCklRZQ.js.map +1 -0
  59. package/dist/{generated-schema-staleness-FlxM2i4i.js → generated-schema-staleness-BvNF3i-s.js} +2 -2
  60. package/dist/{generated-schema-staleness-FlxM2i4i.js.map → generated-schema-staleness-BvNF3i-s.js.map} +1 -1
  61. package/dist/index.d.ts +1 -0
  62. package/dist/index.es.js +70 -11
  63. package/dist/index.es.js.map +1 -1
  64. package/dist/{introspect-db-logic-j2vYhmdP.js → introspect-db-logic-DgQVzccW.js} +2 -2
  65. package/dist/{introspect-db-logic-j2vYhmdP.js.map → introspect-db-logic-DgQVzccW.js.map} +1 -1
  66. package/dist/migrate-policy-stamps-3eqfhes6.js +89 -0
  67. package/dist/migrate-policy-stamps-3eqfhes6.js.map +1 -0
  68. package/dist/{open-rebase-D8rytTP0.js → open-rebase-EmqVmS14.js} +78 -1027
  69. package/dist/open-rebase-EmqVmS14.js.map +1 -0
  70. package/dist/{pg-error-utils-CdsGYMTm.js → pg-error-utils-CFEnm2m9.js} +11 -4
  71. package/dist/pg-error-utils-CFEnm2m9.js.map +1 -0
  72. package/dist/{plan-schema-CsNgK0qi.js → plan-schema-Czf3q4-Q.js} +4 -4
  73. package/dist/{plan-schema-CsNgK0qi.js.map → plan-schema-Czf3q4-Q.js.map} +1 -1
  74. package/dist/{policy-drift-CY5P1ZRu.js → policy-drift-X4A-xAzv.js} +3 -3
  75. package/dist/{policy-drift-CY5P1ZRu.js.map → policy-drift-X4A-xAzv.js.map} +1 -1
  76. package/dist/pool-disconnects.d.ts +28 -0
  77. package/dist/{push-index-prebuild-BDFcfRPo.js → push-index-prebuild-COd7i6wm.js} +4 -4
  78. package/dist/{push-index-prebuild-BDFcfRPo.js.map → push-index-prebuild-COd7i6wm.js.map} +1 -1
  79. package/dist/{push-legacy-renames-D4bvMWAb.js → push-legacy-renames-CLgYYwPE.js} +4 -4
  80. package/dist/{push-legacy-renames-D4bvMWAb.js.map → push-legacy-renames-CLgYYwPE.js.map} +1 -1
  81. package/dist/{push-policy-plan-DTXE6aVa.js → push-policy-plan-D2gpQsdv.js} +23 -7
  82. package/dist/push-policy-plan-D2gpQsdv.js.map +1 -0
  83. package/dist/{render-ddl-CrfEe__B.js → render-ddl-4v88y-Gv.js} +4 -4
  84. package/dist/{render-ddl-CrfEe__B.js.map → render-ddl-4v88y-Gv.js.map} +1 -1
  85. package/dist/{rls-enforcement-D7P4-ZYX.js → rls-enforcement-DDyNoOSy.js} +2 -2
  86. package/dist/{rls-enforcement-D7P4-ZYX.js.map → rls-enforcement-DDyNoOSy.js.map} +1 -1
  87. package/dist/schema/atlas-argv.d.ts +3 -1
  88. package/dist/schema/doctor-cli.js +5 -5
  89. package/dist/schema/doctor.d.ts +20 -2
  90. package/dist/schema/ensure-collection-policies.d.ts +28 -1
  91. package/dist/schema/generate-drizzle-schema.js +1 -1
  92. package/dist/schema/generate-next-step.d.ts +20 -8
  93. package/dist/schema/generate-postgres-ddl.js +2 -2
  94. package/dist/schema/generated-schema-diff.d.ts +21 -0
  95. package/dist/schema/introspect-db.js +6 -6
  96. package/dist/schema/introspect-db.js.map +1 -1
  97. package/dist/schema/migrate-policy-stamps.d.ts +23 -0
  98. package/dist/schema/push-policy-plan.d.ts +10 -2
  99. package/dist/services/BranchService.d.ts +85 -0
  100. package/dist/services/pg-notify-listener.d.ts +2 -0
  101. package/dist/{source-DB6xJv3p.js → source-fZb92pN8.js} +46 -3
  102. package/dist/source-fZb92pN8.js.map +1 -0
  103. package/dist/{vector-index-BFhyEaiz.js → vector-index-DsQ3V2d2.js} +2 -2
  104. package/dist/{vector-index-BFhyEaiz.js.map → vector-index-DsQ3V2d2.js.map} +1 -1
  105. package/dist/{websocket-DMeXOoyv.js → websocket-BKZwsjmf.js} +18 -5
  106. package/dist/websocket-BKZwsjmf.js.map +1 -0
  107. package/package.json +7 -7
  108. package/dist/BranchService-DNg4txyf.js.map +0 -1
  109. package/dist/backup-cli-HCEkPcQj.js +0 -556
  110. package/dist/backup-cli-HCEkPcQj.js.map +0 -1
  111. package/dist/backup-service-D-UpT7DH.js +0 -857
  112. package/dist/backup-service-D-UpT7DH.js.map +0 -1
  113. package/dist/cli-console-C_ISGwJo.js.map +0 -1
  114. package/dist/cli-flags-DUozBFyK.js +0 -205
  115. package/dist/cli-flags-DUozBFyK.js.map +0 -1
  116. package/dist/connection-C938nR3E.js.map +0 -1
  117. package/dist/databasePoolManager-Yqdw14wY.js.map +0 -1
  118. package/dist/doctor-CJyuhV1n.js.map +0 -1
  119. package/dist/ensure-collection-policies-C1ux3V_z.js.map +0 -1
  120. package/dist/open-rebase-D8rytTP0.js.map +0 -1
  121. package/dist/pg-error-utils-CdsGYMTm.js.map +0 -1
  122. package/dist/push-policy-plan-DTXE6aVa.js.map +0 -1
  123. package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +0 -1
  124. package/dist/rolldown-runtime-V7rFbMRe.js +0 -39
  125. package/dist/source-DB6xJv3p.js.map +0 -1
  126. package/dist/websocket-DMeXOoyv.js.map +0 -1
@@ -1,857 +0,0 @@
1
- import { createRequire as __createRequire } from "module";
2
- __createRequire(import.meta.url);
3
- import { n as __exportAll } from "./rolldown-runtime-V7rFbMRe.js";
4
- import { Q as forLibpq, R as resolveLocalBin, et as splitPassword } from "./cli-errors-mx4sSjB8.js";
5
- import { o as execa } from "./rls-bootstrap-sql-Bd-hJk08.js";
6
- import path from "path";
7
- import fs from "fs";
8
- import os from "os";
9
- //#region src/backup/pg-tools.ts
10
- /**
11
- * Pure helpers for the backup/restore commands.
12
- *
13
- * Everything in this file is side-effect free so it can be unit-tested
14
- * without a live Postgres server, matching the constraint that CI must
15
- * not require a database.
16
- */
17
- /**
18
- * Extract the database name from a Postgres connection string.
19
- * Returns `null` when the URL has no database path (e.g. bare host).
20
- */
21
- function parseDbNameFromUrl(connectionString) {
22
- try {
23
- const name = new URL(connectionString).pathname.replace(/^\//, "").trim();
24
- return name.length > 0 ? name : null;
25
- } catch {
26
- const match = connectionString.match(/\/([^/?]+)(\?|$)/);
27
- return match && match[1] ? match[1] : null;
28
- }
29
- }
30
- /**
31
- * Swap the database name in a connection string, preserving credentials,
32
- * host, port and query params. Used by `--target-db` / `--create-db` so a
33
- * restore can target a fresh database instead of clobbering the live one.
34
- */
35
- function withDatabaseName(connectionString, dbName) {
36
- try {
37
- const parsed = new URL(connectionString);
38
- parsed.pathname = `/${dbName}`;
39
- return parsed.toString();
40
- } catch {
41
- return connectionString.replace(/\/([^/?]+)(\?|$)/, `/${dbName}$2`);
42
- }
43
- }
44
- /**
45
- * Parse the major version out of `pg_dump --version` / `pg_restore --version`
46
- * output, e.g. `"pg_dump (PostgreSQL) 16.2"` → `16`. Handles the pre-10
47
- * `9.6.x` scheme (returns `9`) and beta strings like `"17beta1"`.
48
- */
49
- function parsePgToolMajor(versionOutput) {
50
- const match = versionOutput.match(/(\d+)(?:\.(\d+))?/);
51
- if (!match) return null;
52
- const major = Number(match[1]);
53
- if (!Number.isFinite(major)) return null;
54
- if (major === 9 && match[2] !== void 0) return 9;
55
- return major;
56
- }
57
- /**
58
- * Convert `SELECT current_setting('server_version_num')` (e.g. `160002`)
59
- * into a major version (`16`). Also accepts pre-10 encodings like `90603`
60
- * → `9`.
61
- */
62
- function serverVersionNumToMajor(versionNum) {
63
- const num = typeof versionNum === "string" ? Number(versionNum) : versionNum;
64
- if (!Number.isFinite(num) || num <= 0) return null;
65
- if (num < 1e5) return Math.floor(num / 1e4);
66
- return Math.floor(num / 1e4);
67
- }
68
- /**
69
- * pg_dump / pg_restore must be **at least** as new as the server they talk
70
- * to. A newer client against an older server is supported; an older client
71
- * against a newer server is not and produces corrupt or rejected output.
72
- */
73
- function checkToolServerCompatibility(toolMajor, serverMajor) {
74
- if (toolMajor === null) return {
75
- compatible: false,
76
- reason: "Could not determine the client tool version."
77
- };
78
- if (serverMajor === null) return {
79
- compatible: false,
80
- reason: "Could not determine the Postgres server version."
81
- };
82
- if (toolMajor < serverMajor) return {
83
- compatible: false,
84
- reason: `Client tool is Postgres ${toolMajor} but the server is Postgres ${serverMajor}. pg_dump/pg_restore must be the same major version as the server or newer. Install Postgres ${serverMajor} client tools.`
85
- };
86
- return { compatible: true };
87
- }
88
- /**
89
- * Build a deterministic, sortable backup file name:
90
- * `rebase-<db>-<YYYYMMDD>T<HHMMSS>Z.dump`
91
- *
92
- * The UTC timestamp is embedded so retention pruning can recover the
93
- * creation time from the object key alone, without extra metadata.
94
- */
95
- function buildBackupFilename(dbName, date = /* @__PURE__ */ new Date()) {
96
- const stamp = date.toISOString().replace(/\.\d+Z$/, "Z").replace(/[-:]/g, "");
97
- return `rebase-${dbName.replace(/[^a-zA-Z0-9_-]/g, "_")}-${stamp}.dump`;
98
- }
99
- /**
100
- * Recover the creation timestamp encoded in a backup file name by
101
- * {@link buildBackupFilename}. Returns `null` for names that don't match,
102
- * so foreign objects in a shared prefix are never pruned.
103
- */
104
- function parseBackupTimestamp(fileName) {
105
- const match = (fileName.split("/").pop() ?? fileName).match(/-(\d{8})T(\d{6})Z\.dump$/);
106
- if (!match) return null;
107
- const [, ymd, hms] = match;
108
- const iso = `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}T${hms.slice(0, 2)}:${hms.slice(2, 4)}:${hms.slice(4, 6)}Z`;
109
- const date = new Date(iso);
110
- return Number.isNaN(date.getTime()) ? null : date;
111
- }
112
- /**
113
- * Parse a destination string into a structured {@link BackupDestination}.
114
- * `s3://bucket/prefix` and `gs://bucket/prefix` map to object storage;
115
- * anything else is treated as a local path.
116
- */
117
- function parseBackupDestination(out) {
118
- const s3 = out.match(/^s3:\/\/([^/]+)\/?(.*)$/);
119
- if (s3) return {
120
- kind: "s3",
121
- bucket: s3[1],
122
- prefix: stripTrailingSlash(s3[2])
123
- };
124
- const gcs = out.match(/^gs:\/\/([^/]+)\/?(.*)$/);
125
- if (gcs) return {
126
- kind: "gcs",
127
- bucket: gcs[1],
128
- prefix: stripTrailingSlash(gcs[2])
129
- };
130
- return {
131
- kind: "local",
132
- path: out
133
- };
134
- }
135
- function stripTrailingSlash(s) {
136
- return s.replace(/\/+$/, "");
137
- }
138
- /**
139
- * Join a storage prefix and a file name without producing a leading or
140
- * doubled slash.
141
- */
142
- function joinStorageKey(prefix, fileName) {
143
- const clean = prefix.replace(/^\/+|\/+$/g, "");
144
- return clean.length > 0 ? `${clean}/${fileName}` : fileName;
145
- }
146
- /**
147
- * `PGOPTIONS` carrying an identity, for a libpq tool that has no other way to
148
- * set a GUC.
149
- *
150
- * A backslash escape rather than quoting, which is what libpq's `-c` parsing
151
- * takes: a space inside a value ends the option otherwise, so a role list is
152
- * comma-joined and never spaced.
153
- */
154
- function buildRowSecurityPgOptions(identity) {
155
- const escape = (value) => value.replace(/([\\ ])/g, "\\$1");
156
- return [
157
- `-c app.uid=${escape(identity.uid)}`,
158
- `-c app.user_id=${escape(identity.uid)}`,
159
- `-c app.user_roles=${escape(identity.roles.join(","))}`
160
- ].join(" ");
161
- }
162
- /**
163
- * Assemble the `pg_dump` argument vector. Uses the custom format (`-Fc`),
164
- * which is compressed and restorable selectively via `pg_restore`.
165
- */
166
- function buildPgDumpArgs(opts) {
167
- const args = [
168
- "--format=custom",
169
- "--no-password",
170
- `--file=${opts.outFile}`
171
- ];
172
- if (opts.noOwner) args.push("--no-owner");
173
- if (opts.rowSecurity) args.push("--enable-row-security");
174
- for (const schema of opts.excludeSchemas ?? []) args.push(`--exclude-schema=${schema}`);
175
- args.push(forLibpq(opts.connectionString));
176
- return args;
177
- }
178
- /**
179
- * Whether a `pg_dump` failure is the row-security one, and what to do about it.
180
- *
181
- * The error text names the table and nothing else, so the first read of it is
182
- * "why would a backup be affected by RLS at all?" — the answer being that the
183
- * dumping role is not the tables' owner and has no `BYPASSRLS`, which is the
184
- * normal state of the `postgres` user on Cloud SQL, RDS and every other managed
185
- * Postgres. Nothing about that is visible from the message.
186
- *
187
- * Returns `null` for any other failure, so the caller reports it unchanged.
188
- */
189
- function diagnoseRowSecurityDumpFailure(error) {
190
- const text = [error?.stderr, error?.message].map((part) => typeof part === "string" ? part : "").join("\n");
191
- if (!/row-level security policy/i.test(text)) return null;
192
- const table = text.match(/for table "([^"]+)"/)?.[1];
193
- return [
194
- `pg_dump cannot read ${table ? `"${table}"` : "one of the tables"} because row-level security applies to it.`,
195
- "",
196
- " The dumping role is neither the table's owner nor `BYPASSRLS`, which is the normal",
197
- " state of the `postgres` user on Cloud SQL, RDS and other managed Postgres — there is",
198
- " no superuser to hand out.",
199
- "",
200
- " Two ways out:",
201
- "",
202
- " • Grant the dumping role BYPASSRLS, or make it the owner, and run this again. The",
203
- " dump then contains every row, which is what a backup should mean.",
204
- "",
205
- " • Re-run with --enable-row-security to dump as an admin subject instead. Rebase",
206
- " sets `app.uid`/`app.user_roles` so the generated `admin_full_access` policy",
207
- " admits the dump. Read the warning it prints: the result contains exactly the",
208
- " rows those policies admit, and any table whose policies do not include an",
209
- " admin rule comes out short — with no error.",
210
- "",
211
- " Do not reach for a bare `pg_dump --enable-row-security` by hand. Without the",
212
- " settings above it succeeds and silently omits rows."
213
- ].join("\n");
214
- }
215
- /**
216
- * Assemble the `pg_restore` argument vector for a custom-format dump.
217
- */
218
- function buildPgRestoreArgs(opts) {
219
- const args = [
220
- "--format=custom",
221
- "--no-password",
222
- `--dbname=${forLibpq(opts.connectionString)}`
223
- ];
224
- if (opts.clean) args.push("--clean", "--if-exists");
225
- if (opts.noOwner) args.push("--no-owner");
226
- if (opts.exitOnError !== false) args.push("--exit-on-error");
227
- args.push(opts.inputFile);
228
- return args;
229
- }
230
- /**
231
- * Assemble the `pg_restore --list` argument vector. Reading a dump's table
232
- * of contents parses the whole archive without touching a database, so it is
233
- * a cheap integrity check that the file isn't truncated or corrupt.
234
- */
235
- function buildPgRestoreListArgs(inputFile) {
236
- return ["--list", inputFile];
237
- }
238
- /** `<id>; <catalog oid> <oid> <description> <namespace> <tag…> <owner>`, the shape of a TOC entry. */
239
- var TOC_ENTRY = /^\d+;\s+\d+\s+\d+\s+(SCHEMA|TABLE)\s+(?!DATA\s)(\S+)\s+(.+)\s+(\S+)$/;
240
- /**
241
- * The schemas and tables in `pg_restore --list` output: the objects a
242
- * restore without `--clean` fails on when the target already has them.
243
- *
244
- * A TOC line ends in the object's owner, after its namespace and its name;
245
- * a schema's namespace is `-`. The name is what lies between the two, so a
246
- * name with a space in it (`Odd Name`) reads whole. `TABLE DATA` and every
247
- * other kind of entry are not objects of their own here.
248
- */
249
- function parseDumpObjects(list) {
250
- const objects = {
251
- schemas: [],
252
- tables: []
253
- };
254
- for (const line of list.split("\n")) {
255
- const entry = TOC_ENTRY.exec(line.trim());
256
- if (!entry) continue;
257
- const [, kind, namespace, name] = entry;
258
- if (kind === "SCHEMA") objects.schemas.push(name);
259
- else objects.tables.push({
260
- schema: namespace,
261
- table: name
262
- });
263
- }
264
- return objects;
265
- }
266
- /**
267
- * Assemble the `pg_dumpall --globals-only` argument vector. Roles (and other
268
- * cluster-wide objects) live outside any single database, so a per-database
269
- * `pg_dump` omits them. Without the `rebase_user` role the RLS GRANT
270
- * statements in the main dump fail on restore and RLS is silently lost — so
271
- * every backup captures the globals into a sidecar `.globals.sql`.
272
- *
273
- * `--no-role-passwords` keeps role secrets out of the artifact (backups may
274
- * be shipped off-box); roles are recreated password-less and re-secured by
275
- * the operator.
276
- */
277
- function buildPgDumpallGlobalsArgs(opts) {
278
- return [
279
- "--globals-only",
280
- "--no-role-passwords",
281
- "--no-password",
282
- `--file=${opts.outFile}`,
283
- `--dbname=${forLibpq(opts.connectionString)}`
284
- ];
285
- }
286
- /**
287
- * Derive the globals sidecar path/key for a given `.dump` file. Keeps the
288
- * two artifacts adjacent so listing, uploading and pruning can find one from
289
- * the other. A name that doesn't end in `.dump` is returned unchanged with a
290
- * `.globals.sql` suffix appended.
291
- */
292
- function globalsFileForDump(dumpPath) {
293
- return dumpPath.endsWith(".dump") ? dumpPath.slice(0, -5) + ".globals.sql" : dumpPath + ".globals.sql";
294
- }
295
- /**
296
- * Split a `pg_dumpall --globals-only` script into individual statements.
297
- * Used when replaying globals on restore so each `CREATE ROLE` / `GRANT`
298
- * can run independently and a benign "role already exists" on one doesn't
299
- * abort the rest. Drops `--` comment lines and blank statements.
300
- */
301
- function splitGlobalsStatements(sql) {
302
- return sql.split("\n").filter((line) => {
303
- const trimmed = line.trim();
304
- return !trimmed.startsWith("--") && !trimmed.startsWith("\\");
305
- }).join("\n").split(";").map((s) => s.trim()).filter((s) => s.length > 0);
306
- }
307
- /**
308
- * Resolve the Postgres connection string the backup commands should use,
309
- * mirroring the precedence the branch command already relies on.
310
- */
311
- function resolveConnectionString(env) {
312
- return env.DATABASE_URL || env.ADMIN_CONNECTION_STRING || null;
313
- }
314
- //#endregion
315
- //#region src/backup/retention.ts
316
- /**
317
- * Retention pruning for scheduled backups. Pure logic — no I/O — so it can
318
- * be exhaustively unit-tested without touching a storage backend.
319
- */
320
- /**
321
- * Decide which backups to delete under the given retention policy.
322
- *
323
- * Rules, applied in order:
324
- * 1. Objects whose key isn't a recognised rebase backup (no parseable
325
- * timestamp and no `createdAt`) are never selected — foreign files in a
326
- * shared bucket are left untouched.
327
- * 2. The `keepMinimum` newest backups are always retained.
328
- * 3. Of the remainder, any older than `retentionDays` are selected for
329
- * deletion.
330
- *
331
- * Returns the keys to delete, oldest first.
332
- */
333
- function selectBackupsToPrune(backups, options) {
334
- const retentionDays = options.retentionDays ?? 0;
335
- const keepMinimum = Math.max(0, options.keepMinimum ?? 0);
336
- const now = options.now ?? /* @__PURE__ */ new Date();
337
- if (retentionDays <= 0) return [];
338
- const dated = backups.map((b) => {
339
- const createdAt = b.createdAt ?? parseBackupTimestamp(b.key);
340
- return createdAt ? {
341
- key: b.key,
342
- createdAt
343
- } : null;
344
- }).filter((b) => b !== null);
345
- dated.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
346
- const cutoff = now.getTime() - retentionDays * 24 * 60 * 60 * 1e3;
347
- const toDelete = dated.slice(keepMinimum).filter((b) => b.createdAt.getTime() < cutoff);
348
- toDelete.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
349
- return toDelete.map((b) => b.key);
350
- }
351
- //#endregion
352
- //#region src/backup/pg-binary.ts
353
- /**
354
- * Where the PostgreSQL client tools are, without spawning them.
355
- *
356
- * Kept apart from `backup-service.ts`, which pulls in `execa` (ESM-only), so
357
- * `createBackupCron` can check for the tools when it is registered.
358
- */
359
- var OVERRIDE_VARIABLE = {
360
- pg_dump: "PG_DUMP_PATH",
361
- pg_restore: "PG_RESTORE_PATH",
362
- pg_dumpall: "PG_DUMPALL_PATH"
363
- };
364
- /** The variable that points at `tool` when it is not on the PATH. */
365
- function pgToolVariable(tool) {
366
- return OVERRIDE_VARIABLE[tool];
367
- }
368
- /** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */
369
- function resolvePgBinary(tool, env = process.env) {
370
- const override = env[OVERRIDE_VARIABLE[tool]];
371
- if (override && fs.existsSync(override)) return override;
372
- return resolveLocalBin(tool);
373
- }
374
- //#endregion
375
- //#region src/backup/backup-logic.ts
376
- /**
377
- * Pure-ish orchestration for the backup/restore paths, kept free of `execa`
378
- * and `pg` value imports so it can be unit-tested under jest (the impure
379
- * edges — spawning processes, opening connections — live in
380
- * `backup-service.ts`, which is vitest/runtime only).
381
- *
382
- * The functions here take their side-effecting dependency as an argument
383
- * (a statement runner, an object deleter) so tests can inject fakes.
384
- */
385
- /**
386
- * Replay a `pg_dumpall --globals-only` script one statement at a time,
387
- * tolerating per-statement failures. On a same-cluster restore the roles
388
- * usually already exist (`CREATE ROLE` → "already exists") and on a managed
389
- * provider an `ALTER ROLE <superuser>` may be refused; neither should abort
390
- * recreation of the roles that *are* missing. Returns how many statements
391
- * applied vs were skipped.
392
- *
393
- * `runStatement` executes one SQL statement and rejects on error.
394
- */
395
- async function applyGlobalsWith(runStatement, globalsSql, log = () => {}) {
396
- let applied = 0;
397
- let skipped = 0;
398
- for (const statement of splitGlobalsStatements(globalsSql)) try {
399
- await runStatement(statement);
400
- applied++;
401
- } catch (err) {
402
- skipped++;
403
- log(` • Skipped global: ${statement.split("\n")[0].slice(0, 80)} (${err instanceof Error ? err.message : String(err)})`);
404
- }
405
- return {
406
- applied,
407
- skipped
408
- };
409
- }
410
- /**
411
- * Delete each pruned dump together with its `.globals.sql` roles sidecar, so
412
- * pruning never orphans the roles file. The sidecar is best-effort — older
413
- * backups predate it, so a missing-sidecar failure is swallowed while a
414
- * failure deleting the dump itself propagates.
415
- *
416
- * `deleteObject` removes one key and rejects if it cannot (e.g. not found).
417
- */
418
- async function pruneWith(keys, deleteObject) {
419
- for (const key of keys) {
420
- await deleteObject(key);
421
- try {
422
- await deleteObject(globalsFileForDump(key));
423
- } catch {}
424
- }
425
- }
426
- /**
427
- * Remove a dump artifact abandoned by a failed tool run.
428
- *
429
- * `pg_dump` creates its `--file=` target before it finishes connecting, so any
430
- * failure — a URL libpq rejects, a dropped connection, a full disk — leaves a
431
- * 0-byte file behind. That corpse is not inert: `rebase db backups list` used
432
- * to show it as an ordinary entry, and `selectBackupsToPrune` ranks by
433
- * timestamp alone, so it occupies a protected `keepMinimum` slot and can push
434
- * a real backup out of retention.
435
- *
436
- * Best-effort on purpose: the caller is already throwing, and a failed unlink
437
- * must not replace the real diagnosis with an ENOENT/EPERM from the cleanup.
438
- *
439
- * Lives here rather than in `backup-service.ts` for the reason at the top of
440
- * this file — that module's top-level `execa` import makes it unloadable under
441
- * jest, so anything placed there cannot be unit-tested.
442
- *
443
- * `remove` deletes one file and throws if it cannot; `exists` reports presence.
444
- */
445
- function discardPartialDumpWith(exists, remove, file) {
446
- try {
447
- if (exists(file)) remove(file);
448
- } catch {}
449
- }
450
- //#endregion
451
- //#region src/backup/backup-service.ts
452
- /**
453
- * Backup / restore orchestration: thin, well-typed wrappers around
454
- * `pg_dump` and `pg_restore`, plus binary detection, version pre-flight,
455
- * and upload/prune against a storage backend.
456
- *
457
- * The pure argument- and policy-logic lives in `pg-tools.ts` and
458
- * `retention.ts`; this module is the impure edge (spawns processes, talks
459
- * to Postgres and storage) and is intentionally kept small.
460
- */
461
- var backup_service_exports = /* @__PURE__ */ __exportAll({
462
- BackupToolError: () => BackupToolError,
463
- applyGlobals: () => applyGlobals,
464
- createDump: () => createDump,
465
- detectToolMajor: () => detectToolMajor,
466
- discardPartialDump: () => discardPartialDump,
467
- ensureDatabaseExists: () => ensureDatabaseExists,
468
- findRestoreConflicts: () => findRestoreConflicts,
469
- getServerVersionMajor: () => getServerVersionMajor,
470
- listBackups: () => listBackups,
471
- preflight: () => preflight,
472
- pruneBackups: () => pruneBackups,
473
- readDumpObjects: () => readDumpObjects,
474
- resolvePgBinary: () => resolvePgBinary,
475
- restoreDump: () => restoreDump,
476
- uploadBackup: () => uploadBackup,
477
- validateDump: () => validateDump
478
- });
479
- /**
480
- * How a libpq tool is invoked for a connection string: the URL its command
481
- * line carries, and the environment it runs in.
482
- *
483
- * The password goes in `PGPASSWORD`, never on the command line, where every
484
- * process listing could read it and every failure printed it back (execa's
485
- * error repeats the command). See {@link splitPassword}.
486
- */
487
- function libpqInvocation(connectionString, env) {
488
- const split = splitPassword(connectionString);
489
- const toolEnv = {};
490
- for (const [key, value] of Object.entries(env)) if (value !== void 0) toolEnv[key] = value;
491
- if (split.password !== null) toolEnv.PGPASSWORD = split.password;
492
- return {
493
- connectionString: split.connectionString,
494
- env: toolEnv
495
- };
496
- }
497
- /**
498
- * Remove a dump artifact abandoned by a failed tool run. See
499
- * {@link discardPartialDumpWith} for why this exists and why the logic lives
500
- * in the execa-free module.
501
- */
502
- function discardPartialDump(file) {
503
- discardPartialDumpWith((f) => fs.existsSync(f), (f) => fs.unlinkSync(f), file);
504
- }
505
- var BackupToolError = class extends Error {
506
- hint;
507
- constructor(message, hint) {
508
- super(message);
509
- this.hint = hint;
510
- this.name = "BackupToolError";
511
- }
512
- };
513
- /** Run `<bin> --version` and extract the major version. */
514
- async function detectToolMajor(bin) {
515
- try {
516
- const { stdout } = await execa(bin, ["--version"]);
517
- return parsePgToolMajor(stdout);
518
- } catch {
519
- return null;
520
- }
521
- }
522
- /** Query the server for its major version via `server_version_num`. */
523
- async function getServerVersionMajor(connectionString) {
524
- const { Client } = await import("pg");
525
- const client = new Client({ connectionString });
526
- await client.connect();
527
- try {
528
- const raw = (await client.query("SHOW server_version_num")).rows?.[0]?.server_version_num;
529
- return serverVersionNumToMajor(raw);
530
- } finally {
531
- await client.end();
532
- }
533
- }
534
- /**
535
- * Verify the requested client tool exists and its major version is
536
- * compatible with the live server. Throws {@link BackupToolError} — with a
537
- * doctor-style hint — when the binary is missing.
538
- */
539
- async function preflight(tool, connectionString, env = process.env) {
540
- const bin = resolvePgBinary(tool, env);
541
- if (!bin) throw new BackupToolError(`Could not find the '${tool}' binary.`, `Install the PostgreSQL client tools (e.g. 'brew install libpq' or 'apt-get install postgresql-client'), or set ${tool === "pg_dump" ? "PG_DUMP_PATH" : "PG_RESTORE_PATH"} to its full path.`);
542
- const [toolMajor, serverMajor] = await Promise.all([detectToolMajor(bin), getServerVersionMajor(connectionString)]);
543
- return {
544
- bin,
545
- toolMajor,
546
- serverMajor,
547
- ...checkToolServerCompatibility(toolMajor, serverMajor)
548
- };
549
- }
550
- /**
551
- * Produce a custom-format dump on local disk. When `outDir` is omitted the
552
- * file is written to the OS temp directory (used by the upload path, which
553
- * cleans it up afterwards).
554
- *
555
- * Alongside the `-Fc` dump it writes a `<name>.globals.sql` sidecar via
556
- * `pg_dumpall --globals-only` so the roles the dump's GRANT/RLS statements
557
- * depend on can be recreated on restore. Set `includeGlobals: false` to skip
558
- * it (e.g. when the caller has no privilege to read cluster globals).
559
- */
560
- async function createDump(opts) {
561
- const env = opts.env ?? process.env;
562
- const bin = resolvePgBinary("pg_dump", env);
563
- if (!bin) throw new BackupToolError("Could not find the 'pg_dump' binary.", "Install the PostgreSQL client tools or set PG_DUMP_PATH.");
564
- const fileName = opts.fileName ?? buildBackupFilename(opts.dbName);
565
- const outDir = opts.outDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "rebase-backup-"));
566
- fs.mkdirSync(outDir, { recursive: true });
567
- const localFile = path.join(outDir, fileName);
568
- createOwnerOnly(localFile);
569
- const tool = libpqInvocation(opts.connectionString, env);
570
- const args = buildPgDumpArgs({
571
- connectionString: tool.connectionString,
572
- outFile: localFile,
573
- excludeSchemas: opts.excludeSchemas,
574
- noOwner: opts.noOwner,
575
- rowSecurity: opts.rowSecurity
576
- });
577
- const dumpEnv = { ...tool.env };
578
- if (opts.rowSecurity) dumpEnv.PGOPTIONS = [env.PGOPTIONS, buildRowSecurityPgOptions(opts.rowSecurity)].filter(Boolean).join(" ");
579
- try {
580
- await execa(bin, args, {
581
- stdio: opts.inheritStdio ? "inherit" : "pipe",
582
- env: dumpEnv
583
- });
584
- } catch (error) {
585
- discardPartialDump(localFile);
586
- const diagnosis = diagnoseRowSecurityDumpFailure(error);
587
- if (!diagnosis) throw error;
588
- throw new BackupToolError(diagnosis, "Run `rebase db backup --help` for the flag, and read what it says about partial dumps.");
589
- }
590
- const result = {
591
- localFile,
592
- fileName,
593
- sizeBytes: fs.existsSync(localFile) ? fs.statSync(localFile).size : 0
594
- };
595
- if (opts.includeGlobals !== false) {
596
- const dumpallBin = resolvePgBinary("pg_dumpall", env);
597
- if (!dumpallBin) throw new BackupToolError("Could not find the 'pg_dumpall' binary needed to capture cluster roles.", "Install the PostgreSQL client tools or set PG_DUMPALL_PATH. To take a role-incomplete backup anyway, pass includeGlobals: false.");
598
- const globalsFile = globalsFileForDump(localFile);
599
- createOwnerOnly(globalsFile);
600
- try {
601
- await execa(dumpallBin, buildPgDumpallGlobalsArgs({
602
- connectionString: tool.connectionString,
603
- outFile: globalsFile
604
- }), {
605
- stdio: opts.inheritStdio ? "inherit" : "pipe",
606
- env: tool.env
607
- });
608
- } catch (error) {
609
- discardPartialDump(globalsFile);
610
- discardPartialDump(localFile);
611
- throw error;
612
- }
613
- result.globalsFile = globalsFile;
614
- result.globalsSizeBytes = fs.existsSync(globalsFile) ? fs.statSync(globalsFile).size : 0;
615
- }
616
- return result;
617
- }
618
- /**
619
- * Create `file` empty and readable by its owner only, before a tool writes it.
620
- *
621
- * A dump holds every row, and the `.globals.sql` sidecar every role's password
622
- * hash. pg_dump and pg_dumpall create their `--file=` target with the
623
- * process's umask — 0644 on most machines, readable by every user — and keep
624
- * the mode of a file that is already there. Created 0600 first, the file is
625
- * never readable by anyone else, not even while the dump runs; chmod-ing it
626
- * afterwards would leave it open for as long as that takes. A file that
627
- * already existed is narrowed too.
628
- */
629
- function createOwnerOnly(file) {
630
- fs.closeSync(fs.openSync(file, "w", 384));
631
- fs.chmodSync(file, 384);
632
- }
633
- /**
634
- * Cheap integrity check on a freshly written dump: it must be non-empty and
635
- * `pg_restore --list` must parse its table of contents without error. Used
636
- * before pruning older backups so a corrupt-but-exit-0 dump never becomes
637
- * the reason the last good backup is deleted.
638
- */
639
- async function validateDump(localFile, env = process.env) {
640
- if (!fs.existsSync(localFile)) return {
641
- ok: false,
642
- reason: `Dump file does not exist: ${localFile}`
643
- };
644
- if (fs.statSync(localFile).size === 0) return {
645
- ok: false,
646
- reason: "Dump file is empty (0 bytes)."
647
- };
648
- const bin = resolvePgBinary("pg_restore", env);
649
- if (!bin) return {
650
- ok: false,
651
- reason: "Could not find 'pg_restore' to verify the dump."
652
- };
653
- try {
654
- await execa(bin, buildPgRestoreListArgs(localFile), {
655
- stdio: "pipe",
656
- env: { ...env }
657
- });
658
- return { ok: true };
659
- } catch (err) {
660
- return {
661
- ok: false,
662
- reason: `pg_restore --list failed: ${err instanceof Error ? err.message : String(err)}`
663
- };
664
- }
665
- }
666
- /**
667
- * Replay a `pg_dumpall --globals-only` script to recreate cluster roles
668
- * before a restore, so the dump's GRANT/RLS statements (which reference
669
- * `rebase_user` and any owner roles) actually apply. Runs statement by
670
- * statement and tolerates per-statement failures — on a same-cluster restore
671
- * the roles usually already exist (`CREATE ROLE` → "already exists"), and on
672
- * a managed provider an `ALTER ROLE <superuser>` may be refused; neither
673
- * should abort role recreation. Returns how many statements applied vs were
674
- * skipped.
675
- */
676
- async function applyGlobals(connectionString, globalsSql, log = () => {}) {
677
- const { Client } = await import("pg");
678
- const client = new Client({ connectionString });
679
- await client.connect();
680
- try {
681
- return await applyGlobalsWith(async (sql) => {
682
- await client.query(sql);
683
- }, globalsSql, log);
684
- } finally {
685
- await client.end();
686
- }
687
- }
688
- /**
689
- * Restore a custom-format dump into the database named by
690
- * `connectionString`. Destructive when `clean` is set (drops objects
691
- * first). Never called automatically — the CLI gates it behind explicit
692
- * confirmation.
693
- *
694
- * Runs with `--exit-on-error` by default: a restore that logs-and-continues
695
- * past a failed GRANT (because a role was missing) reports success with RLS
696
- * un-enforced. Callers should recreate roles first (see {@link applyGlobals})
697
- * and only set `exitOnError: false` deliberately.
698
- */
699
- async function restoreDump(opts) {
700
- const env = opts.env ?? process.env;
701
- const bin = resolvePgBinary("pg_restore", env);
702
- if (!bin) throw new BackupToolError("Could not find the 'pg_restore' binary.", "Install the PostgreSQL client tools or set PG_RESTORE_PATH.");
703
- const tool = libpqInvocation(opts.connectionString, env);
704
- await execa(bin, buildPgRestoreArgs({
705
- connectionString: tool.connectionString,
706
- inputFile: opts.inputFile,
707
- clean: opts.clean,
708
- noOwner: opts.noOwner,
709
- exitOnError: opts.exitOnError
710
- }), {
711
- stdio: opts.inheritStdio ? "inherit" : "pipe",
712
- env: tool.env
713
- });
714
- }
715
- /** The schemas and tables a dump creates, from `pg_restore --list`. See {@link parseDumpObjects}. */
716
- async function readDumpObjects(inputFile, env = process.env) {
717
- const bin = resolvePgBinary("pg_restore", env);
718
- if (!bin) throw new BackupToolError("Could not find the 'pg_restore' binary.", "Install the PostgreSQL client tools or set PG_RESTORE_PATH.");
719
- const toolEnv = Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== void 0));
720
- const { stdout } = await execa(bin, buildPgRestoreListArgs(inputFile), {
721
- stdio: "pipe",
722
- env: toolEnv
723
- });
724
- return parseDumpObjects(stdout);
725
- }
726
- /**
727
- * Which of a dump's schemas and tables the target database already holds,
728
- * as `schema <name>` / `table <schema>.<name>`: what a restore without
729
- * `--clean` would stop on. `public` is left out of the schemas — every
730
- * database has it, and pg_restore does not create it. None for a database
731
- * that does not exist yet.
732
- */
733
- async function findRestoreConflicts(connectionString, objects) {
734
- const schemas = objects.schemas.filter((schema) => schema !== "public");
735
- if (schemas.length === 0 && objects.tables.length === 0) return [];
736
- const { Client } = await import("pg");
737
- const client = new Client({ connectionString });
738
- try {
739
- await client.connect();
740
- } catch (err) {
741
- if (err instanceof Error && "code" in err && err.code === "3D000") return [];
742
- throw err;
743
- }
744
- try {
745
- const existingSchemas = await client.query("SELECT nspname FROM pg_namespace WHERE nspname = ANY($1::text[]) ORDER BY nspname", [schemas]);
746
- const existingTables = await client.query(`SELECT n.nspname, c.relname
747
- FROM pg_class c
748
- JOIN pg_namespace n ON n.oid = c.relnamespace
749
- JOIN unnest($1::text[], $2::text[]) AS d(schema_name, table_name)
750
- ON d.schema_name = n.nspname AND d.table_name = c.relname
751
- WHERE c.relkind IN ('r', 'p')
752
- ORDER BY n.nspname, c.relname`, [objects.tables.map((t) => t.schema), objects.tables.map((t) => t.table)]);
753
- return [...existingSchemas.rows.map((row) => `schema ${row.nspname}`), ...existingTables.rows.map((row) => `table ${row.nspname}.${row.relname}`)];
754
- } finally {
755
- await client.end();
756
- }
757
- }
758
- /**
759
- * Create a database (if absent) by connecting to the maintenance
760
- * `postgres` database. Used by `restore --create-db`.
761
- */
762
- async function ensureDatabaseExists(adminConnectionString, dbName) {
763
- const { Client } = await import("pg");
764
- const parsed = new URL(adminConnectionString);
765
- parsed.pathname = "/postgres";
766
- const client = new Client({ connectionString: parsed.toString() });
767
- await client.connect();
768
- try {
769
- const existing = await client.query("SELECT 1 FROM pg_database WHERE datname = $1", [dbName]);
770
- if (existing.rowCount && existing.rowCount > 0) return false;
771
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(dbName)) throw new BackupToolError(`Invalid database name: "${dbName}"`);
772
- await client.query(`CREATE DATABASE "${dbName}"`);
773
- return true;
774
- } finally {
775
- await client.end();
776
- }
777
- }
778
- /**
779
- * Upload a local dump to object storage under the destination's prefix.
780
- * Backups may contain secrets/PII, so the object is written with a
781
- * private/octet-stream content type; bucket-level ACLs must stay private.
782
- */
783
- async function uploadBackup(storage, localFile, dest) {
784
- const fileName = path.basename(localFile);
785
- const key = joinStorageKey(dest.prefix, fileName);
786
- const file = new File([await fs.openAsBlob(localFile)], fileName, { type: "application/octet-stream" });
787
- return {
788
- key,
789
- storageUrl: (await storage.putObject({
790
- file,
791
- key,
792
- bucket: dest.bucket,
793
- metadata: { "rebase-backup": "1" }
794
- }, { uploadLimits: false })).storageUrl
795
- };
796
- }
797
- /** Stats in flight at once while listing a local backup folder. */
798
- var LOCAL_STAT_CONCURRENCY = 32;
799
- /** `fn` over `items`, at most `limit` at a time; results in input order. */
800
- async function mapBounded(items, limit, fn) {
801
- const results = new Array(items.length);
802
- let next = 0;
803
- const worker = async () => {
804
- while (next < items.length) {
805
- const i = next++;
806
- results[i] = await fn(items[i]);
807
- }
808
- };
809
- await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
810
- return results;
811
- }
812
- /**
813
- * List existing backups at a destination. For local destinations reads the
814
- * directory; for object storage lists the prefix via the controller.
815
- */
816
- async function listBackups(dest, storage) {
817
- if (dest.kind === "local") {
818
- const stat = await fs.promises.stat(dest.path).catch(() => null);
819
- if (!stat) return [];
820
- const dir = stat.isDirectory() ? dest.path : path.dirname(dest.path);
821
- const dumps = (await fs.promises.readdir(dir)).filter((f) => f.endsWith(".dump"));
822
- const stats = await mapBounded(dumps, LOCAL_STAT_CONCURRENCY, (f) => fs.promises.stat(path.join(dir, f)));
823
- return dumps.map((f, i) => {
824
- const createdAt = parseBackupTimestamp(f) ?? stats[i].mtime;
825
- return {
826
- key: path.join(dir, f),
827
- createdAt,
828
- sizeBytes: stats[i].size
829
- };
830
- }).sort((a, b) => (b.createdAt?.getTime() ?? 0) - (a.createdAt?.getTime() ?? 0));
831
- }
832
- if (!storage) throw new BackupToolError(`Listing ${dest.kind} backups requires a configured storage backend.`);
833
- const { listBackupObjects } = await import("@rebasepro/server");
834
- return (await listBackupObjects(dest, storage)).map((info) => ({
835
- key: info.key,
836
- createdAt: info.createdAt ? new Date(info.createdAt) : void 0
837
- }));
838
- }
839
- /**
840
- * Apply a retention policy to a destination, deleting the backups selected
841
- * by {@link selectBackupsToPrune}. Returns the keys that were removed.
842
- */
843
- async function pruneBackups(dest, options, storage) {
844
- const toDelete = selectBackupsToPrune(await listBackups(dest, storage), options);
845
- await pruneWith(toDelete, dest.kind === "local" ? async (key) => {
846
- if (!fs.existsSync(key)) throw new Error(`not found: ${key}`);
847
- fs.unlinkSync(key);
848
- } : async (key) => {
849
- if (!storage) throw new BackupToolError("Storage backend required to prune object backups.");
850
- await storage.deleteObject(key, dest.bucket);
851
- });
852
- return toDelete;
853
- }
854
- //#endregion
855
- export { parseBackupDestination as A, buildPgRestoreArgs as C, diagnoseRowSecurityDumpFailure as D, checkToolServerCompatibility as E, resolveConnectionString as F, serverVersionNumToMajor as I, splitGlobalsStatements as L, parseDbNameFromUrl as M, parseDumpObjects as N, globalsFileForDump as O, parsePgToolMajor as P, withDatabaseName as R, buildPgDumpallGlobalsArgs as S, buildRowSecurityPgOptions as T, pgToolVariable as _, detectToolMajor as a, buildBackupFilename as b, findRestoreConflicts as c, preflight as d, pruneBackups as f, validateDump as g, uploadBackup as h, createDump as i, parseBackupTimestamp as j, joinStorageKey as k, getServerVersionMajor as l, restoreDump as m, applyGlobals as n, discardPartialDump as o, readDumpObjects as p, backup_service_exports as r, ensureDatabaseExists as s, BackupToolError as t, listBackups as u, resolvePgBinary as v, buildPgRestoreListArgs as w, buildPgDumpArgs as x, selectBackupsToPrune as y };
856
-
857
- //# sourceMappingURL=backup-service-D-UpT7DH.js.map