@rebasepro/server-postgres 0.17.3 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -1
- package/README.md +31 -0
- 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-argv.d.ts +19 -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/branch-argv.d.ts +28 -0
- package/dist/branch-prune.d.ts +77 -0
- package/dist/cli-collections-path.d.ts +31 -0
- package/dist/cli-errors-DxN4yzdH.js +657 -0
- package/dist/cli-errors-DxN4yzdH.js.map +1 -0
- package/dist/cli-errors.d.ts +123 -0
- package/dist/cli-flags.d.ts +101 -0
- package/dist/cli-helpers.d.ts +94 -10
- 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-BHgT_TKd.js +276 -0
- package/dist/collection-index-BHgT_TKd.js.map +1 -0
- 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/{connection-GOKU3Hu5.js → databasePoolManager-Bj5FbeAs.js} +112 -40
- package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
- package/dist/databasePoolManager.d.ts +27 -0
- package/dist/doctor-D7HWmGzs.js +974 -0
- package/dist/doctor-D7HWmGzs.js.map +1 -0
- package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-2c2NV8zX.js} +7 -27
- 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/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js} +126 -984
- 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.d.ts +12 -0
- package/dist/index.es.js +7423 -1133
- 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/{backup-service-DCk7KhhL.js → rls-bootstrap-sql-DLDnUjtx.js} +737 -2689
- package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
- package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-DeYEjYLt.js} +65 -190
- package/dist/rls-enforcement-DeYEjYLt.js.map +1 -0
- 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/column-type-drift.d.ts +79 -0
- package/dist/schema/doctor-cli.js +129 -0
- package/dist/schema/doctor-cli.js.map +1 -0
- package/dist/schema/doctor.d.ts +42 -0
- package/dist/schema/ensure-collection-tables.d.ts +90 -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-next-step.d.ts +30 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -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/generated-schema-staleness.d.ts +37 -0
- package/dist/schema/introspect-db-queries.d.ts +34 -0
- package/dist/schema/introspect-db.js +866 -0
- package/dist/schema/introspect-db.js.map +1 -0
- package/dist/security/rls-enforcement.d.ts +63 -2
- package/dist/services/BranchService.d.ts +86 -1
- package/dist/services/collection-helpers.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +30 -2
- package/dist/services/row-pipeline.d.ts +12 -1
- package/dist/source-Br7L7GOI.js +410 -0
- package/dist/source-Br7L7GOI.js.map +1 -0
- package/dist/utils/pg-error-utils.d.ts +3 -1
- package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
- package/dist/vector-index-DkWxm2de.js +309 -0
- package/dist/vector-index-DkWxm2de.js.map +1 -0
- package/package.json +32 -27
- package/dist/backup-service-DCk7KhhL.js.map +0 -1
- package/dist/collection-index-BRUg10H5.js +0 -5690
- package/dist/collection-index-BRUg10H5.js.map +0 -1
- package/dist/connection-GOKU3Hu5.js.map +0 -1
- package/dist/ensure-collection-policies-UCqgv_8c.js.map +0 -1
- package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
- package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
- package/dist/src-DiB5RP2Z.js +0 -541
- package/dist/src-DiB5RP2Z.js.map +0 -1
- package/dist/websocket-BZ4H5wUz.js +0 -8198
- package/dist/websocket-BZ4H5wUz.js.map +0 -1
- package/src/PostgresAdapter.ts +0 -91
- package/src/PostgresBackendDriver.ts +0 -1857
- package/src/PostgresBootstrapper.ts +0 -1157
- package/src/auth/ensure-tables.ts +0 -896
- package/src/auth/schema-version.ts +0 -260
- package/src/auth/services.ts +0 -1610
- package/src/backup/backup-cli.ts +0 -520
- package/src/backup/backup-cron.ts +0 -212
- package/src/backup/backup-logic.ts +0 -93
- package/src/backup/backup-service.ts +0 -485
- package/src/backup/index.ts +0 -12
- package/src/backup/pg-tools.ts +0 -403
- package/src/backup/retention.ts +0 -86
- package/src/cli-errors.ts +0 -265
- package/src/cli-helpers.ts +0 -569
- package/src/cli-output.ts +0 -43
- package/src/cli.ts +0 -1339
- package/src/collections/PostgresCollectionRegistry.ts +0 -108
- package/src/collections/buildRegistry.ts +0 -68
- package/src/collections/validate-relations.ts +0 -402
- package/src/connection.ts +0 -333
- package/src/data-transformer.ts +0 -845
- package/src/databasePoolManager.ts +0 -95
- package/src/history/HistoryService.ts +0 -254
- package/src/history/ensure-history-table.ts +0 -53
- package/src/index.ts +0 -19
- package/src/interfaces.ts +0 -60
- package/src/module-dir.ts +0 -7
- package/src/schema/atlas-argv.ts +0 -94
- package/src/schema/auth-schema.ts +0 -298
- package/src/schema/auth-users-columns.ts +0 -131
- package/src/schema/carved-out-migration.ts +0 -404
- package/src/schema/classify-change.ts +0 -436
- package/src/schema/collection-index.ts +0 -427
- package/src/schema/collections-schema-version.ts +0 -103
- package/src/schema/destructive-sql.ts +0 -94
- package/src/schema/doctor-cli.ts +0 -66
- package/src/schema/doctor-policy-checks.ts +0 -105
- package/src/schema/doctor.ts +0 -837
- package/src/schema/drizzle-ddl.ts +0 -33
- package/src/schema/dynamic-tables.test.ts +0 -302
- package/src/schema/dynamic-tables.ts +0 -293
- package/src/schema/ensure-collection-policies.ts +0 -198
- package/src/schema/ensure-collection-tables.test.ts +0 -518
- package/src/schema/ensure-collection-tables.ts +0 -1267
- package/src/schema/generate-drizzle-schema-logic.ts +0 -957
- package/src/schema/generate-drizzle-schema.ts +0 -143
- package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
- package/src/schema/generate-postgres-ddl.ts +0 -180
- package/src/schema/generate-schema-commit.ts +0 -267
- package/src/schema/generated-schema-staleness.ts +0 -284
- package/src/schema/introspect-db-constraints.ts +0 -385
- package/src/schema/introspect-db-inference.ts +0 -255
- package/src/schema/introspect-db-logic.ts +0 -1387
- package/src/schema/introspect-db-naming.ts +0 -15
- package/src/schema/introspect-db-project.ts +0 -78
- package/src/schema/introspect-db-queries.ts +0 -326
- package/src/schema/introspect-db-structure.ts +0 -670
- package/src/schema/introspect-db-types.ts +0 -56
- package/src/schema/introspect-db.ts +0 -257
- package/src/schema/introspect-runtime.test.ts +0 -260
- package/src/schema/introspect-runtime.ts +0 -327
- package/src/schema/non-sql-collections.test.ts +0 -131
- package/src/schema/rls-bootstrap-sql.ts +0 -288
- package/src/schema/search-column.ts +0 -643
- package/src/schema/test-schema.ts +0 -11
- package/src/schema/vector-index.ts +0 -491
- package/src/security/anonymous-grants.test.ts +0 -73
- package/src/security/policy-drift.test.ts +0 -435
- package/src/security/policy-drift.ts +0 -473
- package/src/security/rls-enforcement.ts +0 -505
- package/src/services/BranchService.ts +0 -326
- package/src/services/FetchService.ts +0 -2115
- package/src/services/PersistService.ts +0 -557
- package/src/services/RelationService.ts +0 -978
- package/src/services/RelationWriteService.ts +0 -653
- package/src/services/cdc/CdcListener.ts +0 -103
- package/src/services/cdc/junction-tables.ts +0 -91
- package/src/services/cdc/trigger-cdc.ts +0 -173
- package/src/services/channel-bus/ChannelBus.ts +0 -44
- package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
- package/src/services/channel-bus/index.ts +0 -114
- package/src/services/channel-history.ts +0 -424
- package/src/services/channel-presence.ts +0 -185
- package/src/services/collection-helpers.ts +0 -420
- package/src/services/dataService.ts +0 -256
- package/src/services/index.ts +0 -14
- package/src/services/junction-writes.ts +0 -295
- package/src/services/nested-path.ts +0 -145
- package/src/services/pg-notify-listener.ts +0 -151
- package/src/services/realtimeService.ts +0 -2545
- package/src/services/row-pipeline.ts +0 -240
- package/src/services/write-denial.ts +0 -55
- package/src/types.ts +0 -4
- package/src/utils/connection-string.ts +0 -58
- package/src/utils/drizzle-conditions.ts +0 -2743
- package/src/utils/pg-array-null-patch.ts +0 -42
- package/src/utils/pg-error-utils.ts +0 -391
- package/src/websocket.ts +0 -855
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backup / restore orchestration: thin, well-typed wrappers around
|
|
3
|
-
* `pg_dump` and `pg_restore`, plus binary detection, version pre-flight,
|
|
4
|
-
* and upload/prune against a storage backend.
|
|
5
|
-
*
|
|
6
|
-
* The pure argument- and policy-logic lives in `pg-tools.ts` and
|
|
7
|
-
* `retention.ts`; this module is the impure edge (spawns processes, talks
|
|
8
|
-
* to Postgres and storage) and is intentionally kept small.
|
|
9
|
-
*/
|
|
10
|
-
import fs from "fs";
|
|
11
|
-
import os from "os";
|
|
12
|
-
import path from "path";
|
|
13
|
-
import { execa } from "execa";
|
|
14
|
-
import type { StorageController } from "@rebasepro/server";
|
|
15
|
-
import { resolveLocalBin } from "../cli-helpers";
|
|
16
|
-
import {
|
|
17
|
-
BackupDestination,
|
|
18
|
-
buildBackupFilename,
|
|
19
|
-
buildPgDumpArgs,
|
|
20
|
-
buildPgDumpallGlobalsArgs,
|
|
21
|
-
buildRowSecurityPgOptions,
|
|
22
|
-
diagnoseRowSecurityDumpFailure,
|
|
23
|
-
type RowSecurityIdentity,
|
|
24
|
-
buildPgRestoreArgs,
|
|
25
|
-
buildPgRestoreListArgs,
|
|
26
|
-
checkToolServerCompatibility,
|
|
27
|
-
globalsFileForDump,
|
|
28
|
-
joinStorageKey,
|
|
29
|
-
parsePgToolMajor,
|
|
30
|
-
parseBackupTimestamp,
|
|
31
|
-
serverVersionNumToMajor,
|
|
32
|
-
VersionCompatibility
|
|
33
|
-
} from "./pg-tools";
|
|
34
|
-
import { BackupObject, RetentionOptions, selectBackupsToPrune } from "./retention";
|
|
35
|
-
import { applyGlobalsWith, discardPartialDumpWith, pruneWith } from "./backup-logic";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Remove a dump artifact abandoned by a failed tool run. See
|
|
39
|
-
* {@link discardPartialDumpWith} for why this exists and why the logic lives
|
|
40
|
-
* in the execa-free module.
|
|
41
|
-
*/
|
|
42
|
-
export function discardPartialDump(file: string): void {
|
|
43
|
-
discardPartialDumpWith(
|
|
44
|
-
(f) => fs.existsSync(f),
|
|
45
|
-
(f) => fs.unlinkSync(f),
|
|
46
|
-
file
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export class BackupToolError extends Error {
|
|
51
|
-
constructor(message: string, readonly hint?: string) {
|
|
52
|
-
super(message);
|
|
53
|
-
this.name = "BackupToolError";
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */
|
|
58
|
-
export function resolvePgBinary(
|
|
59
|
-
tool: "pg_dump" | "pg_restore" | "pg_dumpall",
|
|
60
|
-
env: Record<string, string | undefined> = process.env
|
|
61
|
-
): string | null {
|
|
62
|
-
const overrideVar =
|
|
63
|
-
tool === "pg_dump" ? env.PG_DUMP_PATH
|
|
64
|
-
: tool === "pg_restore" ? env.PG_RESTORE_PATH
|
|
65
|
-
: env.PG_DUMPALL_PATH;
|
|
66
|
-
if (overrideVar && fs.existsSync(overrideVar)) return overrideVar;
|
|
67
|
-
return resolveLocalBin(tool);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Run `<bin> --version` and extract the major version. */
|
|
71
|
-
export async function detectToolMajor(bin: string): Promise<number | null> {
|
|
72
|
-
try {
|
|
73
|
-
const { stdout } = await execa(bin, ["--version"]);
|
|
74
|
-
return parsePgToolMajor(stdout);
|
|
75
|
-
} catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Query the server for its major version via `server_version_num`. */
|
|
81
|
-
export async function getServerVersionMajor(connectionString: string): Promise<number | null> {
|
|
82
|
-
const { Client } = await import("pg");
|
|
83
|
-
const client = new Client({ connectionString });
|
|
84
|
-
await client.connect();
|
|
85
|
-
try {
|
|
86
|
-
const res = await client.query("SHOW server_version_num");
|
|
87
|
-
const raw = res.rows?.[0]?.server_version_num;
|
|
88
|
-
return serverVersionNumToMajor(raw);
|
|
89
|
-
} finally {
|
|
90
|
-
await client.end();
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export interface PreflightResult extends VersionCompatibility {
|
|
95
|
-
bin: string;
|
|
96
|
-
toolMajor: number | null;
|
|
97
|
-
serverMajor: number | null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Verify the requested client tool exists and its major version is
|
|
102
|
-
* compatible with the live server. Throws {@link BackupToolError} — with a
|
|
103
|
-
* doctor-style hint — when the binary is missing.
|
|
104
|
-
*/
|
|
105
|
-
export async function preflight(
|
|
106
|
-
tool: "pg_dump" | "pg_restore",
|
|
107
|
-
connectionString: string,
|
|
108
|
-
env: Record<string, string | undefined> = process.env
|
|
109
|
-
): Promise<PreflightResult> {
|
|
110
|
-
const bin = resolvePgBinary(tool, env);
|
|
111
|
-
if (!bin) {
|
|
112
|
-
throw new BackupToolError(
|
|
113
|
-
`Could not find the '${tool}' binary.`,
|
|
114
|
-
`Install the PostgreSQL client tools (e.g. 'brew install libpq' or 'apt-get install postgresql-client'), ` +
|
|
115
|
-
`or set ${tool === "pg_dump" ? "PG_DUMP_PATH" : "PG_RESTORE_PATH"} to its full path.`
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
const [toolMajor, serverMajor] = await Promise.all([
|
|
119
|
-
detectToolMajor(bin),
|
|
120
|
-
getServerVersionMajor(connectionString)
|
|
121
|
-
]);
|
|
122
|
-
const compat = checkToolServerCompatibility(toolMajor, serverMajor);
|
|
123
|
-
return { bin, toolMajor, serverMajor, ...compat };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export interface BackupResult {
|
|
127
|
-
/** Absolute path of the produced dump file on local disk. */
|
|
128
|
-
localFile: string;
|
|
129
|
-
fileName: string;
|
|
130
|
-
sizeBytes: number;
|
|
131
|
-
/**
|
|
132
|
-
* Absolute path of the `.globals.sql` sidecar holding cluster-wide roles
|
|
133
|
-
* (present unless globals capture was disabled or unavailable).
|
|
134
|
-
*/
|
|
135
|
-
globalsFile?: string;
|
|
136
|
-
globalsSizeBytes?: number;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Produce a custom-format dump on local disk. When `outDir` is omitted the
|
|
141
|
-
* file is written to the OS temp directory (used by the upload path, which
|
|
142
|
-
* cleans it up afterwards).
|
|
143
|
-
*
|
|
144
|
-
* Alongside the `-Fc` dump it writes a `<name>.globals.sql` sidecar via
|
|
145
|
-
* `pg_dumpall --globals-only` so the roles the dump's GRANT/RLS statements
|
|
146
|
-
* depend on can be recreated on restore. Set `includeGlobals: false` to skip
|
|
147
|
-
* it (e.g. when the caller has no privilege to read cluster globals).
|
|
148
|
-
*/
|
|
149
|
-
export async function createDump(opts: {
|
|
150
|
-
connectionString: string;
|
|
151
|
-
dbName: string;
|
|
152
|
-
outDir?: string;
|
|
153
|
-
fileName?: string;
|
|
154
|
-
excludeSchemas?: string[];
|
|
155
|
-
noOwner?: boolean;
|
|
156
|
-
inheritStdio?: boolean;
|
|
157
|
-
includeGlobals?: boolean;
|
|
158
|
-
env?: Record<string, string | undefined>;
|
|
159
|
-
/**
|
|
160
|
-
* Dump with row security left on, reading as this identity.
|
|
161
|
-
*
|
|
162
|
-
* The escape hatch for a managed Postgres, where the dumping role owns
|
|
163
|
-
* nothing and has no `BYPASSRLS`. Off by default, and deliberately so: with
|
|
164
|
-
* row security on, `pg_dump` stops erroring on rows it cannot see and
|
|
165
|
-
* simply omits them. See {@link RowSecurityIdentity}.
|
|
166
|
-
*/
|
|
167
|
-
rowSecurity?: RowSecurityIdentity;
|
|
168
|
-
}): Promise<BackupResult> {
|
|
169
|
-
const env = opts.env ?? process.env;
|
|
170
|
-
const bin = resolvePgBinary("pg_dump", env);
|
|
171
|
-
if (!bin) {
|
|
172
|
-
throw new BackupToolError(
|
|
173
|
-
"Could not find the 'pg_dump' binary.",
|
|
174
|
-
"Install the PostgreSQL client tools or set PG_DUMP_PATH."
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const fileName = opts.fileName ?? buildBackupFilename(opts.dbName);
|
|
179
|
-
const outDir = opts.outDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "rebase-backup-"));
|
|
180
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
181
|
-
const localFile = path.join(outDir, fileName);
|
|
182
|
-
|
|
183
|
-
const args = buildPgDumpArgs({
|
|
184
|
-
connectionString: opts.connectionString,
|
|
185
|
-
outFile: localFile,
|
|
186
|
-
excludeSchemas: opts.excludeSchemas,
|
|
187
|
-
noOwner: opts.noOwner,
|
|
188
|
-
rowSecurity: opts.rowSecurity
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// `PGOPTIONS` is the only way to set a GUC on a tool that takes no SQL.
|
|
192
|
-
// Built from the same object that added `--enable-row-security`, so the
|
|
193
|
-
// flag cannot travel without the identity that makes it safe.
|
|
194
|
-
const dumpEnv: Record<string, string> = { ...(env as Record<string, string>) };
|
|
195
|
-
if (opts.rowSecurity) {
|
|
196
|
-
dumpEnv.PGOPTIONS = [env.PGOPTIONS, buildRowSecurityPgOptions(opts.rowSecurity)]
|
|
197
|
-
.filter(Boolean).join(" ");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
await execa(bin, args, {
|
|
202
|
-
stdio: opts.inheritStdio ? "inherit" : "pipe",
|
|
203
|
-
env: dumpEnv
|
|
204
|
-
});
|
|
205
|
-
} catch (error) {
|
|
206
|
-
// pg_dump creates its `--file=` target before it finishes connecting,
|
|
207
|
-
// so any failure here — a URL libpq rejects, a dropped connection, a
|
|
208
|
-
// full disk — leaves a 0-byte file behind that nothing else removed.
|
|
209
|
-
// That corpse is not inert: `rebase db backups list` shows it as an
|
|
210
|
-
// ordinary entry, and `selectBackupsToPrune` ranks by timestamp alone,
|
|
211
|
-
// so it occupies a protected `keepMinimum` slot and can push a real
|
|
212
|
-
// backup out of retention. A missing backup is honest; an empty file
|
|
213
|
-
// that reads as a backup is not.
|
|
214
|
-
discardPartialDump(localFile);
|
|
215
|
-
// The RLS failure names a table and no cause. Replace it with the
|
|
216
|
-
// cause and the two ways out; anything else is re-thrown untouched.
|
|
217
|
-
const diagnosis = diagnoseRowSecurityDumpFailure(error);
|
|
218
|
-
if (!diagnosis) throw error;
|
|
219
|
-
throw new BackupToolError(
|
|
220
|
-
diagnosis,
|
|
221
|
-
"Run `rebase db backup --help` for the flag, and read what it says about partial dumps."
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const sizeBytes = fs.existsSync(localFile) ? fs.statSync(localFile).size : 0;
|
|
226
|
-
|
|
227
|
-
const result: BackupResult = { localFile, fileName, sizeBytes };
|
|
228
|
-
|
|
229
|
-
if (opts.includeGlobals !== false) {
|
|
230
|
-
const dumpallBin = resolvePgBinary("pg_dumpall", env);
|
|
231
|
-
if (!dumpallBin) {
|
|
232
|
-
throw new BackupToolError(
|
|
233
|
-
"Could not find the 'pg_dumpall' binary needed to capture cluster roles.",
|
|
234
|
-
"Install the PostgreSQL client tools or set PG_DUMPALL_PATH. " +
|
|
235
|
-
"To take a role-incomplete backup anyway, pass includeGlobals: false."
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
const globalsFile = globalsFileForDump(localFile);
|
|
239
|
-
try {
|
|
240
|
-
await execa(
|
|
241
|
-
dumpallBin,
|
|
242
|
-
buildPgDumpallGlobalsArgs({ connectionString: opts.connectionString, outFile: globalsFile }),
|
|
243
|
-
{ stdio: opts.inheritStdio ? "inherit" : "pipe", env: { ...(env as Record<string, string>) } }
|
|
244
|
-
);
|
|
245
|
-
} catch (error) {
|
|
246
|
-
// Same reasoning as the pg_dump catch above, and the dump goes with
|
|
247
|
-
// it: the pair is uploaded and pruned together, so a dump whose
|
|
248
|
-
// roles sidecar is missing restores without the roles its GRANT and
|
|
249
|
-
// RLS statements need — the exact failure the sidecar exists to
|
|
250
|
-
// prevent. Leaving half a pair on disk would look like a backup.
|
|
251
|
-
discardPartialDump(globalsFile);
|
|
252
|
-
discardPartialDump(localFile);
|
|
253
|
-
throw error;
|
|
254
|
-
}
|
|
255
|
-
result.globalsFile = globalsFile;
|
|
256
|
-
result.globalsSizeBytes = fs.existsSync(globalsFile) ? fs.statSync(globalsFile).size : 0;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return result;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Cheap integrity check on a freshly written dump: it must be non-empty and
|
|
264
|
-
* `pg_restore --list` must parse its table of contents without error. Used
|
|
265
|
-
* before pruning older backups so a corrupt-but-exit-0 dump never becomes
|
|
266
|
-
* the reason the last good backup is deleted.
|
|
267
|
-
*/
|
|
268
|
-
export async function validateDump(
|
|
269
|
-
localFile: string,
|
|
270
|
-
env: Record<string, string | undefined> = process.env
|
|
271
|
-
): Promise<{ ok: boolean; reason?: string }> {
|
|
272
|
-
if (!fs.existsSync(localFile)) {
|
|
273
|
-
return { ok: false, reason: `Dump file does not exist: ${localFile}` };
|
|
274
|
-
}
|
|
275
|
-
if (fs.statSync(localFile).size === 0) {
|
|
276
|
-
return { ok: false, reason: "Dump file is empty (0 bytes)." };
|
|
277
|
-
}
|
|
278
|
-
const bin = resolvePgBinary("pg_restore", env);
|
|
279
|
-
if (!bin) {
|
|
280
|
-
// Can't verify without pg_restore; treat as inconclusive-but-fail so
|
|
281
|
-
// pruning doesn't proceed on an unverified dump.
|
|
282
|
-
return { ok: false, reason: "Could not find 'pg_restore' to verify the dump." };
|
|
283
|
-
}
|
|
284
|
-
try {
|
|
285
|
-
await execa(bin, buildPgRestoreListArgs(localFile), {
|
|
286
|
-
stdio: "pipe",
|
|
287
|
-
env: { ...(env as Record<string, string>) }
|
|
288
|
-
});
|
|
289
|
-
return { ok: true };
|
|
290
|
-
} catch (err) {
|
|
291
|
-
return { ok: false, reason: `pg_restore --list failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Replay a `pg_dumpall --globals-only` script to recreate cluster roles
|
|
297
|
-
* before a restore, so the dump's GRANT/RLS statements (which reference
|
|
298
|
-
* `rebase_user` and any owner roles) actually apply. Runs statement by
|
|
299
|
-
* statement and tolerates per-statement failures — on a same-cluster restore
|
|
300
|
-
* the roles usually already exist (`CREATE ROLE` → "already exists"), and on
|
|
301
|
-
* a managed provider an `ALTER ROLE <superuser>` may be refused; neither
|
|
302
|
-
* should abort role recreation. Returns how many statements applied vs were
|
|
303
|
-
* skipped.
|
|
304
|
-
*/
|
|
305
|
-
export async function applyGlobals(
|
|
306
|
-
connectionString: string,
|
|
307
|
-
globalsSql: string,
|
|
308
|
-
log: (message: string) => void = () => {}
|
|
309
|
-
): Promise<{ applied: number; skipped: number }> {
|
|
310
|
-
const { Client } = await import("pg");
|
|
311
|
-
const client = new Client({ connectionString });
|
|
312
|
-
await client.connect();
|
|
313
|
-
try {
|
|
314
|
-
return await applyGlobalsWith(
|
|
315
|
-
async (sql) => { await client.query(sql); },
|
|
316
|
-
globalsSql,
|
|
317
|
-
log
|
|
318
|
-
);
|
|
319
|
-
} finally {
|
|
320
|
-
await client.end();
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Restore a custom-format dump into the database named by
|
|
326
|
-
* `connectionString`. Destructive when `clean` is set (drops objects
|
|
327
|
-
* first). Never called automatically — the CLI gates it behind explicit
|
|
328
|
-
* confirmation.
|
|
329
|
-
*
|
|
330
|
-
* Runs with `--exit-on-error` by default: a restore that logs-and-continues
|
|
331
|
-
* past a failed GRANT (because a role was missing) reports success with RLS
|
|
332
|
-
* un-enforced. Callers should recreate roles first (see {@link applyGlobals})
|
|
333
|
-
* and only set `exitOnError: false` deliberately.
|
|
334
|
-
*/
|
|
335
|
-
export async function restoreDump(opts: {
|
|
336
|
-
connectionString: string;
|
|
337
|
-
inputFile: string;
|
|
338
|
-
clean?: boolean;
|
|
339
|
-
noOwner?: boolean;
|
|
340
|
-
exitOnError?: boolean;
|
|
341
|
-
inheritStdio?: boolean;
|
|
342
|
-
env?: Record<string, string | undefined>;
|
|
343
|
-
}): Promise<void> {
|
|
344
|
-
const env = opts.env ?? process.env;
|
|
345
|
-
const bin = resolvePgBinary("pg_restore", env);
|
|
346
|
-
if (!bin) {
|
|
347
|
-
throw new BackupToolError(
|
|
348
|
-
"Could not find the 'pg_restore' binary.",
|
|
349
|
-
"Install the PostgreSQL client tools or set PG_RESTORE_PATH."
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
const args = buildPgRestoreArgs({
|
|
353
|
-
connectionString: opts.connectionString,
|
|
354
|
-
inputFile: opts.inputFile,
|
|
355
|
-
clean: opts.clean,
|
|
356
|
-
noOwner: opts.noOwner,
|
|
357
|
-
exitOnError: opts.exitOnError
|
|
358
|
-
});
|
|
359
|
-
await execa(bin, args, {
|
|
360
|
-
stdio: opts.inheritStdio ? "inherit" : "pipe",
|
|
361
|
-
env: { ...(env as Record<string, string>) }
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Create a database (if absent) by connecting to the maintenance
|
|
367
|
-
* `postgres` database. Used by `restore --create-db`.
|
|
368
|
-
*/
|
|
369
|
-
export async function ensureDatabaseExists(
|
|
370
|
-
adminConnectionString: string,
|
|
371
|
-
dbName: string
|
|
372
|
-
): Promise<boolean> {
|
|
373
|
-
const { Client } = await import("pg");
|
|
374
|
-
const parsed = new URL(adminConnectionString);
|
|
375
|
-
parsed.pathname = "/postgres";
|
|
376
|
-
const client = new Client({ connectionString: parsed.toString() });
|
|
377
|
-
await client.connect();
|
|
378
|
-
try {
|
|
379
|
-
const existing = await client.query("SELECT 1 FROM pg_database WHERE datname = $1", [dbName]);
|
|
380
|
-
if (existing.rowCount && existing.rowCount > 0) {
|
|
381
|
-
return false;
|
|
382
|
-
}
|
|
383
|
-
// Identifier can't be parameterised; guard against injection.
|
|
384
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(dbName)) {
|
|
385
|
-
throw new BackupToolError(`Invalid database name: "${dbName}"`);
|
|
386
|
-
}
|
|
387
|
-
await client.query(`CREATE DATABASE "${dbName}"`);
|
|
388
|
-
return true;
|
|
389
|
-
} finally {
|
|
390
|
-
await client.end();
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Upload a local dump to object storage under the destination's prefix.
|
|
396
|
-
* Backups may contain secrets/PII, so the object is written with a
|
|
397
|
-
* private/octet-stream content type; bucket-level ACLs must stay private.
|
|
398
|
-
*/
|
|
399
|
-
export async function uploadBackup(
|
|
400
|
-
storage: StorageController,
|
|
401
|
-
localFile: string,
|
|
402
|
-
dest: Extract<BackupDestination, { kind: "s3" | "gcs" }>
|
|
403
|
-
): Promise<{ key: string; storageUrl: string }> {
|
|
404
|
-
const fileName = path.basename(localFile);
|
|
405
|
-
const key = joinStorageKey(dest.prefix, fileName);
|
|
406
|
-
const buffer = fs.readFileSync(localFile);
|
|
407
|
-
const file = new File([buffer], fileName, { type: "application/octet-stream" });
|
|
408
|
-
const result = await storage.putObject({
|
|
409
|
-
file,
|
|
410
|
-
key,
|
|
411
|
-
bucket: dest.bucket,
|
|
412
|
-
metadata: { "rebase-backup": "1" }
|
|
413
|
-
});
|
|
414
|
-
return { key, storageUrl: result.storageUrl };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* List existing backups at a destination. For local destinations reads the
|
|
419
|
-
* directory; for object storage lists the prefix via the controller.
|
|
420
|
-
*/
|
|
421
|
-
export async function listBackups(
|
|
422
|
-
dest: BackupDestination,
|
|
423
|
-
storage?: StorageController
|
|
424
|
-
): Promise<BackupObject[]> {
|
|
425
|
-
if (dest.kind === "local") {
|
|
426
|
-
if (!fs.existsSync(dest.path)) return [];
|
|
427
|
-
const stat = fs.statSync(dest.path);
|
|
428
|
-
const dir = stat.isDirectory() ? dest.path : path.dirname(dest.path);
|
|
429
|
-
return fs
|
|
430
|
-
.readdirSync(dir)
|
|
431
|
-
.filter((f) => f.endsWith(".dump"))
|
|
432
|
-
.map((f) => {
|
|
433
|
-
const full = path.join(dir, f);
|
|
434
|
-
const stats = fs.statSync(full);
|
|
435
|
-
const createdAt = parseBackupTimestamp(f) ?? stats.mtime;
|
|
436
|
-
return { key: full, createdAt, sizeBytes: stats.size };
|
|
437
|
-
})
|
|
438
|
-
.sort((a, b) => (b.createdAt?.getTime() ?? 0) - (a.createdAt?.getTime() ?? 0));
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (!storage) {
|
|
442
|
-
throw new BackupToolError(
|
|
443
|
-
`Listing ${dest.kind} backups requires a configured storage backend.`
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
const result = await storage.listObjects(dest.prefix ? `${dest.prefix}/` : "", {
|
|
447
|
-
bucket: dest.bucket,
|
|
448
|
-
maxResults: 1000
|
|
449
|
-
});
|
|
450
|
-
return result.items
|
|
451
|
-
.map((item) => item.fullPath)
|
|
452
|
-
.filter((key) => key.endsWith(".dump"))
|
|
453
|
-
.map((key) => ({ key, createdAt: parseBackupTimestamp(key) ?? undefined }))
|
|
454
|
-
.sort((a, b) => (b.createdAt?.getTime() ?? 0) - (a.createdAt?.getTime() ?? 0));
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Apply a retention policy to a destination, deleting the backups selected
|
|
459
|
-
* by {@link selectBackupsToPrune}. Returns the keys that were removed.
|
|
460
|
-
*/
|
|
461
|
-
export async function pruneBackups(
|
|
462
|
-
dest: BackupDestination,
|
|
463
|
-
options: RetentionOptions,
|
|
464
|
-
storage?: StorageController
|
|
465
|
-
): Promise<string[]> {
|
|
466
|
-
const backups = await listBackups(dest, storage);
|
|
467
|
-
const toDelete = selectBackupsToPrune(backups, options);
|
|
468
|
-
|
|
469
|
-
// Delete each dump together with its `.globals.sql` sidecar (pruneWith
|
|
470
|
-
// handles the pairing + best-effort sidecar). The deleter rejects on a
|
|
471
|
-
// missing key so pruneWith can swallow an absent sidecar.
|
|
472
|
-
const deleteObject =
|
|
473
|
-
dest.kind === "local"
|
|
474
|
-
? async (key: string) => {
|
|
475
|
-
if (!fs.existsSync(key)) throw new Error(`not found: ${key}`);
|
|
476
|
-
fs.unlinkSync(key);
|
|
477
|
-
}
|
|
478
|
-
: async (key: string) => {
|
|
479
|
-
if (!storage) throw new BackupToolError("Storage backend required to prune object backups.");
|
|
480
|
-
await storage.deleteObject(key, dest.bucket);
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
await pruneWith(toDelete, deleteObject);
|
|
484
|
-
return toDelete;
|
|
485
|
-
}
|
package/src/backup/index.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backup / restore for self-hosted rebase.
|
|
3
|
-
*
|
|
4
|
-
* - `pg-tools` — pure argument/version/path helpers (unit-tested)
|
|
5
|
-
* - `retention` — pure retention-pruning policy (unit-tested)
|
|
6
|
-
* - `backup-service`— pg_dump/pg_restore + storage orchestration
|
|
7
|
-
* - `backup-cron` — scheduled backups for the server cron system
|
|
8
|
-
*/
|
|
9
|
-
export * from "./pg-tools";
|
|
10
|
-
export * from "./retention";
|
|
11
|
-
export * from "./backup-service";
|
|
12
|
-
export * from "./backup-cron";
|