@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.
Files changed (204) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/BranchService-ByLfM_Tt.js +360 -0
  4. package/dist/BranchService-ByLfM_Tt.js.map +1 -0
  5. package/dist/__vite-browser-external-BnuHet1e.js +46 -0
  6. package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
  7. package/dist/arg-Dni7MzLB.js +87 -0
  8. package/dist/arg-Dni7MzLB.js.map +1 -0
  9. package/dist/auth-users-columns-D2LBFrMH.js +116 -0
  10. package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
  11. package/dist/backup-argv.d.ts +19 -0
  12. package/dist/backup-cli-oM9WQVlV.js +408 -0
  13. package/dist/backup-cli-oM9WQVlV.js.map +1 -0
  14. package/dist/backup-service-Br3tqwsy.js +715 -0
  15. package/dist/backup-service-Br3tqwsy.js.map +1 -0
  16. package/dist/branch-argv.d.ts +28 -0
  17. package/dist/branch-prune.d.ts +77 -0
  18. package/dist/cli-collections-path.d.ts +31 -0
  19. package/dist/cli-errors-DxN4yzdH.js +657 -0
  20. package/dist/cli-errors-DxN4yzdH.js.map +1 -0
  21. package/dist/cli-errors.d.ts +123 -0
  22. package/dist/cli-flags.d.ts +101 -0
  23. package/dist/cli-helpers.d.ts +94 -10
  24. package/dist/cli-output-CNdMql-L.js +47 -0
  25. package/dist/cli-output-CNdMql-L.js.map +1 -0
  26. package/dist/cli.js +2020 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/collection-index-BHgT_TKd.js +276 -0
  29. package/dist/collection-index-BHgT_TKd.js.map +1 -0
  30. package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
  31. package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
  32. package/dist/{connection-GOKU3Hu5.js → databasePoolManager-Bj5FbeAs.js} +112 -40
  33. package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
  34. package/dist/databasePoolManager.d.ts +27 -0
  35. package/dist/doctor-D7HWmGzs.js +974 -0
  36. package/dist/doctor-D7HWmGzs.js.map +1 -0
  37. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-2c2NV8zX.js} +7 -27
  38. package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
  39. package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
  40. package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
  41. package/dist/ensure-tables-CIojK-Rx.js +650 -0
  42. package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
  43. package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
  44. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
  45. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
  46. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
  47. package/dist/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js} +126 -984
  48. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
  49. package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
  50. package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
  51. package/dist/index.d.ts +12 -0
  52. package/dist/index.es.js +7423 -1133
  53. package/dist/index.es.js.map +1 -1
  54. package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
  55. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
  56. package/dist/policy-drift-kNJ6X2Uj.js +313 -0
  57. package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
  58. package/dist/{backup-service-DCk7KhhL.js → rls-bootstrap-sql-DLDnUjtx.js} +737 -2689
  59. package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
  60. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-DeYEjYLt.js} +65 -190
  61. package/dist/rls-enforcement-DeYEjYLt.js.map +1 -0
  62. package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
  63. package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
  64. package/dist/schema/column-type-drift.d.ts +79 -0
  65. package/dist/schema/doctor-cli.js +129 -0
  66. package/dist/schema/doctor-cli.js.map +1 -0
  67. package/dist/schema/doctor.d.ts +42 -0
  68. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  69. package/dist/schema/generate-drizzle-schema.d.ts +1 -1
  70. package/dist/schema/generate-drizzle-schema.js +24 -0
  71. package/dist/schema/generate-drizzle-schema.js.map +1 -0
  72. package/dist/schema/generate-next-step.d.ts +30 -0
  73. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  74. package/dist/schema/generate-postgres-ddl.d.ts +1 -1
  75. package/dist/schema/generate-postgres-ddl.js +138 -0
  76. package/dist/schema/generate-postgres-ddl.js.map +1 -0
  77. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  78. package/dist/schema/introspect-db-queries.d.ts +34 -0
  79. package/dist/schema/introspect-db.js +866 -0
  80. package/dist/schema/introspect-db.js.map +1 -0
  81. package/dist/security/rls-enforcement.d.ts +63 -2
  82. package/dist/services/BranchService.d.ts +86 -1
  83. package/dist/services/collection-helpers.d.ts +1 -1
  84. package/dist/services/realtimeService.d.ts +30 -2
  85. package/dist/services/row-pipeline.d.ts +12 -1
  86. package/dist/source-Br7L7GOI.js +410 -0
  87. package/dist/source-Br7L7GOI.js.map +1 -0
  88. package/dist/utils/pg-error-utils.d.ts +3 -1
  89. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  90. package/dist/vector-index-DkWxm2de.js +309 -0
  91. package/dist/vector-index-DkWxm2de.js.map +1 -0
  92. package/package.json +32 -27
  93. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  94. package/dist/collection-index-BRUg10H5.js +0 -5690
  95. package/dist/collection-index-BRUg10H5.js.map +0 -1
  96. package/dist/connection-GOKU3Hu5.js.map +0 -1
  97. package/dist/ensure-collection-policies-UCqgv_8c.js.map +0 -1
  98. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  99. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  100. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  101. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  102. package/dist/src-DiB5RP2Z.js +0 -541
  103. package/dist/src-DiB5RP2Z.js.map +0 -1
  104. package/dist/websocket-BZ4H5wUz.js +0 -8198
  105. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  106. package/src/PostgresAdapter.ts +0 -91
  107. package/src/PostgresBackendDriver.ts +0 -1857
  108. package/src/PostgresBootstrapper.ts +0 -1157
  109. package/src/auth/ensure-tables.ts +0 -896
  110. package/src/auth/schema-version.ts +0 -260
  111. package/src/auth/services.ts +0 -1610
  112. package/src/backup/backup-cli.ts +0 -520
  113. package/src/backup/backup-cron.ts +0 -212
  114. package/src/backup/backup-logic.ts +0 -93
  115. package/src/backup/backup-service.ts +0 -485
  116. package/src/backup/index.ts +0 -12
  117. package/src/backup/pg-tools.ts +0 -403
  118. package/src/backup/retention.ts +0 -86
  119. package/src/cli-errors.ts +0 -265
  120. package/src/cli-helpers.ts +0 -569
  121. package/src/cli-output.ts +0 -43
  122. package/src/cli.ts +0 -1339
  123. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  124. package/src/collections/buildRegistry.ts +0 -68
  125. package/src/collections/validate-relations.ts +0 -402
  126. package/src/connection.ts +0 -333
  127. package/src/data-transformer.ts +0 -845
  128. package/src/databasePoolManager.ts +0 -95
  129. package/src/history/HistoryService.ts +0 -254
  130. package/src/history/ensure-history-table.ts +0 -53
  131. package/src/index.ts +0 -19
  132. package/src/interfaces.ts +0 -60
  133. package/src/module-dir.ts +0 -7
  134. package/src/schema/atlas-argv.ts +0 -94
  135. package/src/schema/auth-schema.ts +0 -298
  136. package/src/schema/auth-users-columns.ts +0 -131
  137. package/src/schema/carved-out-migration.ts +0 -404
  138. package/src/schema/classify-change.ts +0 -436
  139. package/src/schema/collection-index.ts +0 -427
  140. package/src/schema/collections-schema-version.ts +0 -103
  141. package/src/schema/destructive-sql.ts +0 -94
  142. package/src/schema/doctor-cli.ts +0 -66
  143. package/src/schema/doctor-policy-checks.ts +0 -105
  144. package/src/schema/doctor.ts +0 -837
  145. package/src/schema/drizzle-ddl.ts +0 -33
  146. package/src/schema/dynamic-tables.test.ts +0 -302
  147. package/src/schema/dynamic-tables.ts +0 -293
  148. package/src/schema/ensure-collection-policies.ts +0 -198
  149. package/src/schema/ensure-collection-tables.test.ts +0 -518
  150. package/src/schema/ensure-collection-tables.ts +0 -1267
  151. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  152. package/src/schema/generate-drizzle-schema.ts +0 -143
  153. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  154. package/src/schema/generate-postgres-ddl.ts +0 -180
  155. package/src/schema/generate-schema-commit.ts +0 -267
  156. package/src/schema/generated-schema-staleness.ts +0 -284
  157. package/src/schema/introspect-db-constraints.ts +0 -385
  158. package/src/schema/introspect-db-inference.ts +0 -255
  159. package/src/schema/introspect-db-logic.ts +0 -1387
  160. package/src/schema/introspect-db-naming.ts +0 -15
  161. package/src/schema/introspect-db-project.ts +0 -78
  162. package/src/schema/introspect-db-queries.ts +0 -326
  163. package/src/schema/introspect-db-structure.ts +0 -670
  164. package/src/schema/introspect-db-types.ts +0 -56
  165. package/src/schema/introspect-db.ts +0 -257
  166. package/src/schema/introspect-runtime.test.ts +0 -260
  167. package/src/schema/introspect-runtime.ts +0 -327
  168. package/src/schema/non-sql-collections.test.ts +0 -131
  169. package/src/schema/rls-bootstrap-sql.ts +0 -288
  170. package/src/schema/search-column.ts +0 -643
  171. package/src/schema/test-schema.ts +0 -11
  172. package/src/schema/vector-index.ts +0 -491
  173. package/src/security/anonymous-grants.test.ts +0 -73
  174. package/src/security/policy-drift.test.ts +0 -435
  175. package/src/security/policy-drift.ts +0 -473
  176. package/src/security/rls-enforcement.ts +0 -505
  177. package/src/services/BranchService.ts +0 -326
  178. package/src/services/FetchService.ts +0 -2115
  179. package/src/services/PersistService.ts +0 -557
  180. package/src/services/RelationService.ts +0 -978
  181. package/src/services/RelationWriteService.ts +0 -653
  182. package/src/services/cdc/CdcListener.ts +0 -103
  183. package/src/services/cdc/junction-tables.ts +0 -91
  184. package/src/services/cdc/trigger-cdc.ts +0 -173
  185. package/src/services/channel-bus/ChannelBus.ts +0 -44
  186. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  187. package/src/services/channel-bus/index.ts +0 -114
  188. package/src/services/channel-history.ts +0 -424
  189. package/src/services/channel-presence.ts +0 -185
  190. package/src/services/collection-helpers.ts +0 -420
  191. package/src/services/dataService.ts +0 -256
  192. package/src/services/index.ts +0 -14
  193. package/src/services/junction-writes.ts +0 -295
  194. package/src/services/nested-path.ts +0 -145
  195. package/src/services/pg-notify-listener.ts +0 -151
  196. package/src/services/realtimeService.ts +0 -2545
  197. package/src/services/row-pipeline.ts +0 -240
  198. package/src/services/write-denial.ts +0 -55
  199. package/src/types.ts +0 -4
  200. package/src/utils/connection-string.ts +0 -58
  201. package/src/utils/drizzle-conditions.ts +0 -2743
  202. package/src/utils/pg-array-null-patch.ts +0 -42
  203. package/src/utils/pg-error-utils.ts +0 -391
  204. 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";