@rebasepro/server-postgres 0.17.3-canary.gdd23447 → 0.18.0

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