@rebasepro/server-postgres 0.18.0 → 0.19.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/BranchService-ByLfM_Tt.js +360 -0
- package/dist/BranchService-ByLfM_Tt.js.map +1 -0
- package/dist/__vite-browser-external-BnuHet1e.js +46 -0
- package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
- package/dist/arg-Dni7MzLB.js +87 -0
- package/dist/arg-Dni7MzLB.js.map +1 -0
- package/dist/auth-users-columns-D2LBFrMH.js +116 -0
- package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
- package/dist/backup-cli-oM9WQVlV.js +408 -0
- package/dist/backup-cli-oM9WQVlV.js.map +1 -0
- package/dist/backup-service-Br3tqwsy.js +715 -0
- package/dist/backup-service-Br3tqwsy.js.map +1 -0
- package/dist/cli-errors-DxN4yzdH.js +657 -0
- package/dist/cli-errors-DxN4yzdH.js.map +1 -0
- package/dist/cli-output-CNdMql-L.js +47 -0
- package/dist/cli-output-CNdMql-L.js.map +1 -0
- package/dist/cli.js +2020 -0
- package/dist/cli.js.map +1 -0
- package/dist/{collection-index-C7LopMyX.js → collection-index-BHgT_TKd.js} +32 -3
- package/dist/{collection-index-C7LopMyX.js.map → collection-index-BHgT_TKd.js.map} +1 -1
- package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
- package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
- package/dist/databasePoolManager-Bj5FbeAs.js +349 -0
- package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
- package/dist/doctor-D7HWmGzs.js +974 -0
- package/dist/doctor-D7HWmGzs.js.map +1 -0
- package/dist/{ensure-collection-policies-CagOzEPS.js → ensure-collection-policies-2c2NV8zX.js} +5 -25
- package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
- package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
- package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
- package/dist/ensure-tables-CIojK-Rx.js +650 -0
- package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
- package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
- package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +1312 -0
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
- package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
- package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
- package/dist/index.es.js +849 -10807
- package/dist/index.es.js.map +1 -1
- package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
- package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
- package/dist/policy-drift-kNJ6X2Uj.js +313 -0
- package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
- package/dist/rls-bootstrap-sql-DLDnUjtx.js +7092 -0
- package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
- package/dist/{rls-enforcement-ClJoTboT.js → rls-enforcement-DeYEjYLt.js} +14 -3
- package/dist/{rls-enforcement-ClJoTboT.js.map → rls-enforcement-DeYEjYLt.js.map} +1 -1
- package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
- package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
- package/dist/schema/doctor-cli.js +129 -0
- package/dist/schema/doctor-cli.js.map +1 -0
- package/dist/schema/generate-drizzle-schema.d.ts +1 -1
- package/dist/schema/generate-drizzle-schema.js +24 -0
- package/dist/schema/generate-drizzle-schema.js.map +1 -0
- package/dist/schema/generate-postgres-ddl.d.ts +1 -1
- package/dist/schema/generate-postgres-ddl.js +138 -0
- package/dist/schema/generate-postgres-ddl.js.map +1 -0
- package/dist/schema/introspect-db.js +866 -0
- package/dist/schema/introspect-db.js.map +1 -0
- package/dist/source-Br7L7GOI.js +410 -0
- package/dist/source-Br7L7GOI.js.map +1 -0
- package/dist/vector-index-DkWxm2de.js +309 -0
- package/dist/vector-index-DkWxm2de.js.map +1 -0
- package/package.json +7 -7
- package/dist/ensure-collection-policies-CagOzEPS.js.map +0 -1
- package/dist/ensure-collection-tables-DbAhrjnG.js +0 -2732
- package/dist/ensure-collection-tables-DbAhrjnG.js.map +0 -1
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2020 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
__createRequire(import.meta.url);
|
|
3
|
+
import { a as __toESM } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
|
+
import { i as diagnoseDbError, r as diagnoseAtlasFailure, s as reportCommandFailure, t as checkDatabaseConnectivity } from "./cli-errors-DxN4yzdH.js";
|
|
5
|
+
import { n as outError, r as outWarn, t as out } from "./cli-output-CNdMql-L.js";
|
|
6
|
+
import { C as resolveLocalBin, S as readVectorDdl, T as forLibpq, _ as getSearchExcludes, a as ExcludeIntrospectionError, b as promptConfirm, c as describeBuildScriptRemedy, d as detectProjectPackageManager, f as diagnoseMissingBin, g as getForeignIndexExcludes, h as getDevDatabaseUrl, l as describeDevAddCommand, m as ensureDevDatabaseExists, o as applySearchDdl, p as dropDevDatabase, r as dropLegacyAuthSchema, s as applyVectorDdl, t as RLS_BOOTSTRAP_SQL, u as describeReinstallCommand, v as getTableExcludes, w as execa, x as readSearchDdl, y as getVectorExcludes } from "./rls-bootstrap-sql-DLDnUjtx.js";
|
|
7
|
+
import { t as chalk } from "./source-Br7L7GOI.js";
|
|
8
|
+
import { t as require_arg } from "./arg-Dni7MzLB.js";
|
|
9
|
+
import { logger } from "@rebasepro/server";
|
|
10
|
+
import { formatRelativeTime } from "@rebasepro/utils";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
//#region src/schema/destructive-sql.ts
|
|
15
|
+
var import_arg = /* @__PURE__ */ __toESM(require_arg(), 1);
|
|
16
|
+
/**
|
|
17
|
+
* Pure helpers that classify an Atlas declarative-apply plan as destructive
|
|
18
|
+
* or not, and decide what `rebase db push` should do about it.
|
|
19
|
+
*
|
|
20
|
+
* `db push` runs `atlas schema apply` to make the live database match the
|
|
21
|
+
* generated `schema.sql`. Removing a collection field compiles to
|
|
22
|
+
* `DROP COLUMN`; renaming compiles to drop-then-add — either destroys data.
|
|
23
|
+
* We first run the apply with `--dry-run` to obtain the planned SQL, scan it
|
|
24
|
+
* here, and refuse to auto-approve anything destructive without an explicit
|
|
25
|
+
* opt-in.
|
|
26
|
+
*
|
|
27
|
+
* Everything in this file is side-effect free so it can be unit-tested
|
|
28
|
+
* without Atlas or a database.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* SQL fragments that destroy data or data-bearing objects. Matched
|
|
32
|
+
* case-insensitively against each statement of the plan. `IF EXISTS` /
|
|
33
|
+
* whitespace variations are tolerated by the regexes below.
|
|
34
|
+
*/
|
|
35
|
+
var DESTRUCTIVE_PATTERNS = [
|
|
36
|
+
{
|
|
37
|
+
label: "DROP TABLE",
|
|
38
|
+
re: /\bDROP\s+TABLE\b/i
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
label: "DROP COLUMN",
|
|
42
|
+
re: /\bDROP\s+COLUMN\b/i
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: "DROP SCHEMA",
|
|
46
|
+
re: /\bDROP\s+SCHEMA\b/i
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: "DROP VIEW",
|
|
50
|
+
re: /\bDROP\s+(MATERIALIZED\s+)?VIEW\b/i
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: "DROP TYPE",
|
|
54
|
+
re: /\bDROP\s+TYPE\b/i
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
label: "TRUNCATE",
|
|
58
|
+
re: /\bTRUNCATE\b/i
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* Split a SQL script into individual statements, dropping blank lines and
|
|
63
|
+
* `--` comment lines. Deliberately simple: Atlas emits one plain statement
|
|
64
|
+
* per `;`, without string literals that contain semicolons in a schema DDL
|
|
65
|
+
* plan, so a naive split is safe and keeps this dependency-free.
|
|
66
|
+
*/
|
|
67
|
+
function splitSqlStatements(sql) {
|
|
68
|
+
return sql.split("\n").filter((line) => !line.trim().startsWith("--")).join("\n").split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Scan an Atlas plan (the SQL printed by `schema apply --dry-run`) and return
|
|
72
|
+
* the statements that would destroy data. An empty array means the plan is
|
|
73
|
+
* safe to auto-approve.
|
|
74
|
+
*/
|
|
75
|
+
function detectDestructiveStatements(planSql) {
|
|
76
|
+
const found = [];
|
|
77
|
+
for (const statement of splitSqlStatements(planSql)) for (const { label, re } of DESTRUCTIVE_PATTERNS) if (re.test(statement)) {
|
|
78
|
+
found.push({
|
|
79
|
+
statement,
|
|
80
|
+
kind: label
|
|
81
|
+
});
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
return found;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Decide how `db push` should proceed given the plan's destructiveness and
|
|
88
|
+
* the invocation context.
|
|
89
|
+
*
|
|
90
|
+
* - No destructive statements → `apply` (safe to auto-approve).
|
|
91
|
+
* - Destructive + `--allow-destructive` → `apply` (operator opted in).
|
|
92
|
+
* - Destructive + interactive TTY → `confirm` (prompt before applying).
|
|
93
|
+
* - Destructive + non-interactive → `refuse` (never silently drop data in
|
|
94
|
+
* CI / scripts / agents).
|
|
95
|
+
*/
|
|
96
|
+
function decidePushSafety(opts) {
|
|
97
|
+
if (opts.destructiveCount === 0) return "apply";
|
|
98
|
+
if (opts.allowDestructive) return "apply";
|
|
99
|
+
return opts.interactive ? "confirm" : "refuse";
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/schema/carved-out-migration.ts
|
|
103
|
+
/**
|
|
104
|
+
* Keep the objects Rebase carved out of Atlas's view out of the migrations
|
|
105
|
+
* Atlas writes.
|
|
106
|
+
*
|
|
107
|
+
* `db push` protects them with `--exclude`. `atlas migrate diff` has no such
|
|
108
|
+
* flag — measured on the pinned 1.2.3, `--exclude` is rejected outright with
|
|
109
|
+
* `unknown flag`, and the `exclude` attribute of an `atlas.hcl` `env` block is
|
|
110
|
+
* accepted and then silently ignored on that path. So the diff sees the search
|
|
111
|
+
* column that the migration directory's own appended DDL created when it
|
|
112
|
+
* replayed, does not see it in `schema.sql`, and plans a drop:
|
|
113
|
+
*
|
|
114
|
+
* ALTER TABLE "public"."talents" DROP COLUMN "search_vector";
|
|
115
|
+
*
|
|
116
|
+
* That statement is removed here, after Atlas has written the file and before
|
|
117
|
+
* anything is appended to it — so the input is always Atlas's own plain output,
|
|
118
|
+
* never the dollar-quoted helper bodies that get appended afterwards.
|
|
119
|
+
*
|
|
120
|
+
* The drop does not arrive alone. Atlas folds every change to one table into a
|
|
121
|
+
* single statement, so a real edit in the same run produces
|
|
122
|
+
*
|
|
123
|
+
* ALTER TABLE "public"."talents"
|
|
124
|
+
* DROP COLUMN "search_vector", DROP COLUMN "interests", ADD COLUMN "headline" text NULL;
|
|
125
|
+
*
|
|
126
|
+
* and dropping the whole statement would silently throw away a migration the
|
|
127
|
+
* project asked for. The removal is therefore per *clause*.
|
|
128
|
+
*
|
|
129
|
+
* Fail-safe throughout: a statement that mentions a carved-out object but does
|
|
130
|
+
* not match a shape this understands is left exactly as it is and reported as
|
|
131
|
+
* `unhandled`, so the caller can say so out loud. A spurious drop the developer
|
|
132
|
+
* can see beats a migration quietly rewritten wrong.
|
|
133
|
+
*/
|
|
134
|
+
/** Anything Postgres accepts in an unquoted identifier past ASCII. */
|
|
135
|
+
var IDENT = "(?:\"(?:[^\"]|\"\")*\"|[A-Za-z_\\u0080-\\uffff][\\w$\\u0080-\\uffff]*)";
|
|
136
|
+
var QUALIFIED = `${IDENT}(?:\\s*\\.\\s*${IDENT})*`;
|
|
137
|
+
var ALTER_TABLE_RE = new RegExp(`^ALTER\\s+TABLE\\s+(?:ONLY\\s+)?(${QUALIFIED})\\s+([\\s\\S]+)$`, "i");
|
|
138
|
+
var DROP_COLUMN_RE = new RegExp(`^DROP\\s+(?:COLUMN\\s+)?(?:IF\\s+EXISTS\\s+)?(${IDENT})$`, "i");
|
|
139
|
+
var DROP_INDEX_RE = new RegExp(`^DROP\\s+INDEX\\s+(?:CONCURRENTLY\\s+)?(?:IF\\s+EXISTS\\s+)?(${QUALIFIED})$`, "i");
|
|
140
|
+
/**
|
|
141
|
+
* Does this statement destroy anything at all?
|
|
142
|
+
*
|
|
143
|
+
* The gate on reporting. A statement that merely *names* a carved-out object —
|
|
144
|
+
* `COMMENT ON COLUMN … "search_vector"` — is not a problem and must not be
|
|
145
|
+
* reported as one, because an unrewritable drop stops the caller outright.
|
|
146
|
+
*/
|
|
147
|
+
var MENTIONS_DROP = /\bDROP\b/i;
|
|
148
|
+
/**
|
|
149
|
+
* Split `schema.table.object` patterns. Anything not in three parts is
|
|
150
|
+
* ignored rather than guessed at — the two-part form is the one Atlas itself
|
|
151
|
+
* silently mis-reads, and repeating that mistake here would be worse than
|
|
152
|
+
* doing nothing.
|
|
153
|
+
*/
|
|
154
|
+
function parseExcludePatterns(patterns) {
|
|
155
|
+
const parsed = [];
|
|
156
|
+
for (const pattern of patterns) {
|
|
157
|
+
const parts = pattern.split(".");
|
|
158
|
+
if (parts.length !== 3) continue;
|
|
159
|
+
if (parts.some((p) => p.length === 0)) continue;
|
|
160
|
+
parsed.push({
|
|
161
|
+
schema: parts[0],
|
|
162
|
+
table: parts[1],
|
|
163
|
+
object: parts[2]
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return parsed;
|
|
167
|
+
}
|
|
168
|
+
/** `"public"` and `public` are the same name; `"Public"` is not `public`. */
|
|
169
|
+
function unquoteIdentifier(raw) {
|
|
170
|
+
const trimmed = raw.trim();
|
|
171
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length >= 2) return trimmed.slice(1, -1).replace(/""/g, "\"");
|
|
172
|
+
return trimmed.toLowerCase();
|
|
173
|
+
}
|
|
174
|
+
/** Split a dotted identifier at its top-level dots, respecting quoting. */
|
|
175
|
+
function splitQualifiedName(raw) {
|
|
176
|
+
const parts = [];
|
|
177
|
+
let current = "";
|
|
178
|
+
let inQuote = false;
|
|
179
|
+
for (let i = 0; i < raw.length; i++) {
|
|
180
|
+
const ch = raw[i];
|
|
181
|
+
if (inQuote) {
|
|
182
|
+
if (ch === "\"") {
|
|
183
|
+
if (raw[i + 1] === "\"") {
|
|
184
|
+
current += "\"\"";
|
|
185
|
+
i++;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
inQuote = false;
|
|
189
|
+
}
|
|
190
|
+
current += ch;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (ch === "\"") {
|
|
194
|
+
inQuote = true;
|
|
195
|
+
current += ch;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (ch === ".") {
|
|
199
|
+
parts.push(current);
|
|
200
|
+
current = "";
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
current += ch;
|
|
204
|
+
}
|
|
205
|
+
if (inQuote) return null;
|
|
206
|
+
parts.push(current);
|
|
207
|
+
return parts.map(unquoteIdentifier);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Walk the script and locate every statement. Quote-, comment- and
|
|
211
|
+
* dollar-aware: the appended halves of a migration are not passed here, but a
|
|
212
|
+
* splitter that shreds a `DO $tag$ ... $tag$` block is a trap worth not
|
|
213
|
+
* leaving behind — it is how the derived-names renderer broke.
|
|
214
|
+
*
|
|
215
|
+
* Returns null if the text ends inside a quote or a dollar-quoted body, or
|
|
216
|
+
* trails off in a statement with no semicolon. Either means this cannot be
|
|
217
|
+
* reasoned about safely.
|
|
218
|
+
*/
|
|
219
|
+
function locateStatements(sql) {
|
|
220
|
+
const statements = [];
|
|
221
|
+
let i = 0;
|
|
222
|
+
let statementStart = 0;
|
|
223
|
+
let sawCode = false;
|
|
224
|
+
const pushStatement = (endExclusive) => {
|
|
225
|
+
const leading = sql.slice(statementStart, endExclusive);
|
|
226
|
+
let consumed = 0;
|
|
227
|
+
for (const line of leading.split("\n")) {
|
|
228
|
+
const trimmed = line.trim();
|
|
229
|
+
if (trimmed === "" || trimmed.startsWith("--")) {
|
|
230
|
+
consumed += line.length + 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
const bodyStart = statementStart + consumed;
|
|
236
|
+
statements.push({
|
|
237
|
+
start: statementStart,
|
|
238
|
+
end: endExclusive,
|
|
239
|
+
bodyStart,
|
|
240
|
+
body: sql.slice(bodyStart, endExclusive).replace(/;\s*$/, "").trim()
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
while (i < sql.length) {
|
|
244
|
+
const ch = sql[i];
|
|
245
|
+
if (ch === "-" && sql[i + 1] === "-") {
|
|
246
|
+
const nl = sql.indexOf("\n", i);
|
|
247
|
+
i = nl === -1 ? sql.length : nl + 1;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (ch === "/" && sql[i + 1] === "*") {
|
|
251
|
+
const close = sql.indexOf("*/", i + 2);
|
|
252
|
+
if (close === -1) return null;
|
|
253
|
+
i = close + 2;
|
|
254
|
+
sawCode = true;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (ch === "'" || ch === "\"") {
|
|
258
|
+
const quote = ch;
|
|
259
|
+
i++;
|
|
260
|
+
let closed = false;
|
|
261
|
+
while (i < sql.length) {
|
|
262
|
+
if (sql[i] === quote) {
|
|
263
|
+
if (sql[i + 1] === quote) {
|
|
264
|
+
i += 2;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
i++;
|
|
268
|
+
closed = true;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
i++;
|
|
272
|
+
}
|
|
273
|
+
if (!closed) return null;
|
|
274
|
+
sawCode = true;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (ch === "$") {
|
|
278
|
+
const tag = /^\$(?:[A-Za-z_\u0080-\uffff][\w\u0080-\uffff]*)?\$/.exec(sql.slice(i));
|
|
279
|
+
if (tag) {
|
|
280
|
+
const close = sql.indexOf(tag[0], i + tag[0].length);
|
|
281
|
+
if (close === -1) return null;
|
|
282
|
+
i = close + tag[0].length;
|
|
283
|
+
sawCode = true;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (ch === ";") {
|
|
288
|
+
pushStatement(i + 1);
|
|
289
|
+
i++;
|
|
290
|
+
statementStart = i;
|
|
291
|
+
sawCode = false;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (!/\s/.test(ch)) sawCode = true;
|
|
295
|
+
i++;
|
|
296
|
+
}
|
|
297
|
+
if (sawCode) return null;
|
|
298
|
+
return statements;
|
|
299
|
+
}
|
|
300
|
+
/** Split an ALTER TABLE action list at its top-level commas. */
|
|
301
|
+
function splitClauses(actions) {
|
|
302
|
+
const clauses = [];
|
|
303
|
+
let depth = 0;
|
|
304
|
+
let current = "";
|
|
305
|
+
let i = 0;
|
|
306
|
+
while (i < actions.length) {
|
|
307
|
+
const ch = actions[i];
|
|
308
|
+
if (ch === "'" || ch === "\"") {
|
|
309
|
+
const quote = ch;
|
|
310
|
+
current += ch;
|
|
311
|
+
i++;
|
|
312
|
+
let closed = false;
|
|
313
|
+
while (i < actions.length) {
|
|
314
|
+
current += actions[i];
|
|
315
|
+
if (actions[i] === quote) {
|
|
316
|
+
if (actions[i + 1] === quote) {
|
|
317
|
+
current += actions[i + 1];
|
|
318
|
+
i += 2;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
i++;
|
|
322
|
+
closed = true;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
i++;
|
|
326
|
+
}
|
|
327
|
+
if (!closed) return null;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (ch === "(") {
|
|
331
|
+
depth++;
|
|
332
|
+
current += ch;
|
|
333
|
+
i++;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (ch === ")") {
|
|
337
|
+
depth--;
|
|
338
|
+
current += ch;
|
|
339
|
+
i++;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (ch === "," && depth === 0) {
|
|
343
|
+
clauses.push(current);
|
|
344
|
+
current = "";
|
|
345
|
+
i++;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
current += ch;
|
|
349
|
+
i++;
|
|
350
|
+
}
|
|
351
|
+
if (depth !== 0) return null;
|
|
352
|
+
clauses.push(current);
|
|
353
|
+
return clauses.map((c) => c.trim()).filter((c) => c.length > 0);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Remove every clause and statement that would drop one of `patterns`.
|
|
357
|
+
*
|
|
358
|
+
* `patterns` are the `schema.table.object` strings the CLI already builds for
|
|
359
|
+
* Atlas's `--exclude`, so the diff path and the apply path protect exactly the
|
|
360
|
+
* same set by construction.
|
|
361
|
+
*/
|
|
362
|
+
function stripCarvedOutStatements(migrationSql, patterns) {
|
|
363
|
+
const carved = parseExcludePatterns(patterns);
|
|
364
|
+
if (carved.length === 0) return {
|
|
365
|
+
sql: migrationSql,
|
|
366
|
+
removed: [],
|
|
367
|
+
unhandled: [],
|
|
368
|
+
empty: isBlank(migrationSql)
|
|
369
|
+
};
|
|
370
|
+
const located = locateStatements(migrationSql);
|
|
371
|
+
if (located === null) return {
|
|
372
|
+
sql: migrationSql,
|
|
373
|
+
removed: [],
|
|
374
|
+
unhandled: MENTIONS_DROP.test(migrationSql) && carved.some((c) => migrationSql.includes(c.object)) ? [migrationSql.trim()] : [],
|
|
375
|
+
empty: isBlank(migrationSql)
|
|
376
|
+
};
|
|
377
|
+
const removed = [];
|
|
378
|
+
const unhandled = [];
|
|
379
|
+
const edits = [];
|
|
380
|
+
for (const statement of located) {
|
|
381
|
+
if (!carved.some((c) => statement.body.includes(c.object))) continue;
|
|
382
|
+
const dropIndex = DROP_INDEX_RE.exec(statement.body);
|
|
383
|
+
if (dropIndex) {
|
|
384
|
+
const parts = splitQualifiedName(dropIndex[1]);
|
|
385
|
+
const name = parts?.[parts.length - 1];
|
|
386
|
+
const schema = parts && parts.length > 1 ? parts[parts.length - 2] : void 0;
|
|
387
|
+
if (name !== void 0 && carved.some((c) => c.object === name && (schema === void 0 || c.schema === schema))) {
|
|
388
|
+
removed.push(statement.body);
|
|
389
|
+
edits.push({
|
|
390
|
+
start: statement.start,
|
|
391
|
+
end: statement.end,
|
|
392
|
+
replacement: ""
|
|
393
|
+
});
|
|
394
|
+
} else unhandled.push(statement.body);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const alter = ALTER_TABLE_RE.exec(statement.body);
|
|
398
|
+
if (!alter) {
|
|
399
|
+
if (MENTIONS_DROP.test(statement.body)) unhandled.push(statement.body);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const target = splitQualifiedName(alter[1]);
|
|
403
|
+
if (!target) {
|
|
404
|
+
if (MENTIONS_DROP.test(statement.body)) unhandled.push(statement.body);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const table = target[target.length - 1];
|
|
408
|
+
const schema = target.length > 1 ? target[target.length - 2] : void 0;
|
|
409
|
+
const clauses = splitClauses(alter[2]);
|
|
410
|
+
if (!clauses) {
|
|
411
|
+
if (MENTIONS_DROP.test(statement.body)) unhandled.push(statement.body);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const survivors = [];
|
|
415
|
+
let removedHere = 0;
|
|
416
|
+
let unhandledHere = false;
|
|
417
|
+
for (const clause of clauses) {
|
|
418
|
+
const drop = DROP_COLUMN_RE.exec(clause);
|
|
419
|
+
const column = drop ? unquoteIdentifier(drop[1]) : void 0;
|
|
420
|
+
if (column !== void 0 && carved.some((c) => c.object === column && c.table === table && (schema === void 0 || c.schema === schema))) {
|
|
421
|
+
removed.push(`ALTER TABLE ${alter[1]}: ${clause}`);
|
|
422
|
+
removedHere++;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (MENTIONS_DROP.test(clause) && carved.some((c) => clause.includes(c.object))) unhandledHere = true;
|
|
426
|
+
survivors.push(clause);
|
|
427
|
+
}
|
|
428
|
+
if (unhandledHere) unhandled.push(statement.body);
|
|
429
|
+
if (removedHere === 0) continue;
|
|
430
|
+
if (survivors.length === 0) edits.push({
|
|
431
|
+
start: statement.start,
|
|
432
|
+
end: statement.end,
|
|
433
|
+
replacement: ""
|
|
434
|
+
});
|
|
435
|
+
else edits.push({
|
|
436
|
+
start: statement.bodyStart,
|
|
437
|
+
end: statement.end,
|
|
438
|
+
replacement: `ALTER TABLE ${alter[1]} ${survivors.join(", ")};`
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
let sql = migrationSql;
|
|
442
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) sql = sql.slice(0, edit.start) + edit.replacement + sql.slice(edit.end);
|
|
443
|
+
if (edits.length > 0) sql = tidy(sql);
|
|
444
|
+
return {
|
|
445
|
+
sql,
|
|
446
|
+
removed,
|
|
447
|
+
unhandled,
|
|
448
|
+
empty: isBlank(sql)
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
/** Only comments and whitespace left — nothing a migration would do. */
|
|
452
|
+
function isBlank(sql) {
|
|
453
|
+
return sql.split("\n").every((line) => line.trim() === "" || line.trim().startsWith("--"));
|
|
454
|
+
}
|
|
455
|
+
/** Collapse the blank runs an excision leaves behind. */
|
|
456
|
+
function tidy(sql) {
|
|
457
|
+
const trimmed = sql.replace(/\n{3,}/g, "\n\n").replace(/^\s*\n/, "").trimEnd();
|
|
458
|
+
return trimmed.length === 0 ? "" : `${trimmed}\n`;
|
|
459
|
+
}
|
|
460
|
+
//#endregion
|
|
461
|
+
//#region src/schema/atlas-argv.ts
|
|
462
|
+
/**
|
|
463
|
+
* Does this Atlas invocation accept `--exclude`?
|
|
464
|
+
*
|
|
465
|
+
* Only `atlas schema apply` does, on the pinned 1.2.3. Measured against the
|
|
466
|
+
* binary: `migrate diff` and `migrate apply` both answer `unknown flag:
|
|
467
|
+
* --exclude`, and neither lists it in `--help` (`migrate apply` offers
|
|
468
|
+
* `--url/--dir/--format/--revisions-schema/--dry-run/--lock-name/--lock-timeout/`
|
|
469
|
+
* `--skip-lock/--baseline/--to-version/--tx-mode/--exec-order/--allow-dirty`).
|
|
470
|
+
* Hence `domain === "schema"` and not just the subcommand.
|
|
471
|
+
*
|
|
472
|
+
* The `exclude` attribute of an `atlas.hcl` `env` block is not a way round it
|
|
473
|
+
* either: measured, it is accepted and then ignored on the diff path, which is
|
|
474
|
+
* the silent-no-match shape that costs the most time to notice.
|
|
475
|
+
*
|
|
476
|
+
* So the diff keeps the carved-out objects out of the migration afterwards
|
|
477
|
+
* instead, with `stripCarvedOutStatements`.
|
|
478
|
+
*/
|
|
479
|
+
function acceptsExcludeFlag(domain, args) {
|
|
480
|
+
return domain === "schema" && args.includes("apply");
|
|
481
|
+
}
|
|
482
|
+
/** Assemble the full argv, connection flags and all. */
|
|
483
|
+
function buildAtlasArgs(invocation) {
|
|
484
|
+
const { domain, args, url, devUrl, excludes = [] } = invocation;
|
|
485
|
+
const argv = [domain, ...args];
|
|
486
|
+
if (domain === "schema") {
|
|
487
|
+
if (args.includes("apply")) argv.push("--url", url, "--dev-url", devUrl);
|
|
488
|
+
else if (args.includes("clean") || args.includes("inspect")) argv.push("--url", url);
|
|
489
|
+
} else if (domain === "migrate") {
|
|
490
|
+
if (args.includes("diff")) argv.push("--dev-url", devUrl);
|
|
491
|
+
else if (args.includes("apply") || args.includes("status")) {
|
|
492
|
+
argv.push("--url", url, "--revisions-schema", "rebase");
|
|
493
|
+
if (args.includes("apply") && !args.includes("--baseline")) argv.push("--allow-dirty");
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (acceptsExcludeFlag(domain, args)) for (const exclude of excludes) argv.push("--exclude", exclude);
|
|
497
|
+
return argv;
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/branch-argv.ts
|
|
501
|
+
/**
|
|
502
|
+
* Argument shapes for `rebase db branch`, with no dependencies.
|
|
503
|
+
*
|
|
504
|
+
* Its own module for the same reason `schema/atlas-argv.ts` is: `cli.ts` pulls
|
|
505
|
+
* in chalk, execa and the driver runtime, so a unit test that imports it cannot
|
|
506
|
+
* run. Argv parsing is the part most worth testing and the part least in need
|
|
507
|
+
* of any of that.
|
|
508
|
+
*/
|
|
509
|
+
/**
|
|
510
|
+
* Words on a `db branch` command line that no argument accounts for.
|
|
511
|
+
*
|
|
512
|
+
* `rebase db branch create alpha beta` created a branch called `alpha` and
|
|
513
|
+
* threw `beta` away without a word. The shapes that produces are all quiet and
|
|
514
|
+
* all wrong: an unquoted name (`create my feature` → `my`), a flag written
|
|
515
|
+
* without its dashes (`create feat from main` → `feat`), a shell that split
|
|
516
|
+
* something you thought was one token. In each case the command succeeds and
|
|
517
|
+
* the branch is not the one asked for — and branch names are the thing you
|
|
518
|
+
* later type to switch, delete, or point a deploy at.
|
|
519
|
+
*
|
|
520
|
+
* Anything starting with `-` is left alone rather than validated. This runs on
|
|
521
|
+
* every branch subcommand, including ones added later with flags this function
|
|
522
|
+
* has never heard of, and rejecting an unrecognised flag here would break them
|
|
523
|
+
* from a distance. The value after a flag that takes one is skipped for the
|
|
524
|
+
* same reason: it is that flag's argument, not a stray word.
|
|
525
|
+
*
|
|
526
|
+
* @param words The command's own arguments: action, name, then the rest.
|
|
527
|
+
*/
|
|
528
|
+
function unexpectedBranchArgs(words) {
|
|
529
|
+
const extras = [];
|
|
530
|
+
let seenName = false;
|
|
531
|
+
for (let index = 1; index < words.length; index += 1) {
|
|
532
|
+
const word = words[index];
|
|
533
|
+
if (word.startsWith("-")) {
|
|
534
|
+
if (TAKES_A_VALUE.has(word.split("=")[0])) index += 1;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (!seenName) {
|
|
538
|
+
seenName = true;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
extras.push(word);
|
|
542
|
+
}
|
|
543
|
+
return extras;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Branch-line flags whose next word belongs to them.
|
|
547
|
+
*
|
|
548
|
+
* `--from` was the only one listed, and `--database-url` is the one that bit:
|
|
549
|
+
* `rebase db branch list --database-url postgres://…` — which the CLI itself
|
|
550
|
+
* appends when the checkout is on a branch, so `rebase.branches` is read in the
|
|
551
|
+
* parent where it lives — answered `✗ Unexpected argument: postgres://…`. The
|
|
552
|
+
* `=` form carries its value in the same word and needs no skip.
|
|
553
|
+
*/
|
|
554
|
+
var TAKES_A_VALUE = /* @__PURE__ */ new Set([
|
|
555
|
+
"--from",
|
|
556
|
+
"--older-than",
|
|
557
|
+
"--database-url"
|
|
558
|
+
]);
|
|
559
|
+
//#endregion
|
|
560
|
+
//#region src/cli-flags.ts
|
|
561
|
+
/**
|
|
562
|
+
* The flags each driver command takes, in one place, and the check that
|
|
563
|
+
* enforces them.
|
|
564
|
+
*
|
|
565
|
+
* Every parser in `cli.ts` runs `arg(..., { permissive: true })`, which does not
|
|
566
|
+
* mean "be lenient" — it means **an undeclared flag becomes a positional**. So
|
|
567
|
+
* `rebase db push --alow-destructive` did not fail: the typo landed in `_`, the
|
|
568
|
+
* push ran with the destructive gate still closed, and the developer read the
|
|
569
|
+
* refusal as Rebase ignoring the flag they had just typed. `rebase schema
|
|
570
|
+
* generate --ouput src/schema.ts` was worse — it wrote the default path and
|
|
571
|
+
* said nothing, so the next build compiled a file nobody had regenerated.
|
|
572
|
+
*
|
|
573
|
+
* The check has to live at the entry point rather than in those parsers, and
|
|
574
|
+
* that is not a detail: `db push` and `db generate` re-enter `schemaCommand`
|
|
575
|
+
* and `generatePostgresDdlCommand` with the *db* line, so a strict parser
|
|
576
|
+
* inside either of them would reject `--allow-destructive` on a line where it
|
|
577
|
+
* is correct. One validation, once, against the spec for the command the user
|
|
578
|
+
* actually named; the inner parsers keep reading an already-validated line.
|
|
579
|
+
*
|
|
580
|
+
* Commands with their own argument handling — `db branch` (`unexpectedBranchArgs`),
|
|
581
|
+
* `db backup`, `db restore`, `db backups` — are deliberately absent: they own
|
|
582
|
+
* their spec, and a second list here would be one more thing to keep in step.
|
|
583
|
+
* An absent entry is "not checked here", never "takes nothing".
|
|
584
|
+
*/
|
|
585
|
+
/**
|
|
586
|
+
* Flags that reach this process on a line it did not compose.
|
|
587
|
+
*
|
|
588
|
+
* `rebase db …` hands the driver the user's whole line: the CLI *reads*
|
|
589
|
+
* `--database-url` and `--docker` to resolve the database and then relays them
|
|
590
|
+
* verbatim, and `--debug` is what `bin/rebase.js` prints after every failure as
|
|
591
|
+
* the thing to re-run with. Rejecting any of the three would make the driver
|
|
592
|
+
* refuse commands the CLI documents.
|
|
593
|
+
*/
|
|
594
|
+
var RELAYED_FLAGS = {
|
|
595
|
+
"--database-url": String,
|
|
596
|
+
"--docker": Boolean,
|
|
597
|
+
"--debug": Boolean,
|
|
598
|
+
"--help": Boolean,
|
|
599
|
+
"-h": "--help"
|
|
600
|
+
};
|
|
601
|
+
/** Keyed by `"<domain> <subcommand>"`, the way the user types it. */
|
|
602
|
+
var DRIVER_FLAG_SPECS = {
|
|
603
|
+
"db push": {
|
|
604
|
+
"--collections": String,
|
|
605
|
+
"-c": "--collections",
|
|
606
|
+
"--dry-run": Boolean,
|
|
607
|
+
"--allow-destructive": Boolean,
|
|
608
|
+
"--yes": Boolean,
|
|
609
|
+
"-y": "--yes"
|
|
610
|
+
},
|
|
611
|
+
"db generate": {
|
|
612
|
+
"--collections": String,
|
|
613
|
+
"-c": "--collections"
|
|
614
|
+
},
|
|
615
|
+
"db migrate": { "--baseline": String },
|
|
616
|
+
"schema generate": {
|
|
617
|
+
"--collections": String,
|
|
618
|
+
"-c": "--collections",
|
|
619
|
+
"--output": String,
|
|
620
|
+
"-o": "--output",
|
|
621
|
+
"--out": "--output",
|
|
622
|
+
"--watch": Boolean,
|
|
623
|
+
"-w": "--watch"
|
|
624
|
+
},
|
|
625
|
+
"schema introspect": {
|
|
626
|
+
"--output": String,
|
|
627
|
+
"-o": "--output",
|
|
628
|
+
"--out": "--output",
|
|
629
|
+
"--collections": String,
|
|
630
|
+
"-c": "--collections",
|
|
631
|
+
"--force": Boolean,
|
|
632
|
+
"-f": "--force",
|
|
633
|
+
"--schema": String
|
|
634
|
+
},
|
|
635
|
+
"schema stale": {
|
|
636
|
+
"--collections": String,
|
|
637
|
+
"-c": "--collections",
|
|
638
|
+
"--output": String,
|
|
639
|
+
"-o": "--output",
|
|
640
|
+
"--out": "--output",
|
|
641
|
+
"--fix": Boolean
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
/**
|
|
645
|
+
* The `--collections` path the user typed, or `null` if they typed none.
|
|
646
|
+
*
|
|
647
|
+
* Read once, at the entry point, because every command that takes it re-enters
|
|
648
|
+
* the generators with the same line and each of them resolved it again — which
|
|
649
|
+
* is how "Collections path not found" came to be printed four times before the
|
|
650
|
+
* real output, and how a path that does not exist got as far as *writing* an
|
|
651
|
+
* empty schema.
|
|
652
|
+
*
|
|
653
|
+
* Permissive, and it has to be: {@link assertKnownFlags} has already judged the
|
|
654
|
+
* line, and `db push` carries flags the `--collections` spec does not name.
|
|
655
|
+
*/
|
|
656
|
+
function collectionsPathIn(args) {
|
|
657
|
+
try {
|
|
658
|
+
return (0, import_arg.default)({
|
|
659
|
+
"--collections": String,
|
|
660
|
+
"-c": "--collections"
|
|
661
|
+
}, {
|
|
662
|
+
argv: args.slice(2),
|
|
663
|
+
permissive: true
|
|
664
|
+
})["--collections"] ?? null;
|
|
665
|
+
} catch {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Reject a flag the named command does not take.
|
|
671
|
+
*
|
|
672
|
+
* `args` is the driver's whole line — `["db", "push", …]` — so the flags are
|
|
673
|
+
* read from `args.slice(2)`, past the two command words.
|
|
674
|
+
*
|
|
675
|
+
* Throws rather than exiting: `runPluginCommand`'s caller already turns a
|
|
676
|
+
* thrown error into one red line and exit 1, and a thrown error is what the
|
|
677
|
+
* tests can see.
|
|
678
|
+
*/
|
|
679
|
+
function assertKnownFlags(domain, subcommand, args) {
|
|
680
|
+
if (!subcommand) return;
|
|
681
|
+
const spec = DRIVER_FLAG_SPECS[`${domain} ${subcommand}`];
|
|
682
|
+
if (!spec) return;
|
|
683
|
+
try {
|
|
684
|
+
(0, import_arg.default)({
|
|
685
|
+
...RELAYED_FLAGS,
|
|
686
|
+
...spec
|
|
687
|
+
}, { argv: args.slice(2) });
|
|
688
|
+
} catch (err) {
|
|
689
|
+
if (err instanceof Error && err.code === "ARG_UNKNOWN_OPTION") throw new Error(`${err.message} — run \`rebase ${domain} --help\` for the options \`${domain} ${subcommand}\` takes.`);
|
|
690
|
+
throw err;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
//#endregion
|
|
694
|
+
//#region src/cli-collections-path.ts
|
|
695
|
+
/**
|
|
696
|
+
* `--collections <dir>`, checked once, before anything is written.
|
|
697
|
+
*
|
|
698
|
+
* A path that does not resolve used to warn and carry on. Four times over —
|
|
699
|
+
* `schemaCommand`, `generatePostgresDdlCommand` and the Atlas argv assembly
|
|
700
|
+
* each re-enter the loader with the same line — and then both generators wrote
|
|
701
|
+
* an *empty* schema: `drizzle/schema.sql` truncated to `CREATE SCHEMA IF NOT
|
|
702
|
+
* EXISTS "rebase";`, `src/schema.generated.ts` to ten lines. Both are committed
|
|
703
|
+
* artifacts. The push that followed planned a `DROP TABLE` for every table in
|
|
704
|
+
* the database, and the only thing between a typo and that was the destructive
|
|
705
|
+
* gate.
|
|
706
|
+
*
|
|
707
|
+
* The check has to be here rather than inside `loadCollectionsForCli`, which is
|
|
708
|
+
* deliberately forgiving: its callers use it to narrow what Atlas may touch,
|
|
709
|
+
* and making a broken *file* fatal there would block a push over something
|
|
710
|
+
* unrelated. "Does the directory the user named exist" is a different question,
|
|
711
|
+
* it was answerable before the first write, and the code already knew.
|
|
712
|
+
*/
|
|
713
|
+
/**
|
|
714
|
+
* Thrown rather than exited, so the tests can see it and the entry point owns
|
|
715
|
+
* the exit code.
|
|
716
|
+
*
|
|
717
|
+
* `alreadyReported` is read by `reportCommandFailure`: the full diagnosis is on
|
|
718
|
+
* stderr by the time this is thrown, and the point of the whole change is that
|
|
719
|
+
* it is printed once.
|
|
720
|
+
*/
|
|
721
|
+
var CollectionsPathMissing = class extends Error {
|
|
722
|
+
typed;
|
|
723
|
+
resolved;
|
|
724
|
+
alreadyReported = true;
|
|
725
|
+
constructor(typed, resolved) {
|
|
726
|
+
super(`Collections path not found: "${typed}"`);
|
|
727
|
+
this.typed = typed;
|
|
728
|
+
this.resolved = resolved;
|
|
729
|
+
this.name = "CollectionsPathMissing";
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
/**
|
|
733
|
+
* The whole message, in one place, because it is printed by two callers.
|
|
734
|
+
*
|
|
735
|
+
* Names what was typed, what it resolved to, and the working directory it
|
|
736
|
+
* resolved against — a relative `--collections` is resolved against the cwd,
|
|
737
|
+
* and the generated npm script runs with `cwd: backend/`, so "the path is
|
|
738
|
+
* right and the directory is wrong" is the commonest way to arrive here.
|
|
739
|
+
*/
|
|
740
|
+
function describeMissingCollectionsPath(typed, resolved) {
|
|
741
|
+
return [
|
|
742
|
+
chalk.red(`✗ Collections path not found: "${typed}"`),
|
|
743
|
+
chalk.gray(` Resolved to: ${resolved}`),
|
|
744
|
+
chalk.gray(` (relative to cwd: ${process.cwd()})`),
|
|
745
|
+
"",
|
|
746
|
+
chalk.gray(" Nothing was generated and nothing was applied — a schema generated from a"),
|
|
747
|
+
chalk.gray(" directory that is not there is an empty one, and applying it would drop"),
|
|
748
|
+
chalk.gray(" every table it does not describe."),
|
|
749
|
+
"",
|
|
750
|
+
chalk.gray(" Pass an absolute path, or one relative to where you run the command.")
|
|
751
|
+
].join("\n");
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Refuse a `--collections` path that does not exist.
|
|
755
|
+
*
|
|
756
|
+
* `null` means the flag was not given: the default (`../config/collections`) is
|
|
757
|
+
* left to the loader, which warns and continues — a project may legitimately
|
|
758
|
+
* have no collections directory, and a headless scaffold does.
|
|
759
|
+
*/
|
|
760
|
+
function assertCollectionsPathExists(typed) {
|
|
761
|
+
if (typed === null) return;
|
|
762
|
+
const resolved = path.resolve(process.cwd(), typed);
|
|
763
|
+
if (fs.existsSync(resolved)) return;
|
|
764
|
+
outError("");
|
|
765
|
+
outError(describeMissingCollectionsPath(typed, resolved));
|
|
766
|
+
outError("");
|
|
767
|
+
throw new CollectionsPathMissing(typed, resolved);
|
|
768
|
+
}
|
|
769
|
+
//#endregion
|
|
770
|
+
//#region src/backup-argv.ts
|
|
771
|
+
/**
|
|
772
|
+
* @param rawArgs the driver's whole line, `["db", "backup", …]`.
|
|
773
|
+
*/
|
|
774
|
+
function backupActionOf(rawArgs) {
|
|
775
|
+
return rawArgs[2] === "list" ? "list" : "create";
|
|
776
|
+
}
|
|
777
|
+
//#endregion
|
|
778
|
+
//#region src/branch-prune.ts
|
|
779
|
+
/** True when nothing at all needs doing. */
|
|
780
|
+
function planIsEmpty(plan, includeDevDiff) {
|
|
781
|
+
return plan.staleRows.length === 0 && plan.orphanDatabases.length === 0 && plan.expired.length === 0 && (!includeDevDiff || plan.devDiff.length === 0);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Age in whole days, floored.
|
|
785
|
+
*
|
|
786
|
+
* Floored rather than rounded so `--older-than 7` never catches something six
|
|
787
|
+
* and a half days old: a prune that removes more than it said it would is worse
|
|
788
|
+
* than one that waits another twelve hours.
|
|
789
|
+
*/
|
|
790
|
+
function ageInDays(createdAt, now) {
|
|
791
|
+
return Math.floor((now.getTime() - createdAt.getTime()) / 864e5);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Parse `--older-than`. Accepts a bare number of days, or `7d` / `2w`.
|
|
795
|
+
*
|
|
796
|
+
* Returns null for anything else, so the caller refuses rather than guessing —
|
|
797
|
+
* silently reading `--older-than 7h` as seven *days* would delete a week of
|
|
798
|
+
* work.
|
|
799
|
+
*/
|
|
800
|
+
function parseOlderThan(value) {
|
|
801
|
+
const match = /^(\d+)\s*([dw])?$/i.exec(value.trim());
|
|
802
|
+
if (!match) return null;
|
|
803
|
+
const amount = Number(match[1]);
|
|
804
|
+
if (!Number.isFinite(amount)) return null;
|
|
805
|
+
return match[2]?.toLowerCase() === "w" ? amount * 7 : amount;
|
|
806
|
+
}
|
|
807
|
+
function planPrune(input, options = {}) {
|
|
808
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
809
|
+
const present = new Set(input.databases);
|
|
810
|
+
const registered = new Set(input.rows.map((row) => row.dbName));
|
|
811
|
+
return {
|
|
812
|
+
staleRows: input.rows.filter((row) => !present.has(row.dbName)),
|
|
813
|
+
orphanDatabases: input.databases.filter((name) => name.startsWith(input.branchPrefix) && !registered.has(name)).sort(),
|
|
814
|
+
expired: options.olderThanDays == null ? [] : input.rows.filter((row) => present.has(row.dbName)).map((branch) => ({
|
|
815
|
+
branch,
|
|
816
|
+
ageDays: ageInDays(branch.createdAt, now)
|
|
817
|
+
})).filter((entry) => entry.ageDays >= options.olderThanDays).sort((a, b) => b.ageDays - a.ageDays),
|
|
818
|
+
devDiff: input.databases.filter((name) => name.endsWith("_dev_diff")).sort()
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region src/cli.ts
|
|
823
|
+
var __cliDirname = path.dirname(fileURLToPath(import.meta.url));
|
|
824
|
+
/**
|
|
825
|
+
* A script this CLI spawns, and how to run it.
|
|
826
|
+
*
|
|
827
|
+
* Four commands — `schema generate`, the DDL plan, `introspect` and the schema
|
|
828
|
+
* doctor — run as child processes, located by path relative to this file. That
|
|
829
|
+
* path used to be `<dir>/schema/<name>.ts`, which worked only because `files`
|
|
830
|
+
* shipped `src`. When the tarball stopped shipping `src`, every one of those
|
|
831
|
+
* commands broke for every npm consumer at once: the file was simply absent,
|
|
832
|
+
* and the parent CLI reported the absence as "Dependencies are not installed"
|
|
833
|
+
* at projects that had installed perfectly. 0.18.0 shipped that way — a
|
|
834
|
+
* scaffolded project's first `rebase dev` could not regenerate its schema and
|
|
835
|
+
* every `GET /api/data/*` answered `Table not found for collection 'posts'`.
|
|
836
|
+
*
|
|
837
|
+
* The build emits each of them beside this file, so a published copy carries
|
|
838
|
+
* the command surface as an artifact. Source still wins where there is no
|
|
839
|
+
* build, which is how the monorepo runs before `pnpm build`.
|
|
840
|
+
*
|
|
841
|
+
* **The interpreter is tsx either way, and that is not incidental.** These
|
|
842
|
+
* scripts load the *project's* collection files, which are TypeScript and
|
|
843
|
+
* import each other as `./authors.js` — the extension TypeScript tells you to
|
|
844
|
+
* write. Node resolves that literally and fails on a file that does not exist;
|
|
845
|
+
* tsx maps it back. Running the built `.js` under plain `node` therefore fails
|
|
846
|
+
* on the user's code rather than on ours, one directory further from anything
|
|
847
|
+
* the error mentions.
|
|
848
|
+
*/
|
|
849
|
+
function resolveChildScript(name) {
|
|
850
|
+
const built = path.join(__cliDirname, "schema", `${name}.js`);
|
|
851
|
+
const source = path.join(__cliDirname, "schema", `${name}.ts`);
|
|
852
|
+
const script = fs.existsSync(built) ? built : fs.existsSync(source) ? source : null;
|
|
853
|
+
if (!script) return {
|
|
854
|
+
ok: false,
|
|
855
|
+
reason: "script"
|
|
856
|
+
};
|
|
857
|
+
const tsxBin = resolveLocalBin("tsx");
|
|
858
|
+
if (!tsxBin) return {
|
|
859
|
+
ok: false,
|
|
860
|
+
reason: "tsx"
|
|
861
|
+
};
|
|
862
|
+
return {
|
|
863
|
+
ok: true,
|
|
864
|
+
bin: tsxBin,
|
|
865
|
+
script
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
/** What to print when {@link resolveChildScript} cannot produce a command. */
|
|
869
|
+
function childScriptError(what, reason) {
|
|
870
|
+
if (reason === "tsx") return `✗ Could not find the tsx binary, which ${what} needs to read your collection files. Install your project's dependencies and try again.`;
|
|
871
|
+
return `✗ The driver's ${what} is missing. This copy of @rebasepro/server-postgres is incomplete — reinstall it, or run its build if you are working in the monorepo.`;
|
|
872
|
+
}
|
|
873
|
+
async function loadEnv() {
|
|
874
|
+
try {
|
|
875
|
+
const dotenv = await import("dotenv");
|
|
876
|
+
const envPaths = [
|
|
877
|
+
process.env.DOTENV_CONFIG_PATH,
|
|
878
|
+
path.resolve(process.cwd(), ".env"),
|
|
879
|
+
path.resolve(process.cwd(), "../.env"),
|
|
880
|
+
path.resolve(process.cwd(), "../../.env")
|
|
881
|
+
].filter(Boolean);
|
|
882
|
+
for (const p of envPaths) if (fs.existsSync(p)) {
|
|
883
|
+
const parsed = dotenv.config({
|
|
884
|
+
path: p,
|
|
885
|
+
quiet: true
|
|
886
|
+
});
|
|
887
|
+
if (parsed.parsed) {
|
|
888
|
+
for (const [key, val] of Object.entries(parsed.parsed)) if (process.env[key] === void 0) process.env[key] = val;
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
} catch {}
|
|
893
|
+
}
|
|
894
|
+
async function runPluginCommand(args) {
|
|
895
|
+
if (process.env.DOTENV_CONFIG_QUIET === void 0) process.env.DOTENV_CONFIG_QUIET = "true";
|
|
896
|
+
await loadEnv();
|
|
897
|
+
const domain = args[0];
|
|
898
|
+
const subcommand = args[1];
|
|
899
|
+
assertKnownFlags(domain, subcommand, args);
|
|
900
|
+
assertCollectionsPathExists(collectionsPathIn(args));
|
|
901
|
+
if (domain === "db") await dbCommand(subcommand, args);
|
|
902
|
+
else if (domain === "schema") await schemaCommand(subcommand, args);
|
|
903
|
+
else if (domain === "doctor") await doctorPluginCommand(args);
|
|
904
|
+
else {
|
|
905
|
+
outError(chalk.red(`Unknown domain command: ${domain}`));
|
|
906
|
+
process.exit(1);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
/** The migration files on disk, sorted, or none if the directory is absent. */
|
|
910
|
+
function listMigrationFiles() {
|
|
911
|
+
const dir = path.resolve(process.cwd(), "drizzle", "migrations");
|
|
912
|
+
if (!fs.existsSync(dir)) return [];
|
|
913
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* The newest migration's version — the numeric prefix Atlas records.
|
|
917
|
+
*
|
|
918
|
+
* `20260906101530_init.sql` → `20260906101530`. Named in the baseline remedy so
|
|
919
|
+
* the command it prints can be typed rather than worked out.
|
|
920
|
+
*/
|
|
921
|
+
function latestMigrationVersion() {
|
|
922
|
+
const newest = listMigrationFiles().at(-1);
|
|
923
|
+
if (!newest) return null;
|
|
924
|
+
const match = /^(\d+)/.exec(newest);
|
|
925
|
+
return match ? match[1] : newest.replace(/\.sql$/, "");
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Take Atlas's carve-outs back out of the migration `migrate diff` just wrote.
|
|
929
|
+
*
|
|
930
|
+
* The apply path hands Atlas `--exclude` and it never sees the search or vector
|
|
931
|
+
* columns. The diff path has no such flag — `atlas migrate diff` rejects
|
|
932
|
+
* `--exclude` outright, and an `atlas.hcl` `env` block's `exclude` is accepted
|
|
933
|
+
* and then ignored — so Atlas replays the migration directory (which builds
|
|
934
|
+
* both columns, because that DDL is appended to migrations), does not find them
|
|
935
|
+
* in `schema.sql`, and plans `DROP COLUMN`. Applied, that would take search and
|
|
936
|
+
* the embeddings away from every database the migration reaches.
|
|
937
|
+
*
|
|
938
|
+
* Returns whether a migration Atlas just wrote is still there afterwards: a
|
|
939
|
+
* file whose only content was the spurious drop is deleted, because an empty
|
|
940
|
+
* migration is worse than none — it takes a slot in the revision history and
|
|
941
|
+
* the caller would append the carved-out DDL and the policies to it.
|
|
942
|
+
*
|
|
943
|
+
* @returns true when a new migration survives and may be appended to.
|
|
944
|
+
*/
|
|
945
|
+
async function stripCarvedOutFromNewestMigration(collectionsPath) {
|
|
946
|
+
const patterns = [...await getSearchExcludes(collectionsPath), ...await getVectorExcludes(collectionsPath)];
|
|
947
|
+
if (patterns.length === 0) return true;
|
|
948
|
+
const newest = listMigrationFiles().at(-1);
|
|
949
|
+
if (!newest) return false;
|
|
950
|
+
const file = path.resolve(process.cwd(), "drizzle", "migrations", newest);
|
|
951
|
+
const result = stripCarvedOutStatements(fs.readFileSync(file, "utf-8"), patterns);
|
|
952
|
+
if (result.unhandled.length > 0) {
|
|
953
|
+
outError(chalk.red(`\n ✗ ${newest} drops an object Rebase manages outside Atlas, in a form the CLI`));
|
|
954
|
+
outError(chalk.red(" could not remove:"));
|
|
955
|
+
for (const statement of result.unhandled) outError(chalk.gray(` ${statement.replace(/\s+/g, " ")}`));
|
|
956
|
+
outError(chalk.gray(" Left as Atlas wrote it. Delete those statements by hand before running\n `rebase db migrate` — and please report them: rewriting this is meant to\n be automatic."));
|
|
957
|
+
process.exit(1);
|
|
958
|
+
}
|
|
959
|
+
if (result.removed.length === 0) return true;
|
|
960
|
+
if (result.empty) {
|
|
961
|
+
fs.rmSync(file);
|
|
962
|
+
out(chalk.gray(` ✓ Dropped ${newest} — it planned nothing but the removal of columns\n Atlas cannot see and Rebase applies itself.`));
|
|
963
|
+
} else {
|
|
964
|
+
fs.writeFileSync(file, result.sql, "utf-8");
|
|
965
|
+
out(chalk.gray(` ✓ Kept Rebase's own columns out of ${newest} (${result.removed.length} statement(s))`));
|
|
966
|
+
}
|
|
967
|
+
await runAtlas("migrate", [
|
|
968
|
+
"hash",
|
|
969
|
+
"--dir",
|
|
970
|
+
"file://drizzle/migrations"
|
|
971
|
+
], collectionsPath);
|
|
972
|
+
return !result.empty;
|
|
973
|
+
}
|
|
974
|
+
async function dbCommand(subcommand, rawArgs) {
|
|
975
|
+
const VALID_ACTIONS = [
|
|
976
|
+
"push",
|
|
977
|
+
"generate",
|
|
978
|
+
"migrate",
|
|
979
|
+
"branch",
|
|
980
|
+
"backup",
|
|
981
|
+
"restore",
|
|
982
|
+
"backups"
|
|
983
|
+
];
|
|
984
|
+
if (!subcommand || !VALID_ACTIONS.includes(subcommand)) {
|
|
985
|
+
outError(chalk.red(`Unknown db command. Valid: ${VALID_ACTIONS.join(", ")}`));
|
|
986
|
+
process.exit(1);
|
|
987
|
+
}
|
|
988
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
989
|
+
out("");
|
|
990
|
+
out(chalk.bold(` rebase db ${subcommand}`));
|
|
991
|
+
out(chalk.gray(" Run `rebase db --help` for the full page — this is the driver's own entry point."));
|
|
992
|
+
out("");
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (subcommand === "branch") {
|
|
996
|
+
await branchCommand(rawArgs);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (subcommand === "backup" || subcommand === "restore" || subcommand === "backups") {
|
|
1000
|
+
const { backupCommand, restoreCommand, backupsCommand } = await import("./backup-cli-oM9WQVlV.js");
|
|
1001
|
+
if (subcommand === "backup" && backupActionOf(rawArgs) === "list") await backupsCommand(rawArgs);
|
|
1002
|
+
else if (subcommand === "backup") await backupCommand(rawArgs);
|
|
1003
|
+
else if (subcommand === "restore") await restoreCommand(rawArgs);
|
|
1004
|
+
else await backupsCommand(rawArgs);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const argsList = (0, import_arg.default)({
|
|
1008
|
+
"--collections": String,
|
|
1009
|
+
"--allow-destructive": Boolean,
|
|
1010
|
+
"--dry-run": Boolean,
|
|
1011
|
+
"--yes": Boolean,
|
|
1012
|
+
"--baseline": String,
|
|
1013
|
+
"-c": "--collections",
|
|
1014
|
+
"-y": "--yes"
|
|
1015
|
+
}, {
|
|
1016
|
+
argv: rawArgs.slice(2),
|
|
1017
|
+
permissive: true
|
|
1018
|
+
});
|
|
1019
|
+
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
1020
|
+
if (subcommand === "generate") {
|
|
1021
|
+
out("");
|
|
1022
|
+
out(chalk.bold(" 📦 Rebase DB Generate"));
|
|
1023
|
+
out(chalk.gray(" Step 1/2: Generating Drizzle schema & Postgres DDL from collections..."));
|
|
1024
|
+
out("");
|
|
1025
|
+
await schemaCommand("generate", rawArgs);
|
|
1026
|
+
await generatePostgresDdlCommand(rawArgs);
|
|
1027
|
+
out("");
|
|
1028
|
+
out(chalk.gray(" Step 2/2: Generating SQL migration files with Atlas..."));
|
|
1029
|
+
out("");
|
|
1030
|
+
const migrationName = argsList._[0] || "migration";
|
|
1031
|
+
const migrationsBefore = listMigrationFiles();
|
|
1032
|
+
await runAtlas("migrate", [
|
|
1033
|
+
"diff",
|
|
1034
|
+
migrationName,
|
|
1035
|
+
"--dir",
|
|
1036
|
+
"file://drizzle/migrations",
|
|
1037
|
+
"--to",
|
|
1038
|
+
"file://drizzle/schema.sql"
|
|
1039
|
+
], collectionsPath);
|
|
1040
|
+
let wroteNewMigration = listMigrationFiles().length > migrationsBefore.length;
|
|
1041
|
+
if (wroteNewMigration) wroteNewMigration = await stripCarvedOutFromNewestMigration(collectionsPath);
|
|
1042
|
+
try {
|
|
1043
|
+
const migrationsDir = path.resolve(process.cwd(), "drizzle", "migrations");
|
|
1044
|
+
if (fs.existsSync(migrationsDir) && wroteNewMigration) {
|
|
1045
|
+
const sqlFiles = fs.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
|
|
1046
|
+
if (sqlFiles.length > 0) {
|
|
1047
|
+
const newestMigrationFile = path.join(migrationsDir, sqlFiles[sqlFiles.length - 1]);
|
|
1048
|
+
let migrationContent = fs.readFileSync(newestMigrationFile, "utf-8");
|
|
1049
|
+
migrationContent = migrationContent.replace(/CREATE SCHEMA (?!IF NOT EXISTS)("[^"]+");/g, "CREATE SCHEMA IF NOT EXISTS $1;");
|
|
1050
|
+
const vectorContent = readVectorDdl();
|
|
1051
|
+
if (vectorContent) {
|
|
1052
|
+
migrationContent = `${migrationContent}\n\n${vectorContent}`;
|
|
1053
|
+
out(chalk.gray(" ✓ Appended vector DDL to the migration"));
|
|
1054
|
+
}
|
|
1055
|
+
const searchContent = readSearchDdl();
|
|
1056
|
+
if (searchContent) {
|
|
1057
|
+
migrationContent = `${migrationContent}\n\n${searchContent}`;
|
|
1058
|
+
out(chalk.gray(" ✓ Appended search DDL to the migration"));
|
|
1059
|
+
}
|
|
1060
|
+
fs.writeFileSync(newestMigrationFile, migrationContent, "utf-8");
|
|
1061
|
+
const policiesFile = path.resolve(process.cwd(), "drizzle", "policies.sql");
|
|
1062
|
+
if (fs.existsSync(policiesFile)) {
|
|
1063
|
+
const policiesContent = fs.readFileSync(policiesFile, "utf-8");
|
|
1064
|
+
fs.appendFileSync(newestMigrationFile, "\n\n" + RLS_BOOTSTRAP_SQL + "\n" + policiesContent);
|
|
1065
|
+
out(chalk.gray(` ✓ Appended RLS policies to migration file: ${path.basename(newestMigrationFile)}`));
|
|
1066
|
+
out(chalk.gray(" Re-hashing migration files..."));
|
|
1067
|
+
await runAtlas("migrate", [
|
|
1068
|
+
"hash",
|
|
1069
|
+
"--dir",
|
|
1070
|
+
"file://drizzle/migrations"
|
|
1071
|
+
], collectionsPath);
|
|
1072
|
+
out(chalk.gray(" ✓ Migration directory checksum updated successfully."));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
outWarn(chalk.yellow(` ⚠️ Failed to append policies or re-hash migration: ${err instanceof Error ? err.message : String(err)}`));
|
|
1078
|
+
}
|
|
1079
|
+
if (!wroteNewMigration) out(chalk.gray(" ℹ No migration written — nothing in this change is visible to Atlas.\n Search, vector and RLS DDL are applied by `rebase db push`, and at boot by\n the schema ensure. For a migration-only deployment, add drizzle/search.sql,\n drizzle/vector.sql and drizzle/policies.sql to a migration by hand."));
|
|
1080
|
+
out("");
|
|
1081
|
+
out(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
|
|
1082
|
+
out("");
|
|
1083
|
+
} else {
|
|
1084
|
+
out("");
|
|
1085
|
+
out(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
|
|
1086
|
+
out("");
|
|
1087
|
+
if (subcommand === "push") {
|
|
1088
|
+
out(chalk.gray(" Step 1/3: Generating Drizzle schema & Postgres DDL from collections..."));
|
|
1089
|
+
out("");
|
|
1090
|
+
await schemaCommand("generate", rawArgs);
|
|
1091
|
+
await generatePostgresDdlCommand(rawArgs);
|
|
1092
|
+
out("");
|
|
1093
|
+
const dryRun = argsList["--dry-run"] === true;
|
|
1094
|
+
out(chalk.gray(dryRun ? " Step 2/3: Planning the change against the database (nothing is applied)..." : " Step 2/3: Pushing schema to database with Atlas..."));
|
|
1095
|
+
out("");
|
|
1096
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
1097
|
+
if (databaseUrl && !dryRun) await ensureAuthSchemaAndFunctions(databaseUrl);
|
|
1098
|
+
const plan = await runAtlas("schema", [
|
|
1099
|
+
"apply",
|
|
1100
|
+
"--to",
|
|
1101
|
+
"file://drizzle/schema.sql",
|
|
1102
|
+
"--dry-run"
|
|
1103
|
+
], collectionsPath, { captureStdout: true });
|
|
1104
|
+
const destructive = detectDestructiveStatements(plan);
|
|
1105
|
+
if (dryRun) {
|
|
1106
|
+
out(chalk.gray(" Step 3/3: --dry-run — nothing was applied."));
|
|
1107
|
+
out("");
|
|
1108
|
+
if (plan.trim()) {
|
|
1109
|
+
out(chalk.bold(" Planned changes:"));
|
|
1110
|
+
out(chalk.gray(plan.trim().split("\n").map((l) => ` ${l}`).join("\n")));
|
|
1111
|
+
} else out(chalk.green(" ✓ No changes: the database already matches these collections."));
|
|
1112
|
+
out("");
|
|
1113
|
+
if (destructive.length > 0) {
|
|
1114
|
+
outWarn(chalk.yellow(` ⚠️ ${destructive.length} of those DESTROY data:`));
|
|
1115
|
+
for (const d of destructive) outWarn(chalk.red(` ${d.kind}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
|
|
1116
|
+
outWarn("");
|
|
1117
|
+
outWarn(chalk.yellow(" Applying them needs `rebase db push --allow-destructive`. Back up first: rebase db backup"));
|
|
1118
|
+
outWarn("");
|
|
1119
|
+
}
|
|
1120
|
+
out(chalk.gray(" The auth schema step was skipped, because it writes. A real push runs it first."));
|
|
1121
|
+
out("");
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const allowDestructive = argsList["--allow-destructive"] === true || argsList["--yes"] === true;
|
|
1125
|
+
const decision = decidePushSafety({
|
|
1126
|
+
destructiveCount: destructive.length,
|
|
1127
|
+
allowDestructive,
|
|
1128
|
+
interactive: process.stdin.isTTY === true
|
|
1129
|
+
});
|
|
1130
|
+
if (destructive.length > 0) {
|
|
1131
|
+
outWarn(chalk.yellow(` ⚠️ This push includes ${destructive.length} destructive change(s) that will DESTROY data:`));
|
|
1132
|
+
outWarn("");
|
|
1133
|
+
for (const d of destructive) outWarn(chalk.red(` ${d.kind}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
|
|
1134
|
+
outWarn("");
|
|
1135
|
+
outWarn(chalk.yellow(" Full planned changes:"));
|
|
1136
|
+
outWarn(chalk.gray(plan.trim().split("\n").map((l) => ` ${l}`).join("\n")));
|
|
1137
|
+
outWarn("");
|
|
1138
|
+
if (decision === "refuse") {
|
|
1139
|
+
outError(chalk.red(" ✗ Aborting: destructive changes require confirmation."));
|
|
1140
|
+
outError(chalk.gray(" Re-run interactively, or pass --allow-destructive to proceed. Back up first: rebase db backup"));
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
if (decision === "confirm") {
|
|
1144
|
+
if (!await promptConfirm(chalk.yellow(" Type 'yes' to apply these destructive changes (this cannot be undone): "))) {
|
|
1145
|
+
out(chalk.gray(" Aborted. No changes were made."));
|
|
1146
|
+
process.exit(1);
|
|
1147
|
+
}
|
|
1148
|
+
} else outWarn(chalk.yellow(" Proceeding because --allow-destructive was passed."));
|
|
1149
|
+
}
|
|
1150
|
+
await runAtlas("schema", [
|
|
1151
|
+
"apply",
|
|
1152
|
+
"--to",
|
|
1153
|
+
"file://drizzle/schema.sql",
|
|
1154
|
+
"--auto-approve"
|
|
1155
|
+
], collectionsPath);
|
|
1156
|
+
out("");
|
|
1157
|
+
if (databaseUrl) {
|
|
1158
|
+
await ensureAuthTables(databaseUrl, collectionsPath);
|
|
1159
|
+
await applyVectorDdl(databaseUrl);
|
|
1160
|
+
await applySearchDdl(databaseUrl);
|
|
1161
|
+
await applyPolicies(databaseUrl);
|
|
1162
|
+
await reconcilePolicies(databaseUrl, collectionsPath);
|
|
1163
|
+
await ensureRlsUserRole(databaseUrl);
|
|
1164
|
+
await retireLegacyAuthSchema(databaseUrl);
|
|
1165
|
+
if (scratchDatabaseCreatedHere) await dropDevDatabase(databaseUrl, getDevDatabaseUrl(databaseUrl));
|
|
1166
|
+
} else outWarn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
|
|
1167
|
+
} else if (subcommand === "migrate") {
|
|
1168
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
1169
|
+
if (databaseUrl) await ensureAuthSchemaAndFunctions(databaseUrl);
|
|
1170
|
+
const extraArgs = argsList._.filter((arg) => arg !== "migrate");
|
|
1171
|
+
const baseline = argsList["--baseline"];
|
|
1172
|
+
if (baseline) {
|
|
1173
|
+
out(chalk.gray(` Recording ${baseline} as already applied, then migrating from the next one.`));
|
|
1174
|
+
out("");
|
|
1175
|
+
}
|
|
1176
|
+
await runAtlas("migrate", [
|
|
1177
|
+
"apply",
|
|
1178
|
+
"--dir",
|
|
1179
|
+
"file://drizzle/migrations",
|
|
1180
|
+
...baseline ? ["--baseline", baseline] : [],
|
|
1181
|
+
...extraArgs
|
|
1182
|
+
], collectionsPath);
|
|
1183
|
+
if (databaseUrl) {
|
|
1184
|
+
await ensureRlsUserRole(databaseUrl);
|
|
1185
|
+
await retireLegacyAuthSchema(databaseUrl);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
out("");
|
|
1189
|
+
out(chalk.green(` ✓ rebase db ${subcommand} completed successfully.`));
|
|
1190
|
+
out("");
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
async function ensureAuthSchemaAndFunctions(databaseUrl) {
|
|
1194
|
+
try {
|
|
1195
|
+
const { Client } = await import("pg");
|
|
1196
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
1197
|
+
await client.connect();
|
|
1198
|
+
try {
|
|
1199
|
+
await client.query(RLS_BOOTSTRAP_SQL);
|
|
1200
|
+
} finally {
|
|
1201
|
+
await client.end();
|
|
1202
|
+
}
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
outWarn(chalk.yellow(` ⚠️ Failed to bootstrap the RLS helper functions: ${err instanceof Error ? err.message : String(err)}`));
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Create the framework auth tables (e.g. `rebase.users`) that the generated RLS
|
|
1209
|
+
* policies reference, before `applyPolicies` runs. Mirrors what the server does
|
|
1210
|
+
* at boot (`PostgresBootstrapper.initializeAuth` → `ensureAuthTablesExist`).
|
|
1211
|
+
*
|
|
1212
|
+
* Without this, `rebase db push` against a database that has never booted the
|
|
1213
|
+
* server fails while applying policies with `relation "rebase.users" does not
|
|
1214
|
+
* exist` — the documented first-run does `db push` *before* the first `dev`.
|
|
1215
|
+
* `ensureAuthTablesExist` is idempotent (CREATE TABLE IF NOT EXISTS), so it is
|
|
1216
|
+
* safe to run on every push and harmless once the server has also created them.
|
|
1217
|
+
*/
|
|
1218
|
+
async function ensureAuthTables(databaseUrl, collectionsPath) {
|
|
1219
|
+
try {
|
|
1220
|
+
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
1221
|
+
const { ensureAuthTablesExist } = await import("./ensure-tables-CIojK-Rx.js").then((n) => n.n);
|
|
1222
|
+
const { loadCollections } = await import("./doctor-D7HWmGzs.js").then((n) => n.t);
|
|
1223
|
+
const { Client } = await import("pg");
|
|
1224
|
+
const authCollection = (await loadCollections(path.resolve(process.cwd(), collectionsPath))).find((c) => {
|
|
1225
|
+
const a = c.auth;
|
|
1226
|
+
return a === true || typeof a === "object" && a !== null && a.enabled === true;
|
|
1227
|
+
});
|
|
1228
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
1229
|
+
await client.connect();
|
|
1230
|
+
try {
|
|
1231
|
+
await ensureAuthTablesExist(drizzle(client), authCollection);
|
|
1232
|
+
} finally {
|
|
1233
|
+
await client.end();
|
|
1234
|
+
}
|
|
1235
|
+
} catch (err) {
|
|
1236
|
+
outWarn(chalk.yellow(` ⚠️ Failed to ensure framework auth tables: ${err instanceof Error ? err.message : String(err)}`));
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Provision the restricted `rebase_user` role right after schema changes land,
|
|
1241
|
+
* so grants cover freshly created tables (default privileges cover future
|
|
1242
|
+
* ones). Only needed — and only possible without extra setup — when the
|
|
1243
|
+
* connection would bypass RLS (superuser / BYPASSRLS / table owner).
|
|
1244
|
+
*/
|
|
1245
|
+
async function ensureRlsUserRole(databaseUrl) {
|
|
1246
|
+
const schemas = ["public", "rebase"];
|
|
1247
|
+
let rls;
|
|
1248
|
+
try {
|
|
1249
|
+
rls = await import("./rls-enforcement-DeYEjYLt.js").then((n) => n.a);
|
|
1250
|
+
} catch (err) {
|
|
1251
|
+
outError(chalk.red(`\n ✗ Could not load the RLS provisioning module: ${err instanceof Error ? err.message : String(err)}`));
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
try {
|
|
1255
|
+
const { Client } = await import("pg");
|
|
1256
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
1257
|
+
await client.connect();
|
|
1258
|
+
try {
|
|
1259
|
+
const runSql = async (text) => (await client.query(text)).rows;
|
|
1260
|
+
if ((await rls.detectConnectionPosture(runSql)).privileged) {
|
|
1261
|
+
await rls.ensureAppRole(runSql, schemas);
|
|
1262
|
+
out(chalk.gray(` ✓ RLS role "${rls.REBASE_USER_ROLE}" provisioned/refreshed.`));
|
|
1263
|
+
}
|
|
1264
|
+
} finally {
|
|
1265
|
+
await client.end();
|
|
1266
|
+
}
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
outError(chalk.red(`\n ✗ The schema change was applied, but the "${rls.REBASE_USER_ROLE}" role could not be provisioned: ${err instanceof Error ? err.message : String(err)}`));
|
|
1269
|
+
outError(chalk.gray(rls.appRoleSetupInstructions("your database user", schemas)));
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Retire the pre-1.0 `auth` schema, once nothing depends on it any more.
|
|
1274
|
+
*
|
|
1275
|
+
* Runs last on purpose. Postgres refuses to drop a function an RLS policy still
|
|
1276
|
+
* calls, so this only succeeds after the policies above have been rewritten to
|
|
1277
|
+
* `rebase.uid()`. See `dropLegacyAuthSchema` for what it reports when something
|
|
1278
|
+
* hand-written is still holding the schema open, and DROP_LEGACY_AUTH_SCHEMA_SQL
|
|
1279
|
+
* for the guards that keep it off a Supabase `auth` schema.
|
|
1280
|
+
*/
|
|
1281
|
+
async function retireLegacyAuthSchema(databaseUrl) {
|
|
1282
|
+
try {
|
|
1283
|
+
const { Client } = await import("pg");
|
|
1284
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
1285
|
+
await client.connect();
|
|
1286
|
+
try {
|
|
1287
|
+
await dropLegacyAuthSchema(async (text) => (await client.query(text)).rows, {
|
|
1288
|
+
info: (m) => out(chalk.gray(` ${m}`)),
|
|
1289
|
+
warn: (m) => outWarn(chalk.yellow(` ⚠️ ${m}`))
|
|
1290
|
+
});
|
|
1291
|
+
} finally {
|
|
1292
|
+
await client.end();
|
|
1293
|
+
}
|
|
1294
|
+
} catch {}
|
|
1295
|
+
}
|
|
1296
|
+
async function applyPolicies(databaseUrl) {
|
|
1297
|
+
try {
|
|
1298
|
+
const policiesPath = path.resolve(process.cwd(), "drizzle", "policies.sql");
|
|
1299
|
+
if (!fs.existsSync(policiesPath)) return;
|
|
1300
|
+
out(chalk.gray(" Step 3/3: Applying RLS policies to database..."));
|
|
1301
|
+
out("");
|
|
1302
|
+
const policiesContent = fs.readFileSync(policiesPath, "utf-8");
|
|
1303
|
+
const { Client } = await import("pg");
|
|
1304
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
1305
|
+
await client.connect();
|
|
1306
|
+
try {
|
|
1307
|
+
await client.query(policiesContent);
|
|
1308
|
+
out(chalk.green(" ✓ RLS policies applied successfully."));
|
|
1309
|
+
} finally {
|
|
1310
|
+
await client.end();
|
|
1311
|
+
}
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
const hint = diagnoseDbError(err, databaseUrl);
|
|
1314
|
+
if (hint) outError(hint);
|
|
1315
|
+
else outError(chalk.red(` ✗ Failed to apply RLS policies: ${err instanceof Error ? err.message : String(err)}`));
|
|
1316
|
+
process.exit(1);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Remove the policies an earlier push superseded but never dropped.
|
|
1321
|
+
*
|
|
1322
|
+
* `policies.sql` only DROPs the names it is about to CREATE, and a rule's
|
|
1323
|
+
* generated name contains a hash of its own semantics — so editing a rule
|
|
1324
|
+
* writes a *new* policy and abandons the old one. Postgres ORs PERMISSIVE
|
|
1325
|
+
* policies together, which makes an abandoned grant outrank every tightening
|
|
1326
|
+
* that replaced it, and push reported success the whole time.
|
|
1327
|
+
*
|
|
1328
|
+
* Runs after `applyPolicies` so the current policies are already in place: the
|
|
1329
|
+
* drift check then sees exactly the set that should survive, and anything else
|
|
1330
|
+
* on a managed table is by definition left over.
|
|
1331
|
+
*/
|
|
1332
|
+
async function reconcilePolicies(databaseUrl, collectionsPath) {
|
|
1333
|
+
try {
|
|
1334
|
+
const { checkPolicyDrift, dropOrphanedPolicies, formatPolicyDrift, hasDrift } = await import("./policy-drift-kNJ6X2Uj.js").then((n) => n.a);
|
|
1335
|
+
const { loadCollections } = await import("./doctor-D7HWmGzs.js").then((n) => n.t);
|
|
1336
|
+
const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
|
|
1337
|
+
const { Client } = await import("pg");
|
|
1338
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
1339
|
+
await client.connect();
|
|
1340
|
+
try {
|
|
1341
|
+
const drift = await checkPolicyDrift(client, collections);
|
|
1342
|
+
const { dropped, kept } = await dropOrphanedPolicies(client, drift, collections);
|
|
1343
|
+
for (const p of dropped) out(chalk.gray(` ✓ Dropped superseded policy "${p.name}" on ${p.schema}.${p.table}`));
|
|
1344
|
+
if (dropped.length > 0) out(chalk.green(` ✓ Removed ${dropped.length} superseded RLS ${dropped.length === 1 ? "policy" : "policies"}.`));
|
|
1345
|
+
if (kept.length > 0) {
|
|
1346
|
+
outWarn(chalk.yellow(" ⚠️ Policies in the database that no collection describes:"));
|
|
1347
|
+
for (const p of kept) outWarn(chalk.yellow(` • ${p.schema}.${p.table} → "${p.name}" (${p.command} TO ${p.roles.join(", ")})`));
|
|
1348
|
+
outWarn(chalk.yellow(" These still grant access. Drop them by hand if they are stale."));
|
|
1349
|
+
}
|
|
1350
|
+
const remaining = {
|
|
1351
|
+
...drift,
|
|
1352
|
+
orphaned: kept
|
|
1353
|
+
};
|
|
1354
|
+
if (hasDrift(remaining) && (remaining.missing.length > 0 || remaining.diverged.length > 0)) {
|
|
1355
|
+
outWarn(chalk.yellow(" ⚠️ RLS policies do not match your collections:"));
|
|
1356
|
+
outWarn(formatPolicyDrift({
|
|
1357
|
+
...remaining,
|
|
1358
|
+
orphaned: []
|
|
1359
|
+
}));
|
|
1360
|
+
}
|
|
1361
|
+
} finally {
|
|
1362
|
+
await client.end();
|
|
1363
|
+
}
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
outWarn(chalk.yellow(` ⚠️ Could not reconcile RLS policies: ${err instanceof Error ? err.message : String(err)}`));
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Flags the `branch` subcommands take.
|
|
1370
|
+
*
|
|
1371
|
+
* Declared as an `arg` spec rather than picked out of argv by hand, which is
|
|
1372
|
+
* both how the rest of this file parses flags and how the documentation
|
|
1373
|
+
* verifier discovers them: it reads `arg({ … })` specs out of this source, so a
|
|
1374
|
+
* flag parsed any other way is one it reports as unrunnable in every page that
|
|
1375
|
+
* documents it. Hand-rolled parsing here cost exactly that — `--older-than`
|
|
1376
|
+
* worked and the docs check called it a command a reader cannot run.
|
|
1377
|
+
*/
|
|
1378
|
+
var BRANCH_FLAGS = {
|
|
1379
|
+
"--older-than": String,
|
|
1380
|
+
"--include-dev-diff": Boolean,
|
|
1381
|
+
"--from": String,
|
|
1382
|
+
"--yes": Boolean,
|
|
1383
|
+
"-y": "--yes"
|
|
1384
|
+
};
|
|
1385
|
+
async function branchCommand(rawArgs) {
|
|
1386
|
+
const branchAction = rawArgs[2];
|
|
1387
|
+
if (!branchAction || branchAction === "--help") {
|
|
1388
|
+
printBranchHelp();
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
const extras = unexpectedBranchArgs(rawArgs.slice(2));
|
|
1392
|
+
if (extras.length > 0) {
|
|
1393
|
+
outError("");
|
|
1394
|
+
outError(chalk.red(` ✗ Unexpected argument${extras.length > 1 ? "s" : ""}: ${extras.join(" ")}`));
|
|
1395
|
+
outError("");
|
|
1396
|
+
outError(chalk.gray(` ${chalk.bold("rebase db branch")} takes one name. A branch name may contain letters,`));
|
|
1397
|
+
outError(chalk.gray(" digits, underscores and hyphens — no spaces."));
|
|
1398
|
+
if (rawArgs[3]) outError(chalk.gray(` Did you mean: ${chalk.cyan(`rebase db branch ${branchAction} ${[rawArgs[3], ...extras].join("-")}`)}`));
|
|
1399
|
+
outError("");
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
}
|
|
1402
|
+
try {
|
|
1403
|
+
const dotenv = await import("dotenv");
|
|
1404
|
+
const envPath = process.env.DOTENV_CONFIG_PATH;
|
|
1405
|
+
if (envPath) dotenv.config({
|
|
1406
|
+
path: envPath,
|
|
1407
|
+
quiet: true
|
|
1408
|
+
});
|
|
1409
|
+
else dotenv.config({ quiet: true });
|
|
1410
|
+
} catch {}
|
|
1411
|
+
const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
|
|
1412
|
+
if (!databaseUrl) {
|
|
1413
|
+
outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
1414
|
+
process.exit(1);
|
|
1415
|
+
}
|
|
1416
|
+
const { DatabasePoolManager } = await import("./databasePoolManager-Bj5FbeAs.js").then((n) => n.n);
|
|
1417
|
+
const { BranchService } = await import("./BranchService-ByLfM_Tt.js").then((n) => n.n);
|
|
1418
|
+
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
1419
|
+
const { Pool } = await import("pg");
|
|
1420
|
+
const pool = new Pool({
|
|
1421
|
+
connectionString: databaseUrl,
|
|
1422
|
+
max: 3
|
|
1423
|
+
});
|
|
1424
|
+
const db = drizzle(pool);
|
|
1425
|
+
const poolManager = new DatabasePoolManager(databaseUrl);
|
|
1426
|
+
const branchService = new BranchService(db, poolManager);
|
|
1427
|
+
await branchService.ensureBranchMetadataTable();
|
|
1428
|
+
try {
|
|
1429
|
+
switch (branchAction) {
|
|
1430
|
+
case "create": {
|
|
1431
|
+
const name = rawArgs[3];
|
|
1432
|
+
if (!name) {
|
|
1433
|
+
outError(chalk.red("✗ Branch name is required."));
|
|
1434
|
+
out(chalk.gray(" Usage: rebase db branch create <name> [--from <source>]"));
|
|
1435
|
+
process.exit(1);
|
|
1436
|
+
}
|
|
1437
|
+
let source;
|
|
1438
|
+
const fromIdx = rawArgs.indexOf("--from");
|
|
1439
|
+
if (fromIdx !== -1 && rawArgs[fromIdx + 1]) source = rawArgs[fromIdx + 1];
|
|
1440
|
+
const force = rawArgs.includes("--force");
|
|
1441
|
+
out("");
|
|
1442
|
+
out(chalk.bold(" 🌿 Creating database branch..."));
|
|
1443
|
+
out(chalk.gray(` Name: ${name}`));
|
|
1444
|
+
if (source) out(chalk.gray(` Source: ${source}`));
|
|
1445
|
+
if (force) out(chalk.gray(" Force: other connections to the source will be disconnected"));
|
|
1446
|
+
out("");
|
|
1447
|
+
const branch = await branchService.createBranch(name, {
|
|
1448
|
+
source,
|
|
1449
|
+
force
|
|
1450
|
+
});
|
|
1451
|
+
out(chalk.green(` ✓ Branch "${branch.name}" created successfully.`));
|
|
1452
|
+
out(chalk.gray(` Database: rb_${branch.name}`));
|
|
1453
|
+
out(chalk.gray(` Parent: ${branch.parentDatabase}`));
|
|
1454
|
+
out("");
|
|
1455
|
+
break;
|
|
1456
|
+
}
|
|
1457
|
+
case "list": {
|
|
1458
|
+
const branches = await branchService.listBranches();
|
|
1459
|
+
out("");
|
|
1460
|
+
if (branches.length === 0) out(chalk.gray(" No branches found. Create one with: rebase db branch create <name>"));
|
|
1461
|
+
else {
|
|
1462
|
+
out(chalk.bold(` 🌿 ${branches.length} branch(es):`));
|
|
1463
|
+
out("");
|
|
1464
|
+
for (const b of branches) {
|
|
1465
|
+
const size = b.sizeBytes != null ? chalk.gray(` (${formatBytes(b.sizeBytes)})`) : "";
|
|
1466
|
+
const age = chalk.gray(` — created ${timeAgo(b.createdAt)}`);
|
|
1467
|
+
out(` ${chalk.green("●")} ${chalk.bold(b.name)}${size}${age}`);
|
|
1468
|
+
out(chalk.gray(` from ${b.parentDatabase}`));
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
out("");
|
|
1472
|
+
break;
|
|
1473
|
+
}
|
|
1474
|
+
case "delete": {
|
|
1475
|
+
const name = rawArgs[3];
|
|
1476
|
+
if (!name) {
|
|
1477
|
+
outError(chalk.red("✗ Branch name is required."));
|
|
1478
|
+
out(chalk.gray(" Usage: rebase db branch delete <name>"));
|
|
1479
|
+
process.exit(1);
|
|
1480
|
+
}
|
|
1481
|
+
out("");
|
|
1482
|
+
out(chalk.bold(` 🗑️ Deleting branch "${name}"...`));
|
|
1483
|
+
await branchService.deleteBranch(name, { force: rawArgs.includes("--force") });
|
|
1484
|
+
out(chalk.green(` ✓ Branch "${name}" deleted.`));
|
|
1485
|
+
out("");
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
case "info": {
|
|
1489
|
+
const name = rawArgs[3];
|
|
1490
|
+
if (!name) {
|
|
1491
|
+
outError(chalk.red("✗ Branch name is required."));
|
|
1492
|
+
out(chalk.gray(" Usage: rebase db branch info <name>"));
|
|
1493
|
+
process.exit(1);
|
|
1494
|
+
}
|
|
1495
|
+
const info = await branchService.getBranchInfo(name);
|
|
1496
|
+
out("");
|
|
1497
|
+
if (!info) {
|
|
1498
|
+
outError(chalk.red(` ✗ Branch "${name}" not found.`));
|
|
1499
|
+
out("");
|
|
1500
|
+
process.exit(1);
|
|
1501
|
+
} else {
|
|
1502
|
+
out(chalk.bold(` 🌿 Branch: ${info.name}`));
|
|
1503
|
+
out(chalk.gray(` Database: rb_${info.name}`));
|
|
1504
|
+
out(chalk.gray(` Parent: ${info.parentDatabase}`));
|
|
1505
|
+
out(chalk.gray(` Created: ${info.createdAt.toISOString()}`));
|
|
1506
|
+
if (info.sizeBytes != null) out(chalk.gray(` Size: ${formatBytes(info.sizeBytes)}`));
|
|
1507
|
+
}
|
|
1508
|
+
out("");
|
|
1509
|
+
break;
|
|
1510
|
+
}
|
|
1511
|
+
case "prune": {
|
|
1512
|
+
const parsed = (0, import_arg.default)(BRANCH_FLAGS, {
|
|
1513
|
+
argv: rawArgs.slice(3),
|
|
1514
|
+
permissive: true
|
|
1515
|
+
});
|
|
1516
|
+
const olderThanRaw = parsed["--older-than"] ?? null;
|
|
1517
|
+
const olderThanDays = olderThanRaw == null ? null : parseOlderThan(olderThanRaw);
|
|
1518
|
+
if (olderThanRaw != null && olderThanDays == null) {
|
|
1519
|
+
outError(chalk.red(` ✗ --older-than "${olderThanRaw}" is not a duration.`));
|
|
1520
|
+
out(chalk.gray(" Use a number of days (14), or 14d / 2w."));
|
|
1521
|
+
process.exit(1);
|
|
1522
|
+
}
|
|
1523
|
+
const includeDevDiff = parsed["--include-dev-diff"] === true;
|
|
1524
|
+
const { rows, databases } = await branchService.pruneCandidates();
|
|
1525
|
+
const plan = planPrune({
|
|
1526
|
+
rows,
|
|
1527
|
+
databases,
|
|
1528
|
+
branchPrefix: "rb_"
|
|
1529
|
+
}, { olderThanDays });
|
|
1530
|
+
out("");
|
|
1531
|
+
if (planIsEmpty(plan, includeDevDiff)) {
|
|
1532
|
+
out(chalk.gray(" Nothing to prune."));
|
|
1533
|
+
if (!olderThanDays && rows.length > 0) out(chalk.gray(` ${rows.length} branch(es) are healthy — --older-than 14 also removes old ones.`));
|
|
1534
|
+
if (!includeDevDiff && plan.devDiff.length > 0) {
|
|
1535
|
+
out(chalk.gray(` ${plan.devDiff.length} Atlas scratch database(s) left over from db push;`));
|
|
1536
|
+
out(chalk.gray(" --include-dev-diff removes those too."));
|
|
1537
|
+
}
|
|
1538
|
+
out("");
|
|
1539
|
+
break;
|
|
1540
|
+
}
|
|
1541
|
+
out(chalk.bold(" 🧹 Prune plan"));
|
|
1542
|
+
out("");
|
|
1543
|
+
for (const row of plan.staleRows) out(` ${chalk.yellow("○")} ${chalk.bold(row.name)} ${chalk.gray("— registered, but its database is gone; the entry will be removed")}`);
|
|
1544
|
+
for (const dbName of plan.orphanDatabases) out(` ${chalk.yellow("○")} ${chalk.bold(dbName)} ${chalk.gray("— a branch database with no entry; the database will be dropped")}`);
|
|
1545
|
+
for (const { branch, ageDays } of plan.expired) out(` ${chalk.red("●")} ${chalk.bold(branch.name)} ${chalk.gray(`— ${ageDays} days old; branch and database will be dropped`)}`);
|
|
1546
|
+
if (includeDevDiff) for (const dbName of plan.devDiff) out(` ${chalk.red("●")} ${chalk.bold(dbName)} ${chalk.gray("— Atlas scratch database; will be dropped")}`);
|
|
1547
|
+
else if (plan.devDiff.length > 0) out(chalk.gray(` (${plan.devDiff.length} Atlas scratch database(s) not listed — --include-dev-diff)`));
|
|
1548
|
+
out("");
|
|
1549
|
+
if (parsed["--yes"] !== true) {
|
|
1550
|
+
if (!await promptConfirm(" Remove these? (y/N) ")) {
|
|
1551
|
+
out(chalk.gray(" Nothing was changed."));
|
|
1552
|
+
out("");
|
|
1553
|
+
break;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
let removed = 0;
|
|
1557
|
+
for (const row of plan.staleRows) {
|
|
1558
|
+
await branchService.forgetBranchRow(row.name);
|
|
1559
|
+
removed += 1;
|
|
1560
|
+
}
|
|
1561
|
+
for (const dbName of plan.orphanDatabases) {
|
|
1562
|
+
await branchService.dropDatabase(dbName);
|
|
1563
|
+
removed += 1;
|
|
1564
|
+
}
|
|
1565
|
+
for (const { branch } of plan.expired) {
|
|
1566
|
+
await branchService.deleteBranch(branch.name);
|
|
1567
|
+
removed += 1;
|
|
1568
|
+
}
|
|
1569
|
+
if (includeDevDiff) for (const dbName of plan.devDiff) {
|
|
1570
|
+
await branchService.dropDatabase(dbName);
|
|
1571
|
+
removed += 1;
|
|
1572
|
+
}
|
|
1573
|
+
out(chalk.green(` ✓ Pruned ${removed} item(s).`));
|
|
1574
|
+
out("");
|
|
1575
|
+
break;
|
|
1576
|
+
}
|
|
1577
|
+
default:
|
|
1578
|
+
outError(chalk.red(`Unknown branch action: "${branchAction}".`));
|
|
1579
|
+
printBranchHelp();
|
|
1580
|
+
process.exit(1);
|
|
1581
|
+
}
|
|
1582
|
+
} finally {
|
|
1583
|
+
await poolManager.shutdown();
|
|
1584
|
+
await pool.end();
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
function printBranchHelp() {
|
|
1588
|
+
out(`
|
|
1589
|
+
${chalk.bold("rebase db branch")} — Database branching commands
|
|
1590
|
+
|
|
1591
|
+
${chalk.green.bold("Usage")}
|
|
1592
|
+
rebase db branch ${chalk.blue("<command>")} [options]
|
|
1593
|
+
|
|
1594
|
+
${chalk.green.bold("Commands")}
|
|
1595
|
+
${chalk.blue.bold("create")} <name> [--from <source>] Create a new branch
|
|
1596
|
+
${chalk.blue.bold("list")} List all branches
|
|
1597
|
+
${chalk.blue.bold("switch")} [<name>|--off] Point this checkout at a branch
|
|
1598
|
+
${chalk.blue.bold("delete")} <name> Delete a branch
|
|
1599
|
+
${chalk.blue.bold("info")} <name> Show branch details
|
|
1600
|
+
${chalk.blue.bold("prune")} [--older-than <14|14d|2w>] Remove branches nothing is using
|
|
1601
|
+
|
|
1602
|
+
${chalk.green.bold("Why prune")}
|
|
1603
|
+
Every branch is a full-size copy — CREATE DATABASE ... TEMPLATE duplicates the
|
|
1604
|
+
files on disk, so five branches of a 100GB database cost 500GB. Prune also
|
|
1605
|
+
finds the two ways branches drift: an entry whose database was dropped
|
|
1606
|
+
outside Rebase, and a branch database whose entry never got written.
|
|
1607
|
+
|
|
1608
|
+
${chalk.green.bold("Options")}
|
|
1609
|
+
${chalk.blue.bold("--force")} Disconnect other sessions first.
|
|
1610
|
+
Postgres refuses to copy or drop a database
|
|
1611
|
+
anything else is connected to — usually your
|
|
1612
|
+
own \`rebase dev\`.
|
|
1613
|
+
${chalk.blue.bold("--include-dev-diff")} Also prune the Atlas scratch databases
|
|
1614
|
+
\`db push\` leaves behind.
|
|
1615
|
+
${chalk.blue.bold("--yes, -y")} Skip prune's confirmation prompt. Dropping a
|
|
1616
|
+
branch is not undoable, so this is the flag
|
|
1617
|
+
a CI job passes and a person usually should not.
|
|
1618
|
+
|
|
1619
|
+
${chalk.green.bold("Examples")}
|
|
1620
|
+
${chalk.gray("# Create a branch and work on it")}
|
|
1621
|
+
rebase db branch create feature_auth
|
|
1622
|
+
rebase db branch switch feature_auth
|
|
1623
|
+
|
|
1624
|
+
${chalk.gray("# Create a branch from a specific source")}
|
|
1625
|
+
rebase db branch create staging --from production
|
|
1626
|
+
|
|
1627
|
+
${chalk.gray("# Remove branches older than two weeks, and anything orphaned")}
|
|
1628
|
+
rebase db branch prune --older-than 2w
|
|
1629
|
+
|
|
1630
|
+
${chalk.gray("# List all branches")}
|
|
1631
|
+
rebase db branch list
|
|
1632
|
+
|
|
1633
|
+
${chalk.gray("# Delete a branch")}
|
|
1634
|
+
rebase db branch delete feature_auth
|
|
1635
|
+
`);
|
|
1636
|
+
}
|
|
1637
|
+
function formatBytes(bytes) {
|
|
1638
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1639
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1640
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1641
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
1642
|
+
}
|
|
1643
|
+
/** A branch's age, for "…created 6d ago". */
|
|
1644
|
+
function timeAgo(date) {
|
|
1645
|
+
return formatRelativeTime(date, { maxMs: Number.POSITIVE_INFINITY }) ?? "unknown";
|
|
1646
|
+
}
|
|
1647
|
+
/** Set when this process created the Atlas scratch database; see `ensureDevDatabaseExists`. */
|
|
1648
|
+
var scratchDatabaseCreatedHere = false;
|
|
1649
|
+
async function runAtlas(domain, args, collectionsPath, opts = {}) {
|
|
1650
|
+
const atlasBin = resolveLocalBin("atlas");
|
|
1651
|
+
if (!atlasBin) {
|
|
1652
|
+
outError(chalk.red("\n✗ The atlas binary is missing, so the schema cannot be applied.\n"));
|
|
1653
|
+
const manager = detectProjectPackageManager();
|
|
1654
|
+
const diagnosis = diagnoseMissingBin("@ariga/atlas");
|
|
1655
|
+
if (diagnosis === "bin-link-missing") {
|
|
1656
|
+
outError(chalk.yellow(" @ariga/atlas and its binary are both on disk — only the\n node_modules/.bin/atlas link that puts it on PATH is missing.\n"));
|
|
1657
|
+
outError(chalk.gray(" Usually a half-written install: the link is created before the script that\n downloads the target, so an interrupted or copied tree keeps the binary and\n loses the shim.\n"));
|
|
1658
|
+
outError(" Fix it with:\n");
|
|
1659
|
+
outError(chalk.bold(` ${describeReinstallCommand(manager)}\n`));
|
|
1660
|
+
} else if (diagnosis === "build-script-blocked") {
|
|
1661
|
+
outError(chalk.yellow(" @ariga/atlas IS installed — only its binary is missing.\n"));
|
|
1662
|
+
outError(chalk.gray(" It downloads that binary in a `preinstall` script, and pnpm 10+ and npm 12+\n do not run a dependency's scripts unless you allow it. The install still\n exits 0, so the only sign is a line several screens up: pnpm's\n `Ignored build scripts: @ariga/atlas`, or npm's `install scripts blocked`.\n"));
|
|
1663
|
+
outError(" Fix it with either:\n");
|
|
1664
|
+
for (const line of describeBuildScriptRemedy("@ariga/atlas", manager)) outError(line ? chalk.bold(line) : "");
|
|
1665
|
+
outError("");
|
|
1666
|
+
} else {
|
|
1667
|
+
outError(chalk.gray(" It is not installed in this project.\n"));
|
|
1668
|
+
outError(" Install it with:\n");
|
|
1669
|
+
outError(chalk.bold(` ${describeDevAddCommand("@ariga/atlas", manager)}\n`));
|
|
1670
|
+
outError(chalk.gray(" If the install then reports blocked or ignored build scripts, allow them too —\n the package carries a `preinstall` script that fetches the binary.\n"));
|
|
1671
|
+
}
|
|
1672
|
+
process.exit(1);
|
|
1673
|
+
}
|
|
1674
|
+
const env = { ...process.env };
|
|
1675
|
+
try {
|
|
1676
|
+
const dotenv = await import("dotenv");
|
|
1677
|
+
const envPaths = [
|
|
1678
|
+
process.env.DOTENV_CONFIG_PATH,
|
|
1679
|
+
path.resolve(process.cwd(), ".env"),
|
|
1680
|
+
path.resolve(process.cwd(), "../.env"),
|
|
1681
|
+
path.resolve(process.cwd(), "../../.env")
|
|
1682
|
+
].filter(Boolean);
|
|
1683
|
+
for (const p of envPaths) if (fs.existsSync(p)) {
|
|
1684
|
+
const parsed = dotenv.config({
|
|
1685
|
+
path: p,
|
|
1686
|
+
quiet: true
|
|
1687
|
+
});
|
|
1688
|
+
if (parsed.parsed) {
|
|
1689
|
+
for (const [key, val] of Object.entries(parsed.parsed)) if (env[key] === void 0) env[key] = val;
|
|
1690
|
+
break;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
} catch {}
|
|
1694
|
+
const databaseUrl = env.DATABASE_URL;
|
|
1695
|
+
if (!databaseUrl) {
|
|
1696
|
+
outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
1697
|
+
process.exit(1);
|
|
1698
|
+
}
|
|
1699
|
+
await checkDatabaseConnectivity(databaseUrl);
|
|
1700
|
+
const devDatabaseUrl = getDevDatabaseUrl(databaseUrl);
|
|
1701
|
+
if (await ensureDevDatabaseExists(databaseUrl, devDatabaseUrl)) scratchDatabaseCreatedHere = true;
|
|
1702
|
+
const atlasUrl = forLibpq(databaseUrl);
|
|
1703
|
+
const atlasDevUrl = forLibpq(devDatabaseUrl);
|
|
1704
|
+
const excludes = [];
|
|
1705
|
+
if (collectionsPath && acceptsExcludeFlag(domain, args)) {
|
|
1706
|
+
excludes.push(...await getSearchExcludes(collectionsPath));
|
|
1707
|
+
excludes.push(...await getVectorExcludes(collectionsPath));
|
|
1708
|
+
try {
|
|
1709
|
+
excludes.push(...await getTableExcludes(databaseUrl, collectionsPath));
|
|
1710
|
+
} catch (err) {
|
|
1711
|
+
if (err instanceof ExcludeIntrospectionError) {
|
|
1712
|
+
outError(chalk.red("\n✗ Aborting push: could not determine which tables to protect."));
|
|
1713
|
+
outError(chalk.gray(` ${err.message}`));
|
|
1714
|
+
outError(chalk.gray(" Refusing to apply — a partial exclude list could drop tables Rebase does not manage."));
|
|
1715
|
+
const hint = diagnoseDbError(err.cause ?? err, databaseUrl);
|
|
1716
|
+
if (hint) outError(hint);
|
|
1717
|
+
process.exit(1);
|
|
1718
|
+
}
|
|
1719
|
+
throw err;
|
|
1720
|
+
}
|
|
1721
|
+
try {
|
|
1722
|
+
excludes.push(...await getForeignIndexExcludes(databaseUrl, collectionsPath));
|
|
1723
|
+
} catch (err) {
|
|
1724
|
+
if (err instanceof ExcludeIntrospectionError) {
|
|
1725
|
+
outError(chalk.red("\n✗ Aborting push: could not determine which indexes to protect."));
|
|
1726
|
+
outError(chalk.gray(` ${err.message}`));
|
|
1727
|
+
outError(chalk.gray(" Refusing to apply — a partial exclude list could drop an index Rebase does not manage."));
|
|
1728
|
+
const hint = diagnoseDbError(err.cause ?? err, databaseUrl);
|
|
1729
|
+
if (hint) outError(hint);
|
|
1730
|
+
process.exit(1);
|
|
1731
|
+
}
|
|
1732
|
+
throw err;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
const subprocess = execa(atlasBin, buildAtlasArgs({
|
|
1736
|
+
domain,
|
|
1737
|
+
args,
|
|
1738
|
+
url: atlasUrl,
|
|
1739
|
+
devUrl: atlasDevUrl,
|
|
1740
|
+
excludes
|
|
1741
|
+
}), {
|
|
1742
|
+
cwd: process.cwd(),
|
|
1743
|
+
stdout: opts.captureStdout ? "pipe" : "inherit",
|
|
1744
|
+
stderr: "pipe",
|
|
1745
|
+
env
|
|
1746
|
+
});
|
|
1747
|
+
let stdoutText = "";
|
|
1748
|
+
if (opts.captureStdout) subprocess.stdout?.on("data", (chunk) => {
|
|
1749
|
+
stdoutText += chunk.toString();
|
|
1750
|
+
});
|
|
1751
|
+
let stderrText = "";
|
|
1752
|
+
subprocess.stderr?.on("data", (chunk) => {
|
|
1753
|
+
const text = chunk.toString();
|
|
1754
|
+
stderrText += text;
|
|
1755
|
+
process.stderr.write(text);
|
|
1756
|
+
});
|
|
1757
|
+
try {
|
|
1758
|
+
await subprocess;
|
|
1759
|
+
} catch {
|
|
1760
|
+
outError(chalk.red(`\n✗ atlas ${domain} ${args.join(" ")} failed.\n`));
|
|
1761
|
+
const hint = await diagnoseAtlasFailure({
|
|
1762
|
+
domain,
|
|
1763
|
+
args,
|
|
1764
|
+
stderr: stderrText,
|
|
1765
|
+
databaseUrl,
|
|
1766
|
+
latestMigrationVersion: latestMigrationVersion()
|
|
1767
|
+
}) ?? diagnoseDbError({ message: stderrText }, databaseUrl);
|
|
1768
|
+
if (hint) outError(hint);
|
|
1769
|
+
process.exit(1);
|
|
1770
|
+
}
|
|
1771
|
+
if (opts.captureStdout) return stdoutText.trim().length > 0 ? stdoutText : stderrText;
|
|
1772
|
+
return "";
|
|
1773
|
+
}
|
|
1774
|
+
async function generatePostgresDdlCommand(rawArgs) {
|
|
1775
|
+
const argsList = (0, import_arg.default)({
|
|
1776
|
+
"--collections": String,
|
|
1777
|
+
"--output": String,
|
|
1778
|
+
"-c": "--collections",
|
|
1779
|
+
"-o": "--output"
|
|
1780
|
+
}, {
|
|
1781
|
+
argv: rawArgs.slice(2),
|
|
1782
|
+
permissive: true
|
|
1783
|
+
});
|
|
1784
|
+
const ddl = resolveChildScript("generate-postgres-ddl");
|
|
1785
|
+
if (!ddl.ok) {
|
|
1786
|
+
outError(chalk.red(childScriptError("DDL generator", ddl.reason)));
|
|
1787
|
+
process.exit(1);
|
|
1788
|
+
}
|
|
1789
|
+
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
1790
|
+
const outputPath = argsList["--output"] || path.join("drizzle", "schema.sql");
|
|
1791
|
+
const cmdParts = [
|
|
1792
|
+
ddl.bin,
|
|
1793
|
+
ddl.script,
|
|
1794
|
+
`--collections=${collectionsPath}`,
|
|
1795
|
+
`--output=${outputPath}`
|
|
1796
|
+
];
|
|
1797
|
+
try {
|
|
1798
|
+
await execa(cmdParts[0], cmdParts.slice(1), {
|
|
1799
|
+
cwd: process.cwd(),
|
|
1800
|
+
stdio: "inherit",
|
|
1801
|
+
env: { ...process.env }
|
|
1802
|
+
});
|
|
1803
|
+
} catch (err) {
|
|
1804
|
+
if (typeof err.exitCode === "number") outError(chalk.red("✗ Postgres DDL generation failed — nothing was applied."));
|
|
1805
|
+
else outError(chalk.red(`✗ Failed to run Postgres DDL generator: ${err instanceof Error ? err.message : String(err)}`));
|
|
1806
|
+
process.exit(1);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* `schema stale [--fix]` — is the generated Drizzle schema older than the rule
|
|
1811
|
+
* that derives its foreign-key names?
|
|
1812
|
+
*
|
|
1813
|
+
* This exists for one upgrade path and is worth the command. 0.13 derives
|
|
1814
|
+
* `category_id` where 0.12 derived `categorie_id`; boot-ensure renames the
|
|
1815
|
+
* database column to match, and the project's checked-in
|
|
1816
|
+
* `backend/src/schema.generated.ts` is then wrong in a way nothing the developer
|
|
1817
|
+
* did would explain. Relation validation refuses to boot on it, permanently,
|
|
1818
|
+
* because the rename has already been applied and will not run again.
|
|
1819
|
+
*
|
|
1820
|
+
* `rebase dev` calls this with `--fix` before starting the backend, so the
|
|
1821
|
+
* upgrade behaves the way the release note says it does: the column moves and the
|
|
1822
|
+
* project keeps working. Without `--fix` it reports and exits non-zero, which is
|
|
1823
|
+
* what a build or a CI step wants.
|
|
1824
|
+
*/
|
|
1825
|
+
async function schemaStaleCommand(rawArgs) {
|
|
1826
|
+
const argsList = (0, import_arg.default)({
|
|
1827
|
+
"--collections": String,
|
|
1828
|
+
"--output": String,
|
|
1829
|
+
"--fix": Boolean,
|
|
1830
|
+
"-c": "--collections",
|
|
1831
|
+
"-o": "--output",
|
|
1832
|
+
"--out": "--output"
|
|
1833
|
+
}, {
|
|
1834
|
+
argv: rawArgs.slice(2),
|
|
1835
|
+
permissive: true
|
|
1836
|
+
});
|
|
1837
|
+
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
1838
|
+
const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
|
|
1839
|
+
const schemaFile = path.resolve(process.cwd(), outputPath);
|
|
1840
|
+
const fix = Boolean(argsList["--fix"]);
|
|
1841
|
+
const generatedExists = fs.existsSync(schemaFile);
|
|
1842
|
+
const { loadCollections } = await import("./doctor-D7HWmGzs.js").then((n) => n.t);
|
|
1843
|
+
const { findLegacyForeignKeyNames, staleVerdict } = await import("./generated-schema-staleness-DQdfJl2A.js");
|
|
1844
|
+
let stale = [];
|
|
1845
|
+
let unreadable;
|
|
1846
|
+
if (generatedExists) try {
|
|
1847
|
+
const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
|
|
1848
|
+
stale = findLegacyForeignKeyNames(fs.readFileSync(schemaFile, "utf8"), collections);
|
|
1849
|
+
} catch (err) {
|
|
1850
|
+
unreadable = err instanceof Error ? err.message : String(err);
|
|
1851
|
+
logger.debug(`schema stale: skipped (${unreadable})`);
|
|
1852
|
+
}
|
|
1853
|
+
const verdict = staleVerdict({
|
|
1854
|
+
outputPath,
|
|
1855
|
+
generatedExists,
|
|
1856
|
+
stale,
|
|
1857
|
+
unreadable,
|
|
1858
|
+
fix
|
|
1859
|
+
});
|
|
1860
|
+
for (const line of verdict.lines) if (!line) out("");
|
|
1861
|
+
else if (line.includes("⚠️")) outWarn(chalk.yellow(line));
|
|
1862
|
+
else if (verdict.exitCode !== 0) outError(chalk.red(line));
|
|
1863
|
+
else if (line.includes("✓")) out(chalk.green(line));
|
|
1864
|
+
else out(chalk.gray(line));
|
|
1865
|
+
if (verdict.exitCode !== 0) process.exit(verdict.exitCode);
|
|
1866
|
+
if (!verdict.regenerate) return;
|
|
1867
|
+
await schemaCommand("generate", [
|
|
1868
|
+
"schema",
|
|
1869
|
+
"generate",
|
|
1870
|
+
`--collections=${collectionsPath}`,
|
|
1871
|
+
`--output=${outputPath}`
|
|
1872
|
+
]);
|
|
1873
|
+
}
|
|
1874
|
+
async function schemaCommand(subcommand, rawArgs) {
|
|
1875
|
+
if (subcommand && (rawArgs.includes("--help") || rawArgs.includes("-h"))) {
|
|
1876
|
+
out("");
|
|
1877
|
+
out(chalk.bold(` rebase schema ${subcommand}`));
|
|
1878
|
+
out(chalk.gray(" Run `rebase schema --help` for the full page — this is the driver's own entry point."));
|
|
1879
|
+
out("");
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (subcommand === "stale") {
|
|
1883
|
+
await schemaStaleCommand(rawArgs);
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
if (subcommand === "generate") {
|
|
1887
|
+
const argsList = (0, import_arg.default)({
|
|
1888
|
+
"--collections": String,
|
|
1889
|
+
"--output": String,
|
|
1890
|
+
"--watch": Boolean,
|
|
1891
|
+
"-c": "--collections",
|
|
1892
|
+
"-o": "--output",
|
|
1893
|
+
"--out": "--output",
|
|
1894
|
+
"-w": "--watch"
|
|
1895
|
+
}, {
|
|
1896
|
+
argv: rawArgs.slice(2),
|
|
1897
|
+
permissive: true
|
|
1898
|
+
});
|
|
1899
|
+
const generator = resolveChildScript("generate-drizzle-schema");
|
|
1900
|
+
if (!generator.ok) {
|
|
1901
|
+
outError(chalk.red(childScriptError("schema generator", generator.reason)));
|
|
1902
|
+
process.exit(1);
|
|
1903
|
+
}
|
|
1904
|
+
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
1905
|
+
const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
|
|
1906
|
+
const watch = argsList["--watch"] || false;
|
|
1907
|
+
out("");
|
|
1908
|
+
out(chalk.bold(" 🔧 Rebase Schema Generator"));
|
|
1909
|
+
out("");
|
|
1910
|
+
const cmdParts = [
|
|
1911
|
+
generator.bin,
|
|
1912
|
+
generator.script,
|
|
1913
|
+
`--collections=${collectionsPath}`,
|
|
1914
|
+
`--output=${outputPath}`
|
|
1915
|
+
];
|
|
1916
|
+
if (watch) cmdParts.push("--watch");
|
|
1917
|
+
try {
|
|
1918
|
+
await execa(cmdParts[0], cmdParts.slice(1), {
|
|
1919
|
+
cwd: process.cwd(),
|
|
1920
|
+
stdio: "inherit",
|
|
1921
|
+
env: { ...process.env }
|
|
1922
|
+
});
|
|
1923
|
+
} catch (err) {
|
|
1924
|
+
if (typeof err.exitCode === "number") outError(chalk.red("✗ Drizzle schema generation failed — nothing was applied."));
|
|
1925
|
+
else outError(chalk.red(`✗ Failed to run schema generator: ${err instanceof Error ? err.message : String(err)}`));
|
|
1926
|
+
process.exit(1);
|
|
1927
|
+
}
|
|
1928
|
+
} else if (subcommand === "introspect") {
|
|
1929
|
+
const argsList = (0, import_arg.default)({
|
|
1930
|
+
"--output": String,
|
|
1931
|
+
"--collections": String,
|
|
1932
|
+
"--force": Boolean,
|
|
1933
|
+
"--schema": String,
|
|
1934
|
+
"-o": "--output",
|
|
1935
|
+
"--out": "--output",
|
|
1936
|
+
"-c": "--collections",
|
|
1937
|
+
"-f": "--force"
|
|
1938
|
+
}, {
|
|
1939
|
+
argv: rawArgs.slice(2),
|
|
1940
|
+
permissive: true
|
|
1941
|
+
});
|
|
1942
|
+
const introspect = resolveChildScript("introspect-db");
|
|
1943
|
+
if (!introspect.ok) {
|
|
1944
|
+
outError(chalk.red(childScriptError("introspection script", introspect.reason)));
|
|
1945
|
+
process.exit(1);
|
|
1946
|
+
}
|
|
1947
|
+
const outputPath = argsList["--output"] || argsList["--collections"] || path.join("..", "config", "collections");
|
|
1948
|
+
out("");
|
|
1949
|
+
out(chalk.bold(" 🔍 Rebase Schema Introspector"));
|
|
1950
|
+
out("");
|
|
1951
|
+
const cmdParts = [
|
|
1952
|
+
introspect.bin,
|
|
1953
|
+
introspect.script,
|
|
1954
|
+
`--output=${outputPath}`,
|
|
1955
|
+
...argsList["--force"] ? ["--force"] : [],
|
|
1956
|
+
...argsList["--schema"] ? [`--schema=${argsList["--schema"]}`] : []
|
|
1957
|
+
];
|
|
1958
|
+
try {
|
|
1959
|
+
await execa(cmdParts[0], cmdParts.slice(1), {
|
|
1960
|
+
cwd: process.cwd(),
|
|
1961
|
+
stdio: "inherit",
|
|
1962
|
+
env: { ...process.env }
|
|
1963
|
+
});
|
|
1964
|
+
} catch (err) {
|
|
1965
|
+
outError(chalk.red(`✗ Failed to run schema introspector: ${err instanceof Error ? err.message : String(err)}`));
|
|
1966
|
+
process.exit(1);
|
|
1967
|
+
}
|
|
1968
|
+
} else {
|
|
1969
|
+
outError(chalk.red("Unknown schema command."));
|
|
1970
|
+
process.exit(1);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
async function doctorPluginCommand(rawArgs) {
|
|
1974
|
+
const parsedArgs = (0, import_arg.default)({
|
|
1975
|
+
"--collections": String,
|
|
1976
|
+
"--schema": String,
|
|
1977
|
+
"--sdk": String,
|
|
1978
|
+
"--policies": Boolean,
|
|
1979
|
+
"-c": "--collections",
|
|
1980
|
+
"-s": "--schema",
|
|
1981
|
+
"-k": "--sdk"
|
|
1982
|
+
}, {
|
|
1983
|
+
argv: rawArgs.slice(1),
|
|
1984
|
+
permissive: true
|
|
1985
|
+
});
|
|
1986
|
+
const doctor = resolveChildScript("doctor-cli");
|
|
1987
|
+
if (!doctor.ok) {
|
|
1988
|
+
outError(chalk.red(childScriptError("doctor script", doctor.reason)));
|
|
1989
|
+
process.exit(1);
|
|
1990
|
+
}
|
|
1991
|
+
const collectionsPath = parsedArgs["--collections"] || path.join("..", "config", "collections");
|
|
1992
|
+
const schemaPath = parsedArgs["--schema"] || path.join("src", "schema.generated.ts");
|
|
1993
|
+
const sdkPath = parsedArgs["--sdk"] || path.join("..", "generated", "sdk", "database.types.ts");
|
|
1994
|
+
const cmdParts = [
|
|
1995
|
+
doctor.bin,
|
|
1996
|
+
doctor.script,
|
|
1997
|
+
`--collections=${collectionsPath}`,
|
|
1998
|
+
`--schema=${schemaPath}`,
|
|
1999
|
+
`--sdk=${sdkPath}`,
|
|
2000
|
+
...parsedArgs["--policies"] ? ["--policies"] : []
|
|
2001
|
+
];
|
|
2002
|
+
try {
|
|
2003
|
+
await execa(cmdParts[0], cmdParts.slice(1), {
|
|
2004
|
+
cwd: process.cwd(),
|
|
2005
|
+
stdio: "inherit",
|
|
2006
|
+
env: { ...process.env }
|
|
2007
|
+
});
|
|
2008
|
+
} catch {
|
|
2009
|
+
process.exit(1);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
var argv1Real = process.argv[1] ? fs.realpathSync(process.argv[1]) : "";
|
|
2013
|
+
if (import.meta.url === `file://${argv1Real}`) runPluginCommand(process.argv.slice(2)).catch((error) => {
|
|
2014
|
+
reportCommandFailure(error);
|
|
2015
|
+
process.exit(1);
|
|
2016
|
+
});
|
|
2017
|
+
//#endregion
|
|
2018
|
+
export { runPluginCommand };
|
|
2019
|
+
|
|
2020
|
+
//# sourceMappingURL=cli.js.map
|