@rebasepro/server-postgres 0.25.0 → 0.26.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-iXXZeVYD.js → BranchService-CWBnDhcv.js} +88 -12
- package/dist/BranchService-CWBnDhcv.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +13 -0
- package/dist/PostgresBootstrapper.d.ts +7 -0
- package/dist/auth/services.d.ts +8 -1
- package/dist/auth/verify-auth-tables.d.ts +21 -0
- package/dist/backup/backup-cli.d.ts +40 -3
- package/dist/backup/backup-service.d.ts +11 -0
- package/dist/{backup-service-Bb39plam.js → backup-cli-PoRG-zEq.js} +777 -10
- package/dist/backup-cli-PoRG-zEq.js.map +1 -0
- package/dist/{boot-ddl-s_3Hkz94.js → boot-ddl-EsgqDkx0.js} +3 -3
- package/dist/{boot-ddl-s_3Hkz94.js.map → boot-ddl-EsgqDkx0.js.map} +1 -1
- package/dist/branch-argv.d.ts +15 -0
- package/dist/branch-diff-CxE4VlxT.js +1396 -0
- package/dist/branch-diff-CxE4VlxT.js.map +1 -0
- package/dist/branch-diff-report-DpvVf7hR.js +48 -0
- package/dist/branch-diff-report-DpvVf7hR.js.map +1 -0
- package/dist/branching/branch-diff-report.d.ts +7 -0
- package/dist/branching/branch-diff.d.ts +115 -0
- package/dist/{cli-console-DQ_K-sdM.js → cli-console-f0k6_TsJ.js} +41 -9
- package/dist/cli-console-f0k6_TsJ.js.map +1 -0
- package/dist/cli-console.d.ts +17 -1
- package/dist/{cli-errors-DwQnNATs.js → cli-errors-DsYyNYzd.js} +90 -11
- package/dist/cli-errors-DsYyNYzd.js.map +1 -0
- package/dist/cli-errors.d.ts +14 -0
- package/dist/cli-flags.d.ts +51 -0
- package/dist/{cli-prompt-egtNKkZl.js → cli-prompt-DXG0yk-h.js} +39 -4
- package/dist/{cli-prompt-egtNKkZl.js.map → cli-prompt-DXG0yk-h.js.map} +1 -1
- package/dist/cli.js +121 -271
- package/dist/cli.js.map +1 -1
- package/dist/{collections-schema-version-T6cGvrDi.js → collections-schema-version-D7bDI0PJ.js} +13 -9
- package/dist/collections-schema-version-D7bDI0PJ.js.map +1 -0
- package/dist/{column-plan-helpers-D_V4oaZh.js → column-plan-helpers-IG6uFb1s.js} +2 -2
- package/dist/{column-plan-helpers-D_V4oaZh.js.map → column-plan-helpers-IG6uFb1s.js.map} +1 -1
- package/dist/{connection-C938nR3E.js → connection-czvI7TZL.js} +57 -10
- package/dist/connection-czvI7TZL.js.map +1 -0
- package/dist/{databasePoolManager-Yqdw14wY.js → databasePoolManager-3stMNFL-.js} +4 -6
- package/dist/databasePoolManager-3stMNFL-.js.map +1 -0
- package/dist/db-command-line.d.ts +14 -0
- package/dist/db-subcommands.d.ts +11 -0
- package/dist/{deferred-index-build-Cis_HoGn.js → deferred-index-build-CqaOv1Xx.js} +8 -8
- package/dist/{deferred-index-build-Cis_HoGn.js.map → deferred-index-build-CqaOv1Xx.js.map} +1 -1
- package/dist/{doctor-DvJH-IQG.js → doctor-DW_mlTzz.js} +186 -42
- package/dist/doctor-DW_mlTzz.js.map +1 -0
- package/dist/{ensure-collection-policies-CCqayoUz.js → ensure-collection-policies-DLWgabQ2.js} +13 -10
- package/dist/ensure-collection-policies-DLWgabQ2.js.map +1 -0
- package/dist/{ensure-collection-tables-DvvImXDe.js → ensure-collection-tables-CFIRq8ni.js} +9 -9
- package/dist/{ensure-collection-tables-DvvImXDe.js.map → ensure-collection-tables-CFIRq8ni.js.map} +1 -1
- package/dist/{ensure-tables-Dpgwx5sy.js → ensure-tables-4nA-tfwi.js} +5 -5
- package/dist/{ensure-tables-Dpgwx5sy.js.map → ensure-tables-4nA-tfwi.js.map} +1 -1
- package/dist/{enum-labels-kept-CUlUlRkO.js → enum-labels-kept-PcPW65xy.js} +3 -3
- package/dist/{enum-labels-kept-CUlUlRkO.js.map → enum-labels-kept-PcPW65xy.js.map} +1 -1
- package/dist/{generate-drizzle-schema-C3EAP9HE.js → generate-drizzle-schema-DC0BOUpn.js} +3 -3
- package/dist/{generate-drizzle-schema-C3EAP9HE.js.map → generate-drizzle-schema-DC0BOUpn.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js → generate-drizzle-schema-logic-CPzCRD-8.js} +4 -4
- package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js.map → generate-drizzle-schema-logic-CPzCRD-8.js.map} +1 -1
- package/dist/{generate-files-ClmOUHXj.js → generate-files-BP-4W_gm.js} +6 -6
- package/dist/{generate-files-ClmOUHXj.js.map → generate-files-BP-4W_gm.js.map} +1 -1
- package/dist/{generate-next-step-DCeS4LrJ.js → generate-next-step-DgK5BlBn.js} +2 -2
- package/dist/{generate-next-step-DCeS4LrJ.js.map → generate-next-step-DgK5BlBn.js.map} +1 -1
- package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js → generate-postgres-ddl-logic-DPl1oH_z.js} +4 -4
- package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js.map → generate-postgres-ddl-logic-DPl1oH_z.js.map} +1 -1
- package/dist/generated-schema-diff-eZCklRZQ.js +1025 -0
- package/dist/generated-schema-diff-eZCklRZQ.js.map +1 -0
- package/dist/{generated-schema-staleness-FlxM2i4i.js → generated-schema-staleness-BvNF3i-s.js} +2 -2
- package/dist/{generated-schema-staleness-FlxM2i4i.js.map → generated-schema-staleness-BvNF3i-s.js.map} +1 -1
- package/dist/history/HistoryService.d.ts +7 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.es.js +288 -11
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-CI5CU_n0.js → introspect-db-logic-DgQVzccW.js} +2 -2
- package/dist/{introspect-db-logic-CI5CU_n0.js.map → introspect-db-logic-DgQVzccW.js.map} +1 -1
- package/dist/{rls-bootstrap-sql-Bd-hJk08.js → migrate-baseline-B-2Ef4Mv.js} +718 -41
- package/dist/migrate-baseline-B-2Ef4Mv.js.map +1 -0
- package/dist/migrate-policy-stamps-BQtFHApx.js +89 -0
- package/dist/migrate-policy-stamps-BQtFHApx.js.map +1 -0
- package/dist/{open-rebase-BKA7Nyoe.js → open-rebase-C8jQ6s5a.js} +324 -1047
- package/dist/open-rebase-C8jQ6s5a.js.map +1 -0
- package/dist/open-rebase.d.ts +9 -1
- package/dist/{pg-error-utils-Bv5n2PLn.js → pg-error-utils-DhiUTlVx.js} +2 -2
- package/dist/{pg-error-utils-Bv5n2PLn.js.map → pg-error-utils-DhiUTlVx.js.map} +1 -1
- package/dist/{plan-schema-CXOucCw-.js → plan-schema-Czf3q4-Q.js} +4 -4
- package/dist/{plan-schema-CXOucCw-.js.map → plan-schema-Czf3q4-Q.js.map} +1 -1
- package/dist/{policy-drift-vm0BcvcE.js → policy-drift-X4A-xAzv.js} +3 -3
- package/dist/{policy-drift-vm0BcvcE.js.map → policy-drift-X4A-xAzv.js.map} +1 -1
- package/dist/pool-disconnects.d.ts +28 -0
- package/dist/{push-index-prebuild-DIequ4bV.js → push-index-prebuild-UTvB5pqH.js} +4 -4
- package/dist/{push-index-prebuild-DIequ4bV.js.map → push-index-prebuild-UTvB5pqH.js.map} +1 -1
- package/dist/push-kept-defaults-BGOlOqRQ.js +137 -0
- package/dist/push-kept-defaults-BGOlOqRQ.js.map +1 -0
- package/dist/{push-legacy-renames-BqJut4F3.js → push-legacy-renames-BwaKi8xF.js} +4 -4
- package/dist/{push-legacy-renames-BqJut4F3.js.map → push-legacy-renames-BwaKi8xF.js.map} +1 -1
- package/dist/{push-policy-plan-CvX8hiPb.js → push-policy-plan-DQ2tWceD.js} +5 -5
- package/dist/{push-policy-plan-CvX8hiPb.js.map → push-policy-plan-DQ2tWceD.js.map} +1 -1
- package/dist/{render-ddl-CQNwZdse.js → render-ddl-4v88y-Gv.js} +4 -4
- package/dist/{render-ddl-CQNwZdse.js.map → render-ddl-4v88y-Gv.js.map} +1 -1
- package/dist/{rls-enforcement-D7P4-ZYX.js → rls-enforcement-BOHuHVlc.js} +43 -4
- package/dist/rls-enforcement-BOHuHVlc.js.map +1 -0
- package/dist/schema/destructive-sql.d.ts +16 -0
- package/dist/schema/doctor-cli.js +5 -5
- package/dist/schema/doctor.d.ts +20 -2
- package/dist/schema/ensure-collection-policies.d.ts +4 -1
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl.js +2 -2
- package/dist/schema/generated-schema-diff.d.ts +21 -0
- package/dist/schema/introspect-db.js +5 -6
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/migrate-policy-stamps.d.ts +23 -0
- package/dist/schema/migration-freshness.d.ts +69 -0
- package/dist/schema/push-kept-defaults.d.ts +57 -0
- package/dist/schema/push-policy-plan.d.ts +5 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +47 -10
- package/dist/security/rls-enforcement.d.ts +18 -0
- package/dist/services/BranchService.d.ts +20 -2
- package/dist/services/FetchService.d.ts +12 -0
- package/dist/services/channel-presence.d.ts +3 -1
- package/dist/services/nested-path.d.ts +16 -0
- package/dist/services/pg-notify-listener.d.ts +2 -0
- package/dist/services/realtimeService.d.ts +18 -1
- package/dist/{source-Bv5KPeIx.js → source-fZb92pN8.js} +2 -2
- package/dist/{source-Bv5KPeIx.js.map → source-fZb92pN8.js.map} +1 -1
- package/dist/{vector-index-BFhyEaiz.js → vector-index-DsQ3V2d2.js} +2 -2
- package/dist/{vector-index-BFhyEaiz.js.map → vector-index-DsQ3V2d2.js.map} +1 -1
- package/dist/{websocket-MC0zTBar.js → websocket-CKYc3V4N.js} +56 -14
- package/dist/websocket-CKYc3V4N.js.map +1 -0
- package/package.json +7 -7
- package/dist/BranchService-iXXZeVYD.js.map +0 -1
- package/dist/backup-cli-B2CAn9fR.js +0 -647
- package/dist/backup-cli-B2CAn9fR.js.map +0 -1
- package/dist/backup-service-Bb39plam.js.map +0 -1
- package/dist/cli-console-DQ_K-sdM.js.map +0 -1
- package/dist/cli-errors-DwQnNATs.js.map +0 -1
- package/dist/cli-flags-D-b0qL9T.js +0 -206
- package/dist/cli-flags-D-b0qL9T.js.map +0 -1
- package/dist/collections-schema-version-T6cGvrDi.js.map +0 -1
- package/dist/connection-C938nR3E.js.map +0 -1
- package/dist/databasePoolManager-Yqdw14wY.js.map +0 -1
- package/dist/doctor-DvJH-IQG.js.map +0 -1
- package/dist/ensure-collection-policies-CCqayoUz.js.map +0 -1
- package/dist/open-rebase-BKA7Nyoe.js.map +0 -1
- package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +0 -1
- package/dist/rls-enforcement-D7P4-ZYX.js.map +0 -1
- package/dist/rolldown-runtime-V7rFbMRe.js +0 -39
- package/dist/websocket-MC0zTBar.js.map +0 -1
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { i as __exportAll, t as promptConfirm } from "./cli-prompt-DXG0yk-h.js";
|
|
4
|
+
import { et as forLibpq, nt as splitPassword, t as checkDatabaseConnectivity, tt as redactConnectionPasswords, z as resolveLocalBin } from "./cli-errors-DsYyNYzd.js";
|
|
5
|
+
import { a as outWarn, i as outError, r as out, t as chalk } from "./source-fZb92pN8.js";
|
|
6
|
+
import { T as execa, _ as parseCommandFlags, h as closestWord } from "./migrate-baseline-B-2Ef4Mv.js";
|
|
6
7
|
import path from "path";
|
|
7
8
|
import fs from "fs";
|
|
8
9
|
import os from "os";
|
|
10
|
+
import { Readable } from "stream";
|
|
11
|
+
import { pipeline } from "stream/promises";
|
|
9
12
|
//#region src/backup/pg-tools.ts
|
|
10
13
|
/**
|
|
11
14
|
* Pure helpers for the backup/restore commands.
|
|
@@ -559,6 +562,7 @@ var backup_service_exports = /* @__PURE__ */ __exportAll({
|
|
|
559
562
|
CleanRestoreFailed: () => CleanRestoreFailed,
|
|
560
563
|
applyGlobals: () => applyGlobals,
|
|
561
564
|
createDump: () => createDump,
|
|
565
|
+
describeUnreadableDump: () => describeUnreadableDump,
|
|
562
566
|
detectToolMajor: () => detectToolMajor,
|
|
563
567
|
discardPartialDump: () => discardPartialDump,
|
|
564
568
|
ensureDatabaseExists: () => ensureDatabaseExists,
|
|
@@ -1054,11 +1058,47 @@ async function readDumpObjects(inputFile, env = process.env) {
|
|
|
1054
1058
|
const bin = resolvePgBinary("pg_restore", env);
|
|
1055
1059
|
if (!bin) throw new BackupToolError("Could not find the 'pg_restore' binary.", "Install the PostgreSQL client tools or set PG_RESTORE_PATH.");
|
|
1056
1060
|
const toolEnv = Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== void 0));
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1061
|
+
let listed;
|
|
1062
|
+
try {
|
|
1063
|
+
({stdout: listed} = await execa(bin, buildPgRestoreListArgs(inputFile), {
|
|
1064
|
+
stdio: "pipe",
|
|
1065
|
+
env: toolEnv
|
|
1066
|
+
}));
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
const stderr = err instanceof Error && "stderr" in err && typeof err.stderr === "string" ? err.stderr : "";
|
|
1069
|
+
const { message, hint } = describeUnreadableDump(path.basename(inputFile), stderr);
|
|
1070
|
+
throw new BackupToolError(message, hint);
|
|
1071
|
+
}
|
|
1072
|
+
return parseDumpObjects(listed);
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* What `db restore` says of a file whose table of contents pg_restore cannot
|
|
1076
|
+
* read: before anything is restored, so nothing has changed.
|
|
1077
|
+
*
|
|
1078
|
+
* `pgRestoreStderr` is what `pg_restore --list` printed. Its first error line
|
|
1079
|
+
* is quoted, so the reader sees the tool's own words too.
|
|
1080
|
+
*/
|
|
1081
|
+
function describeUnreadableDump(file, pgRestoreStderr) {
|
|
1082
|
+
const said = pgRestoreStderr.split("\n").map((line) => line.replace(/^pg_restore:\s*(?:error:\s*)?/, "").trim()).find((line) => line.length > 0);
|
|
1083
|
+
const quoted = said ? ` (pg_restore: ${said})` : "";
|
|
1084
|
+
const unchanged = "Nothing was restored or changed.";
|
|
1085
|
+
const elsewhere = "Restore from another backup (`rebase db backups list` lists them), or copy this one again from where it came from.";
|
|
1086
|
+
if (said && /unsupported version \(([\d.]+)\) in file header/.test(said)) return {
|
|
1087
|
+
message: `${file} was written by a newer pg_dump than this pg_restore reads${quoted}. ${unchanged}`,
|
|
1088
|
+
hint: "Install the PostgreSQL client tools of the version that wrote it, or set PG_RESTORE_PATH to that pg_restore."
|
|
1089
|
+
};
|
|
1090
|
+
if (said && /does not appear to be a valid archive/.test(said)) return {
|
|
1091
|
+
message: `${file} is not a backup pg_restore can read${quoted}. \`rebase db restore\` takes the .dump files \`rebase db backup\` writes. ${unchanged}`,
|
|
1092
|
+
hint: elsewhere
|
|
1093
|
+
};
|
|
1094
|
+
if (said && /end of file|too short/.test(said)) return {
|
|
1095
|
+
message: `${file} is truncated: it ends before pg_restore can read what it holds${quoted}. A copy or download that did not finish, or a disk that filled while it was written, leaves a file like this. Nothing was restored or changed.`,
|
|
1096
|
+
hint: elsewhere
|
|
1097
|
+
};
|
|
1098
|
+
return {
|
|
1099
|
+
message: `${file} cannot be read as a backup${quoted}: it is damaged, or not a pg_dump archive. ${unchanged}`,
|
|
1100
|
+
hint: elsewhere
|
|
1101
|
+
};
|
|
1062
1102
|
}
|
|
1063
1103
|
/**
|
|
1064
1104
|
* Which of a dump's schemas and tables the target database already holds,
|
|
@@ -1189,6 +1229,733 @@ async function pruneBackups(dest, options, storage) {
|
|
|
1189
1229
|
return toDelete;
|
|
1190
1230
|
}
|
|
1191
1231
|
//#endregion
|
|
1192
|
-
|
|
1232
|
+
//#region src/backup/backup-cli.ts
|
|
1233
|
+
var backup_cli_exports = /* @__PURE__ */ __exportAll({
|
|
1234
|
+
INVOCATION_CWD_ENV: () => INVOCATION_CWD_ENV,
|
|
1235
|
+
backupCommand: () => backupCommand,
|
|
1236
|
+
backupsCommand: () => backupsCommand,
|
|
1237
|
+
confirmRestore: () => confirmRestore,
|
|
1238
|
+
describeCleanExtras: () => describeCleanExtras,
|
|
1239
|
+
describeCleanRestoreFailure: () => describeCleanRestoreFailure,
|
|
1240
|
+
describeRestoreConflict: () => describeRestoreConflict,
|
|
1241
|
+
excludesRebaseSchema: () => excludesRebaseSchema,
|
|
1242
|
+
invocationDir: () => invocationDir,
|
|
1243
|
+
parseBackupLine: () => parseBackupLine,
|
|
1244
|
+
parseBackupsListLine: () => parseBackupsListLine,
|
|
1245
|
+
parseRestoreLine: () => parseRestoreLine,
|
|
1246
|
+
resolveBackupLocation: () => resolveBackupLocation,
|
|
1247
|
+
resolveStorageForDestination: () => resolveStorageForDestination,
|
|
1248
|
+
restoreCommand: () => restoreCommand,
|
|
1249
|
+
schemaPatternRegex: () => schemaPatternRegex,
|
|
1250
|
+
unknownBackupsAction: () => unknownBackupsAction
|
|
1251
|
+
});
|
|
1252
|
+
function formatBytes(bytes) {
|
|
1253
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1254
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1255
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1256
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
1257
|
+
}
|
|
1258
|
+
/** A web stream's chunks, for piping into a file. */
|
|
1259
|
+
async function* webChunks(stream) {
|
|
1260
|
+
const reader = stream.getReader();
|
|
1261
|
+
try {
|
|
1262
|
+
for (;;) {
|
|
1263
|
+
const { done, value } = await reader.read();
|
|
1264
|
+
if (done) return;
|
|
1265
|
+
yield value;
|
|
1266
|
+
}
|
|
1267
|
+
} finally {
|
|
1268
|
+
reader.releaseLock();
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* The controller for an object-storage destination, or `null` for a local one.
|
|
1273
|
+
*
|
|
1274
|
+
* Built by `backupStorageFromEnv` from `@rebasepro/server` — the function the
|
|
1275
|
+
* Backups panel's routes use — so the CLI and the panel read a bucket the same
|
|
1276
|
+
* way. This file had its own copy, and the two had already drifted once
|
|
1277
|
+
* (`S3_FORCE_PATH_STYLE` read as `=== "true"`, so a MinIO backup addressed the
|
|
1278
|
+
* bucket as a host).
|
|
1279
|
+
*
|
|
1280
|
+
* Exported for its test.
|
|
1281
|
+
*/
|
|
1282
|
+
async function resolveStorageForDestination(dest, env) {
|
|
1283
|
+
if (dest.kind === "local") return null;
|
|
1284
|
+
const { backupStorageFromEnv } = await import("@rebasepro/server");
|
|
1285
|
+
try {
|
|
1286
|
+
return backupStorageFromEnv(dest, env);
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
throw new BackupToolError(error instanceof Error ? error.message : String(error), "Set the same S3_* variables your backend uses for storage.");
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* The directory `rebase` was run from, which `rebase` puts here for the driver.
|
|
1293
|
+
*
|
|
1294
|
+
* The driver runs in the project's `backend/`, so a relative path resolved
|
|
1295
|
+
* against its own working directory named a different place from the one the
|
|
1296
|
+
* user typed it in: `rebase db backup --out ./backups` wrote to `<root>/backups`
|
|
1297
|
+
* (the CLI resolves explicit paths itself) while `rebase db backups list`
|
|
1298
|
+
* looked in `<root>/backend/backups`. Spelled the same in
|
|
1299
|
+
* `packages/cli/src/commands/db.ts`.
|
|
1300
|
+
*/
|
|
1301
|
+
var INVOCATION_CWD_ENV = "REBASE_INVOCATION_CWD";
|
|
1302
|
+
/** Where a relative path the user typed is relative to: where they ran `rebase`. */
|
|
1303
|
+
function invocationDir(env = process.env) {
|
|
1304
|
+
const dir = env[INVOCATION_CWD_ENV]?.trim();
|
|
1305
|
+
return dir && path.isAbsolute(dir) ? dir : process.cwd();
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Where `db backup` writes and `db backups list` reads, by one rule.
|
|
1309
|
+
*
|
|
1310
|
+
* - `--out` is relative to where `rebase` was run, like any path typed on a
|
|
1311
|
+
* command line.
|
|
1312
|
+
* - `BACKUP_DESTINATION` is relative to the backend directory, because the
|
|
1313
|
+
* server reads the same variable there: the Backups panel and the scheduled
|
|
1314
|
+
* backup have to find what the CLI wrote.
|
|
1315
|
+
* - With neither, `./backups` where `rebase` was run, as the help says.
|
|
1316
|
+
*
|
|
1317
|
+
* An `s3://` or `gs://` destination is returned as given.
|
|
1318
|
+
*/
|
|
1319
|
+
function resolveBackupLocation(opts) {
|
|
1320
|
+
const flag = opts.flag?.trim();
|
|
1321
|
+
if (flag) return isRemoteDestination(flag) ? flag : path.resolve(opts.invocationDir, flag);
|
|
1322
|
+
const configured = opts.configured?.trim();
|
|
1323
|
+
if (configured) return isRemoteDestination(configured) ? configured : path.resolve(opts.backendDir, configured);
|
|
1324
|
+
return path.join(opts.invocationDir, "backups");
|
|
1325
|
+
}
|
|
1326
|
+
function isRemoteDestination(value) {
|
|
1327
|
+
return parseBackupDestination(value).kind !== "local";
|
|
1328
|
+
}
|
|
1329
|
+
/** {@link resolveBackupLocation} for this process. */
|
|
1330
|
+
function backupLocation(flag) {
|
|
1331
|
+
return resolveBackupLocation({
|
|
1332
|
+
flag,
|
|
1333
|
+
configured: process.env.BACKUP_DESTINATION,
|
|
1334
|
+
invocationDir: invocationDir(),
|
|
1335
|
+
backendDir: process.cwd()
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
function requireConnection() {
|
|
1339
|
+
const conn = resolveConnectionString(process.env);
|
|
1340
|
+
if (!conn) {
|
|
1341
|
+
outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
1342
|
+
process.exit(1);
|
|
1343
|
+
}
|
|
1344
|
+
return conn;
|
|
1345
|
+
}
|
|
1346
|
+
var BACKUP_FLAGS = {
|
|
1347
|
+
"--out": String,
|
|
1348
|
+
"--output": "--out",
|
|
1349
|
+
"--exclude-schema": [String],
|
|
1350
|
+
"--no-owner": Boolean,
|
|
1351
|
+
"--enable-row-security": Boolean,
|
|
1352
|
+
"--row-security-role": String,
|
|
1353
|
+
"-o": "--out"
|
|
1354
|
+
};
|
|
1355
|
+
/**
|
|
1356
|
+
* Parse a `rebase db backup …` line. An undeclared flag is refused (see
|
|
1357
|
+
* `parseCommandFlags`), and so is a positional: `rebase db backup ./backups`
|
|
1358
|
+
* means `--out ./backups`, and taken as nothing it wrote to the default
|
|
1359
|
+
* directory.
|
|
1360
|
+
*/
|
|
1361
|
+
function parseBackupLine(rawArgs) {
|
|
1362
|
+
const args = parseCommandFlags(BACKUP_FLAGS, rawArgs.slice(2), "db backup");
|
|
1363
|
+
const [typed] = args._;
|
|
1364
|
+
if (typed !== void 0) {
|
|
1365
|
+
const stray = redactConnectionPasswords(typed);
|
|
1366
|
+
throw new Error(`\`rebase db backup\` takes no argument "${stray}". Nothing was run. To choose where the backup goes, pass --out ${stray}.`);
|
|
1367
|
+
}
|
|
1368
|
+
const dropsRebase = (args["--exclude-schema"] ?? []).find(excludesRebaseSchema);
|
|
1369
|
+
if (dropsRebase !== void 0) throw new Error(excludesRebaseMessage(dropsRebase));
|
|
1370
|
+
return args;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* A `pg_dump --exclude-schema` pattern as the regular expression pg_dump
|
|
1374
|
+
* hands the server, by pg_dump's own rules (`patternToSQLRegex` in
|
|
1375
|
+
* PostgreSQL's `fe_utils/string_utils.c`):
|
|
1376
|
+
*
|
|
1377
|
+
* - outside double quotes, an upper-case letter is folded to lower case, `*`
|
|
1378
|
+
* is `.*`, `?` is `.`, and every other character, regular-expression syntax
|
|
1379
|
+
* included (`[r]`, `(a|b)`, `+`), passes through as it is;
|
|
1380
|
+
* - inside double quotes, case is kept and every special character is
|
|
1381
|
+
* matched literally; `""` is one quote;
|
|
1382
|
+
* - `$` is always literal, and `[]` is always the two characters;
|
|
1383
|
+
* - an unquoted `.` separates a database from the schema, so `shop.rebase`
|
|
1384
|
+
* is matched on `rebase`.
|
|
1385
|
+
*
|
|
1386
|
+
* The result is anchored, as pg_dump anchors it: `^(…)$`.
|
|
1387
|
+
*/
|
|
1388
|
+
function schemaPatternRegex(pattern) {
|
|
1389
|
+
let regex = "";
|
|
1390
|
+
let inQuotes = false;
|
|
1391
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
1392
|
+
const char = pattern[index];
|
|
1393
|
+
if (char === "\"") if (inQuotes && pattern[index + 1] === "\"") {
|
|
1394
|
+
regex += "\"";
|
|
1395
|
+
index += 1;
|
|
1396
|
+
} else inQuotes = !inQuotes;
|
|
1397
|
+
else if (!inQuotes && char >= "A" && char <= "Z") regex += char.toLowerCase();
|
|
1398
|
+
else if (!inQuotes && char === "*") regex += ".*";
|
|
1399
|
+
else if (!inQuotes && char === "?") regex += ".";
|
|
1400
|
+
else if (!inQuotes && char === ".") regex = "";
|
|
1401
|
+
else if (char === "$") regex += "\\$";
|
|
1402
|
+
else if (inQuotes && "|*+?()[]{}.^$\\".includes(char)) regex += `\\${char}`;
|
|
1403
|
+
else if (char === "[" && pattern[index + 1] === "]") regex += "\\[";
|
|
1404
|
+
else regex += char;
|
|
1405
|
+
}
|
|
1406
|
+
return `^(${regex})$`;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Whether a `pg_dump --exclude-schema` pattern matches the `rebase` schema, by
|
|
1410
|
+
* pg_dump's own rules (see {@link schemaPatternRegex}).
|
|
1411
|
+
*
|
|
1412
|
+
* Evaluated here with JavaScript's regular expressions, which read the syntax
|
|
1413
|
+
* people write in a pattern (`[r]ebase`, `(rebase|x)`, `reb.+`) as the
|
|
1414
|
+
* server does. The server's engine has syntax JavaScript reads otherwise
|
|
1415
|
+
* (`[[:alpha:]]`, `\m`), so `db backup` asks the server too before it dumps
|
|
1416
|
+
* (see `backupCommand`); this check is the one that answers before anything
|
|
1417
|
+
* connects. A pattern JavaScript cannot compile is left to that one.
|
|
1418
|
+
*/
|
|
1419
|
+
function excludesRebaseSchema(pattern) {
|
|
1420
|
+
try {
|
|
1421
|
+
return new RegExp(schemaPatternRegex(pattern)).test("rebase");
|
|
1422
|
+
} catch {
|
|
1423
|
+
return false;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Whether the server's regular-expression engine matches `name` with `regex`,
|
|
1428
|
+
* the way pg_dump's own query matches a schema pattern
|
|
1429
|
+
* (`nspname OPERATOR(pg_catalog.~) '^(…)$' COLLATE pg_catalog.default`).
|
|
1430
|
+
*
|
|
1431
|
+
* A regex the server will not compile matches nothing here; pg_dump then
|
|
1432
|
+
* refuses it in its own words.
|
|
1433
|
+
*/
|
|
1434
|
+
async function serverRegexMatches(connectionString, regex, name) {
|
|
1435
|
+
const { Client } = await import("pg");
|
|
1436
|
+
const client = new Client({ connectionString });
|
|
1437
|
+
await client.connect();
|
|
1438
|
+
try {
|
|
1439
|
+
const { rows } = await client.query("SELECT $1::text OPERATOR(pg_catalog.~) $2::text COLLATE pg_catalog.default AS matches", [name, regex]);
|
|
1440
|
+
return rows[0]?.matches === true;
|
|
1441
|
+
} catch (err) {
|
|
1442
|
+
if (err instanceof Error && "code" in err && err.code === "2201B") return false;
|
|
1443
|
+
throw err;
|
|
1444
|
+
} finally {
|
|
1445
|
+
await client.end();
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
/** The refusal for a pattern that leaves out the rebase schema. */
|
|
1449
|
+
function excludesRebaseMessage(pattern) {
|
|
1450
|
+
return `--exclude-schema ${pattern} leaves out the rebase schema, and a backup without it cannot be restored: it holds the users, roles and the functions every policy calls. Nothing was run.`;
|
|
1451
|
+
}
|
|
1452
|
+
async function backupCommand(rawArgs) {
|
|
1453
|
+
if (rawArgs.includes("--help")) {
|
|
1454
|
+
printBackupHelp();
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
const args = parseBackupLine(rawArgs);
|
|
1458
|
+
const connectionString = requireConnection();
|
|
1459
|
+
const dbName = parseDbNameFromUrl(connectionString) ?? "database";
|
|
1460
|
+
const out$1 = backupLocation(args["--out"]);
|
|
1461
|
+
const dest = parseBackupDestination(out$1);
|
|
1462
|
+
out("");
|
|
1463
|
+
out(chalk.bold(" 💾 Rebase DB Backup"));
|
|
1464
|
+
out(chalk.gray(` Database: ${dbName}`));
|
|
1465
|
+
out(chalk.gray(` Destination: ${out$1}`));
|
|
1466
|
+
out("");
|
|
1467
|
+
await checkDatabaseConnectivity(connectionString);
|
|
1468
|
+
for (const pattern of args["--exclude-schema"] ?? []) if (await serverRegexMatches(connectionString, schemaPatternRegex(pattern), "rebase")) {
|
|
1469
|
+
outError(chalk.red(` ✗ ${excludesRebaseMessage(pattern)}`));
|
|
1470
|
+
process.exit(1);
|
|
1471
|
+
}
|
|
1472
|
+
const pf = await preflight("pg_dump", connectionString);
|
|
1473
|
+
if (!pf.compatible) {
|
|
1474
|
+
outError(chalk.red(` ✗ ${pf.reason}`));
|
|
1475
|
+
process.exit(1);
|
|
1476
|
+
}
|
|
1477
|
+
out(chalk.gray(` Using pg_dump ${pf.toolMajor} against server ${pf.serverMajor}.`));
|
|
1478
|
+
const rowSecurity = args["--enable-row-security"] ? {
|
|
1479
|
+
uid: "rebase-db-backup",
|
|
1480
|
+
roles: [args["--row-security-role"] || "admin"]
|
|
1481
|
+
} : void 0;
|
|
1482
|
+
if (rowSecurity) {
|
|
1483
|
+
outWarn("");
|
|
1484
|
+
outWarn(chalk.yellow(" ⚠ Dumping with row-level security ON."));
|
|
1485
|
+
outWarn(chalk.gray(` Reading as roles [${rowSecurity.roles.join(", ")}], which satisfies the generated`));
|
|
1486
|
+
outWarn(chalk.gray(" `admin_full_access` policy. This backup contains exactly the rows those"));
|
|
1487
|
+
outWarn(chalk.gray(" policies admit — a table whose policies have no admin rule comes out short,"));
|
|
1488
|
+
outWarn(chalk.gray(" and pg_dump will not say so. Prefer granting the dumping role BYPASSRLS."));
|
|
1489
|
+
outWarn("");
|
|
1490
|
+
}
|
|
1491
|
+
try {
|
|
1492
|
+
if (dest.kind === "local") {
|
|
1493
|
+
const explicitFile = dest.path.endsWith(".dump");
|
|
1494
|
+
const dump = await createDump({
|
|
1495
|
+
connectionString,
|
|
1496
|
+
dbName,
|
|
1497
|
+
outDir: explicitFile ? path.dirname(dest.path) : dest.path,
|
|
1498
|
+
fileName: explicitFile ? path.basename(dest.path) : void 0,
|
|
1499
|
+
excludeSchemas: args["--exclude-schema"],
|
|
1500
|
+
noOwner: args["--no-owner"],
|
|
1501
|
+
inheritStdio: true,
|
|
1502
|
+
rowSecurity
|
|
1503
|
+
});
|
|
1504
|
+
await assertDumpValid(dump.localFile);
|
|
1505
|
+
out("");
|
|
1506
|
+
out(chalk.green(` ✓ Backup written to ${dump.localFile} (${formatBytes(dump.sizeBytes)})`));
|
|
1507
|
+
if (dump.globalsFile) out(chalk.gray(` ✓ Roles captured to ${dump.globalsFile} (needed so RLS survives a restore).`));
|
|
1508
|
+
} else {
|
|
1509
|
+
const storage = await resolveStorageForDestination(dest, process.env);
|
|
1510
|
+
const dump = await createDump({
|
|
1511
|
+
connectionString,
|
|
1512
|
+
dbName,
|
|
1513
|
+
excludeSchemas: args["--exclude-schema"],
|
|
1514
|
+
noOwner: args["--no-owner"],
|
|
1515
|
+
inheritStdio: true,
|
|
1516
|
+
rowSecurity
|
|
1517
|
+
});
|
|
1518
|
+
try {
|
|
1519
|
+
await assertDumpValid(dump.localFile);
|
|
1520
|
+
const uploaded = await uploadBackup(storage, dump.localFile, dest);
|
|
1521
|
+
out("");
|
|
1522
|
+
out(chalk.green(` ✓ Backup uploaded to ${uploaded.storageUrl} (${formatBytes(dump.sizeBytes)})`));
|
|
1523
|
+
if (dump.globalsFile && fs.existsSync(dump.globalsFile)) {
|
|
1524
|
+
const globalsUpload = await uploadBackup(storage, dump.globalsFile, dest);
|
|
1525
|
+
out(chalk.gray(` ✓ Roles uploaded to ${globalsUpload.storageUrl} (needed so RLS survives a restore).`));
|
|
1526
|
+
}
|
|
1527
|
+
out(chalk.gray(" Ensure this bucket is private — backups may contain secrets and PII."));
|
|
1528
|
+
} finally {
|
|
1529
|
+
if (fs.existsSync(dump.localFile)) fs.unlinkSync(dump.localFile);
|
|
1530
|
+
if (dump.globalsFile && fs.existsSync(dump.globalsFile)) fs.unlinkSync(dump.globalsFile);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
out("");
|
|
1534
|
+
} catch (err) {
|
|
1535
|
+
reportError(err);
|
|
1536
|
+
process.exit(1);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
var RESTORE_FLAGS = {
|
|
1540
|
+
"--target-db": String,
|
|
1541
|
+
"--create-db": Boolean,
|
|
1542
|
+
"--clean": Boolean,
|
|
1543
|
+
"--no-owner": Boolean,
|
|
1544
|
+
"--continue-on-error": Boolean,
|
|
1545
|
+
"--yes": Boolean,
|
|
1546
|
+
"-y": "--yes"
|
|
1547
|
+
};
|
|
1548
|
+
/**
|
|
1549
|
+
* Parse a `rebase db restore …` line; the one positional is the backup.
|
|
1550
|
+
*
|
|
1551
|
+
* Strict, because this is the command where a typo costs most: parsed
|
|
1552
|
+
* permissively, `--targt-db x` became a second positional, the target fell
|
|
1553
|
+
* back to DATABASE_URL's own database, and the restore ran against the live
|
|
1554
|
+
* one.
|
|
1555
|
+
*/
|
|
1556
|
+
function parseRestoreLine(rawArgs) {
|
|
1557
|
+
const args = parseCommandFlags(RESTORE_FLAGS, rawArgs.slice(2), "db restore");
|
|
1558
|
+
if (args._.length > 1) throw new Error(`\`rebase db restore\` restores one backup, and was given ${args._.length}: ${args._.map((word) => `"${redactConnectionPasswords(word)}"`).join(", ")}. Nothing was run.`);
|
|
1559
|
+
return args;
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* The restore's one question, asked before anything can change.
|
|
1563
|
+
*
|
|
1564
|
+
* `--yes` skips it. On a terminal it asks for a typed 'yes'. With no terminal
|
|
1565
|
+
* nobody can answer, and the refusal says to pass `--yes`, as `db reset`,
|
|
1566
|
+
* `db pull` and `db branch prune` do: "Aborted. No changes were made." alone
|
|
1567
|
+
* read as though someone had answered no.
|
|
1568
|
+
*
|
|
1569
|
+
* @returns whether to go on. When not, it has said why.
|
|
1570
|
+
*/
|
|
1571
|
+
async function confirmRestore(opts) {
|
|
1572
|
+
if (opts.yes) return true;
|
|
1573
|
+
outWarn(chalk.yellow(` ⚠️ This will restore into "${opts.target}" and may overwrite existing data.`));
|
|
1574
|
+
if (!opts.interactive) {
|
|
1575
|
+
outError(chalk.red(" ✗ Nothing was restored: there is no terminal to confirm on."));
|
|
1576
|
+
outError(chalk.gray(` Re-run with --yes to restore into "${opts.target}".`));
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1579
|
+
const confirmed = await opts.ask(chalk.yellow(" Type 'yes' to continue: "));
|
|
1580
|
+
if (!confirmed) out(chalk.gray(" Aborted. No changes were made."));
|
|
1581
|
+
return confirmed;
|
|
1582
|
+
}
|
|
1583
|
+
/** How many of the conflicting objects a refusal names before counting the rest. */
|
|
1584
|
+
var CONFLICTS_NAMED = 5;
|
|
1585
|
+
/**
|
|
1586
|
+
* What `db restore` says when the target already holds what the backup
|
|
1587
|
+
* creates, and the two ways round it: `--clean` replaces them with the
|
|
1588
|
+
* backup's copy, a new database restores beside them.
|
|
1589
|
+
*
|
|
1590
|
+
* `line` is the rest of what the user typed — the database it names, `--yes`,
|
|
1591
|
+
* `--no-owner` — which both commands keep, so the one copied runs against the
|
|
1592
|
+
* same database and does not stop again to ask. `--target-db` goes on the
|
|
1593
|
+
* `--clean` one only: the other names a new database.
|
|
1594
|
+
*/
|
|
1595
|
+
function describeRestoreConflict(target, backupArg, conflicts, line = {}) {
|
|
1596
|
+
const named = conflicts.slice(0, CONFLICTS_NAMED).join(", ");
|
|
1597
|
+
const more = conflicts.length > CONFLICTS_NAMED ? `, and ${conflicts.length - CONFLICTS_NAMED} more` : "";
|
|
1598
|
+
const carried = line.carried?.length ? ` ${line.carried.join(" ")}` : "";
|
|
1599
|
+
const sameTarget = line.targetDb ? ` --target-db ${line.targetDb}` : "";
|
|
1600
|
+
return {
|
|
1601
|
+
message: `"${target}" already holds ${named}${more}, which this backup creates. Without --clean, pg_restore stops at the first of them. Nothing was restored or changed.`,
|
|
1602
|
+
hint: [`To replace them with the backup's copy: rebase db restore ${backupArg}${sameTarget} --clean${carried}`, `To restore beside them, in a new database: rebase db restore ${backupArg} --create-db --target-db <new name>${carried}`].join("\n ")
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
/** How many of the objects `--clean` drops beyond the backup's own it names before counting the rest. */
|
|
1606
|
+
var EXTRAS_NAMED = 10;
|
|
1607
|
+
/**
|
|
1608
|
+
* What `restore --clean` says, before it asks, about what it drops that the
|
|
1609
|
+
* backup does not hold — see `findCleanExtras`. Nothing when there is none.
|
|
1610
|
+
*/
|
|
1611
|
+
function describeCleanExtras(schemas, extras) {
|
|
1612
|
+
if (extras.length === 0) return [];
|
|
1613
|
+
const replaced = schemas.filter((schema) => schema !== "public");
|
|
1614
|
+
return [
|
|
1615
|
+
replaced.length > 0 ? `--clean replaces ${replaced.length === 1 ? "schema" : "schemas"} ${replaced.join(", ")} whole, so it also drops what this backup does not hold:` : "--clean also drops what this backup does not hold:",
|
|
1616
|
+
...extras.slice(0, EXTRAS_NAMED).map((extra) => ` ${extra}`),
|
|
1617
|
+
...extras.length > EXTRAS_NAMED ? [` and ${extras.length - EXTRAS_NAMED} more`] : []
|
|
1618
|
+
];
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* What a `restore --clean` that stopped says: it ran as one transaction, so
|
|
1622
|
+
* nothing changed. The cluster roles the sidecar recreated are the exception,
|
|
1623
|
+
* and are said to be.
|
|
1624
|
+
*/
|
|
1625
|
+
function describeCleanRestoreFailure(target, failure, rolesApplied) {
|
|
1626
|
+
const why = failure.tool === "pg_restore" ? "pg_restore could not read the whole backup" : failure.lockTimeout ? `it waited too long for a lock on "${target}"` : "a statement failed";
|
|
1627
|
+
const hints = [...failure.lockTimeout ? ["Something holds a lock on it — usually a `rebase dev` serving from it. Stop that, then restore again."] : [], ...rolesApplied > 0 ? [`The ${rolesApplied} cluster role(s) recreated from the backup's sidecar before it stay.`] : []];
|
|
1628
|
+
return {
|
|
1629
|
+
message: `Nothing was restored or changed: the restore ran as one transaction, and it was rolled back when ${why} (above). "${target}" holds what it held before.`,
|
|
1630
|
+
hint: hints.length > 0 ? hints.join("\n ") : void 0
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
async function restoreCommand(rawArgs) {
|
|
1634
|
+
if (rawArgs.includes("--help")) {
|
|
1635
|
+
printRestoreHelp();
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const args = parseRestoreLine(rawArgs);
|
|
1639
|
+
const backupArg = args._[0];
|
|
1640
|
+
if (!backupArg) {
|
|
1641
|
+
printRestoreHelp();
|
|
1642
|
+
process.exit(1);
|
|
1643
|
+
}
|
|
1644
|
+
const baseConnection = requireConnection();
|
|
1645
|
+
const targetDb = args["--target-db"] ?? parseDbNameFromUrl(baseConnection) ?? void 0;
|
|
1646
|
+
const targetConnection = args["--target-db"] ? withDatabaseName(baseConnection, args["--target-db"]) : baseConnection;
|
|
1647
|
+
out("");
|
|
1648
|
+
out(chalk.bold(" ♻️ Rebase DB Restore"));
|
|
1649
|
+
out(chalk.gray(` Source: ${backupArg}`));
|
|
1650
|
+
out(chalk.gray(` Target: ${targetDb ?? "(from DATABASE_URL)"}`));
|
|
1651
|
+
out("");
|
|
1652
|
+
await checkDatabaseConnectivity(baseConnection);
|
|
1653
|
+
let localFile;
|
|
1654
|
+
let globalsSql = null;
|
|
1655
|
+
let cleanupTemp = false;
|
|
1656
|
+
try {
|
|
1657
|
+
if (/^(s3|gs):\/\//.test(backupArg)) {
|
|
1658
|
+
const storage = await resolveStorageForDestination(parseBackupDestination(backupArg.replace(/\/[^/]+$/, "")), process.env);
|
|
1659
|
+
if (!storage) throw new BackupToolError("Could not resolve storage for the given URL.");
|
|
1660
|
+
const key = backupArg.replace(/^(s3|gs):\/\/[^/]+\//, "");
|
|
1661
|
+
const bucket = backupArg.replace(/^(s3|gs):\/\/([^/]+)\/.*$/, "$2");
|
|
1662
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "rebase-restore-"));
|
|
1663
|
+
localFile = path.join(tmpDir, path.basename(key));
|
|
1664
|
+
cleanupTemp = true;
|
|
1665
|
+
const object = storage.getObjectStream ? await storage.getObjectStream(key, { bucket }) : await storage.getObject(key, bucket).then((file) => file && { stream: file.stream() });
|
|
1666
|
+
if (!object) throw new BackupToolError(`Backup not found in storage: ${backupArg}`);
|
|
1667
|
+
await pipeline(Readable.from(webChunks(object.stream)), fs.createWriteStream(localFile));
|
|
1668
|
+
const globalsObj = await storage.getObject(globalsFileForDump(key), bucket);
|
|
1669
|
+
if (globalsObj) globalsSql = Buffer.from(await globalsObj.arrayBuffer()).toString("utf-8");
|
|
1670
|
+
} else {
|
|
1671
|
+
localFile = path.resolve(invocationDir(), backupArg);
|
|
1672
|
+
if (!fs.existsSync(localFile)) throw new BackupToolError(`Backup file not found: ${localFile}`);
|
|
1673
|
+
const globalsPath = globalsFileForDump(localFile);
|
|
1674
|
+
if (fs.existsSync(globalsPath)) globalsSql = fs.readFileSync(globalsPath, "utf-8");
|
|
1675
|
+
}
|
|
1676
|
+
const pf = await preflight("pg_restore", baseConnection);
|
|
1677
|
+
if (!pf.compatible) throw new BackupToolError(pf.reason ?? "pg_restore cannot restore into this server.");
|
|
1678
|
+
if (args["--create-db"] && !targetDb) throw new BackupToolError("--create-db requires a resolvable target database name (use --target-db).");
|
|
1679
|
+
const objects = await readDumpObjects(localFile);
|
|
1680
|
+
if (!args["--clean"]) {
|
|
1681
|
+
const conflicts = await findRestoreConflicts(targetConnection, objects);
|
|
1682
|
+
if (conflicts.length > 0) {
|
|
1683
|
+
const { message, hint } = describeRestoreConflict(targetDb ?? "the target database", backupArg, conflicts, {
|
|
1684
|
+
targetDb: args["--target-db"],
|
|
1685
|
+
carried: [
|
|
1686
|
+
...args["--database-url"] ? ["--database-url", `"${args["--database-url"]}"`] : [],
|
|
1687
|
+
...args["--docker"] ? ["--docker"] : [],
|
|
1688
|
+
...args["--no-owner"] ? ["--no-owner"] : [],
|
|
1689
|
+
...args["--yes"] ? ["--yes"] : []
|
|
1690
|
+
]
|
|
1691
|
+
});
|
|
1692
|
+
throw new BackupToolError(message, hint);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const replacing = args["--clean"] === true && args["--continue-on-error"] !== true;
|
|
1696
|
+
if (replacing) {
|
|
1697
|
+
if (!resolvePgBinary("psql")) throw new BackupToolError("Could not find the 'psql' binary, which runs a --clean restore as one transaction. Nothing was restored or changed.", "Install the PostgreSQL client tools (they ship psql beside pg_restore), or set PSQL_PATH to its full path.");
|
|
1698
|
+
const extras = describeCleanExtras(objects.schemas, await findCleanExtras(targetConnection, objects));
|
|
1699
|
+
if (extras.length > 0) {
|
|
1700
|
+
outWarn(chalk.yellow(` ⚠️ ${extras[0]}`));
|
|
1701
|
+
for (const extra of extras.slice(1)) outWarn(chalk.yellow(` ${extra}`));
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (!await confirmRestore({
|
|
1705
|
+
target: targetDb ?? "the target database",
|
|
1706
|
+
yes: args["--yes"] === true,
|
|
1707
|
+
interactive: process.stdin.isTTY === true,
|
|
1708
|
+
ask: promptConfirm
|
|
1709
|
+
})) {
|
|
1710
|
+
process.exitCode = 1;
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
if (args["--create-db"]) {
|
|
1714
|
+
const created = await ensureDatabaseExists(baseConnection, targetDb);
|
|
1715
|
+
out(chalk.gray(created ? ` ✓ Created database "${targetDb}".` : ` • Database "${targetDb}" already exists.`));
|
|
1716
|
+
}
|
|
1717
|
+
let rolesApplied = 0;
|
|
1718
|
+
if (globalsSql) {
|
|
1719
|
+
out(chalk.gray(" Recreating cluster roles from the backup's roles sidecar…"));
|
|
1720
|
+
const { applied, skipped } = await applyGlobals(targetConnection, globalsSql, (m) => out(chalk.gray(m)));
|
|
1721
|
+
rolesApplied = applied;
|
|
1722
|
+
out(chalk.gray(` ✓ Roles: ${applied} applied, ${skipped} skipped (already present or not permitted).`));
|
|
1723
|
+
} else {
|
|
1724
|
+
outWarn(chalk.yellow(" ⚠️ No roles sidecar (.globals.sql) accompanies this backup."));
|
|
1725
|
+
outWarn(chalk.yellow(" If the dump grants to roles that don't exist (e.g. rebase_user), the restore"));
|
|
1726
|
+
outWarn(chalk.yellow(" will fail — recreate those roles first, or use a backup that includes its globals."));
|
|
1727
|
+
}
|
|
1728
|
+
try {
|
|
1729
|
+
if (replacing) await restoreDumpReplacing({
|
|
1730
|
+
connectionString: targetConnection,
|
|
1731
|
+
inputFile: localFile,
|
|
1732
|
+
objects,
|
|
1733
|
+
noOwner: args["--no-owner"],
|
|
1734
|
+
inheritStdio: true
|
|
1735
|
+
});
|
|
1736
|
+
else await restoreDump({
|
|
1737
|
+
connectionString: targetConnection,
|
|
1738
|
+
inputFile: localFile,
|
|
1739
|
+
clean: args["--clean"],
|
|
1740
|
+
noOwner: args["--no-owner"],
|
|
1741
|
+
exitOnError: !args["--continue-on-error"],
|
|
1742
|
+
inheritStdio: true
|
|
1743
|
+
});
|
|
1744
|
+
} catch (err) {
|
|
1745
|
+
if (err instanceof BackupToolError) throw err;
|
|
1746
|
+
if (err instanceof CleanRestoreFailed) {
|
|
1747
|
+
const { message, hint } = describeCleanRestoreFailure(targetDb ?? "the target database", err, rolesApplied);
|
|
1748
|
+
throw new BackupToolError(message, hint);
|
|
1749
|
+
}
|
|
1750
|
+
throw new BackupToolError(`pg_restore stopped on the error above. What it restored before that stays in "${targetDb ?? "the target database"}".`, rolesApplied > 0 ? `The ${rolesApplied} cluster role(s) recreated from the backup's sidecar before it stay too.` : void 0);
|
|
1751
|
+
}
|
|
1752
|
+
out("");
|
|
1753
|
+
out(chalk.green(` ✓ Restore completed into "${targetDb ?? "the target database"}".`));
|
|
1754
|
+
out("");
|
|
1755
|
+
} catch (err) {
|
|
1756
|
+
reportError(err);
|
|
1757
|
+
process.exitCode = 1;
|
|
1758
|
+
} finally {
|
|
1759
|
+
if (cleanupTemp && typeof localFile === "string" && fs.existsSync(localFile)) fs.rmSync(path.dirname(localFile), {
|
|
1760
|
+
recursive: true,
|
|
1761
|
+
force: true
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
var BACKUPS_ACTIONS = ["list"];
|
|
1766
|
+
var BACKUPS_LIST_FLAGS = {
|
|
1767
|
+
"--out": String,
|
|
1768
|
+
"--output": "--out",
|
|
1769
|
+
"-o": "--out"
|
|
1770
|
+
};
|
|
1771
|
+
/**
|
|
1772
|
+
* Parse a `rebase db backups list …` line, or its `rebase db backup list …`
|
|
1773
|
+
* spelling: the flags start after `list`, the third word of either.
|
|
1774
|
+
*/
|
|
1775
|
+
function parseBackupsListLine(rawArgs) {
|
|
1776
|
+
return parseCommandFlags(BACKUPS_LIST_FLAGS, rawArgs.slice(3), "db backups list");
|
|
1777
|
+
}
|
|
1778
|
+
/** What `db backups <action>` says of an action it does not have: the sentence every `db` command says. */
|
|
1779
|
+
function unknownBackupsAction(action) {
|
|
1780
|
+
const near = closestWord(action, BACKUPS_ACTIONS);
|
|
1781
|
+
return near ? `Unknown db backups command "${action}" — did you mean \`${near}\`? Run \`rebase db backups --help\` for the rest.` : `Unknown db backups command "${action}". Run \`rebase db backups --help\` for the ones there are.`;
|
|
1782
|
+
}
|
|
1783
|
+
async function backupsCommand(rawArgs) {
|
|
1784
|
+
const action = rawArgs[2];
|
|
1785
|
+
if (!action || action === "--help") {
|
|
1786
|
+
printBackupsHelp();
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (action !== "list") {
|
|
1790
|
+
outError(chalk.red(`✗ ${unknownBackupsAction(action)}`));
|
|
1791
|
+
process.exit(1);
|
|
1792
|
+
}
|
|
1793
|
+
const args = parseBackupsListLine(rawArgs);
|
|
1794
|
+
const out$2 = backupLocation(args["--out"]);
|
|
1795
|
+
const dest = parseBackupDestination(out$2);
|
|
1796
|
+
try {
|
|
1797
|
+
const backups = await listBackups(dest, await resolveStorageForDestination(dest, process.env) ?? void 0);
|
|
1798
|
+
out("");
|
|
1799
|
+
if (backups.length === 0) {
|
|
1800
|
+
out(chalk.gray(` No backups found at ${out$2}.`));
|
|
1801
|
+
await mentionBackendBackups(out$2, args["--out"]);
|
|
1802
|
+
} else {
|
|
1803
|
+
out(chalk.bold(` 💾 ${backups.length} backup(s) at ${out$2}:`));
|
|
1804
|
+
out("");
|
|
1805
|
+
for (const b of backups) {
|
|
1806
|
+
const when = b.createdAt ? b.createdAt.toISOString() : "unknown date";
|
|
1807
|
+
const name = dest.kind === "local" ? path.basename(b.key) : b.key;
|
|
1808
|
+
const empty = b.sizeBytes === 0;
|
|
1809
|
+
const size = b.sizeBytes === void 0 ? "" : ` — ${formatBytes(b.sizeBytes)}`;
|
|
1810
|
+
if (empty) out(` ${chalk.red("○")} ${chalk.bold(name)} ${chalk.gray(`— ${when}`)}${chalk.red(" — EMPTY, not restorable")}`);
|
|
1811
|
+
else out(` ${chalk.green("●")} ${chalk.bold(name)} ${chalk.gray(`— ${when}${size}`)}`);
|
|
1812
|
+
}
|
|
1813
|
+
if (backups.some((b) => b.sizeBytes === 0)) {
|
|
1814
|
+
out("");
|
|
1815
|
+
out(chalk.yellow(" ⚠ Empty files above are leftovers from a failed backup. Delete them:"));
|
|
1816
|
+
out(chalk.gray(" they count as recent backups for retention but restore nothing."));
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
out("");
|
|
1820
|
+
} catch (err) {
|
|
1821
|
+
reportError(err);
|
|
1822
|
+
process.exit(1);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Point at `<backend>/backups` when the default listing is empty and that
|
|
1827
|
+
* directory holds dumps.
|
|
1828
|
+
*
|
|
1829
|
+
* Earlier releases resolved the default there, so after an upgrade the
|
|
1830
|
+
* backups a project already has are one directory away from the listing.
|
|
1831
|
+
*/
|
|
1832
|
+
async function mentionBackendBackups(listed, flag) {
|
|
1833
|
+
if (flag || process.env.BACKUP_DESTINATION?.trim()) return;
|
|
1834
|
+
const backendBackups = path.join(process.cwd(), "backups");
|
|
1835
|
+
if (path.resolve(listed) === backendBackups) return;
|
|
1836
|
+
const found = await listBackups({
|
|
1837
|
+
kind: "local",
|
|
1838
|
+
path: backendBackups
|
|
1839
|
+
}).catch(() => []);
|
|
1840
|
+
if (found.length === 0) return;
|
|
1841
|
+
out(chalk.gray(` ${found.length} backup(s) are in ${backendBackups}, where earlier releases of`));
|
|
1842
|
+
out(chalk.gray(` \`rebase db backup\` wrote by default. List them with:`));
|
|
1843
|
+
out(chalk.gray(` rebase db backups list --out ${path.relative(invocationDir(), backendBackups) || "."}`));
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Verify a freshly written dump; abort the command if it looks corrupt.
|
|
1847
|
+
*
|
|
1848
|
+
* Discards the artifact on the way out. Refusing to *report* success was never
|
|
1849
|
+
* enough on its own: the file stayed on disk, `rebase db backups list` showed
|
|
1850
|
+
* it as an ordinary entry, and retention — which ranks by timestamp and never
|
|
1851
|
+
* looks at size — would protect it as one of the `keepMinimum` newest while
|
|
1852
|
+
* pruning a real backup underneath it. The roles sidecar goes too; the two are
|
|
1853
|
+
* uploaded and pruned as a pair, so half a pair is not a backup either.
|
|
1854
|
+
*/
|
|
1855
|
+
async function assertDumpValid(localFile) {
|
|
1856
|
+
const check = await validateDump(localFile);
|
|
1857
|
+
if (!check.ok) {
|
|
1858
|
+
discardPartialDump(globalsFileForDump(localFile));
|
|
1859
|
+
discardPartialDump(localFile);
|
|
1860
|
+
throw new BackupToolError(`The backup failed validation and was discarded: ${check.reason}`, "The dump was corrupt or truncated, so nothing was kept. Investigate before relying on this destination.");
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Say why a backup command failed, with no password in it.
|
|
1865
|
+
*
|
|
1866
|
+
* What reaches here is often a client tool's failure, and execa's message
|
|
1867
|
+
* repeats the tool's command line. The tools get the password through
|
|
1868
|
+
* `PGPASSWORD` now, so it is not on that line, but a connection string can
|
|
1869
|
+
* still arrive inside a message from elsewhere; this is the last place to
|
|
1870
|
+
* stop it reaching a terminal or a CI log.
|
|
1871
|
+
*/
|
|
1872
|
+
function reportError(err) {
|
|
1873
|
+
if (err instanceof BackupToolError) {
|
|
1874
|
+
outError(chalk.red(` ✗ ${redactConnectionPasswords(err.message)}`));
|
|
1875
|
+
if (err.hint) outError(chalk.gray(` ${redactConnectionPasswords(err.hint)}`));
|
|
1876
|
+
} else outError(chalk.red(` ✗ ${redactConnectionPasswords(err instanceof Error ? err.message : String(err))}`));
|
|
1877
|
+
}
|
|
1878
|
+
function printBackupHelp() {
|
|
1879
|
+
out(`
|
|
1880
|
+
${chalk.bold("rebase db backup")} — Create a database backup (pg_dump, custom format)
|
|
1881
|
+
|
|
1882
|
+
${chalk.green.bold("Usage")}
|
|
1883
|
+
rebase db backup [--out <path|s3://bucket/prefix>] [options]
|
|
1884
|
+
|
|
1885
|
+
${chalk.green.bold("Options")}
|
|
1886
|
+
${chalk.blue("--out, -o")} <dest> Local path or s3://…/gs://… URL (default: ./backups)
|
|
1887
|
+
${chalk.blue("--exclude-schema")} <s> Exclude a schema (repeatable). Never rebase: a backup
|
|
1888
|
+
without it cannot be restored
|
|
1889
|
+
${chalk.blue("--no-owner")} Omit ownership commands from the dump
|
|
1890
|
+
${chalk.blue("--enable-row-security")} Dump as an admin subject instead of failing on RLS
|
|
1891
|
+
${chalk.red("(may produce a partial dump — see below)")}
|
|
1892
|
+
${chalk.blue("--row-security-role")} <r> Role to read as with the flag above (default: admin)
|
|
1893
|
+
|
|
1894
|
+
${chalk.green.bold("Row-level security")}
|
|
1895
|
+
On a managed Postgres the dumping role usually owns nothing and has no
|
|
1896
|
+
BYPASSRLS, so pg_dump refuses:
|
|
1897
|
+
|
|
1898
|
+
ERROR: query would be affected by row-level security policy for table "..."
|
|
1899
|
+
|
|
1900
|
+
That refusal is the safe behaviour. --enable-row-security replaces it by
|
|
1901
|
+
reading as an admin subject: Rebase sets app.uid/app.user_roles so the
|
|
1902
|
+
generated admin_full_access policy admits the dump. The dump then contains
|
|
1903
|
+
exactly what those policies admit ${chalk.red("and no error is raised for what they do not")} —
|
|
1904
|
+
a table whose policies lack an admin rule comes out short, silently.
|
|
1905
|
+
|
|
1906
|
+
Granting the dumping role BYPASSRLS is the option that keeps a backup
|
|
1907
|
+
meaning "every row".
|
|
1908
|
+
|
|
1909
|
+
${chalk.green.bold("Notes")}
|
|
1910
|
+
Backups may contain secrets and PII. Use private storage destinations and
|
|
1911
|
+
enable encryption-at-rest. See docs/backups.md.
|
|
1912
|
+
`);
|
|
1913
|
+
}
|
|
1914
|
+
function printRestoreHelp() {
|
|
1915
|
+
out(`
|
|
1916
|
+
${chalk.bold("rebase db restore")} — Restore a database from a backup (pg_restore)
|
|
1917
|
+
|
|
1918
|
+
${chalk.green.bold("Usage")}
|
|
1919
|
+
rebase db restore <backup> [options]
|
|
1920
|
+
|
|
1921
|
+
${chalk.green.bold("Arguments")}
|
|
1922
|
+
<backup> Local .dump file, or s3://…/gs://… object key
|
|
1923
|
+
|
|
1924
|
+
${chalk.green.bold("Options")}
|
|
1925
|
+
${chalk.blue("--target-db")} <name> Restore into this database instead of DATABASE_URL's
|
|
1926
|
+
${chalk.blue("--create-db")} Create the target database first if it doesn't exist
|
|
1927
|
+
${chalk.blue("--clean")} Replace what the backup holds, in one transaction with
|
|
1928
|
+
the restore (runs the dump through psql)
|
|
1929
|
+
${chalk.blue("--no-owner")} Ignore ownership from the dump
|
|
1930
|
+
${chalk.blue("--continue-on-error")} Log and continue past errors ${chalk.red("(may leave RLS un-enforced!)")}
|
|
1931
|
+
${chalk.blue("--yes, -y")} Skip the interactive confirmation ${chalk.red("(destructive!)")}
|
|
1932
|
+
|
|
1933
|
+
${chalk.red.bold("Warning")}
|
|
1934
|
+
Restore is destructive and never runs automatically. Without --yes it
|
|
1935
|
+
asks for a typed 'yes' on a terminal, and with no terminal to ask on it
|
|
1936
|
+
refuses. Prefer --create-db/--target-db to restore into a fresh database
|
|
1937
|
+
rather than overwriting a live one. Into a database that already holds
|
|
1938
|
+
what the backup creates, it needs --clean, and refuses before changing
|
|
1939
|
+
anything without it. --clean drops each schema the backup creates whole,
|
|
1940
|
+
with what it holds that the backup does not (it names those first), and
|
|
1941
|
+
restores in the same transaction: a restore that stops changes nothing.
|
|
1942
|
+
|
|
1943
|
+
By default the restore aborts on the first error (--exit-on-error) so a
|
|
1944
|
+
skipped GRANT never silently leaves RLS un-enforced. Roles are recreated
|
|
1945
|
+
from the backup's .globals.sql sidecar first; keep that file next to the
|
|
1946
|
+
dump. Use --continue-on-error only when you understand the consequences.
|
|
1947
|
+
`);
|
|
1948
|
+
}
|
|
1949
|
+
function printBackupsHelp() {
|
|
1950
|
+
out(`
|
|
1951
|
+
${chalk.bold("rebase db backups")} — Manage stored backups
|
|
1952
|
+
|
|
1953
|
+
${chalk.green.bold("Usage")}
|
|
1954
|
+
rebase db backups list [--out <path|s3://bucket/prefix>]
|
|
1955
|
+
rebase db backup list [--out <path|s3://bucket/prefix>] ${chalk.gray("(the same thing)")}
|
|
1956
|
+
`);
|
|
1957
|
+
}
|
|
1958
|
+
//#endregion
|
|
1959
|
+
export { buildBackupFilename as A, diagnoseRowSecurityDumpFailure as B, restoreDumpReplacing as C, resolvePgBinary as D, pgToolVariable as E, buildPgRestoreScriptArgs as F, parseDbNameFromUrl as G, joinStorageKey as H, buildPsqlScriptArgs as I, resolveConnectionString as J, parseDumpObjects as K, buildRowSecurityPgOptions as L, buildPgDumpallGlobalsArgs as M, buildPgRestoreArgs as N, selectBackupsToPrune as O, buildPgRestoreListArgs as P, checkToolServerCompatibility as R, restoreDump as S, validateDump as T, parseBackupDestination as U, globalsFileForDump as V, parseBackupTimestamp as W, splitGlobalsStatements as X, serverVersionNumToMajor as Y, withDatabaseName as Z, listBackups as _, BackupToolError as a, readDumpObjects as b, backup_service_exports as c, detectToolMajor as d, discardPartialDump as f, getServerVersionMajor as g, findRestoreConflicts as h, parseRestoreLine as i, buildPgDumpArgs as j, CLEAN_LOCK_TIMEOUT as k, createDump as l, findCleanExtras as m, parseBackupLine as n, CleanRestoreFailed as o, ensureDatabaseExists as p, parsePgToolMajor as q, parseBackupsListLine as r, applyGlobals as s, backup_cli_exports as t, describeUnreadableDump as u, preflight as v, uploadBackup as w, resetSharedSession as x, pruneBackups as y, cleanRestorePrelude as z };
|
|
1193
1960
|
|
|
1194
|
-
//# sourceMappingURL=backup-
|
|
1961
|
+
//# sourceMappingURL=backup-cli-PoRG-zEq.js.map
|