@rebasepro/server-postgres 0.18.0 → 0.19.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.
- package/dist/BranchService-ByLfM_Tt.js +360 -0
- package/dist/BranchService-ByLfM_Tt.js.map +1 -0
- package/dist/__vite-browser-external-BnuHet1e.js +46 -0
- package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
- package/dist/arg-Dni7MzLB.js +87 -0
- package/dist/arg-Dni7MzLB.js.map +1 -0
- package/dist/auth-users-columns-D2LBFrMH.js +116 -0
- package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
- package/dist/backup-cli-oM9WQVlV.js +408 -0
- package/dist/backup-cli-oM9WQVlV.js.map +1 -0
- package/dist/backup-service-Br3tqwsy.js +715 -0
- package/dist/backup-service-Br3tqwsy.js.map +1 -0
- package/dist/cli-errors-DxN4yzdH.js +657 -0
- package/dist/cli-errors-DxN4yzdH.js.map +1 -0
- package/dist/cli-output-CNdMql-L.js +47 -0
- package/dist/cli-output-CNdMql-L.js.map +1 -0
- package/dist/cli.js +2020 -0
- package/dist/cli.js.map +1 -0
- package/dist/{collection-index-C7LopMyX.js → collection-index-BHgT_TKd.js} +32 -3
- package/dist/{collection-index-C7LopMyX.js.map → collection-index-BHgT_TKd.js.map} +1 -1
- package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
- package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
- package/dist/databasePoolManager-Bj5FbeAs.js +349 -0
- package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
- package/dist/doctor-D7HWmGzs.js +974 -0
- package/dist/doctor-D7HWmGzs.js.map +1 -0
- package/dist/{ensure-collection-policies-CagOzEPS.js → ensure-collection-policies-2c2NV8zX.js} +5 -25
- package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
- package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
- package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
- package/dist/ensure-tables-CIojK-Rx.js +650 -0
- package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
- package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
- package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +1312 -0
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
- package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
- package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
- package/dist/index.es.js +849 -10807
- package/dist/index.es.js.map +1 -1
- package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
- package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
- package/dist/policy-drift-kNJ6X2Uj.js +313 -0
- package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
- package/dist/rls-bootstrap-sql-DLDnUjtx.js +7092 -0
- package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
- package/dist/{rls-enforcement-ClJoTboT.js → rls-enforcement-DeYEjYLt.js} +14 -3
- package/dist/{rls-enforcement-ClJoTboT.js.map → rls-enforcement-DeYEjYLt.js.map} +1 -1
- package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
- package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
- package/dist/schema/doctor-cli.js +129 -0
- package/dist/schema/doctor-cli.js.map +1 -0
- package/dist/schema/generate-drizzle-schema.d.ts +1 -1
- package/dist/schema/generate-drizzle-schema.js +24 -0
- package/dist/schema/generate-drizzle-schema.js.map +1 -0
- package/dist/schema/generate-postgres-ddl.d.ts +1 -1
- package/dist/schema/generate-postgres-ddl.js +138 -0
- package/dist/schema/generate-postgres-ddl.js.map +1 -0
- package/dist/schema/introspect-db.js +866 -0
- package/dist/schema/introspect-db.js.map +1 -0
- package/dist/source-Br7L7GOI.js +410 -0
- package/dist/source-Br7L7GOI.js.map +1 -0
- package/dist/vector-index-DkWxm2de.js +309 -0
- package/dist/vector-index-DkWxm2de.js.map +1 -0
- package/package.json +7 -7
- package/dist/ensure-collection-policies-CagOzEPS.js.map +0 -1
- package/dist/ensure-collection-tables-DbAhrjnG.js +0 -2732
- package/dist/ensure-collection-tables-DbAhrjnG.js.map +0 -1
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
__createRequire(import.meta.url);
|
|
3
|
+
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
|
+
import { C as resolveLocalBin, T as forLibpq, w as execa } from "./rls-bootstrap-sql-DLDnUjtx.js";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import os from "os";
|
|
8
|
+
//#region src/backup/pg-tools.ts
|
|
9
|
+
/**
|
|
10
|
+
* Pure helpers for the backup/restore commands.
|
|
11
|
+
*
|
|
12
|
+
* Everything in this file is side-effect free so it can be unit-tested
|
|
13
|
+
* without a live Postgres server, matching the constraint that CI must
|
|
14
|
+
* not require a database.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Extract the database name from a Postgres connection string.
|
|
18
|
+
* Returns `null` when the URL has no database path (e.g. bare host).
|
|
19
|
+
*/
|
|
20
|
+
function parseDbNameFromUrl(connectionString) {
|
|
21
|
+
try {
|
|
22
|
+
const name = new URL(connectionString).pathname.replace(/^\//, "").trim();
|
|
23
|
+
return name.length > 0 ? name : null;
|
|
24
|
+
} catch {
|
|
25
|
+
const match = connectionString.match(/\/([^/?]+)(\?|$)/);
|
|
26
|
+
return match && match[1] ? match[1] : null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Swap the database name in a connection string, preserving credentials,
|
|
31
|
+
* host, port and query params. Used by `--target-db` / `--create-db` so a
|
|
32
|
+
* restore can target a fresh database instead of clobbering the live one.
|
|
33
|
+
*/
|
|
34
|
+
function withDatabaseName(connectionString, dbName) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = new URL(connectionString);
|
|
37
|
+
parsed.pathname = `/${dbName}`;
|
|
38
|
+
return parsed.toString();
|
|
39
|
+
} catch {
|
|
40
|
+
return connectionString.replace(/\/([^/?]+)(\?|$)/, `/${dbName}$2`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse the major version out of `pg_dump --version` / `pg_restore --version`
|
|
45
|
+
* output, e.g. `"pg_dump (PostgreSQL) 16.2"` → `16`. Handles the pre-10
|
|
46
|
+
* `9.6.x` scheme (returns `9`) and beta strings like `"17beta1"`.
|
|
47
|
+
*/
|
|
48
|
+
function parsePgToolMajor(versionOutput) {
|
|
49
|
+
const match = versionOutput.match(/(\d+)(?:\.(\d+))?/);
|
|
50
|
+
if (!match) return null;
|
|
51
|
+
const major = Number(match[1]);
|
|
52
|
+
if (!Number.isFinite(major)) return null;
|
|
53
|
+
if (major === 9 && match[2] !== void 0) return 9;
|
|
54
|
+
return major;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Convert `SELECT current_setting('server_version_num')` (e.g. `160002`)
|
|
58
|
+
* into a major version (`16`). Also accepts pre-10 encodings like `90603`
|
|
59
|
+
* → `9`.
|
|
60
|
+
*/
|
|
61
|
+
function serverVersionNumToMajor(versionNum) {
|
|
62
|
+
const num = typeof versionNum === "string" ? Number(versionNum) : versionNum;
|
|
63
|
+
if (!Number.isFinite(num) || num <= 0) return null;
|
|
64
|
+
if (num < 1e5) return Math.floor(num / 1e4);
|
|
65
|
+
return Math.floor(num / 1e4);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* pg_dump / pg_restore must be **at least** as new as the server they talk
|
|
69
|
+
* to. A newer client against an older server is supported; an older client
|
|
70
|
+
* against a newer server is not and produces corrupt or rejected output.
|
|
71
|
+
*/
|
|
72
|
+
function checkToolServerCompatibility(toolMajor, serverMajor) {
|
|
73
|
+
if (toolMajor === null) return {
|
|
74
|
+
compatible: false,
|
|
75
|
+
reason: "Could not determine the client tool version."
|
|
76
|
+
};
|
|
77
|
+
if (serverMajor === null) return {
|
|
78
|
+
compatible: false,
|
|
79
|
+
reason: "Could not determine the Postgres server version."
|
|
80
|
+
};
|
|
81
|
+
if (toolMajor < serverMajor) return {
|
|
82
|
+
compatible: false,
|
|
83
|
+
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.`
|
|
84
|
+
};
|
|
85
|
+
return { compatible: true };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Build a deterministic, sortable backup file name:
|
|
89
|
+
* `rebase-<db>-<YYYYMMDD>T<HHMMSS>Z.dump`
|
|
90
|
+
*
|
|
91
|
+
* The UTC timestamp is embedded so retention pruning can recover the
|
|
92
|
+
* creation time from the object key alone, without extra metadata.
|
|
93
|
+
*/
|
|
94
|
+
function buildBackupFilename(dbName, date = /* @__PURE__ */ new Date()) {
|
|
95
|
+
const stamp = date.toISOString().replace(/\.\d+Z$/, "Z").replace(/[-:]/g, "");
|
|
96
|
+
return `rebase-${dbName.replace(/[^a-zA-Z0-9_-]/g, "_")}-${stamp}.dump`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Recover the creation timestamp encoded in a backup file name by
|
|
100
|
+
* {@link buildBackupFilename}. Returns `null` for names that don't match,
|
|
101
|
+
* so foreign objects in a shared prefix are never pruned.
|
|
102
|
+
*/
|
|
103
|
+
function parseBackupTimestamp(fileName) {
|
|
104
|
+
const match = (fileName.split("/").pop() ?? fileName).match(/-(\d{8})T(\d{6})Z\.dump$/);
|
|
105
|
+
if (!match) return null;
|
|
106
|
+
const [, ymd, hms] = match;
|
|
107
|
+
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`;
|
|
108
|
+
const date = new Date(iso);
|
|
109
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Parse a destination string into a structured {@link BackupDestination}.
|
|
113
|
+
* `s3://bucket/prefix` and `gs://bucket/prefix` map to object storage;
|
|
114
|
+
* anything else is treated as a local path.
|
|
115
|
+
*/
|
|
116
|
+
function parseBackupDestination(out) {
|
|
117
|
+
const s3 = out.match(/^s3:\/\/([^/]+)\/?(.*)$/);
|
|
118
|
+
if (s3) return {
|
|
119
|
+
kind: "s3",
|
|
120
|
+
bucket: s3[1],
|
|
121
|
+
prefix: stripTrailingSlash(s3[2])
|
|
122
|
+
};
|
|
123
|
+
const gcs = out.match(/^gs:\/\/([^/]+)\/?(.*)$/);
|
|
124
|
+
if (gcs) return {
|
|
125
|
+
kind: "gcs",
|
|
126
|
+
bucket: gcs[1],
|
|
127
|
+
prefix: stripTrailingSlash(gcs[2])
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
kind: "local",
|
|
131
|
+
path: out
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function stripTrailingSlash(s) {
|
|
135
|
+
return s.replace(/\/+$/, "");
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Join a storage prefix and a file name without producing a leading or
|
|
139
|
+
* doubled slash.
|
|
140
|
+
*/
|
|
141
|
+
function joinStorageKey(prefix, fileName) {
|
|
142
|
+
const clean = prefix.replace(/^\/+|\/+$/g, "");
|
|
143
|
+
return clean.length > 0 ? `${clean}/${fileName}` : fileName;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* `PGOPTIONS` carrying an identity, for a libpq tool that has no other way to
|
|
147
|
+
* set a GUC.
|
|
148
|
+
*
|
|
149
|
+
* A backslash escape rather than quoting, which is what libpq's `-c` parsing
|
|
150
|
+
* takes: a space inside a value ends the option otherwise, so a role list is
|
|
151
|
+
* comma-joined and never spaced.
|
|
152
|
+
*/
|
|
153
|
+
function buildRowSecurityPgOptions(identity) {
|
|
154
|
+
const escape = (value) => value.replace(/([\\ ])/g, "\\$1");
|
|
155
|
+
return [
|
|
156
|
+
`-c app.uid=${escape(identity.uid)}`,
|
|
157
|
+
`-c app.user_id=${escape(identity.uid)}`,
|
|
158
|
+
`-c app.user_roles=${escape(identity.roles.join(","))}`
|
|
159
|
+
].join(" ");
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Assemble the `pg_dump` argument vector. Uses the custom format (`-Fc`),
|
|
163
|
+
* which is compressed and restorable selectively via `pg_restore`.
|
|
164
|
+
*/
|
|
165
|
+
function buildPgDumpArgs(opts) {
|
|
166
|
+
const args = [
|
|
167
|
+
"--format=custom",
|
|
168
|
+
"--no-password",
|
|
169
|
+
`--file=${opts.outFile}`
|
|
170
|
+
];
|
|
171
|
+
if (opts.noOwner) args.push("--no-owner");
|
|
172
|
+
if (opts.rowSecurity) args.push("--enable-row-security");
|
|
173
|
+
for (const schema of opts.excludeSchemas ?? []) args.push(`--exclude-schema=${schema}`);
|
|
174
|
+
args.push(forLibpq(opts.connectionString));
|
|
175
|
+
return args;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Whether a `pg_dump` failure is the row-security one, and what to do about it.
|
|
179
|
+
*
|
|
180
|
+
* The error text names the table and nothing else, so the first read of it is
|
|
181
|
+
* "why would a backup be affected by RLS at all?" — the answer being that the
|
|
182
|
+
* dumping role is not the tables' owner and has no `BYPASSRLS`, which is the
|
|
183
|
+
* normal state of the `postgres` user on Cloud SQL, RDS and every other managed
|
|
184
|
+
* Postgres. Nothing about that is visible from the message.
|
|
185
|
+
*
|
|
186
|
+
* Returns `null` for any other failure, so the caller reports it unchanged.
|
|
187
|
+
*/
|
|
188
|
+
function diagnoseRowSecurityDumpFailure(error) {
|
|
189
|
+
const text = [error?.stderr, error?.message].map((part) => typeof part === "string" ? part : "").join("\n");
|
|
190
|
+
if (!/row-level security policy/i.test(text)) return null;
|
|
191
|
+
const table = text.match(/for table "([^"]+)"/)?.[1];
|
|
192
|
+
return [
|
|
193
|
+
`pg_dump cannot read ${table ? `"${table}"` : "one of the tables"} because row-level security applies to it.`,
|
|
194
|
+
"",
|
|
195
|
+
" The dumping role is neither the table's owner nor `BYPASSRLS`, which is the normal",
|
|
196
|
+
" state of the `postgres` user on Cloud SQL, RDS and other managed Postgres — there is",
|
|
197
|
+
" no superuser to hand out.",
|
|
198
|
+
"",
|
|
199
|
+
" Two ways out:",
|
|
200
|
+
"",
|
|
201
|
+
" • Grant the dumping role BYPASSRLS, or make it the owner, and run this again. The",
|
|
202
|
+
" dump then contains every row, which is what a backup should mean.",
|
|
203
|
+
"",
|
|
204
|
+
" • Re-run with --enable-row-security to dump as an admin subject instead. Rebase",
|
|
205
|
+
" sets `app.uid`/`app.user_roles` so the generated `admin_full_access` policy",
|
|
206
|
+
" admits the dump. Read the warning it prints: the result contains exactly the",
|
|
207
|
+
" rows those policies admit, and any table whose policies do not include an",
|
|
208
|
+
" admin rule comes out short — with no error.",
|
|
209
|
+
"",
|
|
210
|
+
" Do not reach for a bare `pg_dump --enable-row-security` by hand. Without the",
|
|
211
|
+
" settings above it succeeds and silently omits rows."
|
|
212
|
+
].join("\n");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Assemble the `pg_restore` argument vector for a custom-format dump.
|
|
216
|
+
*/
|
|
217
|
+
function buildPgRestoreArgs(opts) {
|
|
218
|
+
const args = [
|
|
219
|
+
"--format=custom",
|
|
220
|
+
"--no-password",
|
|
221
|
+
`--dbname=${forLibpq(opts.connectionString)}`
|
|
222
|
+
];
|
|
223
|
+
if (opts.clean) args.push("--clean", "--if-exists");
|
|
224
|
+
if (opts.noOwner) args.push("--no-owner");
|
|
225
|
+
if (opts.exitOnError !== false) args.push("--exit-on-error");
|
|
226
|
+
args.push(opts.inputFile);
|
|
227
|
+
return args;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Assemble the `pg_restore --list` argument vector. Reading a dump's table
|
|
231
|
+
* of contents parses the whole archive without touching a database, so it is
|
|
232
|
+
* a cheap integrity check that the file isn't truncated or corrupt.
|
|
233
|
+
*/
|
|
234
|
+
function buildPgRestoreListArgs(inputFile) {
|
|
235
|
+
return ["--list", inputFile];
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Assemble the `pg_dumpall --globals-only` argument vector. Roles (and other
|
|
239
|
+
* cluster-wide objects) live outside any single database, so a per-database
|
|
240
|
+
* `pg_dump` omits them. Without the `rebase_user` role the RLS GRANT
|
|
241
|
+
* statements in the main dump fail on restore and RLS is silently lost — so
|
|
242
|
+
* every backup captures the globals into a sidecar `.globals.sql`.
|
|
243
|
+
*
|
|
244
|
+
* `--no-role-passwords` keeps role secrets out of the artifact (backups may
|
|
245
|
+
* be shipped off-box); roles are recreated password-less and re-secured by
|
|
246
|
+
* the operator.
|
|
247
|
+
*/
|
|
248
|
+
function buildPgDumpallGlobalsArgs(opts) {
|
|
249
|
+
return [
|
|
250
|
+
"--globals-only",
|
|
251
|
+
"--no-role-passwords",
|
|
252
|
+
"--no-password",
|
|
253
|
+
`--file=${opts.outFile}`,
|
|
254
|
+
`--dbname=${forLibpq(opts.connectionString)}`
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Derive the globals sidecar path/key for a given `.dump` file. Keeps the
|
|
259
|
+
* two artifacts adjacent so listing, uploading and pruning can find one from
|
|
260
|
+
* the other. A name that doesn't end in `.dump` is returned unchanged with a
|
|
261
|
+
* `.globals.sql` suffix appended.
|
|
262
|
+
*/
|
|
263
|
+
function globalsFileForDump(dumpPath) {
|
|
264
|
+
return dumpPath.endsWith(".dump") ? dumpPath.slice(0, -5) + ".globals.sql" : dumpPath + ".globals.sql";
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Split a `pg_dumpall --globals-only` script into individual statements.
|
|
268
|
+
* Used when replaying globals on restore so each `CREATE ROLE` / `GRANT`
|
|
269
|
+
* can run independently and a benign "role already exists" on one doesn't
|
|
270
|
+
* abort the rest. Drops `--` comment lines and blank statements.
|
|
271
|
+
*/
|
|
272
|
+
function splitGlobalsStatements(sql) {
|
|
273
|
+
return sql.split("\n").filter((line) => {
|
|
274
|
+
const trimmed = line.trim();
|
|
275
|
+
return !trimmed.startsWith("--") && !trimmed.startsWith("\\");
|
|
276
|
+
}).join("\n").split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Resolve the Postgres connection string the backup commands should use,
|
|
280
|
+
* mirroring the precedence the branch command already relies on.
|
|
281
|
+
*/
|
|
282
|
+
function resolveConnectionString(env) {
|
|
283
|
+
return env.DATABASE_URL || env.ADMIN_CONNECTION_STRING || null;
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/backup/retention.ts
|
|
287
|
+
/**
|
|
288
|
+
* Retention pruning for scheduled backups. Pure logic — no I/O — so it can
|
|
289
|
+
* be exhaustively unit-tested without touching a storage backend.
|
|
290
|
+
*/
|
|
291
|
+
/**
|
|
292
|
+
* Decide which backups to delete under the given retention policy.
|
|
293
|
+
*
|
|
294
|
+
* Rules, applied in order:
|
|
295
|
+
* 1. Objects whose key isn't a recognised rebase backup (no parseable
|
|
296
|
+
* timestamp and no `createdAt`) are never selected — foreign files in a
|
|
297
|
+
* shared bucket are left untouched.
|
|
298
|
+
* 2. The `keepMinimum` newest backups are always retained.
|
|
299
|
+
* 3. Of the remainder, any older than `retentionDays` are selected for
|
|
300
|
+
* deletion.
|
|
301
|
+
*
|
|
302
|
+
* Returns the keys to delete, oldest first.
|
|
303
|
+
*/
|
|
304
|
+
function selectBackupsToPrune(backups, options) {
|
|
305
|
+
const retentionDays = options.retentionDays ?? 0;
|
|
306
|
+
const keepMinimum = Math.max(0, options.keepMinimum ?? 0);
|
|
307
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
308
|
+
if (retentionDays <= 0) return [];
|
|
309
|
+
const dated = backups.map((b) => {
|
|
310
|
+
const createdAt = b.createdAt ?? parseBackupTimestamp(b.key);
|
|
311
|
+
return createdAt ? {
|
|
312
|
+
key: b.key,
|
|
313
|
+
createdAt
|
|
314
|
+
} : null;
|
|
315
|
+
}).filter((b) => b !== null);
|
|
316
|
+
dated.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
317
|
+
const cutoff = now.getTime() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
318
|
+
const toDelete = dated.slice(keepMinimum).filter((b) => b.createdAt.getTime() < cutoff);
|
|
319
|
+
toDelete.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
320
|
+
return toDelete.map((b) => b.key);
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/backup/backup-logic.ts
|
|
324
|
+
/**
|
|
325
|
+
* Pure-ish orchestration for the backup/restore paths, kept free of `execa`
|
|
326
|
+
* and `pg` value imports so it can be unit-tested under jest (the impure
|
|
327
|
+
* edges — spawning processes, opening connections — live in
|
|
328
|
+
* `backup-service.ts`, which is vitest/runtime only).
|
|
329
|
+
*
|
|
330
|
+
* The functions here take their side-effecting dependency as an argument
|
|
331
|
+
* (a statement runner, an object deleter) so tests can inject fakes.
|
|
332
|
+
*/
|
|
333
|
+
/**
|
|
334
|
+
* Replay a `pg_dumpall --globals-only` script one statement at a time,
|
|
335
|
+
* tolerating per-statement failures. On a same-cluster restore the roles
|
|
336
|
+
* usually already exist (`CREATE ROLE` → "already exists") and on a managed
|
|
337
|
+
* provider an `ALTER ROLE <superuser>` may be refused; neither should abort
|
|
338
|
+
* recreation of the roles that *are* missing. Returns how many statements
|
|
339
|
+
* applied vs were skipped.
|
|
340
|
+
*
|
|
341
|
+
* `runStatement` executes one SQL statement and rejects on error.
|
|
342
|
+
*/
|
|
343
|
+
async function applyGlobalsWith(runStatement, globalsSql, log = () => {}) {
|
|
344
|
+
let applied = 0;
|
|
345
|
+
let skipped = 0;
|
|
346
|
+
for (const statement of splitGlobalsStatements(globalsSql)) try {
|
|
347
|
+
await runStatement(statement);
|
|
348
|
+
applied++;
|
|
349
|
+
} catch (err) {
|
|
350
|
+
skipped++;
|
|
351
|
+
log(` • Skipped global: ${statement.split("\n")[0].slice(0, 80)} (${err instanceof Error ? err.message : String(err)})`);
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
applied,
|
|
355
|
+
skipped
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Delete each pruned dump together with its `.globals.sql` roles sidecar, so
|
|
360
|
+
* pruning never orphans the roles file. The sidecar is best-effort — older
|
|
361
|
+
* backups predate it, so a missing-sidecar failure is swallowed while a
|
|
362
|
+
* failure deleting the dump itself propagates.
|
|
363
|
+
*
|
|
364
|
+
* `deleteObject` removes one key and rejects if it cannot (e.g. not found).
|
|
365
|
+
*/
|
|
366
|
+
async function pruneWith(keys, deleteObject) {
|
|
367
|
+
for (const key of keys) {
|
|
368
|
+
await deleteObject(key);
|
|
369
|
+
try {
|
|
370
|
+
await deleteObject(globalsFileForDump(key));
|
|
371
|
+
} catch {}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Remove a dump artifact abandoned by a failed tool run.
|
|
376
|
+
*
|
|
377
|
+
* `pg_dump` creates its `--file=` target before it finishes connecting, so any
|
|
378
|
+
* failure — a URL libpq rejects, a dropped connection, a full disk — leaves a
|
|
379
|
+
* 0-byte file behind. That corpse is not inert: `rebase db backups list` used
|
|
380
|
+
* to show it as an ordinary entry, and `selectBackupsToPrune` ranks by
|
|
381
|
+
* timestamp alone, so it occupies a protected `keepMinimum` slot and can push
|
|
382
|
+
* a real backup out of retention.
|
|
383
|
+
*
|
|
384
|
+
* Best-effort on purpose: the caller is already throwing, and a failed unlink
|
|
385
|
+
* must not replace the real diagnosis with an ENOENT/EPERM from the cleanup.
|
|
386
|
+
*
|
|
387
|
+
* Lives here rather than in `backup-service.ts` for the reason at the top of
|
|
388
|
+
* this file — that module's top-level `execa` import makes it unloadable under
|
|
389
|
+
* jest, so anything placed there cannot be unit-tested.
|
|
390
|
+
*
|
|
391
|
+
* `remove` deletes one file and throws if it cannot; `exists` reports presence.
|
|
392
|
+
*/
|
|
393
|
+
function discardPartialDumpWith(exists, remove, file) {
|
|
394
|
+
try {
|
|
395
|
+
if (exists(file)) remove(file);
|
|
396
|
+
} catch {}
|
|
397
|
+
}
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/backup/backup-service.ts
|
|
400
|
+
/**
|
|
401
|
+
* Backup / restore orchestration: thin, well-typed wrappers around
|
|
402
|
+
* `pg_dump` and `pg_restore`, plus binary detection, version pre-flight,
|
|
403
|
+
* and upload/prune against a storage backend.
|
|
404
|
+
*
|
|
405
|
+
* The pure argument- and policy-logic lives in `pg-tools.ts` and
|
|
406
|
+
* `retention.ts`; this module is the impure edge (spawns processes, talks
|
|
407
|
+
* to Postgres and storage) and is intentionally kept small.
|
|
408
|
+
*/
|
|
409
|
+
var backup_service_exports = /* @__PURE__ */ __exportAll({
|
|
410
|
+
BackupToolError: () => BackupToolError,
|
|
411
|
+
applyGlobals: () => applyGlobals,
|
|
412
|
+
createDump: () => createDump,
|
|
413
|
+
detectToolMajor: () => detectToolMajor,
|
|
414
|
+
discardPartialDump: () => discardPartialDump,
|
|
415
|
+
ensureDatabaseExists: () => ensureDatabaseExists,
|
|
416
|
+
getServerVersionMajor: () => getServerVersionMajor,
|
|
417
|
+
listBackups: () => listBackups,
|
|
418
|
+
preflight: () => preflight,
|
|
419
|
+
pruneBackups: () => pruneBackups,
|
|
420
|
+
resolvePgBinary: () => resolvePgBinary,
|
|
421
|
+
restoreDump: () => restoreDump,
|
|
422
|
+
uploadBackup: () => uploadBackup,
|
|
423
|
+
validateDump: () => validateDump
|
|
424
|
+
});
|
|
425
|
+
/**
|
|
426
|
+
* Remove a dump artifact abandoned by a failed tool run. See
|
|
427
|
+
* {@link discardPartialDumpWith} for why this exists and why the logic lives
|
|
428
|
+
* in the execa-free module.
|
|
429
|
+
*/
|
|
430
|
+
function discardPartialDump(file) {
|
|
431
|
+
discardPartialDumpWith((f) => fs.existsSync(f), (f) => fs.unlinkSync(f), file);
|
|
432
|
+
}
|
|
433
|
+
var BackupToolError = class extends Error {
|
|
434
|
+
hint;
|
|
435
|
+
constructor(message, hint) {
|
|
436
|
+
super(message);
|
|
437
|
+
this.hint = hint;
|
|
438
|
+
this.name = "BackupToolError";
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
/** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */
|
|
442
|
+
function resolvePgBinary(tool, env = process.env) {
|
|
443
|
+
const overrideVar = tool === "pg_dump" ? env.PG_DUMP_PATH : tool === "pg_restore" ? env.PG_RESTORE_PATH : env.PG_DUMPALL_PATH;
|
|
444
|
+
if (overrideVar && fs.existsSync(overrideVar)) return overrideVar;
|
|
445
|
+
return resolveLocalBin(tool);
|
|
446
|
+
}
|
|
447
|
+
/** Run `<bin> --version` and extract the major version. */
|
|
448
|
+
async function detectToolMajor(bin) {
|
|
449
|
+
try {
|
|
450
|
+
const { stdout } = await execa(bin, ["--version"]);
|
|
451
|
+
return parsePgToolMajor(stdout);
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/** Query the server for its major version via `server_version_num`. */
|
|
457
|
+
async function getServerVersionMajor(connectionString) {
|
|
458
|
+
const { Client } = await import("pg");
|
|
459
|
+
const client = new Client({ connectionString });
|
|
460
|
+
await client.connect();
|
|
461
|
+
try {
|
|
462
|
+
const raw = (await client.query("SHOW server_version_num")).rows?.[0]?.server_version_num;
|
|
463
|
+
return serverVersionNumToMajor(raw);
|
|
464
|
+
} finally {
|
|
465
|
+
await client.end();
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Verify the requested client tool exists and its major version is
|
|
470
|
+
* compatible with the live server. Throws {@link BackupToolError} — with a
|
|
471
|
+
* doctor-style hint — when the binary is missing.
|
|
472
|
+
*/
|
|
473
|
+
async function preflight(tool, connectionString, env = process.env) {
|
|
474
|
+
const bin = resolvePgBinary(tool, env);
|
|
475
|
+
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.`);
|
|
476
|
+
const [toolMajor, serverMajor] = await Promise.all([detectToolMajor(bin), getServerVersionMajor(connectionString)]);
|
|
477
|
+
return {
|
|
478
|
+
bin,
|
|
479
|
+
toolMajor,
|
|
480
|
+
serverMajor,
|
|
481
|
+
...checkToolServerCompatibility(toolMajor, serverMajor)
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Produce a custom-format dump on local disk. When `outDir` is omitted the
|
|
486
|
+
* file is written to the OS temp directory (used by the upload path, which
|
|
487
|
+
* cleans it up afterwards).
|
|
488
|
+
*
|
|
489
|
+
* Alongside the `-Fc` dump it writes a `<name>.globals.sql` sidecar via
|
|
490
|
+
* `pg_dumpall --globals-only` so the roles the dump's GRANT/RLS statements
|
|
491
|
+
* depend on can be recreated on restore. Set `includeGlobals: false` to skip
|
|
492
|
+
* it (e.g. when the caller has no privilege to read cluster globals).
|
|
493
|
+
*/
|
|
494
|
+
async function createDump(opts) {
|
|
495
|
+
const env = opts.env ?? process.env;
|
|
496
|
+
const bin = resolvePgBinary("pg_dump", env);
|
|
497
|
+
if (!bin) throw new BackupToolError("Could not find the 'pg_dump' binary.", "Install the PostgreSQL client tools or set PG_DUMP_PATH.");
|
|
498
|
+
const fileName = opts.fileName ?? buildBackupFilename(opts.dbName);
|
|
499
|
+
const outDir = opts.outDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "rebase-backup-"));
|
|
500
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
501
|
+
const localFile = path.join(outDir, fileName);
|
|
502
|
+
const args = buildPgDumpArgs({
|
|
503
|
+
connectionString: opts.connectionString,
|
|
504
|
+
outFile: localFile,
|
|
505
|
+
excludeSchemas: opts.excludeSchemas,
|
|
506
|
+
noOwner: opts.noOwner,
|
|
507
|
+
rowSecurity: opts.rowSecurity
|
|
508
|
+
});
|
|
509
|
+
const dumpEnv = { ...env };
|
|
510
|
+
if (opts.rowSecurity) dumpEnv.PGOPTIONS = [env.PGOPTIONS, buildRowSecurityPgOptions(opts.rowSecurity)].filter(Boolean).join(" ");
|
|
511
|
+
try {
|
|
512
|
+
await execa(bin, args, {
|
|
513
|
+
stdio: opts.inheritStdio ? "inherit" : "pipe",
|
|
514
|
+
env: dumpEnv
|
|
515
|
+
});
|
|
516
|
+
} catch (error) {
|
|
517
|
+
discardPartialDump(localFile);
|
|
518
|
+
const diagnosis = diagnoseRowSecurityDumpFailure(error);
|
|
519
|
+
if (!diagnosis) throw error;
|
|
520
|
+
throw new BackupToolError(diagnosis, "Run `rebase db backup --help` for the flag, and read what it says about partial dumps.");
|
|
521
|
+
}
|
|
522
|
+
const result = {
|
|
523
|
+
localFile,
|
|
524
|
+
fileName,
|
|
525
|
+
sizeBytes: fs.existsSync(localFile) ? fs.statSync(localFile).size : 0
|
|
526
|
+
};
|
|
527
|
+
if (opts.includeGlobals !== false) {
|
|
528
|
+
const dumpallBin = resolvePgBinary("pg_dumpall", env);
|
|
529
|
+
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.");
|
|
530
|
+
const globalsFile = globalsFileForDump(localFile);
|
|
531
|
+
try {
|
|
532
|
+
await execa(dumpallBin, buildPgDumpallGlobalsArgs({
|
|
533
|
+
connectionString: opts.connectionString,
|
|
534
|
+
outFile: globalsFile
|
|
535
|
+
}), {
|
|
536
|
+
stdio: opts.inheritStdio ? "inherit" : "pipe",
|
|
537
|
+
env: { ...env }
|
|
538
|
+
});
|
|
539
|
+
} catch (error) {
|
|
540
|
+
discardPartialDump(globalsFile);
|
|
541
|
+
discardPartialDump(localFile);
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
result.globalsFile = globalsFile;
|
|
545
|
+
result.globalsSizeBytes = fs.existsSync(globalsFile) ? fs.statSync(globalsFile).size : 0;
|
|
546
|
+
}
|
|
547
|
+
return result;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Cheap integrity check on a freshly written dump: it must be non-empty and
|
|
551
|
+
* `pg_restore --list` must parse its table of contents without error. Used
|
|
552
|
+
* before pruning older backups so a corrupt-but-exit-0 dump never becomes
|
|
553
|
+
* the reason the last good backup is deleted.
|
|
554
|
+
*/
|
|
555
|
+
async function validateDump(localFile, env = process.env) {
|
|
556
|
+
if (!fs.existsSync(localFile)) return {
|
|
557
|
+
ok: false,
|
|
558
|
+
reason: `Dump file does not exist: ${localFile}`
|
|
559
|
+
};
|
|
560
|
+
if (fs.statSync(localFile).size === 0) return {
|
|
561
|
+
ok: false,
|
|
562
|
+
reason: "Dump file is empty (0 bytes)."
|
|
563
|
+
};
|
|
564
|
+
const bin = resolvePgBinary("pg_restore", env);
|
|
565
|
+
if (!bin) return {
|
|
566
|
+
ok: false,
|
|
567
|
+
reason: "Could not find 'pg_restore' to verify the dump."
|
|
568
|
+
};
|
|
569
|
+
try {
|
|
570
|
+
await execa(bin, buildPgRestoreListArgs(localFile), {
|
|
571
|
+
stdio: "pipe",
|
|
572
|
+
env: { ...env }
|
|
573
|
+
});
|
|
574
|
+
return { ok: true };
|
|
575
|
+
} catch (err) {
|
|
576
|
+
return {
|
|
577
|
+
ok: false,
|
|
578
|
+
reason: `pg_restore --list failed: ${err instanceof Error ? err.message : String(err)}`
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Replay a `pg_dumpall --globals-only` script to recreate cluster roles
|
|
584
|
+
* before a restore, so the dump's GRANT/RLS statements (which reference
|
|
585
|
+
* `rebase_user` and any owner roles) actually apply. Runs statement by
|
|
586
|
+
* statement and tolerates per-statement failures — on a same-cluster restore
|
|
587
|
+
* the roles usually already exist (`CREATE ROLE` → "already exists"), and on
|
|
588
|
+
* a managed provider an `ALTER ROLE <superuser>` may be refused; neither
|
|
589
|
+
* should abort role recreation. Returns how many statements applied vs were
|
|
590
|
+
* skipped.
|
|
591
|
+
*/
|
|
592
|
+
async function applyGlobals(connectionString, globalsSql, log = () => {}) {
|
|
593
|
+
const { Client } = await import("pg");
|
|
594
|
+
const client = new Client({ connectionString });
|
|
595
|
+
await client.connect();
|
|
596
|
+
try {
|
|
597
|
+
return await applyGlobalsWith(async (sql) => {
|
|
598
|
+
await client.query(sql);
|
|
599
|
+
}, globalsSql, log);
|
|
600
|
+
} finally {
|
|
601
|
+
await client.end();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Restore a custom-format dump into the database named by
|
|
606
|
+
* `connectionString`. Destructive when `clean` is set (drops objects
|
|
607
|
+
* first). Never called automatically — the CLI gates it behind explicit
|
|
608
|
+
* confirmation.
|
|
609
|
+
*
|
|
610
|
+
* Runs with `--exit-on-error` by default: a restore that logs-and-continues
|
|
611
|
+
* past a failed GRANT (because a role was missing) reports success with RLS
|
|
612
|
+
* un-enforced. Callers should recreate roles first (see {@link applyGlobals})
|
|
613
|
+
* and only set `exitOnError: false` deliberately.
|
|
614
|
+
*/
|
|
615
|
+
async function restoreDump(opts) {
|
|
616
|
+
const env = opts.env ?? process.env;
|
|
617
|
+
const bin = resolvePgBinary("pg_restore", env);
|
|
618
|
+
if (!bin) throw new BackupToolError("Could not find the 'pg_restore' binary.", "Install the PostgreSQL client tools or set PG_RESTORE_PATH.");
|
|
619
|
+
await execa(bin, buildPgRestoreArgs({
|
|
620
|
+
connectionString: opts.connectionString,
|
|
621
|
+
inputFile: opts.inputFile,
|
|
622
|
+
clean: opts.clean,
|
|
623
|
+
noOwner: opts.noOwner,
|
|
624
|
+
exitOnError: opts.exitOnError
|
|
625
|
+
}), {
|
|
626
|
+
stdio: opts.inheritStdio ? "inherit" : "pipe",
|
|
627
|
+
env: { ...env }
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Create a database (if absent) by connecting to the maintenance
|
|
632
|
+
* `postgres` database. Used by `restore --create-db`.
|
|
633
|
+
*/
|
|
634
|
+
async function ensureDatabaseExists(adminConnectionString, dbName) {
|
|
635
|
+
const { Client } = await import("pg");
|
|
636
|
+
const parsed = new URL(adminConnectionString);
|
|
637
|
+
parsed.pathname = "/postgres";
|
|
638
|
+
const client = new Client({ connectionString: parsed.toString() });
|
|
639
|
+
await client.connect();
|
|
640
|
+
try {
|
|
641
|
+
const existing = await client.query("SELECT 1 FROM pg_database WHERE datname = $1", [dbName]);
|
|
642
|
+
if (existing.rowCount && existing.rowCount > 0) return false;
|
|
643
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(dbName)) throw new BackupToolError(`Invalid database name: "${dbName}"`);
|
|
644
|
+
await client.query(`CREATE DATABASE "${dbName}"`);
|
|
645
|
+
return true;
|
|
646
|
+
} finally {
|
|
647
|
+
await client.end();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Upload a local dump to object storage under the destination's prefix.
|
|
652
|
+
* Backups may contain secrets/PII, so the object is written with a
|
|
653
|
+
* private/octet-stream content type; bucket-level ACLs must stay private.
|
|
654
|
+
*/
|
|
655
|
+
async function uploadBackup(storage, localFile, dest) {
|
|
656
|
+
const fileName = path.basename(localFile);
|
|
657
|
+
const key = joinStorageKey(dest.prefix, fileName);
|
|
658
|
+
const buffer = fs.readFileSync(localFile);
|
|
659
|
+
const file = new File([buffer], fileName, { type: "application/octet-stream" });
|
|
660
|
+
return {
|
|
661
|
+
key,
|
|
662
|
+
storageUrl: (await storage.putObject({
|
|
663
|
+
file,
|
|
664
|
+
key,
|
|
665
|
+
bucket: dest.bucket,
|
|
666
|
+
metadata: { "rebase-backup": "1" }
|
|
667
|
+
})).storageUrl
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* List existing backups at a destination. For local destinations reads the
|
|
672
|
+
* directory; for object storage lists the prefix via the controller.
|
|
673
|
+
*/
|
|
674
|
+
async function listBackups(dest, storage) {
|
|
675
|
+
if (dest.kind === "local") {
|
|
676
|
+
if (!fs.existsSync(dest.path)) return [];
|
|
677
|
+
const dir = fs.statSync(dest.path).isDirectory() ? dest.path : path.dirname(dest.path);
|
|
678
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(".dump")).map((f) => {
|
|
679
|
+
const full = path.join(dir, f);
|
|
680
|
+
const stats = fs.statSync(full);
|
|
681
|
+
return {
|
|
682
|
+
key: full,
|
|
683
|
+
createdAt: parseBackupTimestamp(f) ?? stats.mtime,
|
|
684
|
+
sizeBytes: stats.size
|
|
685
|
+
};
|
|
686
|
+
}).sort((a, b) => (b.createdAt?.getTime() ?? 0) - (a.createdAt?.getTime() ?? 0));
|
|
687
|
+
}
|
|
688
|
+
if (!storage) throw new BackupToolError(`Listing ${dest.kind} backups requires a configured storage backend.`);
|
|
689
|
+
return (await storage.listObjects(dest.prefix ? `${dest.prefix}/` : "", {
|
|
690
|
+
bucket: dest.bucket,
|
|
691
|
+
maxResults: 1e3
|
|
692
|
+
})).items.map((item) => item.fullPath).filter((key) => key.endsWith(".dump")).map((key) => ({
|
|
693
|
+
key,
|
|
694
|
+
createdAt: parseBackupTimestamp(key) ?? void 0
|
|
695
|
+
})).sort((a, b) => (b.createdAt?.getTime() ?? 0) - (a.createdAt?.getTime() ?? 0));
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Apply a retention policy to a destination, deleting the backups selected
|
|
699
|
+
* by {@link selectBackupsToPrune}. Returns the keys that were removed.
|
|
700
|
+
*/
|
|
701
|
+
async function pruneBackups(dest, options, storage) {
|
|
702
|
+
const toDelete = selectBackupsToPrune(await listBackups(dest, storage), options);
|
|
703
|
+
await pruneWith(toDelete, dest.kind === "local" ? async (key) => {
|
|
704
|
+
if (!fs.existsSync(key)) throw new Error(`not found: ${key}`);
|
|
705
|
+
fs.unlinkSync(key);
|
|
706
|
+
} : async (key) => {
|
|
707
|
+
if (!storage) throw new BackupToolError("Storage backend required to prune object backups.");
|
|
708
|
+
await storage.deleteObject(key, dest.bucket);
|
|
709
|
+
});
|
|
710
|
+
return toDelete;
|
|
711
|
+
}
|
|
712
|
+
//#endregion
|
|
713
|
+
export { parsePgToolMajor as A, checkToolServerCompatibility as C, parseBackupDestination as D, joinStorageKey as E, serverVersionNumToMajor as M, splitGlobalsStatements as N, parseBackupTimestamp as O, withDatabaseName as P, buildRowSecurityPgOptions as S, globalsFileForDump as T, buildBackupFilename as _, detectToolMajor as a, buildPgRestoreArgs as b, getServerVersionMajor as c, pruneBackups as d, resolvePgBinary as f, selectBackupsToPrune as g, validateDump as h, createDump as i, resolveConnectionString as j, parseDbNameFromUrl as k, listBackups as l, uploadBackup as m, applyGlobals as n, discardPartialDump as o, restoreDump as p, backup_service_exports as r, ensureDatabaseExists as s, BackupToolError as t, preflight as u, buildPgDumpArgs as v, diagnoseRowSecurityDumpFailure as w, buildPgRestoreListArgs as x, buildPgDumpallGlobalsArgs as y };
|
|
714
|
+
|
|
715
|
+
//# sourceMappingURL=backup-service-Br3tqwsy.js.map
|