@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,520 +0,0 @@
1
- /**
2
- * CLI handlers for `rebase db backup`, `rebase db restore`, and
3
- * `rebase db backups list`. Kept out of the main `cli.ts` dispatcher so the
4
- * backup surface stays self-contained.
5
- */
6
- import arg from "arg";
7
- import path from "path";
8
- import fs from "fs";
9
- import os from "os";
10
- import readline from "readline";
11
- import chalk from "chalk";
12
- // Aliased: `out` is already a local in two of the commands below (the `--out`
13
- // backup destination).
14
- import { out as print, outWarn, outError } from "../cli-output";
15
- import type { StorageController } from "@rebasepro/server";
16
- import {
17
- BackupDestination,
18
- globalsFileForDump,
19
- parseBackupDestination,
20
- parseDbNameFromUrl,
21
- resolveConnectionString,
22
- withDatabaseName
23
- } from "./pg-tools";
24
- import {
25
- applyGlobals,
26
- BackupToolError,
27
- createDump,
28
- discardPartialDump,
29
- ensureDatabaseExists,
30
- listBackups,
31
- preflight,
32
- restoreDump,
33
- uploadBackup,
34
- validateDump
35
- } from "./backup-service";
36
-
37
- function formatBytes(bytes: number): string {
38
- if (bytes < 1024) return `${bytes} B`;
39
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
40
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
41
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
42
- }
43
-
44
- /**
45
- * Build a StorageController for an object-storage destination from the same
46
- * `S3_*` env vars the backend uses. Returns `null` for local destinations.
47
- */
48
- async function resolveStorageForDestination(
49
- dest: BackupDestination,
50
- env: Record<string, string | undefined>
51
- ): Promise<StorageController | null> {
52
- if (dest.kind === "local") return null;
53
- if (dest.kind === "gcs") {
54
- const { GCSStorageController } = await import("@rebasepro/server");
55
- return new GCSStorageController({ type: "gcs", bucket: dest.bucket });
56
- }
57
- // s3 (also covers R2/MinIO/Hetzner/GCS-interop via S3_ENDPOINT)
58
- if (!env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY) {
59
- throw new BackupToolError(
60
- "S3 destination requires S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY in the environment.",
61
- "Set the same S3_* variables your backend uses for storage."
62
- );
63
- }
64
- const { S3StorageController } = await import("@rebasepro/server");
65
- return new S3StorageController({
66
- type: "s3",
67
- bucket: dest.bucket,
68
- region: env.S3_REGION || "auto",
69
- accessKeyId: env.S3_ACCESS_KEY_ID,
70
- secretAccessKey: env.S3_SECRET_ACCESS_KEY,
71
- endpoint: env.S3_ENDPOINT,
72
- forcePathStyle: env.S3_FORCE_PATH_STYLE === "true"
73
- });
74
- }
75
-
76
- function requireConnection(): string {
77
- const conn = resolveConnectionString(process.env);
78
- if (!conn) {
79
- outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
80
- process.exit(1);
81
- }
82
- return conn;
83
- }
84
-
85
- async function promptConfirm(question: string): Promise<boolean> {
86
- // Non-interactive shells (CI, pipes) can't answer — treat as "no".
87
- if (!process.stdin.isTTY) return false;
88
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
89
- try {
90
- const answer: string = await new Promise((resolve) => rl.question(question, resolve));
91
- return /^y(es)?$/i.test(answer.trim());
92
- } finally {
93
- rl.close();
94
- }
95
- }
96
-
97
- // ─────────────────────────────────────────────────────────────────────────
98
- // rebase db backup
99
- // ─────────────────────────────────────────────────────────────────────────
100
- export async function backupCommand(rawArgs: string[]): Promise<void> {
101
- const args = arg(
102
- {
103
- "--out": String,
104
- "--exclude-schema": [String],
105
- "--no-owner": Boolean,
106
- "--enable-row-security": Boolean,
107
- "--row-security-role": String,
108
- "-o": "--out"
109
- },
110
- { argv: rawArgs.slice(2), permissive: true }
111
- );
112
-
113
- if (args._.includes("--help") || rawArgs.includes("--help")) {
114
- printBackupHelp();
115
- return;
116
- }
117
-
118
- const connectionString = requireConnection();
119
- const dbName = parseDbNameFromUrl(connectionString) ?? "database";
120
- const out = args["--out"] || process.env.BACKUP_DESTINATION || path.join(process.cwd(), "backups");
121
- const dest = parseBackupDestination(out);
122
-
123
- print("");
124
- print(chalk.bold(" 💾 Rebase DB Backup"));
125
- print(chalk.gray(` Database: ${dbName}`));
126
- print(chalk.gray(` Destination: ${out}`));
127
- print("");
128
-
129
- // Version pre-flight (doctor-style).
130
- const pf = await preflight("pg_dump", connectionString);
131
- if (!pf.compatible) {
132
- outError(chalk.red(` ✗ ${pf.reason}`));
133
- process.exit(1);
134
- }
135
- print(chalk.gray(` Using pg_dump ${pf.toolMajor} against server ${pf.serverMajor}.`));
136
-
137
- // Opt-in, and loud. With row security on, pg_dump stops refusing to read
138
- // rows it cannot see and starts leaving them out — an exit-0 backup that
139
- // is quietly short. Anyone choosing that should know they chose it.
140
- const rowSecurity = args["--enable-row-security"]
141
- ? { uid: "rebase-db-backup", roles: [args["--row-security-role"] || "admin"] }
142
- : undefined;
143
-
144
- if (rowSecurity) {
145
- outWarn("");
146
- outWarn(chalk.yellow(" ⚠ Dumping with row-level security ON."));
147
- outWarn(chalk.gray(` Reading as roles [${rowSecurity.roles.join(", ")}], which satisfies the generated`));
148
- outWarn(chalk.gray(" `admin_full_access` policy. This backup contains exactly the rows those"));
149
- outWarn(chalk.gray(" policies admit — a table whose policies have no admin rule comes out short,"));
150
- outWarn(chalk.gray(" and pg_dump will not say so. Prefer granting the dumping role BYPASSRLS."));
151
- outWarn("");
152
- }
153
-
154
- try {
155
- if (dest.kind === "local") {
156
- // Honour an explicit `…/name.dump` path; otherwise treat it as a
157
- // directory and auto-name the file.
158
- const explicitFile = dest.path.endsWith(".dump");
159
- const dump = await createDump({
160
- connectionString,
161
- dbName,
162
- outDir: explicitFile ? path.dirname(dest.path) : dest.path,
163
- fileName: explicitFile ? path.basename(dest.path) : undefined,
164
- excludeSchemas: args["--exclude-schema"],
165
- noOwner: args["--no-owner"],
166
- inheritStdio: true,
167
- rowSecurity
168
- });
169
- await assertDumpValid(dump.localFile);
170
- print("");
171
- print(chalk.green(` ✓ Backup written to ${dump.localFile} (${formatBytes(dump.sizeBytes)})`));
172
- if (dump.globalsFile) {
173
- print(chalk.gray(` ✓ Roles captured to ${dump.globalsFile} (needed so RLS survives a restore).`));
174
- }
175
- } else {
176
- const storage = await resolveStorageForDestination(dest, process.env);
177
- const dump = await createDump({
178
- connectionString,
179
- dbName,
180
- excludeSchemas: args["--exclude-schema"],
181
- noOwner: args["--no-owner"],
182
- inheritStdio: true,
183
- rowSecurity
184
- });
185
- try {
186
- await assertDumpValid(dump.localFile);
187
- const uploaded = await uploadBackup(storage!, dump.localFile, dest);
188
- print("");
189
- print(chalk.green(` ✓ Backup uploaded to ${uploaded.storageUrl} (${formatBytes(dump.sizeBytes)})`));
190
- // Upload the roles sidecar next to the dump so a restore can
191
- // recreate roles the dump's GRANT/RLS statements depend on.
192
- if (dump.globalsFile && fs.existsSync(dump.globalsFile)) {
193
- const globalsUpload = await uploadBackup(storage!, dump.globalsFile, dest);
194
- print(chalk.gray(` ✓ Roles uploaded to ${globalsUpload.storageUrl} (needed so RLS survives a restore).`));
195
- }
196
- print(chalk.gray(" Ensure this bucket is private — backups may contain secrets and PII."));
197
- } finally {
198
- if (fs.existsSync(dump.localFile)) fs.unlinkSync(dump.localFile);
199
- if (dump.globalsFile && fs.existsSync(dump.globalsFile)) fs.unlinkSync(dump.globalsFile);
200
- }
201
- }
202
- print("");
203
- } catch (err) {
204
- reportError(err);
205
- process.exit(1);
206
- }
207
- }
208
-
209
- // ─────────────────────────────────────────────────────────────────────────
210
- // rebase db restore <backup>
211
- // ─────────────────────────────────────────────────────────────────────────
212
- export async function restoreCommand(rawArgs: string[]): Promise<void> {
213
- const args = arg(
214
- {
215
- "--target-db": String,
216
- "--create-db": Boolean,
217
- "--clean": Boolean,
218
- "--no-owner": Boolean,
219
- "--continue-on-error": Boolean,
220
- "--yes": Boolean,
221
- "-y": "--yes"
222
- },
223
- { argv: rawArgs.slice(2), permissive: true }
224
- );
225
-
226
- const backupArg = args._[0];
227
- if (!backupArg || rawArgs.includes("--help")) {
228
- printRestoreHelp();
229
- if (!backupArg && !rawArgs.includes("--help")) process.exit(1);
230
- return;
231
- }
232
-
233
- const baseConnection = requireConnection();
234
-
235
- // Choose the target connection: an explicit --target-db (or --create-db's
236
- // implied fresh db) swaps the database name so the live one isn't clobbered.
237
- const targetDb = args["--target-db"] ?? parseDbNameFromUrl(baseConnection) ?? undefined;
238
- const targetConnection = args["--target-db"]
239
- ? withDatabaseName(baseConnection, args["--target-db"])
240
- : baseConnection;
241
-
242
- print("");
243
- print(chalk.bold(" ♻️ Rebase DB Restore"));
244
- print(chalk.gray(` Source: ${backupArg}`));
245
- print(chalk.gray(` Target: ${targetDb ?? "(from DATABASE_URL)"}`));
246
- print("");
247
-
248
- // Resolve the local file to restore from (download object-storage keys).
249
- // Also resolve the `.globals.sql` roles sidecar so cluster roles can be
250
- // recreated before the restore — without them the dump's GRANT/RLS
251
- // statements fail and RLS is silently lost.
252
- let localFile: string;
253
- let globalsSql: string | null = null;
254
- let cleanupTemp = false;
255
- try {
256
- if (/^(s3|gs):\/\//.test(backupArg)) {
257
- const dest = parseBackupDestination(backupArg.replace(/\/[^/]+$/, ""));
258
- const storage = await resolveStorageForDestination(dest, process.env);
259
- if (!storage) throw new BackupToolError("Could not resolve storage for the given URL.");
260
- const key = backupArg.replace(/^(s3|gs):\/\/[^/]+\//, "");
261
- const bucket = backupArg.replace(/^(s3|gs):\/\/([^/]+)\/.*$/, "$2");
262
- const file = await storage.getObject(key, bucket);
263
- if (!file) throw new BackupToolError(`Backup not found in storage: ${backupArg}`);
264
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "rebase-restore-"));
265
- localFile = path.join(tmpDir, path.basename(key));
266
- fs.writeFileSync(localFile, Buffer.from(await file.arrayBuffer()));
267
- cleanupTemp = true;
268
- const globalsObj = await storage.getObject(globalsFileForDump(key), bucket);
269
- if (globalsObj) globalsSql = Buffer.from(await globalsObj.arrayBuffer()).toString("utf-8");
270
- } else {
271
- localFile = path.resolve(backupArg);
272
- if (!fs.existsSync(localFile)) {
273
- outError(chalk.red(` ✗ Backup file not found: ${localFile}`));
274
- process.exit(1);
275
- }
276
- const globalsPath = globalsFileForDump(localFile);
277
- if (fs.existsSync(globalsPath)) globalsSql = fs.readFileSync(globalsPath, "utf-8");
278
- }
279
-
280
- // Version pre-flight. Check against the base connection — the server
281
- // version is identical for every database, and the target may not
282
- // exist yet when --create-db is used.
283
- const pf = await preflight("pg_restore", baseConnection);
284
- if (!pf.compatible) {
285
- outError(chalk.red(` ✗ ${pf.reason}`));
286
- process.exit(1);
287
- }
288
-
289
- // `--create-db` needs a name before anything else can be decided.
290
- if (args["--create-db"] && !targetDb) {
291
- outError(chalk.red(" ✗ --create-db requires a resolvable target database name (use --target-db)."));
292
- process.exit(1);
293
- }
294
-
295
- // Destructive-action gate. Restores overwrite data; never run without
296
- // an explicit yes (interactive confirmation or --yes).
297
- //
298
- // Ahead of `--create-db`, not after it. Creating the database first
299
- // meant an aborted run had already changed the cluster, while printing
300
- // "No changes were made" — and it left an empty database behind that a
301
- // second, confirmed run then reported as "already exists". The gate is
302
- // now the first thing that can stop the command, so its own message is
303
- // true whichever way the answer goes.
304
- if (!args["--yes"]) {
305
- outWarn(chalk.yellow(
306
- ` ⚠️ This will restore into "${targetDb ?? "the target database"}" and may overwrite existing data.`
307
- ));
308
- const confirmed = await promptConfirm(chalk.yellow(" Type 'yes' to continue: "));
309
- if (!confirmed) {
310
- print(chalk.gray(" Aborted. No changes were made."));
311
- process.exit(1);
312
- }
313
- }
314
-
315
- // Create the target database when requested.
316
- if (args["--create-db"]) {
317
- const created = await ensureDatabaseExists(baseConnection, targetDb!);
318
- print(chalk.gray(created ? ` ✓ Created database "${targetDb}".` : ` • Database "${targetDb}" already exists.`));
319
- }
320
-
321
- // Recreate cluster roles before restoring so GRANT/RLS statements in
322
- // the dump apply. Best-effort and idempotent (see applyGlobals).
323
- if (globalsSql) {
324
- print(chalk.gray(" Recreating cluster roles from the backup's roles sidecar…"));
325
- const { applied, skipped } = await applyGlobals(
326
- targetConnection,
327
- globalsSql,
328
- (m) => print(chalk.gray(m))
329
- );
330
- print(chalk.gray(` ✓ Roles: ${applied} applied, ${skipped} skipped (already present or not permitted).`));
331
- } else {
332
- outWarn(chalk.yellow(" ⚠️ No roles sidecar (.globals.sql) accompanies this backup."));
333
- outWarn(chalk.yellow(" If the dump grants to roles that don't exist (e.g. rebase_user), the restore"));
334
- outWarn(chalk.yellow(" will fail — recreate those roles first, or use a backup that includes its globals."));
335
- }
336
-
337
- await restoreDump({
338
- connectionString: targetConnection,
339
- inputFile: localFile,
340
- clean: args["--clean"],
341
- noOwner: args["--no-owner"],
342
- // Fail loudly by default so a skipped GRANT never leaves RLS off.
343
- exitOnError: !args["--continue-on-error"],
344
- inheritStdio: true
345
- });
346
-
347
- print("");
348
- print(chalk.green(` ✓ Restore completed into "${targetDb ?? "the target database"}".`));
349
- print("");
350
- } catch (err) {
351
- reportError(err);
352
- process.exit(1);
353
- } finally {
354
- if (cleanupTemp && typeof localFile! === "string" && fs.existsSync(localFile!)) {
355
- fs.rmSync(path.dirname(localFile!), { recursive: true, force: true });
356
- }
357
- }
358
- }
359
-
360
- // ─────────────────────────────────────────────────────────────────────────
361
- // rebase db backups list
362
- // ─────────────────────────────────────────────────────────────────────────
363
- export async function backupsCommand(rawArgs: string[]): Promise<void> {
364
- const action = rawArgs[2];
365
- if (!action || action === "--help") {
366
- printBackupsHelp();
367
- return;
368
- }
369
- if (action !== "list") {
370
- outError(chalk.red(`Unknown backups action: "${action}". Valid: list`));
371
- process.exit(1);
372
- }
373
-
374
- const args = arg({ "--out": String, "-o": "--out" }, { argv: rawArgs.slice(3), permissive: true });
375
- const out = args["--out"] || process.env.BACKUP_DESTINATION || path.join(process.cwd(), "backups");
376
- const dest = parseBackupDestination(out);
377
-
378
- try {
379
- const storage = await resolveStorageForDestination(dest, process.env);
380
- const backups = await listBackups(dest, storage ?? undefined);
381
- print("");
382
- if (backups.length === 0) {
383
- print(chalk.gray(` No backups found at ${out}.`));
384
- } else {
385
- print(chalk.bold(` 💾 ${backups.length} backup(s) at ${out}:`));
386
- print("");
387
- for (const b of backups) {
388
- const when = b.createdAt ? b.createdAt.toISOString() : "unknown date";
389
- const name = dest.kind === "local" ? path.basename(b.key) : b.key;
390
- // An empty file is called out rather than listed as a peer of
391
- // the real ones. Older failures could leave a 0-byte dump here
392
- // (fixed at the source now), and retention protects the newest
393
- // by date whatever they contain — so a corpse left in place can
394
- // hold a `keepMinimum` slot against a backup that matters.
395
- const empty = b.sizeBytes === 0;
396
- const size = b.sizeBytes === undefined ? "" : ` — ${formatBytes(b.sizeBytes)}`;
397
- if (empty) {
398
- print(` ${chalk.red("○")} ${chalk.bold(name)} ${chalk.gray(`— ${when}`)}${chalk.red(" — EMPTY, not restorable")}`);
399
- } else {
400
- print(` ${chalk.green("●")} ${chalk.bold(name)} ${chalk.gray(`— ${when}${size}`)}`);
401
- }
402
- }
403
- if (backups.some(b => b.sizeBytes === 0)) {
404
- print("");
405
- print(chalk.yellow(" ⚠ Empty files above are leftovers from a failed backup. Delete them:"));
406
- print(chalk.gray(" they count as recent backups for retention but restore nothing."));
407
- }
408
- }
409
- print("");
410
- } catch (err) {
411
- reportError(err);
412
- process.exit(1);
413
- }
414
- }
415
-
416
- /**
417
- * Verify a freshly written dump; abort the command if it looks corrupt.
418
- *
419
- * Discards the artifact on the way out. Refusing to *report* success was never
420
- * enough on its own: the file stayed on disk, `rebase db backups list` showed
421
- * it as an ordinary entry, and retention — which ranks by timestamp and never
422
- * looks at size — would protect it as one of the `keepMinimum` newest while
423
- * pruning a real backup underneath it. The roles sidecar goes too; the two are
424
- * uploaded and pruned as a pair, so half a pair is not a backup either.
425
- */
426
- async function assertDumpValid(localFile: string): Promise<void> {
427
- const check = await validateDump(localFile);
428
- if (!check.ok) {
429
- discardPartialDump(globalsFileForDump(localFile));
430
- discardPartialDump(localFile);
431
- throw new BackupToolError(
432
- `The backup failed validation and was discarded: ${check.reason}`,
433
- "The dump was corrupt or truncated, so nothing was kept. Investigate before relying on this destination."
434
- );
435
- }
436
- }
437
-
438
- function reportError(err: unknown): void {
439
- if (err instanceof BackupToolError) {
440
- outError(chalk.red(` ✗ ${err.message}`));
441
- if (err.hint) outError(chalk.gray(` ${err.hint}`));
442
- } else {
443
- outError(chalk.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));
444
- }
445
- }
446
-
447
- function printBackupHelp(): void {
448
- print(`
449
- ${chalk.bold("rebase db backup")} — Create a database backup (pg_dump, custom format)
450
-
451
- ${chalk.green.bold("Usage")}
452
- rebase db backup [--out <path|s3://bucket/prefix>] [options]
453
-
454
- ${chalk.green.bold("Options")}
455
- ${chalk.blue("--out, -o")} <dest> Local path or s3://…/gs://… URL (default: ./backups)
456
- ${chalk.blue("--exclude-schema")} <s> Exclude a schema (repeatable)
457
- ${chalk.blue("--no-owner")} Omit ownership commands from the dump
458
- ${chalk.blue("--enable-row-security")} Dump as an admin subject instead of failing on RLS
459
- ${chalk.red("(may produce a partial dump — see below)")}
460
- ${chalk.blue("--row-security-role")} <r> Role to read as with the flag above (default: admin)
461
-
462
- ${chalk.green.bold("Row-level security")}
463
- On a managed Postgres the dumping role usually owns nothing and has no
464
- BYPASSRLS, so pg_dump refuses:
465
-
466
- ERROR: query would be affected by row-level security policy for table "..."
467
-
468
- That refusal is the safe behaviour. --enable-row-security replaces it by
469
- reading as an admin subject: Rebase sets app.uid/app.user_roles so the
470
- generated admin_full_access policy admits the dump. The dump then contains
471
- exactly what those policies admit ${chalk.red("and no error is raised for what they do not")} —
472
- a table whose policies lack an admin rule comes out short, silently.
473
-
474
- Granting the dumping role BYPASSRLS is the option that keeps a backup
475
- meaning "every row".
476
-
477
- ${chalk.green.bold("Notes")}
478
- Backups may contain secrets and PII. Use private storage destinations and
479
- enable encryption-at-rest. See docs/backups.md.
480
- `);
481
- }
482
-
483
- function printRestoreHelp(): void {
484
- print(`
485
- ${chalk.bold("rebase db restore")} — Restore a database from a backup (pg_restore)
486
-
487
- ${chalk.green.bold("Usage")}
488
- rebase db restore <backup> [options]
489
-
490
- ${chalk.green.bold("Arguments")}
491
- <backup> Local .dump file, or s3://…/gs://… object key
492
-
493
- ${chalk.green.bold("Options")}
494
- ${chalk.blue("--target-db")} <name> Restore into this database instead of DATABASE_URL's
495
- ${chalk.blue("--create-db")} Create the target database first if it doesn't exist
496
- ${chalk.blue("--clean")} Drop existing objects before recreating them
497
- ${chalk.blue("--no-owner")} Ignore ownership from the dump
498
- ${chalk.blue("--continue-on-error")} Log and continue past errors ${chalk.red("(may leave RLS un-enforced!)")}
499
- ${chalk.blue("--yes, -y")} Skip the interactive confirmation ${chalk.red("(destructive!)")}
500
-
501
- ${chalk.red.bold("Warning")}
502
- Restore is destructive and never runs automatically. Without --yes it
503
- requires an interactive 'yes'. Prefer --create-db/--target-db to restore
504
- into a fresh database rather than overwriting a live one.
505
-
506
- By default the restore aborts on the first error (--exit-on-error) so a
507
- skipped GRANT never silently leaves RLS un-enforced. Roles are recreated
508
- from the backup's .globals.sql sidecar first; keep that file next to the
509
- dump. Use --continue-on-error only when you understand the consequences.
510
- `);
511
- }
512
-
513
- function printBackupsHelp(): void {
514
- print(`
515
- ${chalk.bold("rebase db backups")} — Manage stored backups
516
-
517
- ${chalk.green.bold("Usage")}
518
- rebase db backups list [--out <path|s3://bucket/prefix>]
519
- `);
520
- }