@rebasepro/server-postgres 0.13.1-canary.gef9608c → 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/PostgresBackendDriver.d.ts +48 -1
- package/dist/PostgresBootstrapper.d.ts +26 -0
- package/dist/auth/services.d.ts +21 -0
- package/dist/{src-CU6WZGYV.js → auth-users-columns-BfQHf9JE.js} +1111 -92
- package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
- package/dist/{backup-service-CD8o_1Sl.js → backup-service-BH0Dzo_h.js} +2 -3
- package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
- package/dist/cli-helpers.d.ts +56 -0
- 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-BLIIACla.js → ensure-collection-tables-CbvaGuVn.js} +162 -16
- package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
- package/dist/index.es.js +1720 -946
- package/dist/index.es.js.map +1 -1
- package/dist/rls-bootstrap-sql-69hYT8nr.js +244 -0
- package/dist/rls-bootstrap-sql-69hYT8nr.js.map +1 -0
- 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/auth-users-columns.d.ts +97 -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/generate-postgres-ddl-logic.d.ts +48 -0
- 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/rls-bootstrap-sql.d.ts +135 -0
- package/dist/schema/search-column.d.ts +248 -0
- package/dist/security/policy-drift.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +61 -5
- package/dist/services/FetchService.d.ts +24 -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 +3 -0
- package/dist/services/junction-writes.d.ts +82 -0
- package/dist/services/realtimeService.d.ts +139 -2
- package/dist/services/write-denial.d.ts +36 -0
- package/dist/{src-DoU9yPqq.js → src-DCdn3Val.js} +124 -3
- package/dist/src-DCdn3Val.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +124 -2
- package/dist/{websocket-B2LsrINK.js → websocket-C8ZqVBiV.js} +75 -18
- package/dist/websocket-C8ZqVBiV.js.map +1 -0
- package/package.json +8 -7
- package/src/PostgresBackendDriver.ts +172 -6
- package/src/PostgresBootstrapper.ts +136 -11
- package/src/auth/ensure-tables.ts +212 -91
- 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 +124 -11
- package/src/cli-output.ts +43 -0
- package/src/cli.ts +299 -168
- package/src/collections/buildRegistry.ts +3 -1
- package/src/data-transformer.ts +129 -25
- package/src/history/ensure-history-table.ts +9 -2
- package/src/schema/auth-schema.ts +17 -1
- package/src/schema/auth-users-columns.ts +131 -0
- 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 +366 -30
- package/src/schema/generate-drizzle-schema-logic.ts +146 -66
- package/src/schema/generate-drizzle-schema.ts +11 -10
- package/src/schema/generate-postgres-ddl-logic.ts +277 -10
- package/src/schema/generate-postgres-ddl.ts +38 -14
- package/src/schema/generated-schema-staleness.ts +14 -7
- 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/rls-bootstrap-sql.ts +288 -0
- package/src/schema/search-column.ts +643 -0
- package/src/security/anonymous-grants.test.ts +4 -2
- package/src/security/policy-drift.test.ts +104 -3
- package/src/security/policy-drift.ts +129 -7
- package/src/security/rls-enforcement.ts +150 -7
- package/src/services/BranchService.ts +5 -0
- package/src/services/FetchService.ts +243 -22
- package/src/services/PersistService.ts +68 -42
- package/src/services/RelationService.ts +37 -696
- package/src/services/RelationWriteService.ts +653 -0
- package/src/services/cdc/trigger-cdc.ts +5 -1
- package/src/services/channel-history.ts +14 -0
- package/src/services/channel-presence.ts +13 -0
- package/src/services/collection-helpers.ts +89 -4
- package/src/services/dataService.ts +3 -0
- package/src/services/junction-writes.ts +295 -0
- package/src/services/pg-notify-listener.ts +1 -1
- package/src/services/realtimeService.ts +347 -86
- package/src/services/write-denial.ts +55 -0
- package/src/utils/drizzle-conditions.ts +433 -35
- package/src/utils/pg-error-utils.ts +8 -3
- package/src/websocket.ts +113 -16
- package/dist/ensure-collection-policies-Bck0ky4u.js +0 -57
- package/dist/ensure-collection-policies-Bck0ky4u.js.map +0 -1
- package/dist/ensure-collection-tables-BLIIACla.js.map +0 -1
- package/dist/policy-CeA1JcxP.js +0 -105
- package/dist/policy-CeA1JcxP.js.map +0 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
- package/dist/src-CU6WZGYV.js.map +0 -1
- package/dist/src-DoU9yPqq.js.map +0 -1
- package/dist/websocket-B2LsrINK.js.map +0 -1
- package/src/schema/auth-bootstrap-sql.ts +0 -47
package/src/cli.ts
CHANGED
|
@@ -5,19 +5,25 @@ 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";
|
|
9
|
+
import { formatRelativeTime } from "@rebasepro/utils";
|
|
8
10
|
import {
|
|
9
11
|
diagnoseMissingBin,
|
|
10
12
|
resolveLocalBin,
|
|
11
13
|
getTableIncludes,
|
|
12
14
|
getDevDatabaseUrl,
|
|
13
15
|
ensureDevDatabaseExists,
|
|
16
|
+
applySearchDdl,
|
|
17
|
+
getSearchExcludes,
|
|
18
|
+
readSearchDdl,
|
|
19
|
+
seedDevDatabaseSearchHelpers,
|
|
14
20
|
getTableExcludes,
|
|
15
21
|
ExcludeIntrospectionError,
|
|
16
22
|
promptConfirm
|
|
17
23
|
} from "./cli-helpers";
|
|
18
24
|
import { checkDatabaseConnectivity, diagnoseDbError } from "./cli-errors";
|
|
19
25
|
import { forLibpq } from "./utils/connection-string";
|
|
20
|
-
import {
|
|
26
|
+
import { dropLegacyAuthSchema, RLS_BOOTSTRAP_SQL } from "./schema/rls-bootstrap-sql";
|
|
21
27
|
import { detectDestructiveStatements, decidePushSafety } from "./schema/destructive-sql";
|
|
22
28
|
|
|
23
29
|
const __cliDirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -36,7 +42,7 @@ async function loadEnv(): Promise<void> {
|
|
|
36
42
|
|
|
37
43
|
for (const p of envPaths) {
|
|
38
44
|
if (fs.existsSync(p)) {
|
|
39
|
-
const parsed = dotenv.config({ path: p });
|
|
45
|
+
const parsed = dotenv.config({ path: p, quiet: true });
|
|
40
46
|
if (parsed.parsed) {
|
|
41
47
|
for (const [key, val] of Object.entries(parsed.parsed)) {
|
|
42
48
|
if (process.env[key] === undefined) {
|
|
@@ -53,6 +59,13 @@ async function loadEnv(): Promise<void> {
|
|
|
53
59
|
}
|
|
54
60
|
|
|
55
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
|
+
}
|
|
56
69
|
await loadEnv();
|
|
57
70
|
const domain = args[0]; // "db" or "schema"
|
|
58
71
|
const subcommand = args[1];
|
|
@@ -64,15 +77,22 @@ export async function runPluginCommand(args: string[]) {
|
|
|
64
77
|
} else if (domain === "doctor") {
|
|
65
78
|
await doctorPluginCommand(args);
|
|
66
79
|
} else {
|
|
67
|
-
|
|
80
|
+
outError(chalk.red(`Unknown domain command: ${domain}`));
|
|
68
81
|
process.exit(1);
|
|
69
82
|
}
|
|
70
83
|
}
|
|
71
84
|
|
|
85
|
+
/** The migration files on disk, sorted, or none if the directory is absent. */
|
|
86
|
+
function listMigrationFiles(): string[] {
|
|
87
|
+
const dir = path.resolve(process.cwd(), "drizzle", "migrations");
|
|
88
|
+
if (!fs.existsSync(dir)) return [];
|
|
89
|
+
return fs.readdirSync(dir).filter(f => f.endsWith(".sql")).sort();
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
73
93
|
const VALID_ACTIONS = ["push", "generate", "migrate", "branch", "backup", "restore", "backups"];
|
|
74
94
|
if (!subcommand || !VALID_ACTIONS.includes(subcommand)) {
|
|
75
|
-
|
|
95
|
+
outError(chalk.red(`Unknown db command. Valid: ${VALID_ACTIONS.join(", ")}`));
|
|
76
96
|
process.exit(1);
|
|
77
97
|
}
|
|
78
98
|
|
|
@@ -105,17 +125,19 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
105
125
|
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
106
126
|
|
|
107
127
|
if (subcommand === "generate") {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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("");
|
|
112
132
|
await schemaCommand("generate", rawArgs);
|
|
113
133
|
await generatePostgresDdlCommand(rawArgs);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
out("");
|
|
135
|
+
out(chalk.gray(" Step 2/2: Generating SQL migration files with Atlas..."));
|
|
136
|
+
out("");
|
|
117
137
|
const migrationName = argsList._[0] || "migration";
|
|
138
|
+
const migrationsBefore = listMigrationFiles();
|
|
118
139
|
await runAtlas("migrate", ["diff", migrationName, "--dir", "file://drizzle/migrations", "--to", "file://drizzle/schema.sql"], collectionsPath);
|
|
140
|
+
const wroteNewMigration = listMigrationFiles().length > migrationsBefore.length;
|
|
119
141
|
|
|
120
142
|
// Post-process the newest migration file
|
|
121
143
|
try {
|
|
@@ -136,45 +158,72 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
136
158
|
/CREATE SCHEMA (?!IF NOT EXISTS)("[^"]+");/g,
|
|
137
159
|
"CREATE SCHEMA IF NOT EXISTS $1;"
|
|
138
160
|
);
|
|
161
|
+
// Atlas never writes the search objects into a migration —
|
|
162
|
+
// it is not shown them — so a migration replayed against a
|
|
163
|
+
// fresh database would build every table except the part
|
|
164
|
+
// that makes search work. Appending, because these are
|
|
165
|
+
// `ALTER TABLE ... ADD COLUMN` over the tables the
|
|
166
|
+
// migration just created.
|
|
167
|
+
//
|
|
168
|
+
// Only into a migration Atlas just wrote. The file picked
|
|
169
|
+
// here is simply the newest one, which when there was no
|
|
170
|
+
// diff is a migration that has already run in production:
|
|
171
|
+
// editing it changes a hash Atlas has recorded, and the
|
|
172
|
+
// appended SQL would never be applied anywhere.
|
|
173
|
+
const searchContent = readSearchDdl();
|
|
174
|
+
if (searchContent && wroteNewMigration) {
|
|
175
|
+
migrationContent = `${migrationContent}\n\n${searchContent}`;
|
|
176
|
+
out(chalk.gray(" ✓ Appended search DDL to the migration"));
|
|
177
|
+
} else if (searchContent) {
|
|
178
|
+
// Reachable whenever a `search` block is the *only*
|
|
179
|
+
// thing that changed: Atlas cannot see one, so it finds
|
|
180
|
+
// nothing to diff and writes no file.
|
|
181
|
+
out(chalk.gray(
|
|
182
|
+
" ℹ Search DDL not written to a migration — this change produced none.\n" +
|
|
183
|
+
" It is applied by `rebase db push`, and at boot by the schema ensure.\n" +
|
|
184
|
+
" For a migration-only deployment, add drizzle/search.sql to a migration by hand."
|
|
185
|
+
));
|
|
186
|
+
}
|
|
187
|
+
|
|
139
188
|
fs.writeFileSync(newestMigrationFile, migrationContent, "utf-8");
|
|
140
189
|
|
|
141
|
-
// Append RLS policies, preceded by the
|
|
190
|
+
// Append RLS policies, preceded by the RLS bootstrap so the
|
|
142
191
|
// migration is self-contained: Atlas replays migrations
|
|
143
|
-
// against a clean dev database where `
|
|
144
|
-
// otherwise exist.
|
|
192
|
+
// against a clean dev database where `rebase.uid()` would
|
|
193
|
+
// not otherwise exist.
|
|
145
194
|
const policiesFile = path.resolve(process.cwd(), "drizzle", "policies.sql");
|
|
146
195
|
if (fs.existsSync(policiesFile)) {
|
|
147
196
|
const policiesContent = fs.readFileSync(policiesFile, "utf-8");
|
|
148
|
-
fs.appendFileSync(newestMigrationFile, "\n\n" +
|
|
149
|
-
|
|
197
|
+
fs.appendFileSync(newestMigrationFile, "\n\n" + RLS_BOOTSTRAP_SQL + "\n" + policiesContent);
|
|
198
|
+
out(chalk.gray(` ✓ Appended RLS policies to migration file: ${path.basename(newestMigrationFile)}`));
|
|
150
199
|
|
|
151
200
|
// Re-hash the migration directory
|
|
152
|
-
|
|
201
|
+
out(chalk.gray(" Re-hashing migration files..."));
|
|
153
202
|
await runAtlas("migrate", ["hash", "--dir", "file://drizzle/migrations"], collectionsPath);
|
|
154
|
-
|
|
203
|
+
out(chalk.gray(" ✓ Migration directory checksum updated successfully."));
|
|
155
204
|
}
|
|
156
205
|
}
|
|
157
206
|
}
|
|
158
207
|
} catch (err) {
|
|
159
|
-
|
|
208
|
+
outWarn(chalk.yellow(` ⚠️ Failed to append policies or re-hash migration: ${err instanceof Error ? err.message : String(err)}`));
|
|
160
209
|
}
|
|
161
210
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
211
|
+
out("");
|
|
212
|
+
out(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
|
|
213
|
+
out("");
|
|
165
214
|
} else {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
215
|
+
out("");
|
|
216
|
+
out(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
|
|
217
|
+
out("");
|
|
169
218
|
|
|
170
219
|
if (subcommand === "push") {
|
|
171
|
-
|
|
172
|
-
|
|
220
|
+
out(chalk.gray(" Step 1/3: Generating Drizzle schema & Postgres DDL from collections..."));
|
|
221
|
+
out("");
|
|
173
222
|
await schemaCommand("generate", rawArgs);
|
|
174
223
|
await generatePostgresDdlCommand(rawArgs);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
224
|
+
out("");
|
|
225
|
+
out(chalk.gray(" Step 2/3: Pushing schema to database with Atlas..."));
|
|
226
|
+
out("");
|
|
178
227
|
const databaseUrl = process.env.DATABASE_URL;
|
|
179
228
|
if (databaseUrl) {
|
|
180
229
|
await ensureAuthSchemaAndFunctions(databaseUrl);
|
|
@@ -199,19 +248,19 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
199
248
|
});
|
|
200
249
|
|
|
201
250
|
if (destructive.length > 0) {
|
|
202
|
-
|
|
203
|
-
|
|
251
|
+
outWarn(chalk.yellow(` ⚠️ This push includes ${destructive.length} destructive change(s) that will DESTROY data:`));
|
|
252
|
+
outWarn("");
|
|
204
253
|
for (const d of destructive) {
|
|
205
|
-
|
|
254
|
+
outWarn(chalk.red(` ${d.kind}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
|
|
206
255
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
256
|
+
outWarn("");
|
|
257
|
+
outWarn(chalk.yellow(" Full planned changes:"));
|
|
258
|
+
outWarn(chalk.gray(plan.trim().split("\n").map((l) => ` ${l}`).join("\n")));
|
|
259
|
+
outWarn("");
|
|
211
260
|
|
|
212
261
|
if (decision === "refuse") {
|
|
213
|
-
|
|
214
|
-
|
|
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"));
|
|
215
264
|
process.exit(1);
|
|
216
265
|
}
|
|
217
266
|
if (decision === "confirm") {
|
|
@@ -219,25 +268,30 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
219
268
|
chalk.yellow(" Type 'yes' to apply these destructive changes (this cannot be undone): ")
|
|
220
269
|
);
|
|
221
270
|
if (!confirmed) {
|
|
222
|
-
|
|
271
|
+
out(chalk.gray(" Aborted. No changes were made."));
|
|
223
272
|
process.exit(1);
|
|
224
273
|
}
|
|
225
274
|
} else {
|
|
226
275
|
// decision === "apply" via --allow-destructive
|
|
227
|
-
|
|
276
|
+
outWarn(chalk.yellow(" Proceeding because --allow-destructive was passed."));
|
|
228
277
|
}
|
|
229
278
|
}
|
|
230
279
|
|
|
231
280
|
await runAtlas("schema", ["apply", "--to", "file://drizzle/schema.sql", "--auto-approve"], collectionsPath);
|
|
232
|
-
|
|
281
|
+
out("");
|
|
233
282
|
|
|
234
283
|
if (databaseUrl) {
|
|
235
284
|
await ensureAuthTables(databaseUrl, collectionsPath);
|
|
285
|
+
// After the tables exist and before the policies: the search
|
|
286
|
+
// column is `ALTER TABLE ... ADD COLUMN`, and a policy may
|
|
287
|
+
// reference the table it is added to.
|
|
288
|
+
await applySearchDdl(databaseUrl);
|
|
236
289
|
await applyPolicies(databaseUrl);
|
|
237
290
|
await reconcilePolicies(databaseUrl, collectionsPath);
|
|
238
291
|
await ensureRlsUserRole(databaseUrl);
|
|
292
|
+
await retireLegacyAuthSchema(databaseUrl);
|
|
239
293
|
} else {
|
|
240
|
-
|
|
294
|
+
outWarn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
|
|
241
295
|
}
|
|
242
296
|
} else if (subcommand === "migrate") {
|
|
243
297
|
const databaseUrl = process.env.DATABASE_URL;
|
|
@@ -248,12 +302,13 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
248
302
|
await runAtlas("migrate", ["apply", "--dir", "file://drizzle/migrations", ...extraArgs], collectionsPath);
|
|
249
303
|
if (databaseUrl) {
|
|
250
304
|
await ensureRlsUserRole(databaseUrl);
|
|
305
|
+
await retireLegacyAuthSchema(databaseUrl);
|
|
251
306
|
}
|
|
252
307
|
}
|
|
253
308
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
309
|
+
out("");
|
|
310
|
+
out(chalk.green(` ✓ rebase db ${subcommand} completed successfully.`));
|
|
311
|
+
out("");
|
|
257
312
|
}
|
|
258
313
|
}
|
|
259
314
|
|
|
@@ -263,16 +318,20 @@ async function ensureAuthSchemaAndFunctions(databaseUrl: string): Promise<void>
|
|
|
263
318
|
const client = new Client({ connectionString: databaseUrl });
|
|
264
319
|
await client.connect();
|
|
265
320
|
try {
|
|
266
|
-
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
|
|
321
|
+
// Creates the `rebase` schema as its first statement, which also
|
|
322
|
+
// covers the schema Atlas puts its revision table in
|
|
323
|
+
// (`--revisions-schema rebase`). That used to need a separate,
|
|
324
|
+
// deliberately migration-stream-excluded statement here, because the
|
|
325
|
+
// helper functions lived in `auth` and creating `rebase` from the
|
|
326
|
+
// preamble would have made Atlas plan a DROP for it. The generator
|
|
327
|
+
// now always declares `rebase` in the desired schema, so there is
|
|
328
|
+
// nothing to keep out.
|
|
329
|
+
await client.query(RLS_BOOTSTRAP_SQL);
|
|
271
330
|
} finally {
|
|
272
331
|
await client.end();
|
|
273
332
|
}
|
|
274
333
|
} catch (err) {
|
|
275
|
-
|
|
334
|
+
outWarn(chalk.yellow(` ⚠️ Failed to bootstrap the RLS helper functions: ${err instanceof Error ? err.message : String(err)}`));
|
|
276
335
|
}
|
|
277
336
|
}
|
|
278
337
|
|
|
@@ -309,7 +368,7 @@ async function ensureAuthTables(databaseUrl: string, collectionsPath: string): P
|
|
|
309
368
|
await client.end();
|
|
310
369
|
}
|
|
311
370
|
} catch (err) {
|
|
312
|
-
|
|
371
|
+
outWarn(chalk.yellow(` ⚠️ Failed to ensure framework auth tables: ${err instanceof Error ? err.message : String(err)}`));
|
|
313
372
|
}
|
|
314
373
|
}
|
|
315
374
|
|
|
@@ -320,19 +379,82 @@ async function ensureAuthTables(databaseUrl: string, collectionsPath: string): P
|
|
|
320
379
|
* connection would bypass RLS (superuser / BYPASSRLS / table owner).
|
|
321
380
|
*/
|
|
322
381
|
async function ensureRlsUserRole(databaseUrl: string): Promise<void> {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
382
|
+
// Every failure in here used to escape uncaught, and `db migrate`'s caller
|
|
383
|
+
// turns that into a bare `process.exit(1)` — so the migration would apply,
|
|
384
|
+
// print `-- ok`, and then the command would die with NO output whatsoever
|
|
385
|
+
// and a non-zero status. Seen with a module-resolution error inside the
|
|
386
|
+
// dynamic import, where the entire diagnosis was one silent exit code.
|
|
387
|
+
//
|
|
388
|
+
// Reported rather than rethrown, and deliberately not fatal: the schema
|
|
389
|
+
// change has already landed at this point, so failing the command implies a
|
|
390
|
+
// rollback that did not happen. What is actually lost is the role
|
|
391
|
+
// provisioning, and the message says so and how to finish it by hand.
|
|
392
|
+
// No `auth`: the RLS helpers live in `rebase` now, and a schema the
|
|
393
|
+
// framework no longer creates must not be granted on — on a Supabase
|
|
394
|
+
// database that would hand the end-user role USAGE on their auth schema.
|
|
395
|
+
const schemas = ["public", "rebase"];
|
|
396
|
+
let rls: typeof import("./security/rls-enforcement");
|
|
397
|
+
try {
|
|
398
|
+
rls = await import("./security/rls-enforcement");
|
|
399
|
+
} catch (err) {
|
|
400
|
+
// A module-resolution failure here is a build problem, not a database
|
|
401
|
+
// one, and it is exactly the case that used to produce the silent exit.
|
|
402
|
+
outError(chalk.red(
|
|
403
|
+
`\n ✗ Could not load the RLS provisioning module: ${err instanceof Error ? err.message : String(err)}`
|
|
404
|
+
));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
327
408
|
try {
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
409
|
+
const { Client } = await import("pg");
|
|
410
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
411
|
+
await client.connect();
|
|
412
|
+
try {
|
|
413
|
+
const runSql = async (text: string) => (await client.query(text)).rows as Record<string, unknown>[];
|
|
414
|
+
const posture = await rls.detectConnectionPosture(runSql);
|
|
415
|
+
if (posture.privileged) {
|
|
416
|
+
await rls.ensureAppRole(runSql, schemas);
|
|
417
|
+
out(chalk.gray(` ✓ RLS role "${rls.REBASE_USER_ROLE}" provisioned/refreshed.`));
|
|
418
|
+
}
|
|
419
|
+
} finally {
|
|
420
|
+
await client.end();
|
|
333
421
|
}
|
|
334
|
-
}
|
|
335
|
-
|
|
422
|
+
} catch (err) {
|
|
423
|
+
outError(chalk.red(
|
|
424
|
+
`\n ✗ The schema change was applied, but the "${rls.REBASE_USER_ROLE}" role could not be ` +
|
|
425
|
+
`provisioned: ${err instanceof Error ? err.message : String(err)}`
|
|
426
|
+
));
|
|
427
|
+
outError(chalk.gray(rls.appRoleSetupInstructions("your database user", schemas)));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Retire the pre-1.0 `auth` schema, once nothing depends on it any more.
|
|
433
|
+
*
|
|
434
|
+
* Runs last on purpose. Postgres refuses to drop a function an RLS policy still
|
|
435
|
+
* calls, so this only succeeds after the policies above have been rewritten to
|
|
436
|
+
* `rebase.uid()`. See `dropLegacyAuthSchema` for what it reports when something
|
|
437
|
+
* hand-written is still holding the schema open, and DROP_LEGACY_AUTH_SCHEMA_SQL
|
|
438
|
+
* for the guards that keep it off a Supabase `auth` schema.
|
|
439
|
+
*/
|
|
440
|
+
async function retireLegacyAuthSchema(databaseUrl: string): Promise<void> {
|
|
441
|
+
try {
|
|
442
|
+
const { Client } = await import("pg");
|
|
443
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
444
|
+
await client.connect();
|
|
445
|
+
try {
|
|
446
|
+
await dropLegacyAuthSchema(
|
|
447
|
+
async (text) => (await client.query(text)).rows as Record<string, unknown>[],
|
|
448
|
+
{
|
|
449
|
+
info: (m) => out(chalk.gray(` ${m}`)),
|
|
450
|
+
warn: (m) => outWarn(chalk.yellow(` ⚠️ ${m}`))
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
} finally {
|
|
454
|
+
await client.end();
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
// Connection-level failure only; the schema is inert either way.
|
|
336
458
|
}
|
|
337
459
|
}
|
|
338
460
|
|
|
@@ -341,8 +463,8 @@ async function applyPolicies(databaseUrl: string): Promise<void> {
|
|
|
341
463
|
const policiesPath = path.resolve(process.cwd(), "drizzle", "policies.sql");
|
|
342
464
|
if (!fs.existsSync(policiesPath)) return;
|
|
343
465
|
|
|
344
|
-
|
|
345
|
-
|
|
466
|
+
out(chalk.gray(" Step 3/3: Applying RLS policies to database..."));
|
|
467
|
+
out("");
|
|
346
468
|
|
|
347
469
|
const policiesContent = fs.readFileSync(policiesPath, "utf-8");
|
|
348
470
|
const { Client } = await import("pg");
|
|
@@ -350,16 +472,16 @@ async function applyPolicies(databaseUrl: string): Promise<void> {
|
|
|
350
472
|
await client.connect();
|
|
351
473
|
try {
|
|
352
474
|
await client.query(policiesContent);
|
|
353
|
-
|
|
475
|
+
out(chalk.green(" ✓ RLS policies applied successfully."));
|
|
354
476
|
} finally {
|
|
355
477
|
await client.end();
|
|
356
478
|
}
|
|
357
479
|
} catch (err) {
|
|
358
480
|
const hint = diagnoseDbError(err, databaseUrl);
|
|
359
481
|
if (hint) {
|
|
360
|
-
|
|
482
|
+
outError(hint);
|
|
361
483
|
} else {
|
|
362
|
-
|
|
484
|
+
outError(chalk.red(` ✗ Failed to apply RLS policies: ${err instanceof Error ? err.message : String(err)}`));
|
|
363
485
|
}
|
|
364
486
|
process.exit(1);
|
|
365
487
|
}
|
|
@@ -393,34 +515,34 @@ async function reconcilePolicies(databaseUrl: string, collectionsPath: string):
|
|
|
393
515
|
const { dropped, kept } = await dropOrphanedPolicies(client as never, drift, collections);
|
|
394
516
|
|
|
395
517
|
for (const p of dropped) {
|
|
396
|
-
|
|
518
|
+
out(chalk.gray(` ✓ Dropped superseded policy "${p.name}" on ${p.schema}.${p.table}`));
|
|
397
519
|
}
|
|
398
520
|
if (dropped.length > 0) {
|
|
399
|
-
|
|
521
|
+
out(chalk.green(` ✓ Removed ${dropped.length} superseded RLS ${dropped.length === 1 ? "policy" : "policies"}.`));
|
|
400
522
|
}
|
|
401
523
|
|
|
402
524
|
// Custom-named orphans are indistinguishable from policies someone
|
|
403
525
|
// wrote in SQL deliberately, so they are reported, never dropped.
|
|
404
526
|
if (kept.length > 0) {
|
|
405
|
-
|
|
527
|
+
outWarn(chalk.yellow(" ⚠️ Policies in the database that no collection describes:"));
|
|
406
528
|
for (const p of kept) {
|
|
407
|
-
|
|
529
|
+
outWarn(chalk.yellow(` • ${p.schema}.${p.table} → "${p.name}" (${p.command} TO ${p.roles.join(", ")})`));
|
|
408
530
|
}
|
|
409
|
-
|
|
531
|
+
outWarn(chalk.yellow(" These still grant access. Drop them by hand if they are stale."));
|
|
410
532
|
}
|
|
411
533
|
|
|
412
534
|
// Missing/diverged are not push's to fix, but staying silent about
|
|
413
535
|
// them is how a database ends up not matching its config.
|
|
414
536
|
const remaining = { ...drift, orphaned: kept };
|
|
415
537
|
if (hasDrift(remaining) && (remaining.missing.length > 0 || remaining.diverged.length > 0)) {
|
|
416
|
-
|
|
417
|
-
|
|
538
|
+
outWarn(chalk.yellow(" ⚠️ RLS policies do not match your collections:"));
|
|
539
|
+
outWarn(formatPolicyDrift({ ...remaining, orphaned: [] }));
|
|
418
540
|
}
|
|
419
541
|
} finally {
|
|
420
542
|
await client.end();
|
|
421
543
|
}
|
|
422
544
|
} catch (err) {
|
|
423
|
-
|
|
545
|
+
outWarn(chalk.yellow(` ⚠️ Could not reconcile RLS policies: ${err instanceof Error ? err.message : String(err)}`));
|
|
424
546
|
}
|
|
425
547
|
}
|
|
426
548
|
|
|
@@ -437,9 +559,9 @@ async function branchCommand(rawArgs: string[]): Promise<void> {
|
|
|
437
559
|
const dotenv = await import("dotenv");
|
|
438
560
|
const envPath = process.env.DOTENV_CONFIG_PATH;
|
|
439
561
|
if (envPath) {
|
|
440
|
-
dotenv.config({ path: envPath });
|
|
562
|
+
dotenv.config({ path: envPath, quiet: true });
|
|
441
563
|
} else {
|
|
442
|
-
dotenv.config();
|
|
564
|
+
dotenv.config({ quiet: true });
|
|
443
565
|
}
|
|
444
566
|
} catch {
|
|
445
567
|
// dotenv may not be installed
|
|
@@ -447,7 +569,7 @@ async function branchCommand(rawArgs: string[]): Promise<void> {
|
|
|
447
569
|
|
|
448
570
|
const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
|
|
449
571
|
if (!databaseUrl) {
|
|
450
|
-
|
|
572
|
+
outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
451
573
|
process.exit(1);
|
|
452
574
|
}
|
|
453
575
|
|
|
@@ -471,8 +593,8 @@ max: 3 });
|
|
|
471
593
|
case "create": {
|
|
472
594
|
const name = rawArgs[3];
|
|
473
595
|
if (!name) {
|
|
474
|
-
|
|
475
|
-
|
|
596
|
+
outError(chalk.red("✗ Branch name is required."));
|
|
597
|
+
out(chalk.gray(" Usage: rebase db branch create <name> [--from <source>]"));
|
|
476
598
|
process.exit(1);
|
|
477
599
|
}
|
|
478
600
|
let source: string | undefined;
|
|
@@ -480,81 +602,81 @@ max: 3 });
|
|
|
480
602
|
if (fromIdx !== -1 && rawArgs[fromIdx + 1]) {
|
|
481
603
|
source = rawArgs[fromIdx + 1];
|
|
482
604
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (source)
|
|
487
|
-
|
|
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("");
|
|
488
610
|
const branch = await branchService.createBranch(name, source ? { source } : undefined);
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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("");
|
|
493
615
|
break;
|
|
494
616
|
}
|
|
495
617
|
|
|
496
618
|
case "list": {
|
|
497
619
|
const branches = await branchService.listBranches();
|
|
498
|
-
|
|
620
|
+
out("");
|
|
499
621
|
if (branches.length === 0) {
|
|
500
|
-
|
|
622
|
+
out(chalk.gray(" No branches found. Create one with: rebase db branch create <name>"));
|
|
501
623
|
} else {
|
|
502
|
-
|
|
503
|
-
|
|
624
|
+
out(chalk.bold(` 🌿 ${branches.length} branch(es):`));
|
|
625
|
+
out("");
|
|
504
626
|
for (const b of branches) {
|
|
505
627
|
const size = b.sizeBytes != null
|
|
506
628
|
? chalk.gray(` (${formatBytes(b.sizeBytes)})`)
|
|
507
629
|
: "";
|
|
508
630
|
const age = chalk.gray(` — created ${timeAgo(b.createdAt)}`);
|
|
509
|
-
|
|
510
|
-
|
|
631
|
+
out(` ${chalk.green("●")} ${chalk.bold(b.name)}${size}${age}`);
|
|
632
|
+
out(chalk.gray(` from ${b.parentDatabase}`));
|
|
511
633
|
}
|
|
512
634
|
}
|
|
513
|
-
|
|
635
|
+
out("");
|
|
514
636
|
break;
|
|
515
637
|
}
|
|
516
638
|
|
|
517
639
|
case "delete": {
|
|
518
640
|
const name = rawArgs[3];
|
|
519
641
|
if (!name) {
|
|
520
|
-
|
|
521
|
-
|
|
642
|
+
outError(chalk.red("✗ Branch name is required."));
|
|
643
|
+
out(chalk.gray(" Usage: rebase db branch delete <name>"));
|
|
522
644
|
process.exit(1);
|
|
523
645
|
}
|
|
524
|
-
|
|
525
|
-
|
|
646
|
+
out("");
|
|
647
|
+
out(chalk.bold(` 🗑️ Deleting branch "${name}"...`));
|
|
526
648
|
await branchService.deleteBranch(name);
|
|
527
|
-
|
|
528
|
-
|
|
649
|
+
out(chalk.green(` ✓ Branch "${name}" deleted.`));
|
|
650
|
+
out("");
|
|
529
651
|
break;
|
|
530
652
|
}
|
|
531
653
|
|
|
532
654
|
case "info": {
|
|
533
655
|
const name = rawArgs[3];
|
|
534
656
|
if (!name) {
|
|
535
|
-
|
|
536
|
-
|
|
657
|
+
outError(chalk.red("✗ Branch name is required."));
|
|
658
|
+
out(chalk.gray(" Usage: rebase db branch info <name>"));
|
|
537
659
|
process.exit(1);
|
|
538
660
|
}
|
|
539
661
|
const info = await branchService.getBranchInfo(name);
|
|
540
|
-
|
|
662
|
+
out("");
|
|
541
663
|
if (!info) {
|
|
542
|
-
|
|
664
|
+
outError(chalk.red(` ✗ Branch "${name}" not found.`));
|
|
543
665
|
} else {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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()}`));
|
|
548
670
|
if (info.sizeBytes != null) {
|
|
549
|
-
|
|
671
|
+
out(chalk.gray(` Size: ${formatBytes(info.sizeBytes)}`));
|
|
550
672
|
}
|
|
551
673
|
}
|
|
552
|
-
|
|
674
|
+
out("");
|
|
553
675
|
break;
|
|
554
676
|
}
|
|
555
677
|
|
|
556
678
|
default:
|
|
557
|
-
|
|
679
|
+
outError(chalk.red(`Unknown branch action: "${branchAction}".`));
|
|
558
680
|
printBranchHelp();
|
|
559
681
|
process.exit(1);
|
|
560
682
|
}
|
|
@@ -565,7 +687,7 @@ max: 3 });
|
|
|
565
687
|
}
|
|
566
688
|
|
|
567
689
|
function printBranchHelp() {
|
|
568
|
-
|
|
690
|
+
out(`
|
|
569
691
|
${chalk.bold("rebase db branch")} — Database branching commands
|
|
570
692
|
|
|
571
693
|
${chalk.green.bold("Usage")}
|
|
@@ -599,15 +721,11 @@ function formatBytes(bytes: number): string {
|
|
|
599
721
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
600
722
|
}
|
|
601
723
|
|
|
724
|
+
/** A branch's age, for "…created 6d ago". */
|
|
602
725
|
function timeAgo(date: Date): string {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (minutes < 60) return `${minutes}m ago`;
|
|
607
|
-
const hours = Math.floor(minutes / 60);
|
|
608
|
-
if (hours < 24) return `${hours}h ago`;
|
|
609
|
-
const days = Math.floor(hours / 24);
|
|
610
|
-
return `${days}d ago`;
|
|
726
|
+
// No horizon: a branch created a year ago should still report an age here,
|
|
727
|
+
// rather than fall back to a bare date in a one-line list entry.
|
|
728
|
+
return formatRelativeTime(date, { maxMs: Number.POSITIVE_INFINITY }) ?? "unknown";
|
|
611
729
|
}
|
|
612
730
|
|
|
613
731
|
|
|
@@ -624,28 +742,28 @@ async function runAtlas(
|
|
|
624
742
|
// other — see `diagnoseMissingBin`. This used to say "Install it with:
|
|
625
743
|
// pnpm add -D @ariga/atlas" unconditionally, which is the exact command
|
|
626
744
|
// that produces the far more common of the two states.
|
|
627
|
-
|
|
745
|
+
outError(chalk.red("\n✗ The atlas binary is missing, so the schema cannot be applied.\n"));
|
|
628
746
|
|
|
629
747
|
if (diagnoseMissingBin("@ariga/atlas") === "build-script-blocked") {
|
|
630
|
-
|
|
631
|
-
|
|
748
|
+
outError(chalk.yellow(" @ariga/atlas IS installed — only its binary is missing.\n"));
|
|
749
|
+
outError(chalk.gray(
|
|
632
750
|
" It downloads that binary in a `preinstall` script, and pnpm 10+ does not\n" +
|
|
633
751
|
" run a dependency's scripts unless you allow it. The install still exits 0,\n" +
|
|
634
752
|
" so the only sign is `Ignored build scripts: @ariga/atlas` in its output.\n"
|
|
635
753
|
));
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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(
|
|
640
758
|
" // package.json\n" +
|
|
641
759
|
" \"pnpm\": { \"onlyBuiltDependencies\": [\"@ariga/atlas\"] }\n"
|
|
642
760
|
));
|
|
643
|
-
|
|
761
|
+
outError(chalk.gray(" Then re-run `pnpm install`.\n"));
|
|
644
762
|
} else {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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(
|
|
649
767
|
" If pnpm then reports `Ignored build scripts`, also run `pnpm approve-builds` —\n" +
|
|
650
768
|
" the package carries a `preinstall` script that fetches the binary.\n"
|
|
651
769
|
));
|
|
@@ -665,7 +783,7 @@ async function runAtlas(
|
|
|
665
783
|
|
|
666
784
|
for (const p of envPaths) {
|
|
667
785
|
if (fs.existsSync(p)) {
|
|
668
|
-
const parsed = dotenv.config({ path: p });
|
|
786
|
+
const parsed = dotenv.config({ path: p, quiet: true });
|
|
669
787
|
if (parsed.parsed) {
|
|
670
788
|
for (const [key, val] of Object.entries(parsed.parsed)) {
|
|
671
789
|
if (env[key] === undefined) {
|
|
@@ -682,7 +800,7 @@ async function runAtlas(
|
|
|
682
800
|
|
|
683
801
|
const databaseUrl = env.DATABASE_URL;
|
|
684
802
|
if (!databaseUrl) {
|
|
685
|
-
|
|
803
|
+
outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
686
804
|
process.exit(1);
|
|
687
805
|
}
|
|
688
806
|
|
|
@@ -693,6 +811,9 @@ async function runAtlas(
|
|
|
693
811
|
|
|
694
812
|
const devDatabaseUrl = getDevDatabaseUrl(databaseUrl);
|
|
695
813
|
await ensureDevDatabaseExists(databaseUrl, devDatabaseUrl);
|
|
814
|
+
if (collectionsPath) {
|
|
815
|
+
await seedDevDatabaseSearchHelpers(devDatabaseUrl, collectionsPath);
|
|
816
|
+
}
|
|
696
817
|
|
|
697
818
|
// Atlas speaks libpq, which rejects the `sslmode=no-verify` that
|
|
698
819
|
// node-postgres accepts — see `forLibpq`. Rewritten only for the argv, so
|
|
@@ -719,6 +840,16 @@ async function runAtlas(
|
|
|
719
840
|
}
|
|
720
841
|
}
|
|
721
842
|
|
|
843
|
+
// Search objects are Rebase's, not Atlas's — on both paths. `schema apply`
|
|
844
|
+
// would drop them as absent from the desired state; `migrate diff` would
|
|
845
|
+
// write that same drop into the next migration file, where it would sit
|
|
846
|
+
// waiting to be applied later.
|
|
847
|
+
if (collectionsPath && (args.includes("apply") || args.includes("diff"))) {
|
|
848
|
+
for (const exc of await getSearchExcludes(collectionsPath)) {
|
|
849
|
+
atlasArgs.push("--exclude", exc);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
722
853
|
if (domain === "schema" && args.includes("apply") && collectionsPath) {
|
|
723
854
|
// Fail CLOSED: the exclude list is the only thing shielding
|
|
724
855
|
// non-collection tables from the auto-approved apply. If we can't
|
|
@@ -729,11 +860,11 @@ async function runAtlas(
|
|
|
729
860
|
excludes = await getTableExcludes(databaseUrl, collectionsPath);
|
|
730
861
|
} catch (err) {
|
|
731
862
|
if (err instanceof ExcludeIntrospectionError) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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."));
|
|
735
866
|
const hint = diagnoseDbError(err.cause ?? err, databaseUrl);
|
|
736
|
-
if (hint)
|
|
867
|
+
if (hint) outError(hint);
|
|
737
868
|
process.exit(1);
|
|
738
869
|
}
|
|
739
870
|
throw err;
|
|
@@ -768,11 +899,11 @@ async function runAtlas(
|
|
|
768
899
|
try {
|
|
769
900
|
await subprocess;
|
|
770
901
|
} catch {
|
|
771
|
-
|
|
902
|
+
outError(chalk.red(`\n✗ atlas ${domain} ${args.join(" ")} failed.\n`));
|
|
772
903
|
// Surface actionable recovery guidance for recognized failures.
|
|
773
904
|
const hint = diagnoseDbError({ message: stderrText }, databaseUrl);
|
|
774
905
|
if (hint) {
|
|
775
|
-
|
|
906
|
+
outError(hint);
|
|
776
907
|
}
|
|
777
908
|
process.exit(1);
|
|
778
909
|
}
|
|
@@ -802,7 +933,7 @@ async function generatePostgresDdlCommand(rawArgs: string[]): Promise<void> {
|
|
|
802
933
|
const ddlScript = path.join(__cliDirname, "schema", "generate-postgres-ddl.ts");
|
|
803
934
|
const tsxBin = resolveLocalBin("tsx");
|
|
804
935
|
if (!tsxBin) {
|
|
805
|
-
|
|
936
|
+
outError(chalk.red("✗ Could not find tsx binary."));
|
|
806
937
|
process.exit(1);
|
|
807
938
|
}
|
|
808
939
|
|
|
@@ -823,7 +954,7 @@ async function generatePostgresDdlCommand(rawArgs: string[]): Promise<void> {
|
|
|
823
954
|
env: { ...process.env as Record<string, string> }
|
|
824
955
|
});
|
|
825
956
|
} catch (err: unknown) {
|
|
826
|
-
|
|
957
|
+
outError(chalk.red(`✗ Failed to run Postgres DDL generator: ${err instanceof Error ? err.message : String(err)}`));
|
|
827
958
|
process.exit(1);
|
|
828
959
|
}
|
|
829
960
|
}
|
|
@@ -882,23 +1013,23 @@ async function schemaStaleCommand(rawArgs: string[]): Promise<void> {
|
|
|
882
1013
|
|
|
883
1014
|
if (stale.length === 0) return;
|
|
884
1015
|
|
|
885
|
-
|
|
886
|
-
|
|
1016
|
+
out("");
|
|
1017
|
+
outWarn(chalk.yellow(
|
|
887
1018
|
` ⚠️ ${outputPath} names ${stale.length} foreign key(s) the way an earlier release did:`
|
|
888
1019
|
));
|
|
889
|
-
|
|
890
|
-
|
|
1020
|
+
out(chalk.gray(describeLegacyForeignKeyNames(stale)));
|
|
1021
|
+
out("");
|
|
891
1022
|
|
|
892
1023
|
if (!argsList["--fix"]) {
|
|
893
|
-
|
|
1024
|
+
outError(chalk.red(
|
|
894
1025
|
" The database column has already been renamed at boot, so the generated schema no " +
|
|
895
1026
|
"longer matches it and the server will refuse to start."
|
|
896
1027
|
));
|
|
897
|
-
|
|
1028
|
+
outError(chalk.red(" Run `rebase schema generate` to regenerate it."));
|
|
898
1029
|
process.exit(1);
|
|
899
1030
|
}
|
|
900
1031
|
|
|
901
|
-
|
|
1032
|
+
out(chalk.gray(" Regenerating the Drizzle schema so it matches..."));
|
|
902
1033
|
await schemaCommand("generate", ["schema", "generate", `--collections=${collectionsPath}`, `--output=${outputPath}`]);
|
|
903
1034
|
}
|
|
904
1035
|
|
|
@@ -928,13 +1059,13 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
|
|
|
928
1059
|
// If installed in node_modules, __cliDirname is node_modules/@rebasepro/server-postgres/dist or src.
|
|
929
1060
|
const generatorScript = path.join(__cliDirname, "schema", "generate-drizzle-schema.ts");
|
|
930
1061
|
if (!fs.existsSync(generatorScript)) {
|
|
931
|
-
|
|
1062
|
+
outError(chalk.red(`✗ Could not find generate-drizzle-schema.ts at ${generatorScript}`));
|
|
932
1063
|
process.exit(1);
|
|
933
1064
|
}
|
|
934
1065
|
|
|
935
1066
|
const tsxBin = resolveLocalBin("tsx");
|
|
936
1067
|
if (!tsxBin) {
|
|
937
|
-
|
|
1068
|
+
outError(chalk.red("✗ Could not find tsx binary."));
|
|
938
1069
|
process.exit(1);
|
|
939
1070
|
}
|
|
940
1071
|
|
|
@@ -942,9 +1073,9 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
|
|
|
942
1073
|
const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
|
|
943
1074
|
const watch = argsList["--watch"] || false;
|
|
944
1075
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1076
|
+
out("");
|
|
1077
|
+
out(chalk.bold(" 🔧 Rebase Schema Generator"));
|
|
1078
|
+
out("");
|
|
948
1079
|
|
|
949
1080
|
const cmdParts = [
|
|
950
1081
|
tsxBin,
|
|
@@ -963,7 +1094,7 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
|
|
|
963
1094
|
env: { ...process.env as Record<string, string> }
|
|
964
1095
|
});
|
|
965
1096
|
} catch (err: unknown) {
|
|
966
|
-
|
|
1097
|
+
outError(chalk.red(`✗ Failed to run schema generator: ${err instanceof Error ? err.message : String(err)}`));
|
|
967
1098
|
process.exit(1);
|
|
968
1099
|
}
|
|
969
1100
|
} else if (subcommand === "introspect") {
|
|
@@ -985,21 +1116,21 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
|
|
|
985
1116
|
|
|
986
1117
|
const introspectScript = path.join(__cliDirname, "schema", "introspect-db.ts");
|
|
987
1118
|
if (!fs.existsSync(introspectScript)) {
|
|
988
|
-
|
|
1119
|
+
outError(chalk.red(`✗ Could not find introspect-db.ts at ${introspectScript}`));
|
|
989
1120
|
process.exit(1);
|
|
990
1121
|
}
|
|
991
1122
|
|
|
992
1123
|
const tsxBin = resolveLocalBin("tsx");
|
|
993
1124
|
if (!tsxBin) {
|
|
994
|
-
|
|
1125
|
+
outError(chalk.red("✗ Could not find tsx binary."));
|
|
995
1126
|
process.exit(1);
|
|
996
1127
|
}
|
|
997
1128
|
|
|
998
1129
|
const outputPath = argsList["--output"] || argsList["--collections"] || path.join("..", "config", "collections");
|
|
999
1130
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1131
|
+
out("");
|
|
1132
|
+
out(chalk.bold(" 🔍 Rebase Schema Introspector"));
|
|
1133
|
+
out("");
|
|
1003
1134
|
|
|
1004
1135
|
const cmdParts = [
|
|
1005
1136
|
tsxBin,
|
|
@@ -1016,11 +1147,11 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
|
|
|
1016
1147
|
env: { ...process.env as Record<string, string> }
|
|
1017
1148
|
});
|
|
1018
1149
|
} catch (err: unknown) {
|
|
1019
|
-
|
|
1150
|
+
outError(chalk.red(`✗ Failed to run schema introspector: ${err instanceof Error ? err.message : String(err)}`));
|
|
1020
1151
|
process.exit(1);
|
|
1021
1152
|
}
|
|
1022
1153
|
} else {
|
|
1023
|
-
|
|
1154
|
+
outError(chalk.red("Unknown schema command."));
|
|
1024
1155
|
process.exit(1);
|
|
1025
1156
|
}
|
|
1026
1157
|
}
|
|
@@ -1044,13 +1175,13 @@ async function doctorPluginCommand(rawArgs: string[]): Promise<void> {
|
|
|
1044
1175
|
|
|
1045
1176
|
const doctorScript = path.join(__cliDirname, "schema", "doctor-cli.ts");
|
|
1046
1177
|
if (!fs.existsSync(doctorScript)) {
|
|
1047
|
-
|
|
1178
|
+
outError(chalk.red(`✗ Could not find doctor.ts at ${doctorScript}`));
|
|
1048
1179
|
process.exit(1);
|
|
1049
1180
|
}
|
|
1050
1181
|
|
|
1051
1182
|
const tsxBin = resolveLocalBin("tsx");
|
|
1052
1183
|
if (!tsxBin) {
|
|
1053
|
-
|
|
1184
|
+
outError(chalk.red("✗ Could not find tsx binary."));
|
|
1054
1185
|
process.exit(1);
|
|
1055
1186
|
}
|
|
1056
1187
|
|