@rebasepro/server-postgres 0.13.1-canary.gf57a27e → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PostgresBootstrapper.d.ts +26 -0
- package/dist/auth/services.d.ts +21 -0
- package/dist/{auth-users-columns-Dt9g712t.js → auth-users-columns-BfQHf9JE.js} +525 -63
- package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
- package/dist/{backup-service-Bww-Lg0s.js → backup-service-BH0Dzo_h.js} +2 -3
- package/dist/{backup-service-Bww-Lg0s.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
- package/dist/cli-output.d.ts +34 -0
- package/dist/data-transformer.d.ts +7 -2
- package/dist/data_driver-ULAyJEi9.js +193 -0
- package/dist/data_driver-ULAyJEi9.js.map +1 -0
- package/dist/ensure-collection-policies-8vuu-n4r.js +124 -0
- package/dist/ensure-collection-policies-8vuu-n4r.js.map +1 -0
- package/dist/{ensure-collection-tables-DRxaUG96.js → ensure-collection-tables-CbvaGuVn.js} +89 -10
- package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
- package/dist/index.es.js +1310 -1060
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-bootstrap-sql-Bpv3nUZo.js → rls-bootstrap-sql-69hYT8nr.js} +2 -2
- package/dist/{rls-bootstrap-sql-Bpv3nUZo.js.map → rls-bootstrap-sql-69hYT8nr.js.map} +1 -1
- package/dist/rls-enforcement-BJ_3wxwg.js +425 -0
- package/dist/rls-enforcement-BJ_3wxwg.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +102 -0
- package/dist/schema/doctor-policy-checks.d.ts +28 -0
- package/dist/schema/doctor.d.ts +41 -25
- package/dist/schema/ensure-collection-policies.d.ts +33 -9
- package/dist/schema/ensure-collection-tables.d.ts +60 -6
- package/dist/schema/generate-drizzle-schema-logic.d.ts +9 -1
- package/dist/schema/introspect-db-inference.d.ts +8 -1
- package/dist/schema/introspect-db-logic.d.ts +49 -0
- package/dist/schema/introspect-db-project.d.ts +21 -0
- package/dist/schema/search-column.d.ts +49 -0
- package/dist/security/policy-drift.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +8 -3
- package/dist/services/FetchService.d.ts +9 -0
- package/dist/services/PersistService.d.ts +21 -17
- package/dist/services/RelationService.d.ts +9 -57
- package/dist/services/RelationWriteService.d.ts +82 -0
- package/dist/services/collection-helpers.d.ts +42 -0
- package/dist/services/dataService.d.ts +2 -0
- package/dist/services/junction-writes.d.ts +82 -0
- package/dist/services/realtimeService.d.ts +137 -2
- package/dist/services/write-denial.d.ts +36 -0
- package/dist/{src-C_wvdMnl.js → src-DCdn3Val.js} +35 -3
- package/dist/src-DCdn3Val.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +54 -1
- package/dist/{websocket-D0TBU3ia.js → websocket-C8ZqVBiV.js} +75 -18
- package/dist/websocket-C8ZqVBiV.js.map +1 -0
- package/package.json +6 -6
- package/src/PostgresBackendDriver.ts +7 -3
- package/src/PostgresBootstrapper.ts +95 -9
- package/src/auth/ensure-tables.ts +27 -5
- package/src/auth/services.ts +82 -5
- package/src/backup/backup-cli.ts +59 -57
- package/src/cli-errors.ts +6 -6
- package/src/cli-helpers.ts +4 -4
- package/src/cli-output.ts +43 -0
- package/src/cli.ts +155 -147
- package/src/collections/buildRegistry.ts +3 -1
- package/src/data-transformer.ts +111 -25
- package/src/history/ensure-history-table.ts +2 -2
- package/src/schema/auth-schema.ts +17 -1
- package/src/schema/doctor-cli.ts +14 -65
- package/src/schema/doctor-policy-checks.ts +105 -0
- package/src/schema/doctor.ts +149 -72
- package/src/schema/ensure-collection-policies.ts +99 -6
- package/src/schema/ensure-collection-tables.ts +214 -17
- package/src/schema/generate-drizzle-schema-logic.ts +121 -65
- package/src/schema/generate-drizzle-schema.ts +11 -10
- package/src/schema/generate-postgres-ddl-logic.ts +28 -1
- package/src/schema/generate-postgres-ddl.ts +14 -13
- package/src/schema/generated-schema-staleness.ts +7 -5
- package/src/schema/introspect-db-inference.ts +9 -2
- package/src/schema/introspect-db-logic.ts +251 -75
- package/src/schema/introspect-db-project.ts +78 -0
- package/src/schema/introspect-db.ts +42 -25
- package/src/schema/introspect-runtime.ts +14 -2
- package/src/schema/search-column.ts +85 -0
- package/src/security/policy-drift.test.ts +104 -3
- package/src/security/policy-drift.ts +129 -7
- package/src/security/rls-enforcement.ts +9 -4
- package/src/services/FetchService.ts +105 -7
- package/src/services/PersistService.ts +68 -42
- package/src/services/RelationService.ts +35 -695
- package/src/services/RelationWriteService.ts +653 -0
- package/src/services/cdc/trigger-cdc.ts +5 -1
- package/src/services/channel-history.ts +9 -3
- package/src/services/channel-presence.ts +10 -3
- package/src/services/collection-helpers.ts +89 -4
- package/src/services/dataService.ts +2 -0
- package/src/services/junction-writes.ts +295 -0
- package/src/services/pg-notify-listener.ts +1 -1
- package/src/services/realtimeService.ts +337 -82
- package/src/services/write-denial.ts +55 -0
- package/src/utils/drizzle-conditions.ts +211 -34
- package/src/utils/pg-error-utils.ts +8 -3
- package/src/websocket.ts +113 -16
- package/dist/auth-users-columns-Dt9g712t.js.map +0 -1
- package/dist/ensure-collection-policies-CwYUliAa.js +0 -57
- package/dist/ensure-collection-policies-CwYUliAa.js.map +0 -1
- package/dist/ensure-collection-tables-DRxaUG96.js.map +0 -1
- package/dist/policy-CPkCqVTz.js +0 -105
- package/dist/policy-CPkCqVTz.js.map +0 -1
- package/dist/src-C_wvdMnl.js.map +0 -1
- package/dist/websocket-D0TBU3ia.js.map +0 -1
package/src/cli.ts
CHANGED
|
@@ -5,6 +5,7 @@ import path from "path";
|
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { logger } from "@rebasepro/server";
|
|
8
|
+
import { out, outWarn, outError } from "./cli-output";
|
|
8
9
|
import { formatRelativeTime } from "@rebasepro/utils";
|
|
9
10
|
import {
|
|
10
11
|
diagnoseMissingBin,
|
|
@@ -41,7 +42,7 @@ async function loadEnv(): Promise<void> {
|
|
|
41
42
|
|
|
42
43
|
for (const p of envPaths) {
|
|
43
44
|
if (fs.existsSync(p)) {
|
|
44
|
-
const parsed = dotenv.config({ path: p });
|
|
45
|
+
const parsed = dotenv.config({ path: p, quiet: true });
|
|
45
46
|
if (parsed.parsed) {
|
|
46
47
|
for (const [key, val] of Object.entries(parsed.parsed)) {
|
|
47
48
|
if (process.env[key] === undefined) {
|
|
@@ -58,6 +59,13 @@ async function loadEnv(): Promise<void> {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
export async function runPluginCommand(args: string[]) {
|
|
62
|
+
// Also set as an environment variable, not just per `config()` call: this
|
|
63
|
+
// command spawns `tsx` subprocesses (the schema generator, the DDL
|
|
64
|
+
// generator, doctor) that load `.env` themselves, and they inherit this.
|
|
65
|
+
// dotenv reads it before `options.quiet`; an explicit `false` still wins.
|
|
66
|
+
if (process.env.DOTENV_CONFIG_QUIET === undefined) {
|
|
67
|
+
process.env.DOTENV_CONFIG_QUIET = "true";
|
|
68
|
+
}
|
|
61
69
|
await loadEnv();
|
|
62
70
|
const domain = args[0]; // "db" or "schema"
|
|
63
71
|
const subcommand = args[1];
|
|
@@ -69,7 +77,7 @@ export async function runPluginCommand(args: string[]) {
|
|
|
69
77
|
} else if (domain === "doctor") {
|
|
70
78
|
await doctorPluginCommand(args);
|
|
71
79
|
} else {
|
|
72
|
-
|
|
80
|
+
outError(chalk.red(`Unknown domain command: ${domain}`));
|
|
73
81
|
process.exit(1);
|
|
74
82
|
}
|
|
75
83
|
}
|
|
@@ -84,7 +92,7 @@ function listMigrationFiles(): string[] {
|
|
|
84
92
|
async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
85
93
|
const VALID_ACTIONS = ["push", "generate", "migrate", "branch", "backup", "restore", "backups"];
|
|
86
94
|
if (!subcommand || !VALID_ACTIONS.includes(subcommand)) {
|
|
87
|
-
|
|
95
|
+
outError(chalk.red(`Unknown db command. Valid: ${VALID_ACTIONS.join(", ")}`));
|
|
88
96
|
process.exit(1);
|
|
89
97
|
}
|
|
90
98
|
|
|
@@ -117,15 +125,15 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
117
125
|
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
118
126
|
|
|
119
127
|
if (subcommand === "generate") {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
out("");
|
|
129
|
+
out(chalk.bold(" 📦 Rebase DB Generate"));
|
|
130
|
+
out(chalk.gray(" Step 1/2: Generating Drizzle schema & Postgres DDL from collections..."));
|
|
131
|
+
out("");
|
|
124
132
|
await schemaCommand("generate", rawArgs);
|
|
125
133
|
await generatePostgresDdlCommand(rawArgs);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
out("");
|
|
135
|
+
out(chalk.gray(" Step 2/2: Generating SQL migration files with Atlas..."));
|
|
136
|
+
out("");
|
|
129
137
|
const migrationName = argsList._[0] || "migration";
|
|
130
138
|
const migrationsBefore = listMigrationFiles();
|
|
131
139
|
await runAtlas("migrate", ["diff", migrationName, "--dir", "file://drizzle/migrations", "--to", "file://drizzle/schema.sql"], collectionsPath);
|
|
@@ -165,12 +173,12 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
165
173
|
const searchContent = readSearchDdl();
|
|
166
174
|
if (searchContent && wroteNewMigration) {
|
|
167
175
|
migrationContent = `${migrationContent}\n\n${searchContent}`;
|
|
168
|
-
|
|
176
|
+
out(chalk.gray(" ✓ Appended search DDL to the migration"));
|
|
169
177
|
} else if (searchContent) {
|
|
170
178
|
// Reachable whenever a `search` block is the *only*
|
|
171
179
|
// thing that changed: Atlas cannot see one, so it finds
|
|
172
180
|
// nothing to diff and writes no file.
|
|
173
|
-
|
|
181
|
+
out(chalk.gray(
|
|
174
182
|
" ℹ Search DDL not written to a migration — this change produced none.\n" +
|
|
175
183
|
" It is applied by `rebase db push`, and at boot by the schema ensure.\n" +
|
|
176
184
|
" For a migration-only deployment, add drizzle/search.sql to a migration by hand."
|
|
@@ -187,35 +195,35 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
187
195
|
if (fs.existsSync(policiesFile)) {
|
|
188
196
|
const policiesContent = fs.readFileSync(policiesFile, "utf-8");
|
|
189
197
|
fs.appendFileSync(newestMigrationFile, "\n\n" + RLS_BOOTSTRAP_SQL + "\n" + policiesContent);
|
|
190
|
-
|
|
198
|
+
out(chalk.gray(` ✓ Appended RLS policies to migration file: ${path.basename(newestMigrationFile)}`));
|
|
191
199
|
|
|
192
200
|
// Re-hash the migration directory
|
|
193
|
-
|
|
201
|
+
out(chalk.gray(" Re-hashing migration files..."));
|
|
194
202
|
await runAtlas("migrate", ["hash", "--dir", "file://drizzle/migrations"], collectionsPath);
|
|
195
|
-
|
|
203
|
+
out(chalk.gray(" ✓ Migration directory checksum updated successfully."));
|
|
196
204
|
}
|
|
197
205
|
}
|
|
198
206
|
}
|
|
199
207
|
} catch (err) {
|
|
200
|
-
|
|
208
|
+
outWarn(chalk.yellow(` ⚠️ Failed to append policies or re-hash migration: ${err instanceof Error ? err.message : String(err)}`));
|
|
201
209
|
}
|
|
202
210
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
211
|
+
out("");
|
|
212
|
+
out(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
|
|
213
|
+
out("");
|
|
206
214
|
} else {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
215
|
+
out("");
|
|
216
|
+
out(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
|
|
217
|
+
out("");
|
|
210
218
|
|
|
211
219
|
if (subcommand === "push") {
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
out(chalk.gray(" Step 1/3: Generating Drizzle schema & Postgres DDL from collections..."));
|
|
221
|
+
out("");
|
|
214
222
|
await schemaCommand("generate", rawArgs);
|
|
215
223
|
await generatePostgresDdlCommand(rawArgs);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
224
|
+
out("");
|
|
225
|
+
out(chalk.gray(" Step 2/3: Pushing schema to database with Atlas..."));
|
|
226
|
+
out("");
|
|
219
227
|
const databaseUrl = process.env.DATABASE_URL;
|
|
220
228
|
if (databaseUrl) {
|
|
221
229
|
await ensureAuthSchemaAndFunctions(databaseUrl);
|
|
@@ -240,19 +248,19 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
240
248
|
});
|
|
241
249
|
|
|
242
250
|
if (destructive.length > 0) {
|
|
243
|
-
|
|
244
|
-
|
|
251
|
+
outWarn(chalk.yellow(` ⚠️ This push includes ${destructive.length} destructive change(s) that will DESTROY data:`));
|
|
252
|
+
outWarn("");
|
|
245
253
|
for (const d of destructive) {
|
|
246
|
-
|
|
254
|
+
outWarn(chalk.red(` ${d.kind}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
|
|
247
255
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
256
|
+
outWarn("");
|
|
257
|
+
outWarn(chalk.yellow(" Full planned changes:"));
|
|
258
|
+
outWarn(chalk.gray(plan.trim().split("\n").map((l) => ` ${l}`).join("\n")));
|
|
259
|
+
outWarn("");
|
|
252
260
|
|
|
253
261
|
if (decision === "refuse") {
|
|
254
|
-
|
|
255
|
-
|
|
262
|
+
outError(chalk.red(" ✗ Aborting: destructive changes require confirmation."));
|
|
263
|
+
outError(chalk.gray(" Re-run interactively, or pass --allow-destructive to proceed. Back up first: rebase db backup"));
|
|
256
264
|
process.exit(1);
|
|
257
265
|
}
|
|
258
266
|
if (decision === "confirm") {
|
|
@@ -260,17 +268,17 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
260
268
|
chalk.yellow(" Type 'yes' to apply these destructive changes (this cannot be undone): ")
|
|
261
269
|
);
|
|
262
270
|
if (!confirmed) {
|
|
263
|
-
|
|
271
|
+
out(chalk.gray(" Aborted. No changes were made."));
|
|
264
272
|
process.exit(1);
|
|
265
273
|
}
|
|
266
274
|
} else {
|
|
267
275
|
// decision === "apply" via --allow-destructive
|
|
268
|
-
|
|
276
|
+
outWarn(chalk.yellow(" Proceeding because --allow-destructive was passed."));
|
|
269
277
|
}
|
|
270
278
|
}
|
|
271
279
|
|
|
272
280
|
await runAtlas("schema", ["apply", "--to", "file://drizzle/schema.sql", "--auto-approve"], collectionsPath);
|
|
273
|
-
|
|
281
|
+
out("");
|
|
274
282
|
|
|
275
283
|
if (databaseUrl) {
|
|
276
284
|
await ensureAuthTables(databaseUrl, collectionsPath);
|
|
@@ -283,7 +291,7 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
283
291
|
await ensureRlsUserRole(databaseUrl);
|
|
284
292
|
await retireLegacyAuthSchema(databaseUrl);
|
|
285
293
|
} else {
|
|
286
|
-
|
|
294
|
+
outWarn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
|
|
287
295
|
}
|
|
288
296
|
} else if (subcommand === "migrate") {
|
|
289
297
|
const databaseUrl = process.env.DATABASE_URL;
|
|
@@ -298,9 +306,9 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
298
306
|
}
|
|
299
307
|
}
|
|
300
308
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
309
|
+
out("");
|
|
310
|
+
out(chalk.green(` ✓ rebase db ${subcommand} completed successfully.`));
|
|
311
|
+
out("");
|
|
304
312
|
}
|
|
305
313
|
}
|
|
306
314
|
|
|
@@ -323,7 +331,7 @@ async function ensureAuthSchemaAndFunctions(databaseUrl: string): Promise<void>
|
|
|
323
331
|
await client.end();
|
|
324
332
|
}
|
|
325
333
|
} catch (err) {
|
|
326
|
-
|
|
334
|
+
outWarn(chalk.yellow(` ⚠️ Failed to bootstrap the RLS helper functions: ${err instanceof Error ? err.message : String(err)}`));
|
|
327
335
|
}
|
|
328
336
|
}
|
|
329
337
|
|
|
@@ -360,7 +368,7 @@ async function ensureAuthTables(databaseUrl: string, collectionsPath: string): P
|
|
|
360
368
|
await client.end();
|
|
361
369
|
}
|
|
362
370
|
} catch (err) {
|
|
363
|
-
|
|
371
|
+
outWarn(chalk.yellow(` ⚠️ Failed to ensure framework auth tables: ${err instanceof Error ? err.message : String(err)}`));
|
|
364
372
|
}
|
|
365
373
|
}
|
|
366
374
|
|
|
@@ -391,7 +399,7 @@ async function ensureRlsUserRole(databaseUrl: string): Promise<void> {
|
|
|
391
399
|
} catch (err) {
|
|
392
400
|
// A module-resolution failure here is a build problem, not a database
|
|
393
401
|
// one, and it is exactly the case that used to produce the silent exit.
|
|
394
|
-
|
|
402
|
+
outError(chalk.red(
|
|
395
403
|
`\n ✗ Could not load the RLS provisioning module: ${err instanceof Error ? err.message : String(err)}`
|
|
396
404
|
));
|
|
397
405
|
return;
|
|
@@ -406,17 +414,17 @@ async function ensureRlsUserRole(databaseUrl: string): Promise<void> {
|
|
|
406
414
|
const posture = await rls.detectConnectionPosture(runSql);
|
|
407
415
|
if (posture.privileged) {
|
|
408
416
|
await rls.ensureAppRole(runSql, schemas);
|
|
409
|
-
|
|
417
|
+
out(chalk.gray(` ✓ RLS role "${rls.REBASE_USER_ROLE}" provisioned/refreshed.`));
|
|
410
418
|
}
|
|
411
419
|
} finally {
|
|
412
420
|
await client.end();
|
|
413
421
|
}
|
|
414
422
|
} catch (err) {
|
|
415
|
-
|
|
423
|
+
outError(chalk.red(
|
|
416
424
|
`\n ✗ The schema change was applied, but the "${rls.REBASE_USER_ROLE}" role could not be ` +
|
|
417
425
|
`provisioned: ${err instanceof Error ? err.message : String(err)}`
|
|
418
426
|
));
|
|
419
|
-
|
|
427
|
+
outError(chalk.gray(rls.appRoleSetupInstructions("your database user", schemas)));
|
|
420
428
|
}
|
|
421
429
|
}
|
|
422
430
|
|
|
@@ -438,8 +446,8 @@ async function retireLegacyAuthSchema(databaseUrl: string): Promise<void> {
|
|
|
438
446
|
await dropLegacyAuthSchema(
|
|
439
447
|
async (text) => (await client.query(text)).rows as Record<string, unknown>[],
|
|
440
448
|
{
|
|
441
|
-
info: (m) =>
|
|
442
|
-
warn: (m) =>
|
|
449
|
+
info: (m) => out(chalk.gray(` ${m}`)),
|
|
450
|
+
warn: (m) => outWarn(chalk.yellow(` ⚠️ ${m}`))
|
|
443
451
|
}
|
|
444
452
|
);
|
|
445
453
|
} finally {
|
|
@@ -455,8 +463,8 @@ async function applyPolicies(databaseUrl: string): Promise<void> {
|
|
|
455
463
|
const policiesPath = path.resolve(process.cwd(), "drizzle", "policies.sql");
|
|
456
464
|
if (!fs.existsSync(policiesPath)) return;
|
|
457
465
|
|
|
458
|
-
|
|
459
|
-
|
|
466
|
+
out(chalk.gray(" Step 3/3: Applying RLS policies to database..."));
|
|
467
|
+
out("");
|
|
460
468
|
|
|
461
469
|
const policiesContent = fs.readFileSync(policiesPath, "utf-8");
|
|
462
470
|
const { Client } = await import("pg");
|
|
@@ -464,16 +472,16 @@ async function applyPolicies(databaseUrl: string): Promise<void> {
|
|
|
464
472
|
await client.connect();
|
|
465
473
|
try {
|
|
466
474
|
await client.query(policiesContent);
|
|
467
|
-
|
|
475
|
+
out(chalk.green(" ✓ RLS policies applied successfully."));
|
|
468
476
|
} finally {
|
|
469
477
|
await client.end();
|
|
470
478
|
}
|
|
471
479
|
} catch (err) {
|
|
472
480
|
const hint = diagnoseDbError(err, databaseUrl);
|
|
473
481
|
if (hint) {
|
|
474
|
-
|
|
482
|
+
outError(hint);
|
|
475
483
|
} else {
|
|
476
|
-
|
|
484
|
+
outError(chalk.red(` ✗ Failed to apply RLS policies: ${err instanceof Error ? err.message : String(err)}`));
|
|
477
485
|
}
|
|
478
486
|
process.exit(1);
|
|
479
487
|
}
|
|
@@ -507,34 +515,34 @@ async function reconcilePolicies(databaseUrl: string, collectionsPath: string):
|
|
|
507
515
|
const { dropped, kept } = await dropOrphanedPolicies(client as never, drift, collections);
|
|
508
516
|
|
|
509
517
|
for (const p of dropped) {
|
|
510
|
-
|
|
518
|
+
out(chalk.gray(` ✓ Dropped superseded policy "${p.name}" on ${p.schema}.${p.table}`));
|
|
511
519
|
}
|
|
512
520
|
if (dropped.length > 0) {
|
|
513
|
-
|
|
521
|
+
out(chalk.green(` ✓ Removed ${dropped.length} superseded RLS ${dropped.length === 1 ? "policy" : "policies"}.`));
|
|
514
522
|
}
|
|
515
523
|
|
|
516
524
|
// Custom-named orphans are indistinguishable from policies someone
|
|
517
525
|
// wrote in SQL deliberately, so they are reported, never dropped.
|
|
518
526
|
if (kept.length > 0) {
|
|
519
|
-
|
|
527
|
+
outWarn(chalk.yellow(" ⚠️ Policies in the database that no collection describes:"));
|
|
520
528
|
for (const p of kept) {
|
|
521
|
-
|
|
529
|
+
outWarn(chalk.yellow(` • ${p.schema}.${p.table} → "${p.name}" (${p.command} TO ${p.roles.join(", ")})`));
|
|
522
530
|
}
|
|
523
|
-
|
|
531
|
+
outWarn(chalk.yellow(" These still grant access. Drop them by hand if they are stale."));
|
|
524
532
|
}
|
|
525
533
|
|
|
526
534
|
// Missing/diverged are not push's to fix, but staying silent about
|
|
527
535
|
// them is how a database ends up not matching its config.
|
|
528
536
|
const remaining = { ...drift, orphaned: kept };
|
|
529
537
|
if (hasDrift(remaining) && (remaining.missing.length > 0 || remaining.diverged.length > 0)) {
|
|
530
|
-
|
|
531
|
-
|
|
538
|
+
outWarn(chalk.yellow(" ⚠️ RLS policies do not match your collections:"));
|
|
539
|
+
outWarn(formatPolicyDrift({ ...remaining, orphaned: [] }));
|
|
532
540
|
}
|
|
533
541
|
} finally {
|
|
534
542
|
await client.end();
|
|
535
543
|
}
|
|
536
544
|
} catch (err) {
|
|
537
|
-
|
|
545
|
+
outWarn(chalk.yellow(` ⚠️ Could not reconcile RLS policies: ${err instanceof Error ? err.message : String(err)}`));
|
|
538
546
|
}
|
|
539
547
|
}
|
|
540
548
|
|
|
@@ -551,9 +559,9 @@ async function branchCommand(rawArgs: string[]): Promise<void> {
|
|
|
551
559
|
const dotenv = await import("dotenv");
|
|
552
560
|
const envPath = process.env.DOTENV_CONFIG_PATH;
|
|
553
561
|
if (envPath) {
|
|
554
|
-
dotenv.config({ path: envPath });
|
|
562
|
+
dotenv.config({ path: envPath, quiet: true });
|
|
555
563
|
} else {
|
|
556
|
-
dotenv.config();
|
|
564
|
+
dotenv.config({ quiet: true });
|
|
557
565
|
}
|
|
558
566
|
} catch {
|
|
559
567
|
// dotenv may not be installed
|
|
@@ -561,7 +569,7 @@ async function branchCommand(rawArgs: string[]): Promise<void> {
|
|
|
561
569
|
|
|
562
570
|
const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
|
|
563
571
|
if (!databaseUrl) {
|
|
564
|
-
|
|
572
|
+
outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
565
573
|
process.exit(1);
|
|
566
574
|
}
|
|
567
575
|
|
|
@@ -585,8 +593,8 @@ max: 3 });
|
|
|
585
593
|
case "create": {
|
|
586
594
|
const name = rawArgs[3];
|
|
587
595
|
if (!name) {
|
|
588
|
-
|
|
589
|
-
|
|
596
|
+
outError(chalk.red("✗ Branch name is required."));
|
|
597
|
+
out(chalk.gray(" Usage: rebase db branch create <name> [--from <source>]"));
|
|
590
598
|
process.exit(1);
|
|
591
599
|
}
|
|
592
600
|
let source: string | undefined;
|
|
@@ -594,81 +602,81 @@ max: 3 });
|
|
|
594
602
|
if (fromIdx !== -1 && rawArgs[fromIdx + 1]) {
|
|
595
603
|
source = rawArgs[fromIdx + 1];
|
|
596
604
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if (source)
|
|
601
|
-
|
|
605
|
+
out("");
|
|
606
|
+
out(chalk.bold(" 🌿 Creating database branch..."));
|
|
607
|
+
out(chalk.gray(` Name: ${name}`));
|
|
608
|
+
if (source) out(chalk.gray(` Source: ${source}`));
|
|
609
|
+
out("");
|
|
602
610
|
const branch = await branchService.createBranch(name, source ? { source } : undefined);
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
611
|
+
out(chalk.green(` ✓ Branch "${branch.name}" created successfully.`));
|
|
612
|
+
out(chalk.gray(` Database: rb_${branch.name}`));
|
|
613
|
+
out(chalk.gray(` Parent: ${branch.parentDatabase}`));
|
|
614
|
+
out("");
|
|
607
615
|
break;
|
|
608
616
|
}
|
|
609
617
|
|
|
610
618
|
case "list": {
|
|
611
619
|
const branches = await branchService.listBranches();
|
|
612
|
-
|
|
620
|
+
out("");
|
|
613
621
|
if (branches.length === 0) {
|
|
614
|
-
|
|
622
|
+
out(chalk.gray(" No branches found. Create one with: rebase db branch create <name>"));
|
|
615
623
|
} else {
|
|
616
|
-
|
|
617
|
-
|
|
624
|
+
out(chalk.bold(` 🌿 ${branches.length} branch(es):`));
|
|
625
|
+
out("");
|
|
618
626
|
for (const b of branches) {
|
|
619
627
|
const size = b.sizeBytes != null
|
|
620
628
|
? chalk.gray(` (${formatBytes(b.sizeBytes)})`)
|
|
621
629
|
: "";
|
|
622
630
|
const age = chalk.gray(` — created ${timeAgo(b.createdAt)}`);
|
|
623
|
-
|
|
624
|
-
|
|
631
|
+
out(` ${chalk.green("●")} ${chalk.bold(b.name)}${size}${age}`);
|
|
632
|
+
out(chalk.gray(` from ${b.parentDatabase}`));
|
|
625
633
|
}
|
|
626
634
|
}
|
|
627
|
-
|
|
635
|
+
out("");
|
|
628
636
|
break;
|
|
629
637
|
}
|
|
630
638
|
|
|
631
639
|
case "delete": {
|
|
632
640
|
const name = rawArgs[3];
|
|
633
641
|
if (!name) {
|
|
634
|
-
|
|
635
|
-
|
|
642
|
+
outError(chalk.red("✗ Branch name is required."));
|
|
643
|
+
out(chalk.gray(" Usage: rebase db branch delete <name>"));
|
|
636
644
|
process.exit(1);
|
|
637
645
|
}
|
|
638
|
-
|
|
639
|
-
|
|
646
|
+
out("");
|
|
647
|
+
out(chalk.bold(` 🗑️ Deleting branch "${name}"...`));
|
|
640
648
|
await branchService.deleteBranch(name);
|
|
641
|
-
|
|
642
|
-
|
|
649
|
+
out(chalk.green(` ✓ Branch "${name}" deleted.`));
|
|
650
|
+
out("");
|
|
643
651
|
break;
|
|
644
652
|
}
|
|
645
653
|
|
|
646
654
|
case "info": {
|
|
647
655
|
const name = rawArgs[3];
|
|
648
656
|
if (!name) {
|
|
649
|
-
|
|
650
|
-
|
|
657
|
+
outError(chalk.red("✗ Branch name is required."));
|
|
658
|
+
out(chalk.gray(" Usage: rebase db branch info <name>"));
|
|
651
659
|
process.exit(1);
|
|
652
660
|
}
|
|
653
661
|
const info = await branchService.getBranchInfo(name);
|
|
654
|
-
|
|
662
|
+
out("");
|
|
655
663
|
if (!info) {
|
|
656
|
-
|
|
664
|
+
outError(chalk.red(` ✗ Branch "${name}" not found.`));
|
|
657
665
|
} else {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
666
|
+
out(chalk.bold(` 🌿 Branch: ${info.name}`));
|
|
667
|
+
out(chalk.gray(` Database: rb_${info.name}`));
|
|
668
|
+
out(chalk.gray(` Parent: ${info.parentDatabase}`));
|
|
669
|
+
out(chalk.gray(` Created: ${info.createdAt.toISOString()}`));
|
|
662
670
|
if (info.sizeBytes != null) {
|
|
663
|
-
|
|
671
|
+
out(chalk.gray(` Size: ${formatBytes(info.sizeBytes)}`));
|
|
664
672
|
}
|
|
665
673
|
}
|
|
666
|
-
|
|
674
|
+
out("");
|
|
667
675
|
break;
|
|
668
676
|
}
|
|
669
677
|
|
|
670
678
|
default:
|
|
671
|
-
|
|
679
|
+
outError(chalk.red(`Unknown branch action: "${branchAction}".`));
|
|
672
680
|
printBranchHelp();
|
|
673
681
|
process.exit(1);
|
|
674
682
|
}
|
|
@@ -679,7 +687,7 @@ max: 3 });
|
|
|
679
687
|
}
|
|
680
688
|
|
|
681
689
|
function printBranchHelp() {
|
|
682
|
-
|
|
690
|
+
out(`
|
|
683
691
|
${chalk.bold("rebase db branch")} — Database branching commands
|
|
684
692
|
|
|
685
693
|
${chalk.green.bold("Usage")}
|
|
@@ -734,28 +742,28 @@ async function runAtlas(
|
|
|
734
742
|
// other — see `diagnoseMissingBin`. This used to say "Install it with:
|
|
735
743
|
// pnpm add -D @ariga/atlas" unconditionally, which is the exact command
|
|
736
744
|
// that produces the far more common of the two states.
|
|
737
|
-
|
|
745
|
+
outError(chalk.red("\n✗ The atlas binary is missing, so the schema cannot be applied.\n"));
|
|
738
746
|
|
|
739
747
|
if (diagnoseMissingBin("@ariga/atlas") === "build-script-blocked") {
|
|
740
|
-
|
|
741
|
-
|
|
748
|
+
outError(chalk.yellow(" @ariga/atlas IS installed — only its binary is missing.\n"));
|
|
749
|
+
outError(chalk.gray(
|
|
742
750
|
" It downloads that binary in a `preinstall` script, and pnpm 10+ does not\n" +
|
|
743
751
|
" run a dependency's scripts unless you allow it. The install still exits 0,\n" +
|
|
744
752
|
" so the only sign is `Ignored build scripts: @ariga/atlas` in its output.\n"
|
|
745
753
|
));
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
754
|
+
outError(" Fix it with either:\n");
|
|
755
|
+
outError(chalk.bold(" pnpm approve-builds\n"));
|
|
756
|
+
outError(" or, to record it in the project (what `rebase init` scaffolds):\n");
|
|
757
|
+
outError(chalk.bold(
|
|
750
758
|
" // package.json\n" +
|
|
751
759
|
" \"pnpm\": { \"onlyBuiltDependencies\": [\"@ariga/atlas\"] }\n"
|
|
752
760
|
));
|
|
753
|
-
|
|
761
|
+
outError(chalk.gray(" Then re-run `pnpm install`.\n"));
|
|
754
762
|
} else {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
763
|
+
outError(chalk.gray(" It is not installed in this project.\n"));
|
|
764
|
+
outError(" Install it with:\n");
|
|
765
|
+
outError(chalk.bold(" pnpm add -D @ariga/atlas\n"));
|
|
766
|
+
outError(chalk.gray(
|
|
759
767
|
" If pnpm then reports `Ignored build scripts`, also run `pnpm approve-builds` —\n" +
|
|
760
768
|
" the package carries a `preinstall` script that fetches the binary.\n"
|
|
761
769
|
));
|
|
@@ -775,7 +783,7 @@ async function runAtlas(
|
|
|
775
783
|
|
|
776
784
|
for (const p of envPaths) {
|
|
777
785
|
if (fs.existsSync(p)) {
|
|
778
|
-
const parsed = dotenv.config({ path: p });
|
|
786
|
+
const parsed = dotenv.config({ path: p, quiet: true });
|
|
779
787
|
if (parsed.parsed) {
|
|
780
788
|
for (const [key, val] of Object.entries(parsed.parsed)) {
|
|
781
789
|
if (env[key] === undefined) {
|
|
@@ -792,7 +800,7 @@ async function runAtlas(
|
|
|
792
800
|
|
|
793
801
|
const databaseUrl = env.DATABASE_URL;
|
|
794
802
|
if (!databaseUrl) {
|
|
795
|
-
|
|
803
|
+
outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
796
804
|
process.exit(1);
|
|
797
805
|
}
|
|
798
806
|
|
|
@@ -852,11 +860,11 @@ async function runAtlas(
|
|
|
852
860
|
excludes = await getTableExcludes(databaseUrl, collectionsPath);
|
|
853
861
|
} catch (err) {
|
|
854
862
|
if (err instanceof ExcludeIntrospectionError) {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
863
|
+
outError(chalk.red("\n✗ Aborting push: could not determine which tables to protect."));
|
|
864
|
+
outError(chalk.gray(` ${err.message}`));
|
|
865
|
+
outError(chalk.gray(" Refusing to apply — a partial exclude list could drop tables Rebase does not manage."));
|
|
858
866
|
const hint = diagnoseDbError(err.cause ?? err, databaseUrl);
|
|
859
|
-
if (hint)
|
|
867
|
+
if (hint) outError(hint);
|
|
860
868
|
process.exit(1);
|
|
861
869
|
}
|
|
862
870
|
throw err;
|
|
@@ -891,11 +899,11 @@ async function runAtlas(
|
|
|
891
899
|
try {
|
|
892
900
|
await subprocess;
|
|
893
901
|
} catch {
|
|
894
|
-
|
|
902
|
+
outError(chalk.red(`\n✗ atlas ${domain} ${args.join(" ")} failed.\n`));
|
|
895
903
|
// Surface actionable recovery guidance for recognized failures.
|
|
896
904
|
const hint = diagnoseDbError({ message: stderrText }, databaseUrl);
|
|
897
905
|
if (hint) {
|
|
898
|
-
|
|
906
|
+
outError(hint);
|
|
899
907
|
}
|
|
900
908
|
process.exit(1);
|
|
901
909
|
}
|
|
@@ -925,7 +933,7 @@ async function generatePostgresDdlCommand(rawArgs: string[]): Promise<void> {
|
|
|
925
933
|
const ddlScript = path.join(__cliDirname, "schema", "generate-postgres-ddl.ts");
|
|
926
934
|
const tsxBin = resolveLocalBin("tsx");
|
|
927
935
|
if (!tsxBin) {
|
|
928
|
-
|
|
936
|
+
outError(chalk.red("✗ Could not find tsx binary."));
|
|
929
937
|
process.exit(1);
|
|
930
938
|
}
|
|
931
939
|
|
|
@@ -946,7 +954,7 @@ async function generatePostgresDdlCommand(rawArgs: string[]): Promise<void> {
|
|
|
946
954
|
env: { ...process.env as Record<string, string> }
|
|
947
955
|
});
|
|
948
956
|
} catch (err: unknown) {
|
|
949
|
-
|
|
957
|
+
outError(chalk.red(`✗ Failed to run Postgres DDL generator: ${err instanceof Error ? err.message : String(err)}`));
|
|
950
958
|
process.exit(1);
|
|
951
959
|
}
|
|
952
960
|
}
|
|
@@ -1005,23 +1013,23 @@ async function schemaStaleCommand(rawArgs: string[]): Promise<void> {
|
|
|
1005
1013
|
|
|
1006
1014
|
if (stale.length === 0) return;
|
|
1007
1015
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1016
|
+
out("");
|
|
1017
|
+
outWarn(chalk.yellow(
|
|
1010
1018
|
` ⚠️ ${outputPath} names ${stale.length} foreign key(s) the way an earlier release did:`
|
|
1011
1019
|
));
|
|
1012
|
-
|
|
1013
|
-
|
|
1020
|
+
out(chalk.gray(describeLegacyForeignKeyNames(stale)));
|
|
1021
|
+
out("");
|
|
1014
1022
|
|
|
1015
1023
|
if (!argsList["--fix"]) {
|
|
1016
|
-
|
|
1024
|
+
outError(chalk.red(
|
|
1017
1025
|
" The database column has already been renamed at boot, so the generated schema no " +
|
|
1018
1026
|
"longer matches it and the server will refuse to start."
|
|
1019
1027
|
));
|
|
1020
|
-
|
|
1028
|
+
outError(chalk.red(" Run `rebase schema generate` to regenerate it."));
|
|
1021
1029
|
process.exit(1);
|
|
1022
1030
|
}
|
|
1023
1031
|
|
|
1024
|
-
|
|
1032
|
+
out(chalk.gray(" Regenerating the Drizzle schema so it matches..."));
|
|
1025
1033
|
await schemaCommand("generate", ["schema", "generate", `--collections=${collectionsPath}`, `--output=${outputPath}`]);
|
|
1026
1034
|
}
|
|
1027
1035
|
|
|
@@ -1051,13 +1059,13 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
|
|
|
1051
1059
|
// If installed in node_modules, __cliDirname is node_modules/@rebasepro/server-postgres/dist or src.
|
|
1052
1060
|
const generatorScript = path.join(__cliDirname, "schema", "generate-drizzle-schema.ts");
|
|
1053
1061
|
if (!fs.existsSync(generatorScript)) {
|
|
1054
|
-
|
|
1062
|
+
outError(chalk.red(`✗ Could not find generate-drizzle-schema.ts at ${generatorScript}`));
|
|
1055
1063
|
process.exit(1);
|
|
1056
1064
|
}
|
|
1057
1065
|
|
|
1058
1066
|
const tsxBin = resolveLocalBin("tsx");
|
|
1059
1067
|
if (!tsxBin) {
|
|
1060
|
-
|
|
1068
|
+
outError(chalk.red("✗ Could not find tsx binary."));
|
|
1061
1069
|
process.exit(1);
|
|
1062
1070
|
}
|
|
1063
1071
|
|
|
@@ -1065,9 +1073,9 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
|
|
|
1065
1073
|
const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
|
|
1066
1074
|
const watch = argsList["--watch"] || false;
|
|
1067
1075
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1076
|
+
out("");
|
|
1077
|
+
out(chalk.bold(" 🔧 Rebase Schema Generator"));
|
|
1078
|
+
out("");
|
|
1071
1079
|
|
|
1072
1080
|
const cmdParts = [
|
|
1073
1081
|
tsxBin,
|
|
@@ -1086,7 +1094,7 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
|
|
|
1086
1094
|
env: { ...process.env as Record<string, string> }
|
|
1087
1095
|
});
|
|
1088
1096
|
} catch (err: unknown) {
|
|
1089
|
-
|
|
1097
|
+
outError(chalk.red(`✗ Failed to run schema generator: ${err instanceof Error ? err.message : String(err)}`));
|
|
1090
1098
|
process.exit(1);
|
|
1091
1099
|
}
|
|
1092
1100
|
} else if (subcommand === "introspect") {
|
|
@@ -1108,21 +1116,21 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
|
|
|
1108
1116
|
|
|
1109
1117
|
const introspectScript = path.join(__cliDirname, "schema", "introspect-db.ts");
|
|
1110
1118
|
if (!fs.existsSync(introspectScript)) {
|
|
1111
|
-
|
|
1119
|
+
outError(chalk.red(`✗ Could not find introspect-db.ts at ${introspectScript}`));
|
|
1112
1120
|
process.exit(1);
|
|
1113
1121
|
}
|
|
1114
1122
|
|
|
1115
1123
|
const tsxBin = resolveLocalBin("tsx");
|
|
1116
1124
|
if (!tsxBin) {
|
|
1117
|
-
|
|
1125
|
+
outError(chalk.red("✗ Could not find tsx binary."));
|
|
1118
1126
|
process.exit(1);
|
|
1119
1127
|
}
|
|
1120
1128
|
|
|
1121
1129
|
const outputPath = argsList["--output"] || argsList["--collections"] || path.join("..", "config", "collections");
|
|
1122
1130
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1131
|
+
out("");
|
|
1132
|
+
out(chalk.bold(" 🔍 Rebase Schema Introspector"));
|
|
1133
|
+
out("");
|
|
1126
1134
|
|
|
1127
1135
|
const cmdParts = [
|
|
1128
1136
|
tsxBin,
|
|
@@ -1139,11 +1147,11 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
|
|
|
1139
1147
|
env: { ...process.env as Record<string, string> }
|
|
1140
1148
|
});
|
|
1141
1149
|
} catch (err: unknown) {
|
|
1142
|
-
|
|
1150
|
+
outError(chalk.red(`✗ Failed to run schema introspector: ${err instanceof Error ? err.message : String(err)}`));
|
|
1143
1151
|
process.exit(1);
|
|
1144
1152
|
}
|
|
1145
1153
|
} else {
|
|
1146
|
-
|
|
1154
|
+
outError(chalk.red("Unknown schema command."));
|
|
1147
1155
|
process.exit(1);
|
|
1148
1156
|
}
|
|
1149
1157
|
}
|
|
@@ -1167,13 +1175,13 @@ async function doctorPluginCommand(rawArgs: string[]): Promise<void> {
|
|
|
1167
1175
|
|
|
1168
1176
|
const doctorScript = path.join(__cliDirname, "schema", "doctor-cli.ts");
|
|
1169
1177
|
if (!fs.existsSync(doctorScript)) {
|
|
1170
|
-
|
|
1178
|
+
outError(chalk.red(`✗ Could not find doctor.ts at ${doctorScript}`));
|
|
1171
1179
|
process.exit(1);
|
|
1172
1180
|
}
|
|
1173
1181
|
|
|
1174
1182
|
const tsxBin = resolveLocalBin("tsx");
|
|
1175
1183
|
if (!tsxBin) {
|
|
1176
|
-
|
|
1184
|
+
outError(chalk.red("✗ Could not find tsx binary."));
|
|
1177
1185
|
process.exit(1);
|
|
1178
1186
|
}
|
|
1179
1187
|
|