@rebasepro/server-postgres 0.23.0 → 0.24.1-canary.g56af4a4
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-DGPL6G_C.js → BranchService-Bsn2wfaj.js} +36 -18
- package/dist/BranchService-Bsn2wfaj.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +166 -13
- package/dist/auth/ensure-tables.d.ts +12 -0
- package/dist/auth/services.d.ts +78 -14
- package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
- package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
- package/dist/backup/backup-cli.d.ts +14 -0
- package/dist/backup/backup-service.d.ts +2 -2
- package/dist/backup/pg-binary.d.ts +5 -0
- package/dist/{backup-cli-24v4OlSp.js → backup-cli-aUfRv16h.js} +37 -29
- package/dist/backup-cli-aUfRv16h.js.map +1 -0
- package/dist/{backup-service-HQ9GC4tN.js → backup-service-BTBB7RXn.js} +60 -28
- package/dist/backup-service-BTBB7RXn.js.map +1 -0
- package/dist/boot-ddl-DVpsFN6r.js +105 -0
- package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
- package/dist/cli-errors-CaI4ntyD.js +1919 -0
- package/dist/cli-errors-CaI4ntyD.js.map +1 -0
- package/dist/cli-errors.d.ts +93 -5
- package/dist/{cli-flags-BglvpjHv.js → cli-flags-B-Dawocl.js} +5 -2
- package/dist/cli-flags-B-Dawocl.js.map +1 -0
- package/dist/cli-helpers.d.ts +52 -12
- package/dist/cli-output.d.ts +16 -28
- package/dist/cli-push.d.ts +87 -0
- package/dist/cli.js +625 -190
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-drZIsSNf.js} +42 -26
- package/dist/column-plan-helpers-drZIsSNf.js.map +1 -0
- package/dist/{databasePoolManager-Bj5FbeAs.js → connection-BPoNILnD.js} +19 -110
- package/dist/connection-BPoNILnD.js.map +1 -0
- package/dist/connection.d.ts +9 -1
- package/dist/data-transformer.d.ts +0 -8
- package/dist/databasePoolManager-BkEy2w18.js +119 -0
- package/dist/databasePoolManager-BkEy2w18.js.map +1 -0
- package/dist/deferred-index-build-DP1_SNFo.js +1051 -0
- package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
- package/dist/{doctor-Cb2thZ8s.js → doctor-C4WiftHm.js} +279 -87
- package/dist/doctor-C4WiftHm.js.map +1 -0
- package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
- package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
- package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
- package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
- package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-CHsgyLqg.js} +178 -75
- package/dist/ensure-tables-CHsgyLqg.js.map +1 -0
- package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
- package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
- package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
- package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
- package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-B74WN37S.js} +28 -10
- package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
- package/dist/generate-files-CdEckX6c.js +199 -0
- package/dist/generate-files-CdEckX6c.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-B64O2oZF.js +152 -0
- package/dist/generate-postgres-ddl-logic-B64O2oZF.js.map +1 -0
- package/dist/generated-sql.d.ts +34 -0
- package/dist/history/HistoryService.d.ts +38 -6
- package/dist/history/ensure-history-table.d.ts +42 -4
- package/dist/index.es.js +5557 -7612
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-C-Di8dA9.js} +562 -350
- package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
- package/dist/introspect-db-queries-D6kAqReI.js +339 -0
- package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
- package/dist/pg-error-utils-vJezlXdI.js +388 -0
- package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
- package/dist/{plan-schema-C0fxM8dY.js → plan-schema-B1YF_tYq.js} +278 -533
- package/dist/plan-schema-B1YF_tYq.js.map +1 -0
- package/dist/{policy-drift-DljYdrpW.js → policy-drift-Dm8i-s1J.js} +24 -7
- package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
- package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
- package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
- package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
- package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-CtC3y9Ol.js} +17 -151
- package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
- package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1046
- package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
- package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-DvzqlaL1.js} +137 -17
- package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +172 -2
- package/dist/schema/boot-ddl.d.ts +47 -0
- package/dist/schema/classify-change.d.ts +29 -1
- package/dist/schema/collection-index.d.ts +45 -0
- package/dist/schema/column-plan-helpers.d.ts +38 -21
- package/dist/schema/deferred-index-build.d.ts +131 -0
- package/dist/schema/destructive-sql.d.ts +27 -1
- package/dist/schema/doctor-cli.js +110 -6
- package/dist/schema/doctor-cli.js.map +1 -1
- package/dist/schema/doctor-search-scale.d.ts +45 -0
- package/dist/schema/doctor.d.ts +53 -1
- package/dist/schema/ensure-collection-policies.d.ts +95 -35
- package/dist/schema/ensure-collection-tables.d.ts +56 -3
- package/dist/schema/enum-labels-kept.d.ts +26 -0
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-files.d.ts +44 -0
- package/dist/schema/generate-next-step.d.ts +0 -13
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
- package/dist/schema/generate-postgres-ddl.js +10 -81
- package/dist/schema/generate-postgres-ddl.js.map +1 -1
- package/dist/schema/generate-schema-commit.d.ts +12 -0
- package/dist/schema/introspect-db-logic.d.ts +43 -0
- package/dist/schema/introspect-db-queries.d.ts +12 -2
- package/dist/schema/introspect-db-search.d.ts +22 -0
- package/dist/schema/introspect-db-storage.d.ts +83 -0
- package/dist/schema/introspect-db-structure.d.ts +10 -0
- package/dist/schema/introspect-db.js +17 -6
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/migration-post-process.d.ts +34 -0
- package/dist/schema/plan/diff-plan.d.ts +127 -1
- package/dist/schema/plan/plan-schema.d.ts +16 -8
- package/dist/schema/plan/render-ddl.d.ts +6 -0
- package/dist/schema/plan/types.d.ts +45 -11
- package/dist/schema/push-index-prebuild.d.ts +25 -0
- package/dist/schema/push-legacy-renames.d.ts +20 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
- package/dist/search-column-BM-GV6vH.js +442 -0
- package/dist/search-column-BM-GV6vH.js.map +1 -0
- package/dist/security/policy-drift.d.ts +1 -1
- package/dist/security/rls-enforcement.d.ts +39 -4
- package/dist/services/BranchService.d.ts +22 -1
- package/dist/services/FetchService.d.ts +63 -12
- package/dist/services/PersistService.d.ts +15 -0
- package/dist/services/RelationService.d.ts +106 -3
- package/dist/services/cdc/CdcListener.d.ts +26 -10
- package/dist/services/cdc/identity-columns.d.ts +19 -0
- package/dist/services/cdc/trigger-cdc.d.ts +180 -18
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
- package/dist/services/channel-history.d.ts +32 -5
- package/dist/services/collection-helpers.d.ts +26 -1
- package/dist/services/field-viewer.d.ts +15 -1
- package/dist/services/pg-notify-listener.d.ts +95 -6
- package/dist/services/realtimeService.d.ts +467 -53
- package/dist/services/row-pipeline.d.ts +16 -9
- package/dist/services/socket-liveness.d.ts +85 -0
- package/dist/services/soft-delete.d.ts +12 -0
- package/dist/services/sql-script.d.ts +275 -0
- package/dist/services/vector-iterative-scan.d.ts +40 -0
- package/dist/services/write-depth.d.ts +22 -0
- package/dist/services/write-transaction-scope.d.ts +15 -3
- package/dist/socket-liveness-D3UD5_BW.js +7570 -0
- package/dist/socket-liveness-D3UD5_BW.js.map +1 -0
- package/dist/socket-opt-ins.d.ts +64 -0
- package/dist/source-DBDbBTL4.js +811 -0
- package/dist/source-DBDbBTL4.js.map +1 -0
- package/dist/tsx-loader.d.ts +14 -0
- package/dist/utils/drizzle-conditions.d.ts +63 -3
- package/dist/utils/error-cause.d.ts +7 -0
- package/dist/utils/pg-error-utils.d.ts +2 -4
- package/dist/utils/result-rows.d.ts +10 -0
- package/dist/websocket.d.ts +63 -18
- package/package.json +7 -7
- package/dist/BranchService-DGPL6G_C.js.map +0 -1
- package/dist/backup-cli-24v4OlSp.js.map +0 -1
- package/dist/backup-service-HQ9GC4tN.js.map +0 -1
- package/dist/cli-errors-B8qHg02P.js +0 -798
- package/dist/cli-errors-B8qHg02P.js.map +0 -1
- package/dist/cli-flags-BglvpjHv.js.map +0 -1
- package/dist/cli-output-CNdMql-L.js +0 -47
- package/dist/cli-output-CNdMql-L.js.map +0 -1
- package/dist/collection-index-DdnsxiJ_.js +0 -286
- package/dist/collection-index-DdnsxiJ_.js.map +0 -1
- package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
- package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
- package/dist/doctor-Cb2thZ8s.js.map +0 -1
- package/dist/ensure-collection-policies-RHUcEp4v.js +0 -105
- package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
- package/dist/ensure-collection-tables-BEEjn5cn.js +0 -857
- package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
- package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
- package/dist/generate-drizzle-schema-yzY_BLhr.js +0 -123
- package/dist/generate-drizzle-schema-yzY_BLhr.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
- package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
- package/dist/plan-schema-C0fxM8dY.js.map +0 -1
- package/dist/policy-drift-DljYdrpW.js.map +0 -1
- package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
- package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
- package/dist/source-Br7L7GOI.js +0 -410
- package/dist/source-Br7L7GOI.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
3
|
import { a as __toESM } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
|
-
import { a as formatForeignGeneratedColumnBanner, c as reportCommandFailure, i as diagnoseDbError, r as diagnoseAtlasFailure, t as checkDatabaseConnectivity } from "./cli-errors-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { t as chalk } from "./source-Br7L7GOI.js";
|
|
4
|
+
import { A as queryColumnTypes, B as prepareGeneratedSqlDir, C as getForeignIndexExcludes, D as getVectorExcludes, E as getTriggerExcludes, F as resolveLocalBin, G as isSyncedPlan, H as detectDestructiveStatements, I as withAppClient, L as DESIRED_STATE_URL, M as readSearchDdl, N as readTriggersDdl, O as loadCollectionsForCli, P as readVectorDdl, R as GENERATED_SQL_DIR, S as ensureDevDatabaseExists, T as getTableExcludes, U as extractPlanStatements, V as decidePushSafety, W as fromAlterTable, _ as describeReinstallCommand, a as formatForeignGeneratedColumnBanner, b as dropDevDatabase, c as reportCommandFailure, d as applyTriggersDdl, f as applyVectorDdl, g as describeDevAddCommand, h as describeBuildScriptRemedy, i as diagnoseDbError, j as queryGeneratedColumnDependencies, k as promptConfirm, l as ExcludeIntrospectionError, m as closeAppClients, p as atlasDevDatabaseUrl, r as diagnoseAtlasFailure, t as checkDatabaseConnectivity, u as applySearchDdl, v as detectProjectPackageManager, w as getSearchExcludes, x as dropGeneratedColumns, y as diagnoseMissingBin, z as generatedSqlDir } from "./cli-errors-CaI4ntyD.js";
|
|
5
|
+
import { a as outWarn, i as outError, r as out, t as chalk } from "./source-DBDbBTL4.js";
|
|
6
|
+
import { o as execa, r as dropLegacyAuthSchema, s as forLibpq, t as RLS_BOOTSTRAP_SQL } from "./rls-bootstrap-sql-RQUljnJ1.js";
|
|
8
7
|
import { t as require_arg } from "./arg-Dni7MzLB.js";
|
|
9
|
-
import { n as collectionsPathIn, r as parseDriverLine, t as assertKnownFlags } from "./cli-flags-
|
|
10
|
-
import { logger } from "@rebasepro/server";
|
|
8
|
+
import { n as collectionsPathIn, r as parseDriverLine, t as assertKnownFlags } from "./cli-flags-B-Dawocl.js";
|
|
11
9
|
import { formatRelativeTime } from "@rebasepro/utils";
|
|
12
|
-
import fs from "fs";
|
|
13
10
|
import path from "path";
|
|
14
|
-
import
|
|
15
|
-
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
13
|
+
import { createRequire } from "module";
|
|
14
|
+
//#region src/tsx-loader.ts
|
|
16
15
|
var import_arg = /* @__PURE__ */ __toESM(require_arg(), 1);
|
|
17
16
|
/**
|
|
17
|
+
* tsx's loader, as the URL `node --import` takes, or null when none resolves.
|
|
18
|
+
*
|
|
19
|
+
* For running one of this driver's scripts in one Node process. The `tsx`
|
|
20
|
+
* binary is a relay — pnpm's `.bin/tsx` shell shim, then tsx's CLI in one Node,
|
|
21
|
+
* which spawns a second Node with this same loader to run the script — so
|
|
22
|
+
* spawning through it costs a shell and a Node start (~50–100 ms) per child.
|
|
23
|
+
*
|
|
24
|
+
* Resolved the way Node resolves the package: from the project the driver
|
|
25
|
+
* runs in first (its cwd is the backend, where a scaffold declares tsx), then
|
|
26
|
+
* from this package. A URL rather than a path: a Windows path is not a valid
|
|
27
|
+
* `--import` specifier.
|
|
28
|
+
*/
|
|
29
|
+
function resolveTsxLoader() {
|
|
30
|
+
const bases = [path.join(process.cwd(), "noop.js"), import.meta.url];
|
|
31
|
+
for (const base of bases) try {
|
|
32
|
+
return pathToFileURL(createRequire(base).resolve("tsx")).href;
|
|
33
|
+
} catch {}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/schema/generated-column-conflicts.ts
|
|
38
|
+
/**
|
|
18
39
|
* Which columns an Atlas plan cannot touch, because a generated column reads
|
|
19
40
|
* them.
|
|
20
41
|
*
|
|
@@ -51,16 +72,6 @@ var ALTER_COLUMN_RE = new RegExp(String.raw`\bALTER\s+(?:COLUMN\s+)?(${IDENT$1})
|
|
|
51
72
|
var DROP_COLUMN_RE$1 = new RegExp(String.raw`\bDROP\s+COLUMN\s+(?:IF\s+EXISTS\s+)?(${IDENT$1})`, "gi");
|
|
52
73
|
var unquote = (ident) => ident.startsWith("\"") ? ident.slice(1, -1) : ident;
|
|
53
74
|
/**
|
|
54
|
-
* Strip Atlas's plan rendering from a statement.
|
|
55
|
-
*
|
|
56
|
-
* What `schema apply --dry-run` prints is not a SQL file: each statement is
|
|
57
|
-
* indented under a `-- modify "posts" table` heading and prefixed with `-> `,
|
|
58
|
-
* with an `-- ok (12µs)` line after it. `splitSqlStatements` drops the comment
|
|
59
|
-
* lines, and this drops the arrow — without it an anchored `^ALTER TABLE`
|
|
60
|
-
* matches nothing at all, which is a silent no-op rather than an error.
|
|
61
|
-
*/
|
|
62
|
-
var stripPlanMarker = (statement) => statement.replace(/^\s*->\s*/, "").trim();
|
|
63
|
-
/**
|
|
64
75
|
* Every column an Atlas plan retypes or drops, with the table it belongs to.
|
|
65
76
|
*
|
|
66
77
|
* One statement carries many clauses — Atlas emits `ALTER TABLE "public"."posts"
|
|
@@ -69,8 +80,8 @@ var stripPlanMarker = (statement) => statement.replace(/^\s*->\s*/, "").trim();
|
|
|
69
80
|
*/
|
|
70
81
|
function parseColumnMutations(planSql) {
|
|
71
82
|
const mutations = [];
|
|
72
|
-
for (const raw of
|
|
73
|
-
const statement =
|
|
83
|
+
for (const raw of extractPlanStatements(planSql)) {
|
|
84
|
+
const statement = fromAlterTable(raw);
|
|
74
85
|
const table = TABLE_RE.exec(statement);
|
|
75
86
|
if (!table) continue;
|
|
76
87
|
const [schema, name] = table[2] ? [unquote(table[1]), unquote(table[2])] : [void 0, unquote(table[1])];
|
|
@@ -543,6 +554,60 @@ function tidy(sql) {
|
|
|
543
554
|
return trimmed.length === 0 ? "" : `${trimmed}\n`;
|
|
544
555
|
}
|
|
545
556
|
//#endregion
|
|
557
|
+
//#region src/schema/migration-post-process.ts
|
|
558
|
+
/**
|
|
559
|
+
* Run the steps that finish a migration Atlas just wrote — the search, vector
|
|
560
|
+
* and trigger DDL, the RLS bootstrap and policies, the re-hash — and take the
|
|
561
|
+
* migration back out when any of them fails.
|
|
562
|
+
*
|
|
563
|
+
* Atlas's own file creates the tables and nothing else: no `ENABLE ROW LEVEL
|
|
564
|
+
* SECURITY`, no policy. Those arrive in the append. A failure here used to be a
|
|
565
|
+
* yellow warning followed by "You can now run rebase db migrate", and when it
|
|
566
|
+
* struck before the file was rewritten the checksum still matched — so the
|
|
567
|
+
* migration applied cleanly and created every new table with RLS off while
|
|
568
|
+
* the schema's default privileges handed `rebase_user` full DML on it. On a
|
|
569
|
+
* deployment that runs migrations and never provisions at boot
|
|
570
|
+
* (`REBASE_MIGRATE_ON_BOOT=none`), nothing closed it again.
|
|
571
|
+
*
|
|
572
|
+
* Deleting the file is safe because it is the one this run created: running
|
|
573
|
+
* `db generate` again, once the cause is fixed, writes the same diff.
|
|
574
|
+
*/
|
|
575
|
+
async function finishNewMigration(file, steps, rehash) {
|
|
576
|
+
try {
|
|
577
|
+
await steps();
|
|
578
|
+
return { ok: true };
|
|
579
|
+
} catch (err) {
|
|
580
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
581
|
+
let removed = false;
|
|
582
|
+
try {
|
|
583
|
+
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
584
|
+
removed = !fs.existsSync(file);
|
|
585
|
+
} catch {
|
|
586
|
+
removed = false;
|
|
587
|
+
}
|
|
588
|
+
if (!removed) return {
|
|
589
|
+
ok: false,
|
|
590
|
+
error,
|
|
591
|
+
removed
|
|
592
|
+
};
|
|
593
|
+
try {
|
|
594
|
+
await rehash();
|
|
595
|
+
return {
|
|
596
|
+
ok: false,
|
|
597
|
+
error,
|
|
598
|
+
removed
|
|
599
|
+
};
|
|
600
|
+
} catch (hashErr) {
|
|
601
|
+
return {
|
|
602
|
+
ok: false,
|
|
603
|
+
error,
|
|
604
|
+
removed,
|
|
605
|
+
rehashError: hashErr instanceof Error ? hashErr.message : String(hashErr)
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
//#endregion
|
|
546
611
|
//#region src/schema/atlas-argv.ts
|
|
547
612
|
/**
|
|
548
613
|
* Does this Atlas invocation accept `--exclude`?
|
|
@@ -637,6 +702,90 @@ var ScratchDatabase = class {
|
|
|
637
702
|
}
|
|
638
703
|
};
|
|
639
704
|
//#endregion
|
|
705
|
+
//#region src/cli-push.ts
|
|
706
|
+
/**
|
|
707
|
+
* Apply a planned push.
|
|
708
|
+
*
|
|
709
|
+
* A dry-run that answered "Schema is synced" skips the second Atlas run: that
|
|
710
|
+
* run would plan the same diff again (≈0.9 s at 111 tables, 5.5 s at 411 —
|
|
711
|
+
* Atlas's cost grows faster than the table count) and then apply nothing. The
|
|
712
|
+
* steps after it still run, because Atlas cannot see what they own. A plan with
|
|
713
|
+
* anything in it keeps the old flow, including Atlas re-planning at apply time.
|
|
714
|
+
*
|
|
715
|
+
* @returns `"synced"` when Atlas was not run again, `"applied"` otherwise.
|
|
716
|
+
*/
|
|
717
|
+
async function applyPushPlan(plan, steps) {
|
|
718
|
+
const synced = isSyncedPlan(plan.dryRun) && plan.rebuildable === 0;
|
|
719
|
+
if (synced) steps.reportSynced();
|
|
720
|
+
else {
|
|
721
|
+
if (plan.rebuildable > 0) await steps.dropRebuildable();
|
|
722
|
+
try {
|
|
723
|
+
await steps.atlasApply();
|
|
724
|
+
} catch (err) {
|
|
725
|
+
if (plan.rebuildable > 0) await steps.rebuildAfterFailure();
|
|
726
|
+
throw err;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
await steps.afterApply();
|
|
730
|
+
return synced ? "synced" : "applied";
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* The tables whose stamp `db push` may write: those every one of whose planned
|
|
734
|
+
* statements `policies.sql` carries, verbatim.
|
|
735
|
+
*
|
|
736
|
+
* A stamp says "this stored definition is what the plan's statement made". It
|
|
737
|
+
* is only true of a policy the statement *did* make — and since both are
|
|
738
|
+
* rendered by the same function from the same collections, a table whose
|
|
739
|
+
* statements are all in the file is one whose policies the file just created.
|
|
740
|
+
* Anything else (a hand-edited file, a file generated elsewhere) is not
|
|
741
|
+
* stamped, and the next boot re-applies that table, as before.
|
|
742
|
+
*/
|
|
743
|
+
function stampableTables(plans, policiesSql) {
|
|
744
|
+
return plans.filter((plan) => plan.policyStatements.length > 0 && plan.policyStatements.every((statement) => policiesSql.includes(statement)));
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Run `policies.sql`, then stamp the policies it created the way a boot does,
|
|
748
|
+
* in the same transaction.
|
|
749
|
+
*
|
|
750
|
+
* Before stamps the push's policies carried none, so the first boot after
|
|
751
|
+
* every push re-applied them all — a DROP and a CREATE per policy, an ACCESS
|
|
752
|
+
* EXCLUSIVE lock per table. Stamped here, that boot finds them current.
|
|
753
|
+
*
|
|
754
|
+
* The file runs in one transaction, as it did as one implicit one: a policy
|
|
755
|
+
* that fails leaves the previous ones in place. The stamps run after it, under
|
|
756
|
+
* a savepoint: a stamp that cannot be written costs the next boot a re-apply,
|
|
757
|
+
* which is what it did before, so it is reported and the policies are kept.
|
|
758
|
+
* Nothing is left set on the connection: `SET LOCAL` ends with the transaction.
|
|
759
|
+
*/
|
|
760
|
+
async function applyPoliciesFile(client, policiesSql, stamping) {
|
|
761
|
+
await client.query("BEGIN");
|
|
762
|
+
let stamped = 0;
|
|
763
|
+
let stampError;
|
|
764
|
+
try {
|
|
765
|
+
await client.query(policiesSql);
|
|
766
|
+
if (stamping.statements.length > 0) {
|
|
767
|
+
await client.query("SAVEPOINT rebase_policy_stamps");
|
|
768
|
+
try {
|
|
769
|
+
if (stamping.searchPath) await client.query(`SET LOCAL search_path TO ${stamping.searchPath}`);
|
|
770
|
+
await client.query(stamping.statements.join("\n"));
|
|
771
|
+
await client.query("RELEASE SAVEPOINT rebase_policy_stamps");
|
|
772
|
+
stamped = stamping.statements.length;
|
|
773
|
+
} catch (err) {
|
|
774
|
+
await client.query("ROLLBACK TO SAVEPOINT rebase_policy_stamps");
|
|
775
|
+
stampError = err instanceof Error ? err.message : String(err);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
await client.query("COMMIT");
|
|
779
|
+
} catch (err) {
|
|
780
|
+
await client.query("ROLLBACK").catch(() => void 0);
|
|
781
|
+
throw err;
|
|
782
|
+
}
|
|
783
|
+
return {
|
|
784
|
+
stamped,
|
|
785
|
+
stampError
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
//#endregion
|
|
640
789
|
//#region src/branch-argv.ts
|
|
641
790
|
/**
|
|
642
791
|
* Argument shapes for `rebase db branch`, with no dependencies.
|
|
@@ -705,8 +854,8 @@ var TAKES_A_VALUE = /* @__PURE__ */ new Set([
|
|
|
705
854
|
* `schemaCommand`, `generatePostgresDdlCommand` and the Atlas argv assembly
|
|
706
855
|
* each re-enter the loader with the same line — and then both generators wrote
|
|
707
856
|
* an *empty* schema: `drizzle/schema.sql` truncated to `CREATE SCHEMA IF NOT
|
|
708
|
-
* EXISTS "rebase";`, `src/schema.generated.ts` to ten lines. Both
|
|
709
|
-
* artifacts. The push that followed planned a `DROP TABLE` for every table in
|
|
857
|
+
* EXISTS "rebase";`, `src/schema.generated.ts` to ten lines. Both were committed
|
|
858
|
+
* artifacts then. The push that followed planned a `DROP TABLE` for every table in
|
|
710
859
|
* the database, and the only thing between a typo and that was the destructive
|
|
711
860
|
* gate.
|
|
712
861
|
*
|
|
@@ -851,6 +1000,11 @@ var __cliDirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
851
1000
|
* tsx maps it back. Running the built `.js` under plain `node` therefore fails
|
|
852
1001
|
* on the user's code rather than on ours, one directory further from anything
|
|
853
1002
|
* the error mentions.
|
|
1003
|
+
*
|
|
1004
|
+
* tsx's loader in one Node (`node --import`) when it resolves, rather than the
|
|
1005
|
+
* `tsx` binary, which is a shell shim and a relay Node in front of that same
|
|
1006
|
+
* loader (see `tsx-loader.ts`). The binary stays the fallback — a global tsx.
|
|
1007
|
+
* `args` is everything after `bin`, the script included.
|
|
854
1008
|
*/
|
|
855
1009
|
function resolveChildScript(name) {
|
|
856
1010
|
const built = path.join(__cliDirname, "schema", `${name}.js`);
|
|
@@ -860,6 +1014,16 @@ function resolveChildScript(name) {
|
|
|
860
1014
|
ok: false,
|
|
861
1015
|
reason: "script"
|
|
862
1016
|
};
|
|
1017
|
+
const tsxLoader = resolveTsxLoader();
|
|
1018
|
+
if (tsxLoader) return {
|
|
1019
|
+
ok: true,
|
|
1020
|
+
bin: process.execPath,
|
|
1021
|
+
args: [
|
|
1022
|
+
"--import",
|
|
1023
|
+
tsxLoader,
|
|
1024
|
+
script
|
|
1025
|
+
]
|
|
1026
|
+
};
|
|
863
1027
|
const tsxBin = resolveLocalBin("tsx");
|
|
864
1028
|
if (!tsxBin) return {
|
|
865
1029
|
ok: false,
|
|
@@ -868,7 +1032,7 @@ function resolveChildScript(name) {
|
|
|
868
1032
|
return {
|
|
869
1033
|
ok: true,
|
|
870
1034
|
bin: tsxBin,
|
|
871
|
-
script
|
|
1035
|
+
args: [script]
|
|
872
1036
|
};
|
|
873
1037
|
}
|
|
874
1038
|
/** What to print when {@link resolveChildScript} cannot produce a command. */
|
|
@@ -876,6 +1040,40 @@ function childScriptError(what, reason) {
|
|
|
876
1040
|
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.`;
|
|
877
1041
|
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.`;
|
|
878
1042
|
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Whether this process can run the generators itself — that is, whether it
|
|
1045
|
+
* runs under tsx, the interpreter the project's collection files need (see
|
|
1046
|
+
* {@link resolveChildScript}). `rebase` always starts the driver that way when
|
|
1047
|
+
* tsx is installed, which a scaffold guarantees; a driver started with plain
|
|
1048
|
+
* `node` keeps the child process, which brings its own tsx.
|
|
1049
|
+
*/
|
|
1050
|
+
function generatesInProcess() {
|
|
1051
|
+
return [...process.execArgv, process.env.NODE_OPTIONS ?? ""].some((flag) => /(?:^|[\s\\/=])tsx(?:$|[\s\\/@])/.test(flag));
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Run one of the generators in this process — see `schema/generate-files.ts`
|
|
1055
|
+
* for why a one-shot generation no longer gets a process of its own.
|
|
1056
|
+
*
|
|
1057
|
+
* The messages and the exit code are the child's: a generator that failed has
|
|
1058
|
+
* printed its own error, and this adds the one-line verdict; one that could not
|
|
1059
|
+
* even be loaded is reported the way a child that never started was.
|
|
1060
|
+
*/
|
|
1061
|
+
async function generateInProcess(what, label, run) {
|
|
1062
|
+
let generators;
|
|
1063
|
+
try {
|
|
1064
|
+
generators = await import("./generate-files-CdEckX6c.js").then((n) => n.t);
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
outError(chalk.red(`✗ Failed to run ${what}: ${err instanceof Error ? err.message : String(err)}`));
|
|
1067
|
+
process.exit(1);
|
|
1068
|
+
}
|
|
1069
|
+
try {
|
|
1070
|
+
await run(generators);
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
if (err instanceof generators.GenerationFailed) outError(chalk.red(`✗ ${label} generation failed — nothing was applied.`));
|
|
1073
|
+
else outError(chalk.red(`✗ Failed to run ${what}: ${err instanceof Error ? err.message : String(err)}`));
|
|
1074
|
+
process.exit(1);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
879
1077
|
async function loadEnv() {
|
|
880
1078
|
try {
|
|
881
1079
|
const dotenv = await import("dotenv");
|
|
@@ -904,12 +1102,16 @@ async function runPluginCommand(args) {
|
|
|
904
1102
|
const subcommand = args[1];
|
|
905
1103
|
assertKnownFlags(domain, subcommand, args);
|
|
906
1104
|
assertCollectionsPathExists(collectionsPathIn(args));
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1105
|
+
try {
|
|
1106
|
+
if (domain === "db") await scratchDatabase.around(() => dbCommand(subcommand, args));
|
|
1107
|
+
else if (domain === "schema") await schemaCommand(subcommand, args);
|
|
1108
|
+
else if (domain === "doctor") await doctorPluginCommand(args);
|
|
1109
|
+
else {
|
|
1110
|
+
outError(chalk.red(`Unknown domain command: ${domain}`));
|
|
1111
|
+
process.exit(1);
|
|
1112
|
+
}
|
|
1113
|
+
} finally {
|
|
1114
|
+
await closeAppClients();
|
|
913
1115
|
}
|
|
914
1116
|
}
|
|
915
1117
|
/** The migration files on disk, sorted, or none if the directory is absent. */
|
|
@@ -982,6 +1184,48 @@ async function stripCarvedOutFromNewestMigration(collectionsPath) {
|
|
|
982
1184
|
return !result.empty;
|
|
983
1185
|
}
|
|
984
1186
|
/**
|
|
1187
|
+
* One line per destructive statement, as `db push` prints them — shared by
|
|
1188
|
+
* push, `db generate` and `db migrate --refuse-destructive`, so the three
|
|
1189
|
+
* describe the same statement the same way.
|
|
1190
|
+
*/
|
|
1191
|
+
function destructiveLines(destructive) {
|
|
1192
|
+
return destructive.map((d) => chalk.red(` ${d.kind}${d.detail ? ` (${d.detail})` : ""}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* The column types `detectDestructiveStatements` reads a type change against,
|
|
1196
|
+
* or none when the database cannot be asked — every type change then counts
|
|
1197
|
+
* as destructive, which is the safe side of a report.
|
|
1198
|
+
*/
|
|
1199
|
+
async function columnTypesOrNone(databaseUrl) {
|
|
1200
|
+
if (!databaseUrl) return [];
|
|
1201
|
+
try {
|
|
1202
|
+
return await queryColumnTypes(databaseUrl);
|
|
1203
|
+
} catch {
|
|
1204
|
+
return [];
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Say what in the migration `db generate` just wrote destroys data.
|
|
1209
|
+
*
|
|
1210
|
+
* `db push` stops before a destructive change; `db generate` wrote the same
|
|
1211
|
+
* `DROP COLUMN` into a file without a word, and `db migrate` applied it. Only
|
|
1212
|
+
* the part Atlas planned is read: what is appended after it (the search
|
|
1213
|
+
* rebuild, policies, triggers) is Rebase's own and puts back what it drops.
|
|
1214
|
+
* A report, not a refusal — the exit code is unchanged.
|
|
1215
|
+
*/
|
|
1216
|
+
async function reportDestructiveMigration(migrationSql, migrationFile, databaseUrl) {
|
|
1217
|
+
const destructive = detectDestructiveStatements(migrationSql, await columnTypesOrNone(databaseUrl));
|
|
1218
|
+
if (destructive.length === 0) return;
|
|
1219
|
+
outWarn("");
|
|
1220
|
+
outWarn(chalk.yellow(` ⚠️ ${migrationFile} includes ${destructive.length} destructive change(s) that will DESTROY data when applied:`));
|
|
1221
|
+
outWarn("");
|
|
1222
|
+
for (const line of destructiveLines(destructive)) outWarn(line);
|
|
1223
|
+
outWarn("");
|
|
1224
|
+
outWarn(chalk.yellow(" Review it before `rebase db migrate`. `rebase db migrate --refuse-destructive` will not apply it."));
|
|
1225
|
+
outWarn(chalk.yellow(" Back up first: rebase db backup"));
|
|
1226
|
+
outWarn("");
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
985
1229
|
* The flags `rebase db` reads for the subcommands it parses itself. Hoisted
|
|
986
1230
|
* and named `…FLAGS` so the documentation verifier, which reads specs out of
|
|
987
1231
|
* this source, sees them.
|
|
@@ -989,6 +1233,7 @@ async function stripCarvedOutFromNewestMigration(collectionsPath) {
|
|
|
989
1233
|
var DB_FLAGS = {
|
|
990
1234
|
"--collections": String,
|
|
991
1235
|
"--allow-destructive": Boolean,
|
|
1236
|
+
"--refuse-destructive": Boolean,
|
|
992
1237
|
"--dry-run": Boolean,
|
|
993
1238
|
"--yes": Boolean,
|
|
994
1239
|
"--baseline": String,
|
|
@@ -1021,7 +1266,7 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
1021
1266
|
return;
|
|
1022
1267
|
}
|
|
1023
1268
|
if (subcommand === "backup" || subcommand === "restore" || subcommand === "backups") {
|
|
1024
|
-
const { backupCommand, restoreCommand, backupsCommand } = await import("./backup-cli-
|
|
1269
|
+
const { backupCommand, restoreCommand, backupsCommand } = await import("./backup-cli-aUfRv16h.js");
|
|
1025
1270
|
if (subcommand === "backup" && backupActionOf(rawArgs) === "list") await backupsCommand(rawArgs);
|
|
1026
1271
|
else if (subcommand === "backup") await backupCommand(rawArgs);
|
|
1027
1272
|
else if (subcommand === "restore") await restoreCommand(rawArgs);
|
|
@@ -1035,7 +1280,7 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
1035
1280
|
out(chalk.bold(" 📦 Rebase DB Generate"));
|
|
1036
1281
|
out(chalk.gray(" Step 1/2: Generating Drizzle schema & Postgres DDL from collections..."));
|
|
1037
1282
|
out("");
|
|
1038
|
-
await schemaCommand("generate", rawArgs);
|
|
1283
|
+
await schemaCommand("generate", rawArgs, { nextStep: false });
|
|
1039
1284
|
await generatePostgresDdlCommand(rawArgs);
|
|
1040
1285
|
out("");
|
|
1041
1286
|
out(chalk.gray(" Step 2/2: Generating SQL migration files with Atlas..."));
|
|
@@ -1048,60 +1293,74 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
1048
1293
|
"--dir",
|
|
1049
1294
|
"file://drizzle/migrations",
|
|
1050
1295
|
"--to",
|
|
1051
|
-
|
|
1296
|
+
DESIRED_STATE_URL
|
|
1052
1297
|
], collectionsPath);
|
|
1053
1298
|
let wroteNewMigration = listMigrationFiles().length > migrationsBefore.length;
|
|
1054
1299
|
if (wroteNewMigration) wroteNewMigration = await stripCarvedOutFromNewestMigration(collectionsPath);
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
}
|
|
1077
|
-
migrationContent = `${migrationContent}\n\n${searchContent}`;
|
|
1078
|
-
out(chalk.gray(" ✓ Appended search DDL to the migration"));
|
|
1079
|
-
}
|
|
1080
|
-
const triggersContent = readTriggersDdl();
|
|
1081
|
-
if (triggersContent) {
|
|
1082
|
-
migrationContent = `${migrationContent}\n\n${triggersContent}`;
|
|
1083
|
-
out(chalk.gray(" ✓ Appended `updated_at` triggers to the migration"));
|
|
1084
|
-
}
|
|
1085
|
-
fs.writeFileSync(newestMigrationFile, migrationContent, "utf-8");
|
|
1086
|
-
const policiesFile = path.resolve(process.cwd(), "drizzle", "policies.sql");
|
|
1087
|
-
if (fs.existsSync(policiesFile)) {
|
|
1088
|
-
const policiesContent = fs.readFileSync(policiesFile, "utf-8");
|
|
1089
|
-
fs.appendFileSync(newestMigrationFile, "\n\n" + RLS_BOOTSTRAP_SQL + "\n" + policiesContent);
|
|
1090
|
-
out(chalk.gray(` ✓ Appended RLS policies to migration file: ${path.basename(newestMigrationFile)}`));
|
|
1091
|
-
out(chalk.gray(" Re-hashing migration files..."));
|
|
1092
|
-
await runAtlas("migrate", [
|
|
1093
|
-
"hash",
|
|
1094
|
-
"--dir",
|
|
1095
|
-
"file://drizzle/migrations"
|
|
1096
|
-
], collectionsPath);
|
|
1097
|
-
out(chalk.gray(" ✓ Migration directory checksum updated successfully."));
|
|
1300
|
+
const newestWritten = wroteNewMigration ? listMigrationFiles().at(-1) : void 0;
|
|
1301
|
+
if (newestWritten) await reportDestructiveMigration(fs.readFileSync(path.resolve(process.cwd(), "drizzle", "migrations", newestWritten), "utf-8"), newestWritten, process.env.DATABASE_URL);
|
|
1302
|
+
const migrationsDir = path.resolve(process.cwd(), "drizzle", "migrations");
|
|
1303
|
+
const newestSql = fs.existsSync(migrationsDir) && wroteNewMigration ? fs.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort().at(-1) : void 0;
|
|
1304
|
+
if (newestSql) {
|
|
1305
|
+
const newestMigrationFile = path.join(migrationsDir, newestSql);
|
|
1306
|
+
const finished = await finishNewMigration(newestMigrationFile, async () => {
|
|
1307
|
+
let migrationContent = fs.readFileSync(newestMigrationFile, "utf-8");
|
|
1308
|
+
migrationContent = migrationContent.replace(/CREATE SCHEMA (?!IF NOT EXISTS)("[^"]+");/g, "CREATE SCHEMA IF NOT EXISTS $1;");
|
|
1309
|
+
const vectorContent = readVectorDdl();
|
|
1310
|
+
if (vectorContent) {
|
|
1311
|
+
migrationContent = `${migrationContent}\n\n${vectorContent}`;
|
|
1312
|
+
out(chalk.gray(" ✓ Appended vector DDL to the migration"));
|
|
1313
|
+
}
|
|
1314
|
+
const searchContent = readSearchDdl();
|
|
1315
|
+
if (searchContent) {
|
|
1316
|
+
const { searchColumnDependencies } = await import("./generate-postgres-ddl-logic-B64O2oZF.js").then((n) => n.o);
|
|
1317
|
+
const migrationConflicts = findGeneratedColumnConflicts(parseColumnMutations(migrationContent), searchColumnDependencies(await loadCollectionsForCli(collectionsPath)));
|
|
1318
|
+
const preamble = migrationDropPreamble(migrationConflicts);
|
|
1319
|
+
if (preamble) {
|
|
1320
|
+
migrationContent = `${preamble}\n${migrationContent}`;
|
|
1321
|
+
for (const conflict of migrationConflicts) out(chalk.gray(` ✓ ${describeConflict(conflict)} is dropped and rebuilt inside the migration`));
|
|
1098
1322
|
}
|
|
1323
|
+
migrationContent = `${migrationContent}\n\n${searchContent}`;
|
|
1324
|
+
out(chalk.gray(" ✓ Appended search DDL to the migration"));
|
|
1099
1325
|
}
|
|
1326
|
+
const triggersContent = readTriggersDdl();
|
|
1327
|
+
if (triggersContent) {
|
|
1328
|
+
migrationContent = `${migrationContent}\n\n${triggersContent}`;
|
|
1329
|
+
out(chalk.gray(" ✓ Appended `updated_at` triggers to the migration"));
|
|
1330
|
+
}
|
|
1331
|
+
fs.writeFileSync(newestMigrationFile, migrationContent, "utf-8");
|
|
1332
|
+
const policiesFile = path.join(generatedSqlDir(), "policies.sql");
|
|
1333
|
+
if (fs.existsSync(policiesFile)) {
|
|
1334
|
+
const policiesContent = fs.readFileSync(policiesFile, "utf-8");
|
|
1335
|
+
fs.appendFileSync(newestMigrationFile, "\n\n" + RLS_BOOTSTRAP_SQL + "\n" + policiesContent);
|
|
1336
|
+
out(chalk.gray(` ✓ Appended RLS policies to migration file: ${path.basename(newestMigrationFile)}`));
|
|
1337
|
+
out(chalk.gray(" Re-hashing migration files..."));
|
|
1338
|
+
await runAtlas("migrate", [
|
|
1339
|
+
"hash",
|
|
1340
|
+
"--dir",
|
|
1341
|
+
"file://drizzle/migrations"
|
|
1342
|
+
], collectionsPath, { throwOnFailure: true });
|
|
1343
|
+
out(chalk.gray(" ✓ Migration directory checksum updated successfully."));
|
|
1344
|
+
}
|
|
1345
|
+
}, async () => {
|
|
1346
|
+
await runAtlas("migrate", [
|
|
1347
|
+
"hash",
|
|
1348
|
+
"--dir",
|
|
1349
|
+
"file://drizzle/migrations"
|
|
1350
|
+
], collectionsPath, { throwOnFailure: true });
|
|
1351
|
+
});
|
|
1352
|
+
if (!finished.ok) {
|
|
1353
|
+
outError(chalk.red(`\n ✗ Could not finish ${newestSql}: ${finished.error}`));
|
|
1354
|
+
outError(chalk.gray(" Atlas's half of a migration creates the tables; the part that failed adds their\n row-level security, policies, search, vector and trigger DDL. Applied alone, it\n would serve the new tables with no row filtering."));
|
|
1355
|
+
outError(chalk.gray(finished.removed ? ` ${newestSql} was removed. Fix the cause and run \`rebase db generate\` again.` : ` ${newestSql} could not be removed — delete it by hand before \`rebase db migrate\`.`));
|
|
1356
|
+
if (finished.rehashError) outError(chalk.gray(` Re-hashing failed too (${finished.rehashError}); run \`atlas migrate hash\` or generate again.`));
|
|
1357
|
+
process.exit(1);
|
|
1100
1358
|
}
|
|
1101
|
-
} catch (err) {
|
|
1102
|
-
outWarn(chalk.yellow(` ⚠️ Failed to append policies or re-hash migration: ${err instanceof Error ? err.message : String(err)}`));
|
|
1103
1359
|
}
|
|
1104
|
-
if (!wroteNewMigration) out(chalk.gray(
|
|
1360
|
+
if (!wroteNewMigration) out(chalk.gray(` ℹ No migration written — nothing in this change is visible to Atlas.
|
|
1361
|
+
Search, vector, trigger and RLS DDL are applied by \`rebase db push\`, and at
|
|
1362
|
+
boot by the schema ensure. For a migration-only deployment, add
|
|
1363
|
+
${GENERATED_SQL_DIR}/search.sql, vector.sql, triggers.sql and\n policies.sql to a migration by hand.`));
|
|
1105
1364
|
out("");
|
|
1106
1365
|
out(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
|
|
1107
1366
|
out("");
|
|
@@ -1110,32 +1369,46 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
1110
1369
|
out(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
|
|
1111
1370
|
out("");
|
|
1112
1371
|
if (subcommand === "push") {
|
|
1113
|
-
|
|
1372
|
+
const dryRun = argsList["--dry-run"] === true;
|
|
1373
|
+
out(chalk.gray(dryRun ? " Step 1/3: Generating the Postgres DDL from collections (the plan's input)..." : " Step 1/3: Generating Drizzle schema & Postgres DDL from collections..."));
|
|
1114
1374
|
out("");
|
|
1115
|
-
|
|
1116
|
-
await
|
|
1375
|
+
if (dryRun) out(chalk.gray(" --dry-run: src/schema.generated.ts is left as it is."));
|
|
1376
|
+
else await schemaCommand("generate", rawArgs, { nextStep: false });
|
|
1377
|
+
await generatePostgresDdlCommand(rawArgs, { retireLegacyCopies: !dryRun });
|
|
1117
1378
|
out("");
|
|
1118
|
-
const dryRun = argsList["--dry-run"] === true;
|
|
1119
1379
|
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..."));
|
|
1120
1380
|
out("");
|
|
1121
1381
|
const databaseUrl = process.env.DATABASE_URL;
|
|
1122
1382
|
if (databaseUrl && !dryRun) await ensureAuthSchemaAndFunctions(databaseUrl);
|
|
1383
|
+
if (databaseUrl) await renameLegacyForeignKeys(databaseUrl, collectionsPath, dryRun);
|
|
1384
|
+
const prebuilt = databaseUrl ? await prebuildForeignKeyIndexes(databaseUrl, collectionsPath, dryRun) : [];
|
|
1123
1385
|
const plan = await runAtlas("schema", [
|
|
1124
1386
|
"apply",
|
|
1125
1387
|
"--to",
|
|
1126
|
-
|
|
1388
|
+
DESIRED_STATE_URL,
|
|
1127
1389
|
"--dry-run"
|
|
1128
1390
|
], collectionsPath, { captureStdout: true });
|
|
1129
1391
|
const destructive = detectDestructiveStatements(plan, databaseUrl ? await queryColumnTypes(databaseUrl) : []);
|
|
1392
|
+
const unreadable = destructive.some((d) => d.kind === "UNREADABLE PLAN");
|
|
1393
|
+
const planStatements = extractPlanStatements(plan);
|
|
1394
|
+
const planListing = unreadable ? plan.trim() : planStatements.map((statement) => `${statement};`).join("\n");
|
|
1130
1395
|
const { managed: rebuildable, foreign: unowned } = partitionByOwnership(databaseUrl ? findGeneratedColumnConflicts(parseColumnMutations(plan), await queryGeneratedColumnDependencies(databaseUrl)) : [], collectionsPath ? await getSearchExcludes(collectionsPath) : []);
|
|
1396
|
+
if (databaseUrl) await reportKeptEnumLabels(databaseUrl, collectionsPath);
|
|
1131
1397
|
if (dryRun) {
|
|
1132
1398
|
out(chalk.gray(" Step 3/3: --dry-run — nothing was applied."));
|
|
1133
1399
|
out("");
|
|
1134
|
-
if (
|
|
1400
|
+
if (planStatements.length > 0 || unreadable) {
|
|
1135
1401
|
out(chalk.bold(" Planned changes:"));
|
|
1136
|
-
out(chalk.gray(
|
|
1402
|
+
out(chalk.gray(planListing.split("\n").map((l) => ` ${l}`).join("\n")));
|
|
1137
1403
|
} else out(chalk.green(" ✓ No changes: the database already matches these collections."));
|
|
1138
1404
|
out("");
|
|
1405
|
+
if (prebuilt.length > 0) {
|
|
1406
|
+
out(chalk.gray(` ${prebuilt.length} foreign-key index(es) on existing tables would be built first,`));
|
|
1407
|
+
out(chalk.gray(" CONCURRENTLY, so the apply would not build them under a write lock (the plan"));
|
|
1408
|
+
out(chalk.gray(" above lists them because they do not exist yet):"));
|
|
1409
|
+
for (const name of prebuilt) out(chalk.gray(` ${name}`));
|
|
1410
|
+
out("");
|
|
1411
|
+
}
|
|
1139
1412
|
if (rebuildable.length > 0) {
|
|
1140
1413
|
out(chalk.gray(` ${rebuildable.length} generated column(s) would be dropped before the apply and`));
|
|
1141
1414
|
out(chalk.gray(" rebuilt from search.sql after it — Postgres cannot retype a column"));
|
|
@@ -1146,7 +1419,7 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
1146
1419
|
if (unowned.length > 0) outWarn(formatForeignGeneratedColumnBanner(unowned));
|
|
1147
1420
|
if (destructive.length > 0) {
|
|
1148
1421
|
outWarn(chalk.yellow(` ⚠️ ${destructive.length} of those DESTROY data:`));
|
|
1149
|
-
for (const
|
|
1422
|
+
for (const line of destructiveLines(destructive)) outWarn(line);
|
|
1150
1423
|
outWarn("");
|
|
1151
1424
|
outWarn(chalk.yellow(" Applying them needs `rebase db push --allow-destructive`. Back up first: rebase db backup"));
|
|
1152
1425
|
outWarn("");
|
|
@@ -1168,10 +1441,10 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
1168
1441
|
if (destructive.length > 0) {
|
|
1169
1442
|
outWarn(chalk.yellow(` ⚠️ This push includes ${destructive.length} destructive change(s) that will DESTROY data:`));
|
|
1170
1443
|
outWarn("");
|
|
1171
|
-
for (const
|
|
1444
|
+
for (const line of destructiveLines(destructive)) outWarn(line);
|
|
1172
1445
|
outWarn("");
|
|
1173
1446
|
outWarn(chalk.yellow(" Full planned changes:"));
|
|
1174
|
-
outWarn(chalk.gray(
|
|
1447
|
+
outWarn(chalk.gray(planListing.split("\n").map((l) => ` ${l}`).join("\n")));
|
|
1175
1448
|
outWarn("");
|
|
1176
1449
|
if (decision === "refuse") {
|
|
1177
1450
|
outError(chalk.red(" ✗ Aborting: destructive changes require confirmation."));
|
|
@@ -1185,39 +1458,65 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
1185
1458
|
}
|
|
1186
1459
|
} else outWarn(chalk.yellow(" Proceeding because --allow-destructive was passed."));
|
|
1187
1460
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1461
|
+
await applyPushPlan({
|
|
1462
|
+
dryRun: plan,
|
|
1463
|
+
rebuildable: databaseUrl ? rebuildable.length : 0
|
|
1464
|
+
}, {
|
|
1465
|
+
dropRebuildable: async () => {
|
|
1466
|
+
if (!databaseUrl) return;
|
|
1467
|
+
for (const conflict of rebuildable) out(chalk.gray(` Dropping ${describeConflict(conflict)} — it reads a column this push retypes.`));
|
|
1468
|
+
await dropGeneratedColumns(databaseUrl, dropGeneratedColumnStatements(rebuildable));
|
|
1469
|
+
},
|
|
1470
|
+
atlasApply: async () => {
|
|
1471
|
+
await runAtlas("schema", [
|
|
1472
|
+
"apply",
|
|
1473
|
+
"--to",
|
|
1474
|
+
DESIRED_STATE_URL,
|
|
1475
|
+
"--auto-approve"
|
|
1476
|
+
], collectionsPath, { throwOnFailure: true });
|
|
1477
|
+
},
|
|
1478
|
+
reportSynced: () => out("Schema is synced, no changes to be made"),
|
|
1479
|
+
rebuildAfterFailure: async () => {
|
|
1480
|
+
if (!databaseUrl) return;
|
|
1201
1481
|
out(chalk.gray(" The apply failed — rebuilding the generated columns it needed out of the way."));
|
|
1202
1482
|
await applySearchDdl(databaseUrl);
|
|
1483
|
+
},
|
|
1484
|
+
afterApply: async () => {
|
|
1485
|
+
out("");
|
|
1486
|
+
if (!databaseUrl) {
|
|
1487
|
+
outWarn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
await lockManagedTables(databaseUrl, collectionsPath);
|
|
1491
|
+
await ensureAuthTables(databaseUrl, collectionsPath);
|
|
1492
|
+
await applyVectorDdl(databaseUrl);
|
|
1493
|
+
await applySearchDdl(databaseUrl);
|
|
1494
|
+
await applyTriggersDdl(databaseUrl);
|
|
1495
|
+
await applyPolicies(databaseUrl, collectionsPath);
|
|
1496
|
+
await reconcilePolicies(databaseUrl, collectionsPath);
|
|
1497
|
+
await ensureRlsUserRole(databaseUrl);
|
|
1498
|
+
await retireLegacyAuthSchema(databaseUrl);
|
|
1203
1499
|
}
|
|
1204
|
-
|
|
1205
|
-
}
|
|
1206
|
-
out("");
|
|
1207
|
-
if (databaseUrl) {
|
|
1208
|
-
await ensureAuthTables(databaseUrl, collectionsPath);
|
|
1209
|
-
await applyVectorDdl(databaseUrl);
|
|
1210
|
-
await applySearchDdl(databaseUrl);
|
|
1211
|
-
await applyTriggersDdl(databaseUrl);
|
|
1212
|
-
await applyPolicies(databaseUrl);
|
|
1213
|
-
await reconcilePolicies(databaseUrl, collectionsPath);
|
|
1214
|
-
await ensureRlsUserRole(databaseUrl);
|
|
1215
|
-
await retireLegacyAuthSchema(databaseUrl);
|
|
1216
|
-
} else outWarn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
|
|
1500
|
+
});
|
|
1217
1501
|
} else if (subcommand === "migrate") {
|
|
1218
1502
|
const databaseUrl = process.env.DATABASE_URL;
|
|
1219
|
-
if (databaseUrl) await ensureAuthSchemaAndFunctions(databaseUrl);
|
|
1220
1503
|
const baseline = argsList["--baseline"];
|
|
1504
|
+
if (argsList["--refuse-destructive"] === true) {
|
|
1505
|
+
const plan = await runAtlas("migrate", [...migrateApplyArgs(argsList._, baseline), "--dry-run"], collectionsPath, { captureStdout: true });
|
|
1506
|
+
const destructive = detectDestructiveStatements(plan, await columnTypesOrNone(databaseUrl));
|
|
1507
|
+
if (destructive.length > 0) {
|
|
1508
|
+
outError(chalk.red(` ✗ Refusing to migrate: the pending migrations include ${destructive.length} destructive change(s):`));
|
|
1509
|
+
outError("");
|
|
1510
|
+
for (const line of destructiveLines(destructive)) outError(line);
|
|
1511
|
+
outError("");
|
|
1512
|
+
outError(chalk.gray(" Nothing was applied. Run `rebase db migrate` without --refuse-destructive to apply them."));
|
|
1513
|
+
outError(chalk.gray(" Back up first: rebase db backup"));
|
|
1514
|
+
process.exit(1);
|
|
1515
|
+
}
|
|
1516
|
+
out(chalk.gray(` --refuse-destructive: ${extractPlanStatements(plan).length} pending statement(s), none destructive.`));
|
|
1517
|
+
out("");
|
|
1518
|
+
}
|
|
1519
|
+
if (databaseUrl) await ensureAuthSchemaAndFunctions(databaseUrl);
|
|
1221
1520
|
if (baseline) {
|
|
1222
1521
|
out(chalk.gray(` Recording ${baseline} as already applied, then migrating from the next one.`));
|
|
1223
1522
|
out("");
|
|
@@ -1233,21 +1532,129 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
1233
1532
|
out("");
|
|
1234
1533
|
}
|
|
1235
1534
|
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Build, CONCURRENTLY and before Atlas plans, the derived foreign-key indexes
|
|
1537
|
+
* an existing table is missing — see `schema/push-index-prebuild.ts`. With
|
|
1538
|
+
* `dryRun`, only find them.
|
|
1539
|
+
*
|
|
1540
|
+
* Never fatal: an index not built here is one Atlas builds inside the push, as
|
|
1541
|
+
* it always did.
|
|
1542
|
+
*
|
|
1543
|
+
* @returns `schema.index` of each one missing when this started.
|
|
1544
|
+
*/
|
|
1545
|
+
/** Renames relation columns that carry their pre-0.13 derived name, as boot does; a dry-run only names them. */
|
|
1546
|
+
async function renameLegacyForeignKeys(databaseUrl, collectionsPath, dryRun) {
|
|
1547
|
+
let renames;
|
|
1548
|
+
try {
|
|
1549
|
+
const { loadCollections } = await import("./doctor-C4WiftHm.js").then((n) => n.t);
|
|
1550
|
+
const { legacyForeignKeyRenames } = await import("./push-legacy-renames-BK6ilQq2.js");
|
|
1551
|
+
const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
|
|
1552
|
+
renames = await withAppClient(databaseUrl, (client) => legacyForeignKeyRenames({ query: async (text) => ({ rows: (await client.query(text)).rows }) }, collections));
|
|
1553
|
+
} catch {
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
if (renames.length === 0) return;
|
|
1557
|
+
out(chalk.gray(dryRun ? ` ${renames.length} relation column(s) carry the name Rebase derived before 0.13; a push renames them first,` : ` Renaming ${renames.length} relation column(s) that carry the name Rebase derived before 0.13 (data, indexes and`));
|
|
1558
|
+
out(chalk.gray(dryRun ? " keeping their data (the plan below still shows them as a drop and an add, because nothing ran):" : " constraints are kept — the same rename `rebase dev` runs):"));
|
|
1559
|
+
for (const rename of renames) out(chalk.gray(` ${rename.table}: "${rename.legacy}" → "${rename.expected}"`));
|
|
1560
|
+
if (!dryRun) await withAppClient(databaseUrl, async (client) => {
|
|
1561
|
+
for (const rename of renames) await client.query(rename.sql);
|
|
1562
|
+
});
|
|
1563
|
+
out(chalk.gray(" To keep an old name instead, name the column on the relation (`localKey`, or `through` on a many-to-many)."));
|
|
1564
|
+
out("");
|
|
1565
|
+
}
|
|
1566
|
+
/** Names the enum labels the database keeps after the collections dropped them. See `schema/enum-labels-kept.ts`. */
|
|
1567
|
+
async function reportKeptEnumLabels(databaseUrl, collectionsPath) {
|
|
1568
|
+
try {
|
|
1569
|
+
const { loadCollections } = await import("./doctor-C4WiftHm.js").then((n) => n.t);
|
|
1570
|
+
const { keptEnumLabels, formatKeptEnumLabelsNotice } = await import("./enum-labels-kept-D8fI1MoX.js");
|
|
1571
|
+
const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
|
|
1572
|
+
const kept = await withAppClient(databaseUrl, (client) => keptEnumLabels({ query: async (text) => ({ rows: (await client.query(text)).rows }) }, collections));
|
|
1573
|
+
if (kept.length > 0) outWarn(chalk.yellow(formatKeptEnumLabelsNotice(kept)));
|
|
1574
|
+
} catch {}
|
|
1575
|
+
}
|
|
1576
|
+
async function prebuildForeignKeyIndexes(databaseUrl, collectionsPath, dryRun) {
|
|
1577
|
+
let missing;
|
|
1578
|
+
try {
|
|
1579
|
+
const { loadCollections } = await import("./doctor-C4WiftHm.js").then((n) => n.t);
|
|
1580
|
+
const { missingForeignKeyIndexes } = await import("./push-index-prebuild-Db4AQF4i.js");
|
|
1581
|
+
const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
|
|
1582
|
+
missing = await withAppClient(databaseUrl, (client) => missingForeignKeyIndexes({ query: async (text) => ({ rows: (await client.query(text)).rows }) }, collections));
|
|
1583
|
+
} catch {
|
|
1584
|
+
return [];
|
|
1585
|
+
}
|
|
1586
|
+
const names = missing.map((index) => `${index.schema}.${index.indexName}`);
|
|
1587
|
+
if (missing.length === 0 || dryRun) return names;
|
|
1588
|
+
out(chalk.gray(` Building ${missing.length} foreign-key index(es) on existing tables first — CONCURRENTLY, so writes go on:`));
|
|
1589
|
+
try {
|
|
1590
|
+
const { buildDeferredIndexes } = await import("./deferred-index-build-DP1_SNFo.js").then((n) => n.r);
|
|
1591
|
+
const outcome = await withAppClient(databaseUrl, (client) => buildDeferredIndexes(async () => ({
|
|
1592
|
+
dedicated: true,
|
|
1593
|
+
query: async (text) => ({ rows: (await client.query(text)).rows }),
|
|
1594
|
+
release: () => void 0
|
|
1595
|
+
}), missing, { log: (message) => out(chalk.gray(` ${message}`)) }));
|
|
1596
|
+
for (const failure of outcome.failed) outWarn(chalk.yellow(` ⚠️ ${failure.index} was not built ahead (${failure.error}); the push builds it inside its transaction instead.`));
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
outWarn(chalk.yellow(` ⚠️ The foreign-key indexes were not built ahead (${err instanceof Error ? err.message : String(err)}); the push builds them inside its transaction instead.`));
|
|
1599
|
+
}
|
|
1600
|
+
out("");
|
|
1601
|
+
return names;
|
|
1602
|
+
}
|
|
1236
1603
|
async function ensureAuthSchemaAndFunctions(databaseUrl) {
|
|
1237
1604
|
try {
|
|
1238
|
-
|
|
1239
|
-
const client = new Client({ connectionString: databaseUrl });
|
|
1240
|
-
await client.connect();
|
|
1241
|
-
try {
|
|
1605
|
+
await withAppClient(databaseUrl, async (client) => {
|
|
1242
1606
|
await client.query(RLS_BOOTSTRAP_SQL);
|
|
1243
|
-
}
|
|
1244
|
-
await client.end();
|
|
1245
|
-
}
|
|
1607
|
+
});
|
|
1246
1608
|
} catch (err) {
|
|
1247
1609
|
outWarn(chalk.yellow(` ⚠️ Failed to bootstrap the RLS helper functions: ${err instanceof Error ? err.message : String(err)}`));
|
|
1248
1610
|
}
|
|
1249
1611
|
}
|
|
1250
1612
|
/**
|
|
1613
|
+
* Switch row-level security on for every table the collections declare, right
|
|
1614
|
+
* after Atlas has applied the schema.
|
|
1615
|
+
*
|
|
1616
|
+
* Atlas cannot carry `ENABLE ROW LEVEL SECURITY` in `schema.sql`: its free tier
|
|
1617
|
+
* neither plans it nor reads it back, so a table it creates is born with RLS
|
|
1618
|
+
* off — and `rebase_user`, the role every authenticated request runs as, holds
|
|
1619
|
+
* DML on it from that moment through the schema's default privileges. RLS used
|
|
1620
|
+
* to arrive with the policies, the last step of the push; a refusal from the
|
|
1621
|
+
* vector, search or trigger step in between left the new table readable and
|
|
1622
|
+
* writable by every caller, with no policy, until a later push or boot fixed
|
|
1623
|
+
* it — and a deployment with `REBASE_MIGRATE_ON_BOOT=none` never did. Locked
|
|
1624
|
+
* first, a failure later leaves a table that denies, not one that is open.
|
|
1625
|
+
*
|
|
1626
|
+
* Fatal when a table cannot be locked, and before the steps that follow: the
|
|
1627
|
+
* role provisioning at the end re-grants on every table in the schema, which
|
|
1628
|
+
* would undo the revoke that is the fallback here.
|
|
1629
|
+
*/
|
|
1630
|
+
async function lockManagedTables(databaseUrl, collectionsPath) {
|
|
1631
|
+
let unsecured;
|
|
1632
|
+
let userRole;
|
|
1633
|
+
try {
|
|
1634
|
+
const { lockCollectionTables } = await import("./ensure-collection-policies-CXpbeNag.js");
|
|
1635
|
+
const { loadCollections } = await import("./doctor-C4WiftHm.js").then((n) => n.t);
|
|
1636
|
+
userRole = (await import("./rls-enforcement-DvzqlaL1.js").then((n) => n.c)).REBASE_USER_ROLE;
|
|
1637
|
+
const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
|
|
1638
|
+
({unsecured} = await withAppClient(databaseUrl, (client) => lockCollectionTables({ query: async (text) => ({ rows: (await client.query(text)).rows }) }, collections)));
|
|
1639
|
+
} catch (err) {
|
|
1640
|
+
outError(chalk.red(`\n ✗ The schema was applied, but row-level security could not be switched on for its tables: ${err instanceof Error ? err.message : String(err)}`));
|
|
1641
|
+
outError(chalk.gray(" A table this push created may be readable and writable by every authenticated request\n until it is. Re-run `rebase db push` — it locks every collection table before anything else."));
|
|
1642
|
+
process.exit(1);
|
|
1643
|
+
}
|
|
1644
|
+
if (unsecured.length === 0) return;
|
|
1645
|
+
outError(chalk.red("\n ✗ The schema was applied, but row-level security could not be switched on for:"));
|
|
1646
|
+
for (const u of unsecured) {
|
|
1647
|
+
outError(chalk.red(` ${u.table}: ${u.error}`));
|
|
1648
|
+
outError(chalk.gray(u.grantWithdrawn ? ` Its privileges were taken back from ${userRole}, so it is unreachable rather than unprotected.` : ` ${userRole} still holds its privileges: every authenticated request can read and write every row.`));
|
|
1649
|
+
}
|
|
1650
|
+
outError(chalk.gray(" Nothing after the schema apply ran. Fix the cause — the connection role must own these\n tables, or be able to ALTER them — and run `rebase db push` again, or as their owner:"));
|
|
1651
|
+
for (const u of unsecured) {
|
|
1652
|
+
const [schema, ...table] = u.table.split(".");
|
|
1653
|
+
outError(chalk.bold(` ALTER TABLE "${schema}"."${table.join(".")}" ENABLE ROW LEVEL SECURITY;`));
|
|
1654
|
+
}
|
|
1655
|
+
process.exit(1);
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1251
1658
|
* Create the framework auth tables (e.g. `rebase.users`) that the generated RLS
|
|
1252
1659
|
* policies reference, before `applyPolicies` runs. Mirrors what the server does
|
|
1253
1660
|
* at boot (`PostgresBootstrapper.initializeAuth` → `ensureAuthTablesExist`).
|
|
@@ -1261,20 +1668,13 @@ async function ensureAuthSchemaAndFunctions(databaseUrl) {
|
|
|
1261
1668
|
async function ensureAuthTables(databaseUrl, collectionsPath) {
|
|
1262
1669
|
try {
|
|
1263
1670
|
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
1264
|
-
const { ensureAuthTablesExist } = await import("./ensure-tables-
|
|
1265
|
-
const { loadCollections } = await import("./doctor-
|
|
1266
|
-
const { Client } = await import("pg");
|
|
1671
|
+
const { ensureAuthTablesExist } = await import("./ensure-tables-CHsgyLqg.js").then((n) => n.n);
|
|
1672
|
+
const { loadCollections } = await import("./doctor-C4WiftHm.js").then((n) => n.t);
|
|
1267
1673
|
const authCollection = (await loadCollections(path.resolve(process.cwd(), collectionsPath))).find((c) => {
|
|
1268
1674
|
const a = c.auth;
|
|
1269
1675
|
return a === true || typeof a === "object" && a !== null && a.enabled === true;
|
|
1270
1676
|
});
|
|
1271
|
-
|
|
1272
|
-
await client.connect();
|
|
1273
|
-
try {
|
|
1274
|
-
await ensureAuthTablesExist(drizzle(client), authCollection);
|
|
1275
|
-
} finally {
|
|
1276
|
-
await client.end();
|
|
1277
|
-
}
|
|
1677
|
+
await withAppClient(databaseUrl, (client) => ensureAuthTablesExist(drizzle(client), authCollection));
|
|
1278
1678
|
} catch (err) {
|
|
1279
1679
|
outWarn(chalk.yellow(` ⚠️ Failed to ensure framework auth tables: ${err instanceof Error ? err.message : String(err)}`));
|
|
1280
1680
|
}
|
|
@@ -1289,24 +1689,19 @@ async function ensureRlsUserRole(databaseUrl) {
|
|
|
1289
1689
|
const schemas = ["public", "rebase"];
|
|
1290
1690
|
let rls;
|
|
1291
1691
|
try {
|
|
1292
|
-
rls = await import("./rls-enforcement-
|
|
1692
|
+
rls = await import("./rls-enforcement-DvzqlaL1.js").then((n) => n.c);
|
|
1293
1693
|
} catch (err) {
|
|
1294
1694
|
outError(chalk.red(`\n ✗ Could not load the RLS provisioning module: ${err instanceof Error ? err.message : String(err)}`));
|
|
1295
1695
|
return;
|
|
1296
1696
|
}
|
|
1297
1697
|
try {
|
|
1298
|
-
|
|
1299
|
-
const client = new Client({ connectionString: databaseUrl });
|
|
1300
|
-
await client.connect();
|
|
1301
|
-
try {
|
|
1698
|
+
await withAppClient(databaseUrl, async (client) => {
|
|
1302
1699
|
const runSql = async (text) => (await client.query(text)).rows;
|
|
1303
1700
|
if ((await rls.detectConnectionPosture(runSql)).privileged) {
|
|
1304
1701
|
await rls.ensureAppRole(runSql, schemas);
|
|
1305
1702
|
out(chalk.gray(` ✓ RLS role "${rls.REBASE_USER_ROLE}" provisioned/refreshed.`));
|
|
1306
1703
|
}
|
|
1307
|
-
}
|
|
1308
|
-
await client.end();
|
|
1309
|
-
}
|
|
1704
|
+
});
|
|
1310
1705
|
} catch (err) {
|
|
1311
1706
|
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)}`));
|
|
1312
1707
|
outError(chalk.gray(rls.appRoleSetupInstructions("your database user", schemas)));
|
|
@@ -1323,35 +1718,23 @@ async function ensureRlsUserRole(databaseUrl) {
|
|
|
1323
1718
|
*/
|
|
1324
1719
|
async function retireLegacyAuthSchema(databaseUrl) {
|
|
1325
1720
|
try {
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
await dropLegacyAuthSchema(async (text) => (await client.query(text)).rows, {
|
|
1331
|
-
info: (m) => out(chalk.gray(` ${m}`)),
|
|
1332
|
-
warn: (m) => outWarn(chalk.yellow(` ⚠️ ${m}`))
|
|
1333
|
-
});
|
|
1334
|
-
} finally {
|
|
1335
|
-
await client.end();
|
|
1336
|
-
}
|
|
1721
|
+
await withAppClient(databaseUrl, (client) => dropLegacyAuthSchema(async (text) => (await client.query(text)).rows, {
|
|
1722
|
+
info: (m) => out(chalk.gray(` ${m}`)),
|
|
1723
|
+
warn: (m) => outWarn(chalk.yellow(` ⚠️ ${m}`))
|
|
1724
|
+
}));
|
|
1337
1725
|
} catch {}
|
|
1338
1726
|
}
|
|
1339
|
-
async function applyPolicies(databaseUrl) {
|
|
1727
|
+
async function applyPolicies(databaseUrl, collectionsPath) {
|
|
1340
1728
|
try {
|
|
1341
|
-
const policiesPath = path.
|
|
1729
|
+
const policiesPath = path.join(generatedSqlDir(), "policies.sql");
|
|
1342
1730
|
if (!fs.existsSync(policiesPath)) return;
|
|
1343
1731
|
out(chalk.gray(" Step 3/3: Applying RLS policies to database..."));
|
|
1344
1732
|
out("");
|
|
1345
1733
|
const policiesContent = fs.readFileSync(policiesPath, "utf-8");
|
|
1346
|
-
const
|
|
1347
|
-
const
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
await client.query(policiesContent);
|
|
1351
|
-
out(chalk.green(" ✓ RLS policies applied successfully."));
|
|
1352
|
-
} finally {
|
|
1353
|
-
await client.end();
|
|
1354
|
-
}
|
|
1734
|
+
const stamping = await policyStamping(databaseUrl, collectionsPath, policiesContent);
|
|
1735
|
+
const { stampError } = await withAppClient(databaseUrl, (client) => applyPoliciesFile(client, policiesContent, stamping));
|
|
1736
|
+
out(chalk.green(" ✓ RLS policies applied successfully."));
|
|
1737
|
+
if (stampError) outWarn(chalk.yellow(` ⚠️ The policies could not be stamped (${stampError}); the next boot applies them again.`));
|
|
1355
1738
|
} catch (err) {
|
|
1356
1739
|
const hint = diagnoseDbError(err, databaseUrl);
|
|
1357
1740
|
if (hint) outError(hint);
|
|
@@ -1360,6 +1743,25 @@ async function applyPolicies(databaseUrl) {
|
|
|
1360
1743
|
}
|
|
1361
1744
|
}
|
|
1362
1745
|
/**
|
|
1746
|
+
* The stamps a boot looks for, for the policies `policies.sql` is about to
|
|
1747
|
+
* create — see `applyPoliciesFile`. None when the collections cannot be read
|
|
1748
|
+
* here, which costs only the shortcut: the next boot re-applies, as before.
|
|
1749
|
+
*/
|
|
1750
|
+
async function policyStamping(databaseUrl, collectionsPath, policiesSql) {
|
|
1751
|
+
try {
|
|
1752
|
+
const { loadCollections } = await import("./doctor-C4WiftHm.js").then((n) => n.t);
|
|
1753
|
+
const { planCollectionPolicies } = await import("./generate-postgres-ddl-logic-B64O2oZF.js").then((n) => n.o);
|
|
1754
|
+
const { policyStampSql } = await import("./ensure-collection-policies-CXpbeNag.js");
|
|
1755
|
+
const { pinSearchPath } = await import("./connection-BPoNILnD.js").then((n) => n.n);
|
|
1756
|
+
return {
|
|
1757
|
+
statements: stampableTables(planCollectionPolicies(await loadCollections(path.resolve(process.cwd(), collectionsPath))), policiesSql).map((plan) => policyStampSql(plan)).filter((statement) => statement !== void 0),
|
|
1758
|
+
searchPath: pinSearchPath(databaseUrl) === databaseUrl ? void 0 : "public"
|
|
1759
|
+
};
|
|
1760
|
+
} catch {
|
|
1761
|
+
return { statements: [] };
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1363
1765
|
* Remove the policies an earlier push superseded but never dropped.
|
|
1364
1766
|
*
|
|
1365
1767
|
* `policies.sql` only DROPs the names it is about to CREATE, and a rule's
|
|
@@ -1374,13 +1776,10 @@ async function applyPolicies(databaseUrl) {
|
|
|
1374
1776
|
*/
|
|
1375
1777
|
async function reconcilePolicies(databaseUrl, collectionsPath) {
|
|
1376
1778
|
try {
|
|
1377
|
-
const { checkPolicyDrift, dropOrphanedPolicies, formatPolicyDrift, hasDrift } = await import("./policy-drift-
|
|
1378
|
-
const { loadCollections } = await import("./doctor-
|
|
1779
|
+
const { checkPolicyDrift, dropOrphanedPolicies, formatPolicyDrift, hasDrift } = await import("./policy-drift-Dm8i-s1J.js").then((n) => n.a);
|
|
1780
|
+
const { loadCollections } = await import("./doctor-C4WiftHm.js").then((n) => n.t);
|
|
1379
1781
|
const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
|
|
1380
|
-
|
|
1381
|
-
const client = new Client({ connectionString: databaseUrl });
|
|
1382
|
-
await client.connect();
|
|
1383
|
-
try {
|
|
1782
|
+
await withAppClient(databaseUrl, async (client) => {
|
|
1384
1783
|
const drift = await checkPolicyDrift(client, collections);
|
|
1385
1784
|
const { dropped, kept } = await dropOrphanedPolicies(client, drift, collections);
|
|
1386
1785
|
for (const p of dropped) out(chalk.gray(` ✓ Dropped superseded policy "${p.name}" on ${p.schema}.${p.table}`));
|
|
@@ -1401,9 +1800,7 @@ async function reconcilePolicies(databaseUrl, collectionsPath) {
|
|
|
1401
1800
|
orphaned: []
|
|
1402
1801
|
}));
|
|
1403
1802
|
}
|
|
1404
|
-
}
|
|
1405
|
-
await client.end();
|
|
1406
|
-
}
|
|
1803
|
+
});
|
|
1407
1804
|
} catch (err) {
|
|
1408
1805
|
outWarn(chalk.yellow(` ⚠️ Could not reconcile RLS policies: ${err instanceof Error ? err.message : String(err)}`));
|
|
1409
1806
|
}
|
|
@@ -1456,8 +1853,8 @@ async function branchCommand(rawArgs) {
|
|
|
1456
1853
|
outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
1457
1854
|
process.exit(1);
|
|
1458
1855
|
}
|
|
1459
|
-
const { DatabasePoolManager } = await import("./databasePoolManager-
|
|
1460
|
-
const { BranchService } = await import("./BranchService-
|
|
1856
|
+
const { DatabasePoolManager } = await import("./databasePoolManager-BkEy2w18.js").then((n) => n.n);
|
|
1857
|
+
const { BranchService } = await import("./BranchService-Bsn2wfaj.js").then((n) => n.n);
|
|
1461
1858
|
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
1462
1859
|
const { Pool } = await import("pg");
|
|
1463
1860
|
const pool = new Pool({
|
|
@@ -1746,8 +2143,15 @@ async function runAtlas(domain, args, collectionsPath, opts = {}) {
|
|
|
1746
2143
|
process.exit(1);
|
|
1747
2144
|
}
|
|
1748
2145
|
await checkDatabaseConnectivity(databaseUrl);
|
|
1749
|
-
|
|
1750
|
-
|
|
2146
|
+
let devDatabase;
|
|
2147
|
+
try {
|
|
2148
|
+
devDatabase = atlasDevDatabaseUrl(databaseUrl, env);
|
|
2149
|
+
} catch (err) {
|
|
2150
|
+
outError(chalk.red(`✗ ${err instanceof Error ? err.message : String(err)}`));
|
|
2151
|
+
process.exit(1);
|
|
2152
|
+
}
|
|
2153
|
+
const devDatabaseUrl = devDatabase.url;
|
|
2154
|
+
if (takesDevUrl(domain, args) && !devDatabase.named) await scratchDatabase.ensure(databaseUrl, devDatabaseUrl);
|
|
1751
2155
|
const atlasUrl = forLibpq(databaseUrl);
|
|
1752
2156
|
const atlasDevUrl = forLibpq(devDatabaseUrl);
|
|
1753
2157
|
const excludes = [];
|
|
@@ -1816,12 +2220,13 @@ async function runAtlas(domain, args, collectionsPath, opts = {}) {
|
|
|
1816
2220
|
latestMigrationVersion: latestMigrationVersion()
|
|
1817
2221
|
}) ?? diagnoseDbError({ message: stderrText }, databaseUrl);
|
|
1818
2222
|
if (hint) outError(hint);
|
|
2223
|
+
if (opts.throwOnFailure) throw Object.assign(/* @__PURE__ */ new Error(`atlas ${domain} ${args.join(" ")} failed`), { alreadyReported: true });
|
|
1819
2224
|
process.exit(1);
|
|
1820
2225
|
}
|
|
1821
2226
|
if (opts.captureStdout) return stdoutText.trim().length > 0 ? stdoutText : stderrText;
|
|
1822
2227
|
return "";
|
|
1823
2228
|
}
|
|
1824
|
-
async function generatePostgresDdlCommand(rawArgs) {
|
|
2229
|
+
async function generatePostgresDdlCommand(rawArgs, options = {}) {
|
|
1825
2230
|
const argsList = (0, import_arg.default)({
|
|
1826
2231
|
"--collections": String,
|
|
1827
2232
|
"--output": String,
|
|
@@ -1831,16 +2236,31 @@ async function generatePostgresDdlCommand(rawArgs) {
|
|
|
1831
2236
|
argv: rawArgs.slice(2),
|
|
1832
2237
|
permissive: true
|
|
1833
2238
|
});
|
|
1834
|
-
const ddl = resolveChildScript("generate-postgres-ddl");
|
|
1835
|
-
if (!ddl.ok) {
|
|
2239
|
+
const ddl = generatesInProcess() ? void 0 : resolveChildScript("generate-postgres-ddl");
|
|
2240
|
+
if (ddl && !ddl.ok) {
|
|
1836
2241
|
outError(chalk.red(childScriptError("DDL generator", ddl.reason)));
|
|
1837
2242
|
process.exit(1);
|
|
1838
2243
|
}
|
|
1839
2244
|
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
1840
|
-
const outputPath = argsList["--output"] || path.join("
|
|
2245
|
+
const outputPath = argsList["--output"] || path.join(".rebase/sql", "schema.sql");
|
|
2246
|
+
if (!argsList["--output"]) {
|
|
2247
|
+
const retire = options.retireLegacyCopies !== false;
|
|
2248
|
+
const retired = prepareGeneratedSqlDir(process.cwd(), { retire });
|
|
2249
|
+
if (retired.length > 0 && !retire) out(chalk.gray(` The next \`rebase db push\` or \`rebase db generate\` removes the old copies an earlier release committed:\n ${retired.join(", ")}`));
|
|
2250
|
+
else if (retired.length > 0) out(chalk.gray(` Generated SQL is written to ${GENERATED_SQL_DIR}/ now, which is not committed. Removed the old copies — commit the deletion:\n ${retired.join(", ")}`));
|
|
2251
|
+
}
|
|
2252
|
+
if (!ddl) {
|
|
2253
|
+
await generateInProcess("Postgres DDL generator", "Postgres DDL", async ({ writePostgresDdl }) => {
|
|
2254
|
+
await writePostgresDdl({
|
|
2255
|
+
collectionsPath: path.resolve(process.cwd(), collectionsPath),
|
|
2256
|
+
outputPath: path.resolve(process.cwd(), outputPath)
|
|
2257
|
+
});
|
|
2258
|
+
});
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
1841
2261
|
const cmdParts = [
|
|
1842
2262
|
ddl.bin,
|
|
1843
|
-
ddl.
|
|
2263
|
+
...ddl.args,
|
|
1844
2264
|
`--collections=${collectionsPath}`,
|
|
1845
2265
|
`--output=${outputPath}`
|
|
1846
2266
|
];
|
|
@@ -1889,9 +2309,9 @@ async function schemaStaleCommand(rawArgs) {
|
|
|
1889
2309
|
const schemaFile = path.resolve(process.cwd(), outputPath);
|
|
1890
2310
|
const fix = Boolean(argsList["--fix"]);
|
|
1891
2311
|
const generatedExists = fs.existsSync(schemaFile);
|
|
1892
|
-
const { loadCollections } = await import("./doctor-
|
|
2312
|
+
const { loadCollections } = await import("./doctor-C4WiftHm.js").then((n) => n.t);
|
|
1893
2313
|
const { findLegacyForeignKeyNames, compareGeneratedDeclarations, staleVerdict } = await import("./generated-schema-staleness-Di9c4ZxN.js").then((n) => n.r);
|
|
1894
|
-
const { generateSchema } = await import("./generate-drizzle-schema-logic-
|
|
2314
|
+
const { generateSchema } = await import("./generate-drizzle-schema-logic-B74WN37S.js").then((n) => n.n);
|
|
1895
2315
|
const { relationalCollections } = await import("@rebasepro/common");
|
|
1896
2316
|
let stale = [];
|
|
1897
2317
|
let differences = [];
|
|
@@ -1904,6 +2324,7 @@ async function schemaStaleCommand(rawArgs) {
|
|
|
1904
2324
|
if (postgresCollections.length > 0) differences = compareGeneratedDeclarations(generateSchema(postgresCollections), onDisk);
|
|
1905
2325
|
} catch (err) {
|
|
1906
2326
|
unreadable = err instanceof Error ? err.message : String(err);
|
|
2327
|
+
const { logger } = await import("@rebasepro/server");
|
|
1907
2328
|
logger.debug(`schema stale: skipped (${unreadable})`);
|
|
1908
2329
|
}
|
|
1909
2330
|
const verdict = staleVerdict({
|
|
@@ -1928,7 +2349,7 @@ async function schemaStaleCommand(rawArgs) {
|
|
|
1928
2349
|
`--output=${outputPath}`
|
|
1929
2350
|
]);
|
|
1930
2351
|
}
|
|
1931
|
-
async function schemaCommand(subcommand, rawArgs) {
|
|
2352
|
+
async function schemaCommand(subcommand, rawArgs, options = {}) {
|
|
1932
2353
|
if (subcommand && (rawArgs.includes("--help") || rawArgs.includes("-h"))) {
|
|
1933
2354
|
out("");
|
|
1934
2355
|
out(chalk.bold(` rebase schema ${subcommand}`));
|
|
@@ -1953,24 +2374,38 @@ async function schemaCommand(subcommand, rawArgs) {
|
|
|
1953
2374
|
argv: rawArgs.slice(2),
|
|
1954
2375
|
permissive: true
|
|
1955
2376
|
});
|
|
2377
|
+
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
2378
|
+
const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
|
|
2379
|
+
const watch = argsList["--watch"] || false;
|
|
2380
|
+
if (!watch && generatesInProcess()) {
|
|
2381
|
+
out("");
|
|
2382
|
+
out(chalk.bold(" 🔧 Rebase Schema Generator"));
|
|
2383
|
+
out("");
|
|
2384
|
+
await generateInProcess("schema generator", "Drizzle schema", async ({ writeDrizzleSchema }) => {
|
|
2385
|
+
await writeDrizzleSchema({
|
|
2386
|
+
collectionsPath: path.resolve(process.cwd(), collectionsPath),
|
|
2387
|
+
outputPath: path.resolve(process.cwd(), outputPath),
|
|
2388
|
+
nextStep: options.nextStep
|
|
2389
|
+
});
|
|
2390
|
+
});
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
1956
2393
|
const generator = resolveChildScript("generate-drizzle-schema");
|
|
1957
2394
|
if (!generator.ok) {
|
|
1958
2395
|
outError(chalk.red(childScriptError("schema generator", generator.reason)));
|
|
1959
2396
|
process.exit(1);
|
|
1960
2397
|
}
|
|
1961
|
-
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
1962
|
-
const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
|
|
1963
|
-
const watch = argsList["--watch"] || false;
|
|
1964
2398
|
out("");
|
|
1965
2399
|
out(chalk.bold(" 🔧 Rebase Schema Generator"));
|
|
1966
2400
|
out("");
|
|
1967
2401
|
const cmdParts = [
|
|
1968
2402
|
generator.bin,
|
|
1969
|
-
generator.
|
|
2403
|
+
...generator.args,
|
|
1970
2404
|
`--collections=${collectionsPath}`,
|
|
1971
2405
|
`--output=${outputPath}`
|
|
1972
2406
|
];
|
|
1973
2407
|
if (watch) cmdParts.push("--watch");
|
|
2408
|
+
if (options.nextStep === false) cmdParts.push("--no-next-step");
|
|
1974
2409
|
try {
|
|
1975
2410
|
await execa(cmdParts[0], cmdParts.slice(1), {
|
|
1976
2411
|
cwd: process.cwd(),
|
|
@@ -2007,7 +2442,7 @@ async function schemaCommand(subcommand, rawArgs) {
|
|
|
2007
2442
|
out("");
|
|
2008
2443
|
const cmdParts = [
|
|
2009
2444
|
introspect.bin,
|
|
2010
|
-
introspect.
|
|
2445
|
+
...introspect.args,
|
|
2011
2446
|
`--output=${outputPath}`,
|
|
2012
2447
|
...argsList["--force"] ? ["--force"] : [],
|
|
2013
2448
|
...argsList["--schema"] ? [`--schema=${argsList["--schema"]}`] : []
|
|
@@ -2050,7 +2485,7 @@ async function doctorPluginCommand(rawArgs) {
|
|
|
2050
2485
|
const sdkPath = parsedArgs["--sdk"] || path.join("..", "generated", "sdk", "database.types.ts");
|
|
2051
2486
|
const cmdParts = [
|
|
2052
2487
|
doctor.bin,
|
|
2053
|
-
doctor.
|
|
2488
|
+
...doctor.args,
|
|
2054
2489
|
`--collections=${collectionsPath}`,
|
|
2055
2490
|
`--schema=${schemaPath}`,
|
|
2056
2491
|
`--sdk=${sdkPath}`,
|