@abloatai/ablo 0.16.3 → 0.17.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/CHANGELOG.md +34 -0
- package/dist/BaseSyncedStore.d.ts +3 -1
- package/dist/BaseSyncedStore.js +27 -4
- package/dist/cli.cjs +366 -211
- package/dist/client/Ablo.js +24 -11
- package/dist/schema/diff.d.ts +14 -0
- package/docs/data-sources.md +164 -287
- package/docs/quickstart.md +51 -63
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -277378,7 +277378,7 @@ var ErrorBodyShapeSchema = import_zod4.z.object({
|
|
|
277378
277378
|
}).passthrough();
|
|
277379
277379
|
|
|
277380
277380
|
// src/cli/migrate.ts
|
|
277381
|
-
var
|
|
277381
|
+
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
277382
277382
|
var import_fs5 = require("fs");
|
|
277383
277383
|
|
|
277384
277384
|
// node_modules/postgres/src/index.js
|
|
@@ -278098,7 +278098,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
278098
278098
|
async function cancel({ pid, secret }, resolve6, reject) {
|
|
278099
278099
|
try {
|
|
278100
278100
|
cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
|
|
278101
|
-
await
|
|
278101
|
+
await connect2();
|
|
278102
278102
|
socket.once("error", reject);
|
|
278103
278103
|
socket.once("close", resolve6);
|
|
278104
278104
|
} catch (error2) {
|
|
@@ -278231,7 +278231,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
278231
278231
|
incomings = null;
|
|
278232
278232
|
}
|
|
278233
278233
|
}
|
|
278234
|
-
async function
|
|
278234
|
+
async function connect2() {
|
|
278235
278235
|
terminated = false;
|
|
278236
278236
|
backendParameters = {};
|
|
278237
278237
|
socket || (socket = await createSocket());
|
|
@@ -278250,7 +278250,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
278250
278250
|
hostIndex = (hostIndex + 1) % port.length;
|
|
278251
278251
|
}
|
|
278252
278252
|
function reconnect() {
|
|
278253
|
-
setTimeout(
|
|
278253
|
+
setTimeout(connect2, closedTime ? Math.max(0, closedTime + delay - import_perf_hooks.performance.now()) : 0);
|
|
278254
278254
|
}
|
|
278255
278255
|
function connected() {
|
|
278256
278256
|
try {
|
|
@@ -279252,7 +279252,7 @@ function Postgres(a, b4) {
|
|
|
279252
279252
|
const c = open.length ? open.shift() : await new Promise((resolve6, reject) => {
|
|
279253
279253
|
const query = { reserve: resolve6, reject };
|
|
279254
279254
|
queries.push(query);
|
|
279255
|
-
closed.length &&
|
|
279255
|
+
closed.length && connect2(closed.shift(), query);
|
|
279256
279256
|
});
|
|
279257
279257
|
move(c, reserved);
|
|
279258
279258
|
c.reserved = () => queue.length ? c.execute(queue.shift()) : move(c, reserved);
|
|
@@ -279339,7 +279339,7 @@ function Postgres(a, b4) {
|
|
|
279339
279339
|
if (open.length)
|
|
279340
279340
|
return go(open.shift(), query);
|
|
279341
279341
|
if (closed.length)
|
|
279342
|
-
return
|
|
279342
|
+
return connect2(closed.shift(), query);
|
|
279343
279343
|
busy.length ? go(busy.shift(), query) : queries.push(query);
|
|
279344
279344
|
}
|
|
279345
279345
|
function go(c, query) {
|
|
@@ -279372,7 +279372,7 @@ function Postgres(a, b4) {
|
|
|
279372
279372
|
queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
|
|
279373
279373
|
resolve6();
|
|
279374
279374
|
}
|
|
279375
|
-
function
|
|
279375
|
+
function connect2(c, query) {
|
|
279376
279376
|
move(c, connecting);
|
|
279377
279377
|
c.connect(query);
|
|
279378
279378
|
return c;
|
|
@@ -279397,7 +279397,7 @@ function Postgres(a, b4) {
|
|
|
279397
279397
|
c.reserved = null;
|
|
279398
279398
|
c.onclose && (c.onclose(e2), c.onclose = null);
|
|
279399
279399
|
options.onclose && options.onclose(c.id);
|
|
279400
|
-
queries.length &&
|
|
279400
|
+
queries.length && connect2(c, queries.shift());
|
|
279401
279401
|
}
|
|
279402
279402
|
}
|
|
279403
279403
|
function parseOptions(a, b4) {
|
|
@@ -279517,158 +279517,8 @@ function osUsername() {
|
|
|
279517
279517
|
|
|
279518
279518
|
// src/cli/dbRole.ts
|
|
279519
279519
|
init_cjs_shims();
|
|
279520
|
-
var import_crypto2 = require("crypto");
|
|
279521
|
-
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
279522
279520
|
var import_fs2 = require("fs");
|
|
279523
279521
|
var import_path = require("path");
|
|
279524
|
-
var DEFAULT_SCOPED_ROLE = "ablo_app";
|
|
279525
|
-
async function detectRoleSafety(sql) {
|
|
279526
|
-
const rows = await sql`SELECT rolname, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user`;
|
|
279527
|
-
const row = rows[0];
|
|
279528
|
-
if (!row) return { role: "unknown", superuser: false, bypassRls: false, unsafe: false };
|
|
279529
|
-
return {
|
|
279530
|
-
role: row.rolname,
|
|
279531
|
-
superuser: row.rolsuper,
|
|
279532
|
-
bypassRls: row.rolbypassrls,
|
|
279533
|
-
unsafe: row.rolsuper || row.rolbypassrls
|
|
279534
|
-
};
|
|
279535
|
-
}
|
|
279536
|
-
function generateRolePassword() {
|
|
279537
|
-
return (0, import_crypto2.randomBytes)(24).toString("base64url");
|
|
279538
|
-
}
|
|
279539
|
-
function scramSha256Verifier(password, iterations = 4096) {
|
|
279540
|
-
const salt = (0, import_crypto2.randomBytes)(16);
|
|
279541
|
-
const saltedPassword = (0, import_crypto2.pbkdf2Sync)(password, salt, iterations, 32, "sha256");
|
|
279542
|
-
const clientKey = (0, import_crypto2.createHmac)("sha256", saltedPassword).update("Client Key").digest();
|
|
279543
|
-
const storedKey = (0, import_crypto2.createHash)("sha256").update(clientKey).digest();
|
|
279544
|
-
const serverKey = (0, import_crypto2.createHmac)("sha256", saltedPassword).update("Server Key").digest();
|
|
279545
|
-
return `SCRAM-SHA-256$${iterations}:${salt.toString("base64")}$${storedKey.toString("base64")}:${serverKey.toString("base64")}`;
|
|
279546
|
-
}
|
|
279547
|
-
function scopedRoleStatements(input) {
|
|
279548
|
-
const role = input.role ?? DEFAULT_SCOPED_ROLE;
|
|
279549
|
-
const q2 = (id) => `"${id.replace(/"/g, '""')}"`;
|
|
279550
|
-
const pw = (input.passwordMode ?? "scram-verifier") === "scram-verifier" ? scramSha256Verifier(input.password) : input.password.replace(/'/g, "''");
|
|
279551
|
-
return [
|
|
279552
|
-
`DO $$ BEGIN
|
|
279553
|
-
CREATE ROLE ${q2(role)} LOGIN PASSWORD '${pw}'
|
|
279554
|
-
NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE;
|
|
279555
|
-
EXCEPTION WHEN duplicate_object THEN
|
|
279556
|
-
-- Rerun: rotate ONLY the password. Re-asserting attributes here trips
|
|
279557
|
-
-- managed-Postgres permission walls (Neon: "permission denied to alter
|
|
279558
|
-
-- role" for attribute changes by non-superusers); the attributes were set
|
|
279559
|
-
-- at creation, and the server-side probe still audits the live role.
|
|
279560
|
-
ALTER ROLE ${q2(role)} WITH LOGIN PASSWORD '${pw}';
|
|
279561
|
-
END $$;`,
|
|
279562
|
-
`GRANT CREATE, CONNECT ON DATABASE ${q2(input.database)} TO ${q2(role)};`,
|
|
279563
|
-
`GRANT CREATE, USAGE ON SCHEMA public TO ${q2(role)};`
|
|
279564
|
-
];
|
|
279565
|
-
}
|
|
279566
|
-
function rewriteDatabaseUrl(ownerUrl, role, password) {
|
|
279567
|
-
const url = new URL(ownerUrl);
|
|
279568
|
-
url.username = role;
|
|
279569
|
-
url.password = password;
|
|
279570
|
-
return url.toString();
|
|
279571
|
-
}
|
|
279572
|
-
async function createScopedRole(ownerUrl, options) {
|
|
279573
|
-
const role = options?.role ?? DEFAULT_SCOPED_ROLE;
|
|
279574
|
-
const password = generateRolePassword();
|
|
279575
|
-
const database = new URL(ownerUrl).pathname.replace(/^\//, "") || "postgres";
|
|
279576
|
-
const sql = src_default(ownerUrl, { max: 1, prepare: false, onnotice: () => {
|
|
279577
|
-
} });
|
|
279578
|
-
try {
|
|
279579
|
-
try {
|
|
279580
|
-
for (const statement of scopedRoleStatements({ database, role, password })) {
|
|
279581
|
-
await sql.unsafe(statement);
|
|
279582
|
-
}
|
|
279583
|
-
} catch (err) {
|
|
279584
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
279585
|
-
if (!/plaintext password/i.test(message)) throw err;
|
|
279586
|
-
for (const statement of scopedRoleStatements({ database, role, password, passwordMode: "plaintext" })) {
|
|
279587
|
-
await sql.unsafe(statement);
|
|
279588
|
-
}
|
|
279589
|
-
}
|
|
279590
|
-
} finally {
|
|
279591
|
-
await sql.end({ timeout: 5 });
|
|
279592
|
-
}
|
|
279593
|
-
return { role, databaseUrl: rewriteDatabaseUrl(ownerUrl, role, password) };
|
|
279594
|
-
}
|
|
279595
|
-
async function ensureScopedRoleInteractive(dbUrl) {
|
|
279596
|
-
let safety;
|
|
279597
|
-
try {
|
|
279598
|
-
const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
|
|
279599
|
-
} });
|
|
279600
|
-
try {
|
|
279601
|
-
safety = await detectRoleSafety(sql);
|
|
279602
|
-
} finally {
|
|
279603
|
-
await sql.end({ timeout: 5 });
|
|
279604
|
-
}
|
|
279605
|
-
} catch {
|
|
279606
|
-
return dbUrl;
|
|
279607
|
-
}
|
|
279608
|
-
if (!safety.unsafe) return dbUrl;
|
|
279609
|
-
const why = safety.superuser ? "a superuser" : "BYPASSRLS";
|
|
279610
|
-
console.log(
|
|
279611
|
-
`
|
|
279612
|
-
${import_picocolors2.default.yellow("!")} DATABASE_URL connects as ${import_picocolors2.default.bold(safety.role)} \u2014 ${why}, so row-level security can't be enforced.
|
|
279613
|
-
Ablo's server will refuse this connection (${import_picocolors2.default.bold("database_role_cannot_enforce_rls")}).`
|
|
279614
|
-
);
|
|
279615
|
-
if (!process.stdout.isTTY) {
|
|
279616
|
-
console.log(
|
|
279617
|
-
import_picocolors2.default.dim(
|
|
279618
|
-
` Create a scoped role and update DATABASE_URL \u2014 run \`npx ablo migrate\` interactively
|
|
279619
|
-
to do it automatically, or see https://docs.abloatai.com/quickstart#scoped-role`
|
|
279620
|
-
)
|
|
279621
|
-
);
|
|
279622
|
-
return dbUrl;
|
|
279623
|
-
}
|
|
279624
|
-
const proceed = await ye({
|
|
279625
|
-
message: `Create a scoped role ${DEFAULT_SCOPED_ROLE} (NOSUPERUSER, NOBYPASSRLS) and update DATABASE_URL?`,
|
|
279626
|
-
initialValue: true
|
|
279627
|
-
});
|
|
279628
|
-
if (pD(proceed) || !proceed) {
|
|
279629
|
-
console.log(import_picocolors2.default.dim(" Skipped \u2014 see https://docs.abloatai.com/quickstart#scoped-role for the manual recipe."));
|
|
279630
|
-
return dbUrl;
|
|
279631
|
-
}
|
|
279632
|
-
const { role, databaseUrl } = await createScopedRole(dbUrl);
|
|
279633
|
-
const where = persistDatabaseUrl(databaseUrl);
|
|
279634
|
-
console.log(
|
|
279635
|
-
` ${import_picocolors2.default.green("\u2713")} Created role ${import_picocolors2.default.bold(role)} and updated ${import_picocolors2.default.bold("DATABASE_URL")} in ${import_picocolors2.default.bold(where)}.
|
|
279636
|
-
` + import_picocolors2.default.dim(` The owner credential never left this machine; the new password was written, not printed.`)
|
|
279637
|
-
);
|
|
279638
|
-
return databaseUrl;
|
|
279639
|
-
}
|
|
279640
|
-
function persistDatabaseUrl(databaseUrl, cwd = process.cwd()) {
|
|
279641
|
-
const line = `DATABASE_URL=${databaseUrl}`;
|
|
279642
|
-
for (const name of [".env.local", ".env"]) {
|
|
279643
|
-
const path = (0, import_path.resolve)(cwd, name);
|
|
279644
|
-
if (!(0, import_fs2.existsSync)(path)) continue;
|
|
279645
|
-
const content = (0, import_fs2.readFileSync)(path, "utf8");
|
|
279646
|
-
if (/^DATABASE_URL=/m.test(content)) {
|
|
279647
|
-
(0, import_fs2.writeFileSync)(path, content.replace(/^DATABASE_URL=.*$/m, line));
|
|
279648
|
-
return name;
|
|
279649
|
-
}
|
|
279650
|
-
}
|
|
279651
|
-
const envLocal = (0, import_path.resolve)(cwd, ".env.local");
|
|
279652
|
-
if ((0, import_fs2.existsSync)(envLocal)) {
|
|
279653
|
-
const content = (0, import_fs2.readFileSync)(envLocal, "utf8");
|
|
279654
|
-
(0, import_fs2.appendFileSync)(envLocal, `${content.endsWith("\n") || content.length === 0 ? "" : "\n"}${line}
|
|
279655
|
-
`);
|
|
279656
|
-
} else {
|
|
279657
|
-
(0, import_fs2.writeFileSync)(envLocal, `${line}
|
|
279658
|
-
`, { mode: 384 });
|
|
279659
|
-
}
|
|
279660
|
-
const gitignorePath = (0, import_path.resolve)(cwd, ".gitignore");
|
|
279661
|
-
const gitignore = (0, import_fs2.existsSync)(gitignorePath) ? (0, import_fs2.readFileSync)(gitignorePath, "utf8") : "";
|
|
279662
|
-
if (!/^(\.env\.local|\.env\*|\.env\.\*|\.env.*)$/m.test(gitignore)) {
|
|
279663
|
-
(0, import_fs2.writeFileSync)(
|
|
279664
|
-
gitignorePath,
|
|
279665
|
-
`${gitignore.endsWith("\n") || gitignore.length === 0 ? gitignore : `${gitignore}
|
|
279666
|
-
`}.env.local
|
|
279667
|
-
`
|
|
279668
|
-
);
|
|
279669
|
-
}
|
|
279670
|
-
return ".env.local";
|
|
279671
|
-
}
|
|
279672
279522
|
function readProjectDatabaseUrl(cwd = process.cwd()) {
|
|
279673
279523
|
const fromEnv = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
|
|
279674
279524
|
if (fromEnv) return fromEnv;
|
|
@@ -279687,7 +279537,7 @@ var import_source = require("@abloatai/ablo/source");
|
|
|
279687
279537
|
|
|
279688
279538
|
// src/cli/push.ts
|
|
279689
279539
|
init_cjs_shims();
|
|
279690
|
-
var
|
|
279540
|
+
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
279691
279541
|
|
|
279692
279542
|
// src/auth/credentialPolicy.ts
|
|
279693
279543
|
init_cjs_shims();
|
|
@@ -279929,7 +279779,16 @@ var DEFAULT_URL = "https://api.abloatai.com";
|
|
|
279929
279779
|
function fmtSignal(s) {
|
|
279930
279780
|
const sig = s;
|
|
279931
279781
|
const where = sig.field ? `${sig.model}.${sig.field}` : sig.model;
|
|
279932
|
-
|
|
279782
|
+
let line = ` \u2022 ${import_picocolors2.default.bold(where ?? "?")} \u2014 ${sig.detail ?? ""}`;
|
|
279783
|
+
if (sig.shadowed) {
|
|
279784
|
+
const env = sig.shadowed.environment ?? "production";
|
|
279785
|
+
const ver = sig.shadowed.version != null ? `v${sig.shadowed.version}` : "active";
|
|
279786
|
+
const when = sig.shadowed.pushedAt ? new Date(sig.shadowed.pushedAt).toISOString().slice(0, 10) : "unknown date";
|
|
279787
|
+
const by = sig.shadowed.pushedBy ? ` by ${sig.shadowed.pushedBy}` : "";
|
|
279788
|
+
line += `
|
|
279789
|
+
${import_picocolors2.default.dim(`\u21B3 baseline: ${env} ${ver}, pushed ${when}${by}`)}`;
|
|
279790
|
+
}
|
|
279791
|
+
return line;
|
|
279933
279792
|
}
|
|
279934
279793
|
async function pushSchema(schema, args) {
|
|
279935
279794
|
const schemaJson = JSON.parse((0, import_schema2.serializeSchema)(schema));
|
|
@@ -280010,7 +279869,7 @@ async function loadSchema(schemaPath, exportName) {
|
|
|
280010
279869
|
const abs = (0, import_path3.resolve)(process.cwd(), schemaPath);
|
|
280011
279870
|
if (!(0, import_fs4.existsSync)(abs)) {
|
|
280012
279871
|
throw new AbloValidationError(
|
|
280013
|
-
`schema not found at ${
|
|
279872
|
+
`schema not found at ${import_picocolors2.default.bold(schemaPath)}. Run ${import_picocolors2.default.bold("npx ablo init")} or pass ${import_picocolors2.default.bold("--schema <path>")}.`,
|
|
280014
279873
|
{ code: "cli_invalid_arguments" }
|
|
280015
279874
|
);
|
|
280016
279875
|
}
|
|
@@ -280021,7 +279880,7 @@ async function loadSchema(schemaPath, exportName) {
|
|
|
280021
279880
|
const schema = mod[exportName] ?? nested?.[exportName];
|
|
280022
279881
|
if (!schema || typeof schema !== "object" || !("models" in schema)) {
|
|
280023
279882
|
throw new AbloValidationError(
|
|
280024
|
-
`${
|
|
279883
|
+
`${import_picocolors2.default.bold(schemaPath)} has no \`${exportName}\` export that looks like a Schema. Did you \`export const ${exportName} = defineSchema({ ... })\`?`,
|
|
280025
279884
|
{ code: "cli_invalid_arguments" }
|
|
280026
279885
|
);
|
|
280027
279886
|
}
|
|
@@ -280032,14 +279891,14 @@ async function push(argv) {
|
|
|
280032
279891
|
try {
|
|
280033
279892
|
args = parsePushArgs(argv);
|
|
280034
279893
|
} catch (err) {
|
|
280035
|
-
console.error(
|
|
279894
|
+
console.error(import_picocolors2.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
280036
279895
|
process.exit(1);
|
|
280037
279896
|
}
|
|
280038
279897
|
if (!args.apiKey) args.apiKey = resolveApiKey();
|
|
280039
279898
|
if (!args.apiKey) {
|
|
280040
279899
|
console.error(
|
|
280041
|
-
|
|
280042
|
-
` Run ${
|
|
279900
|
+
import_picocolors2.default.red(` No API key.`) + import_picocolors2.default.dim(
|
|
279901
|
+
` Run ${import_picocolors2.default.bold("npx ablo login")} for the sandbox dev loop \u2014 or set ${import_picocolors2.default.bold("ABLO_API_KEY")} (${import_picocolors2.default.bold("sk_test_")} = sandbox; ${import_picocolors2.default.bold("sk_live_")} = deliberate production deploy). Mode is currently '${getMode()}'.`
|
|
280043
279902
|
)
|
|
280044
279903
|
);
|
|
280045
279904
|
process.exit(1);
|
|
@@ -280047,17 +279906,17 @@ async function push(argv) {
|
|
|
280047
279906
|
const schema = await loadSchema(args.schemaPath, args.exportName);
|
|
280048
279907
|
const hash = (0, import_schema2.schemaHash)(schema);
|
|
280049
279908
|
console.log(
|
|
280050
|
-
` Pushing ${
|
|
279909
|
+
` Pushing ${import_picocolors2.default.bold(args.schemaPath)} ${import_picocolors2.default.dim(`(${Object.keys(schema.models).length} models, hash ${hash})`)} \u2192 ${import_picocolors2.default.dim(args.url)}`
|
|
280051
279910
|
);
|
|
280052
279911
|
const { ok: resOk, status: status2, body, bodyText } = await pushSchema(schema, args);
|
|
280053
279912
|
if (resOk) {
|
|
280054
279913
|
if (body.unchanged) {
|
|
280055
|
-
console.log(` ${
|
|
279914
|
+
console.log(` ${import_picocolors2.default.dim("\u25CB")} No changes \u2014 schema already active (v${body.version}).`);
|
|
280056
279915
|
} else {
|
|
280057
|
-
console.log(` ${
|
|
279916
|
+
console.log(` ${import_picocolors2.default.green("\u2713")} Activated ${import_picocolors2.default.bold(`v${body.version}`)} ${import_picocolors2.default.dim(`(hash ${body.hash})`)}`);
|
|
280058
279917
|
if (Array.isArray(body.warnings) && body.warnings.length > 0) {
|
|
280059
|
-
console.log(
|
|
280060
|
-
for (const w2 of body.warnings) console.log(
|
|
279918
|
+
console.log(import_picocolors2.default.yellow(` Applied ${body.warnings.length} destructive change(s):`));
|
|
279919
|
+
for (const w2 of body.warnings) console.log(import_picocolors2.default.yellow(fmtSignal(w2)));
|
|
280061
279920
|
}
|
|
280062
279921
|
}
|
|
280063
279922
|
return;
|
|
@@ -280065,27 +279924,56 @@ async function push(argv) {
|
|
|
280065
279924
|
if (status2 === 409) {
|
|
280066
279925
|
const unexecutable = Array.isArray(body.unexecutable) ? body.unexecutable : [];
|
|
280067
279926
|
const warnings = Array.isArray(body.warnings) ? body.warnings : [];
|
|
280068
|
-
console.error(
|
|
279927
|
+
console.error(import_picocolors2.default.red(" Incompatible change \u2014 this push is not safe to apply as-is."));
|
|
280069
279928
|
if (unexecutable.length > 0) {
|
|
280070
|
-
console.error(
|
|
280071
|
-
for (const u2 of unexecutable) console.error(
|
|
279929
|
+
console.error(import_picocolors2.default.red(` Unexecutable (would fail on existing rows):`));
|
|
279930
|
+
for (const u2 of unexecutable) console.error(import_picocolors2.default.red(fmtSignal(u2)));
|
|
280072
279931
|
}
|
|
280073
279932
|
if (warnings.length > 0) {
|
|
280074
|
-
console.error(
|
|
280075
|
-
for (const w2 of warnings) console.error(
|
|
279933
|
+
console.error(import_picocolors2.default.yellow(` Destructive (data loss):`));
|
|
279934
|
+
for (const w2 of warnings) console.error(import_picocolors2.default.yellow(fmtSignal(w2)));
|
|
279935
|
+
}
|
|
279936
|
+
const hasShadowed = [...unexecutable, ...warnings].some(
|
|
279937
|
+
(s) => s.shadowed != null
|
|
279938
|
+
);
|
|
279939
|
+
if (hasShadowed) {
|
|
279940
|
+
console.error(
|
|
279941
|
+
import_picocolors2.default.dim(
|
|
279942
|
+
" These models exist in the baseline above but not in your push. Sandbox readers fall"
|
|
279943
|
+
)
|
|
279944
|
+
);
|
|
279945
|
+
console.error(
|
|
279946
|
+
import_picocolors2.default.dim(
|
|
279947
|
+
" back to the production schema until you push your own, so applying this drops them."
|
|
279948
|
+
)
|
|
279949
|
+
);
|
|
280076
279950
|
}
|
|
280077
|
-
console.error(
|
|
279951
|
+
console.error(import_picocolors2.default.dim(` Re-push with ${import_picocolors2.default.bold("--force")} to override, or use ${import_picocolors2.default.bold("--rename old:new")} if you renamed a model.`));
|
|
280078
279952
|
} else if (status2 === 403) {
|
|
280079
|
-
|
|
280080
|
-
|
|
279953
|
+
const code = body.code ?? body.reason;
|
|
279954
|
+
const serverMsg = body.message ?? body.reason;
|
|
279955
|
+
console.error(import_picocolors2.default.red(` Forbidden${code ? ` [${code}]` : ""}: ${serverMsg ?? "permission denied"}`));
|
|
279956
|
+
if (code === "database_role_cannot_enforce_rls") {
|
|
280081
279957
|
console.error(
|
|
280082
|
-
|
|
280083
|
-
`
|
|
279958
|
+
import_picocolors2.default.dim(
|
|
279959
|
+
` Your database role bypasses row-level security. Run ${import_picocolors2.default.bold("npx ablo migrate")} to create a scoped (NOBYPASSRLS) role and repoint DATABASE_URL, then re-push.`
|
|
279960
|
+
)
|
|
279961
|
+
);
|
|
279962
|
+
} else if (code === "database_tables_unforced_rls") {
|
|
279963
|
+
console.error(
|
|
279964
|
+
import_picocolors2.default.dim(
|
|
279965
|
+
` One or more synced tables don't have FORCE ROW LEVEL SECURITY. Run ${import_picocolors2.default.bold("npx ablo migrate")} to (re)apply the tenant policies, then re-push.`
|
|
279966
|
+
)
|
|
279967
|
+
);
|
|
279968
|
+
} else if (args.apiKey != null && classifyCredentialKind(args.apiKey) === "restricted") {
|
|
279969
|
+
console.error(
|
|
279970
|
+
import_picocolors2.default.dim(
|
|
279971
|
+
` Schema pushes need a SECRET key: ${import_picocolors2.default.bold("sk_test_")} (sandbox dev loop) or a dashboard ${import_picocolors2.default.bold("sk_live_")} (production deploy: ${import_picocolors2.default.bold("ABLO_API_KEY=sk_live_\u2026 npx ablo push")}).`
|
|
280084
279972
|
)
|
|
280085
279973
|
);
|
|
280086
279974
|
}
|
|
280087
279975
|
} else {
|
|
280088
|
-
console.error(
|
|
279976
|
+
console.error(import_picocolors2.default.red(` Push failed (${status2}): ${body.message ?? body.reason ?? bodyText}`));
|
|
280089
279977
|
}
|
|
280090
279978
|
process.exit(1);
|
|
280091
279979
|
}
|
|
@@ -280093,6 +279981,10 @@ async function push(argv) {
|
|
|
280093
279981
|
// src/cli/migrate.ts
|
|
280094
279982
|
var MIGRATE_USAGE = ` ablo migrate \u2014 provision your schema's tables in your own Postgres (DATABASE_URL)
|
|
280095
279983
|
|
|
279984
|
+
To connect a real database, run \`ablo connect\` \u2014 Ablo reads your WAL via logical
|
|
279985
|
+
replication, the one way. \`migrate\` is the optional escape hatch for provisioning
|
|
279986
|
+
tables when you don't already have them.
|
|
279987
|
+
|
|
280096
279988
|
Usage:
|
|
280097
279989
|
npx ablo migrate Create the synced-model tables (with row-level security)
|
|
280098
279990
|
npx ablo migrate --dry-run Print the SQL without executing it
|
|
@@ -280141,8 +280033,8 @@ function planFor(schema, targetSchema = "public") {
|
|
|
280141
280033
|
}
|
|
280142
280034
|
var log = {
|
|
280143
280035
|
info: (msg, fields) => console.log(`[migrate] ${msg}`, fields),
|
|
280144
|
-
warn: (msg, fields) => console.warn(
|
|
280145
|
-
error: (msg, fields) => console.error(
|
|
280036
|
+
warn: (msg, fields) => console.warn(import_picocolors3.default.yellow(`[migrate] ${msg}`), fields),
|
|
280037
|
+
error: (msg, fields) => console.error(import_picocolors3.default.red(`[migrate] ${msg}`), fields)
|
|
280146
280038
|
};
|
|
280147
280039
|
var PG_LOCK_NOT_AVAILABLE = "55P03";
|
|
280148
280040
|
var LOCK_TIMEOUT = process.env.ABLO_SCHEMA_LOCK_TIMEOUT ?? process.env.ABLO_DDL_LOCK_TIMEOUT ?? "5s";
|
|
@@ -280217,7 +280109,7 @@ async function migrate(argv) {
|
|
|
280217
280109
|
try {
|
|
280218
280110
|
args = parseMigrateArgs(argv);
|
|
280219
280111
|
} catch (err) {
|
|
280220
|
-
console.error(
|
|
280112
|
+
console.error(import_picocolors3.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
280221
280113
|
process.exit(1);
|
|
280222
280114
|
}
|
|
280223
280115
|
const schema = await loadSchema(args.schemaPath, args.exportName);
|
|
@@ -280228,31 +280120,290 @@ async function migrate(argv) {
|
|
|
280228
280120
|
].join("\n");
|
|
280229
280121
|
const totalStatements = plan.statements.length + plan.concurrent.length;
|
|
280230
280122
|
console.log(
|
|
280231
|
-
` ${
|
|
280123
|
+
` ${import_picocolors3.default.dim("Schema")} ${import_picocolors3.default.bold(args.schemaPath)} \u2192 ${import_picocolors3.default.dim(`${Object.keys(schema.models).length} models, ${totalStatements} statements`)}`
|
|
280232
280124
|
);
|
|
280233
280125
|
if (args.outputFile) {
|
|
280234
280126
|
(0, import_fs5.writeFileSync)(args.outputFile, sql + "\n");
|
|
280235
|
-
console.log(` ${
|
|
280127
|
+
console.log(` ${import_picocolors3.default.green("\u2713")} SQL written to ${import_picocolors3.default.bold(args.outputFile)}`);
|
|
280236
280128
|
return;
|
|
280237
280129
|
}
|
|
280238
280130
|
if (args.dryRun) {
|
|
280239
280131
|
console.log("\n" + sql + "\n");
|
|
280240
280132
|
return;
|
|
280241
280133
|
}
|
|
280242
|
-
const dbUrl =
|
|
280134
|
+
const dbUrl = readProjectDatabaseUrl();
|
|
280243
280135
|
if (!dbUrl) {
|
|
280244
|
-
console.error(
|
|
280136
|
+
console.error(
|
|
280137
|
+
import_picocolors3.default.red(
|
|
280138
|
+
" No DATABASE_URL found (checked process env, .env.local, .env). Set it to apply, or use --dry-run to preview."
|
|
280139
|
+
)
|
|
280140
|
+
);
|
|
280245
280141
|
process.exit(1);
|
|
280246
280142
|
}
|
|
280247
|
-
const effectiveUrl = await ensureScopedRoleInteractive(dbUrl);
|
|
280248
280143
|
try {
|
|
280249
|
-
await applyStatements(
|
|
280250
|
-
console.log(` ${
|
|
280144
|
+
await applyStatements(dbUrl, args.targetSchema, plan.statements, plan.concurrent);
|
|
280145
|
+
console.log(` ${import_picocolors3.default.green("\u2713")} Migration complete`);
|
|
280251
280146
|
} catch {
|
|
280252
280147
|
process.exit(1);
|
|
280253
280148
|
}
|
|
280254
280149
|
}
|
|
280255
280150
|
|
|
280151
|
+
// src/cli/connect.ts
|
|
280152
|
+
init_cjs_shims();
|
|
280153
|
+
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
280154
|
+
|
|
280155
|
+
// src/cli/theme.ts
|
|
280156
|
+
init_cjs_shims();
|
|
280157
|
+
var RESET = "\x1B[0m";
|
|
280158
|
+
var PAPER_BG = "\x1B[48;2;250;250;250m";
|
|
280159
|
+
var BLACK_FG = "\x1B[38;2;0;0;0m";
|
|
280160
|
+
function colorEnabled() {
|
|
280161
|
+
return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
280162
|
+
}
|
|
280163
|
+
function brand(label = "ablo") {
|
|
280164
|
+
if (!colorEnabled()) return label;
|
|
280165
|
+
return `${PAPER_BG}${BLACK_FG} ${label} ${RESET}`;
|
|
280166
|
+
}
|
|
280167
|
+
|
|
280168
|
+
// src/cli/connect.ts
|
|
280169
|
+
var ABLO_PUBLICATION = "ablo_publication";
|
|
280170
|
+
var ABLO_REPLICATION_ROLE = "ablo_replicator";
|
|
280171
|
+
function parseConnectArgs(argv) {
|
|
280172
|
+
let check2 = false;
|
|
280173
|
+
let tables = [];
|
|
280174
|
+
let role = ABLO_REPLICATION_ROLE;
|
|
280175
|
+
for (let i = 0; i < argv.length; i++) {
|
|
280176
|
+
const arg = argv[i];
|
|
280177
|
+
switch (arg) {
|
|
280178
|
+
case "--check":
|
|
280179
|
+
check2 = true;
|
|
280180
|
+
break;
|
|
280181
|
+
case "--tables": {
|
|
280182
|
+
const value = argv[++i] ?? "";
|
|
280183
|
+
tables = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
280184
|
+
break;
|
|
280185
|
+
}
|
|
280186
|
+
case "--role":
|
|
280187
|
+
role = argv[++i] ?? role;
|
|
280188
|
+
break;
|
|
280189
|
+
default:
|
|
280190
|
+
throw new AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
|
|
280191
|
+
}
|
|
280192
|
+
}
|
|
280193
|
+
return { check: check2, tables, role };
|
|
280194
|
+
}
|
|
280195
|
+
function quoteIdent(id) {
|
|
280196
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
280197
|
+
}
|
|
280198
|
+
function connectSetupSql(input) {
|
|
280199
|
+
const role = input.role && input.role.length > 0 ? input.role : ABLO_REPLICATION_ROLE;
|
|
280200
|
+
const tables = input.tables ?? [];
|
|
280201
|
+
const publicationTarget = tables.length > 0 ? `FOR TABLE ${tables.map(quoteIdent).join(", ")}` : "FOR ALL TABLES";
|
|
280202
|
+
return [
|
|
280203
|
+
// 1. Turn on logical decoding. Requires a restart (it's not reloadable).
|
|
280204
|
+
`ALTER SYSTEM SET wal_level = 'logical';`,
|
|
280205
|
+
// 2. Publish the tables Ablo should read.
|
|
280206
|
+
`CREATE PUBLICATION ${quoteIdent(ABLO_PUBLICATION)} ${publicationTarget};`,
|
|
280207
|
+
// 3. A least-privilege role: it can stream replication and SELECT, nothing more.
|
|
280208
|
+
`CREATE ROLE ${quoteIdent(role)} WITH REPLICATION LOGIN PASSWORD '<password>';`,
|
|
280209
|
+
`GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${quoteIdent(role)};`,
|
|
280210
|
+
// Future tables get SELECT automatically, so the publication doesn't outgrow the grant.
|
|
280211
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${quoteIdent(role)};`
|
|
280212
|
+
];
|
|
280213
|
+
}
|
|
280214
|
+
function printConnectRecipe(args) {
|
|
280215
|
+
const sql = connectSetupSql({ tables: args.tables, role: args.role });
|
|
280216
|
+
console.log(`
|
|
280217
|
+
${brand("ablo")} ${import_picocolors4.default.dim("connect")} ${import_picocolors4.default.dim("logical replication \u2014 the one way to connect a real database")}
|
|
280218
|
+
`);
|
|
280219
|
+
console.log(
|
|
280220
|
+
` Ablo READS your write-ahead log (WAL) and ${import_picocolors4.default.bold("never")} runs DDL, owns, or migrates your
|
|
280221
|
+
schema. Your app keeps writing through your own backend \u2014 Ablo only tails the changes.
|
|
280222
|
+
Run this once against your Postgres ${import_picocolors4.default.dim("(as a superuser / the DB owner)")}:
|
|
280223
|
+
`
|
|
280224
|
+
);
|
|
280225
|
+
console.log(` ${import_picocolors4.default.bold("1.")} Enable logical decoding ${import_picocolors4.default.dim("(then RESTART Postgres \u2014 wal_level is not reloadable)")}`);
|
|
280226
|
+
console.log(` ${import_picocolors4.default.cyan(sql[0])}`);
|
|
280227
|
+
console.log(
|
|
280228
|
+
import_picocolors4.default.dim(
|
|
280229
|
+
` On Amazon RDS / Aurora you can't ALTER SYSTEM: set ${import_picocolors4.default.bold("rds.logical_replication = 1")} in the
|
|
280230
|
+
instance's parameter group instead, then reboot.`
|
|
280231
|
+
)
|
|
280232
|
+
);
|
|
280233
|
+
console.log(`
|
|
280234
|
+
${import_picocolors4.default.bold("2.")} Publish the tables Ablo should read`);
|
|
280235
|
+
console.log(` ${import_picocolors4.default.cyan(sql[1])}`);
|
|
280236
|
+
if (args.tables.length === 0) {
|
|
280237
|
+
console.log(import_picocolors4.default.dim(` (Scope it with ${import_picocolors4.default.bold("ablo connect --tables a,b,c")} to publish a subset.)`));
|
|
280238
|
+
}
|
|
280239
|
+
console.log(`
|
|
280240
|
+
${import_picocolors4.default.bold("3.")} Create a least-privilege replication role ${import_picocolors4.default.dim("(pick your own password)")}`);
|
|
280241
|
+
console.log(` ${import_picocolors4.default.cyan(sql[2])}`);
|
|
280242
|
+
console.log(` ${import_picocolors4.default.cyan(sql[3])}`);
|
|
280243
|
+
console.log(` ${import_picocolors4.default.cyan(sql[4])}`);
|
|
280244
|
+
console.log(
|
|
280245
|
+
import_picocolors4.default.dim(
|
|
280246
|
+
` On Amazon RDS, the REPLICATION attribute is granted, not set directly:
|
|
280247
|
+
${import_picocolors4.default.bold(`GRANT rds_replication TO ${quoteIdent(args.role)};`)}`
|
|
280248
|
+
)
|
|
280249
|
+
);
|
|
280250
|
+
console.log(
|
|
280251
|
+
`
|
|
280252
|
+
${import_picocolors4.default.bold("4.")} Put the role's connection string in ${import_picocolors4.default.bold("DATABASE_URL")}, then verify:
|
|
280253
|
+
${import_picocolors4.default.cyan("npx ablo connect --check")}
|
|
280254
|
+
`
|
|
280255
|
+
);
|
|
280256
|
+
console.log(
|
|
280257
|
+
import_picocolors4.default.dim(
|
|
280258
|
+
` Reminder: Ablo never writes to your database on this path. Provisioning tables with
|
|
280259
|
+
${import_picocolors4.default.bold("ablo migrate")} is a separate, optional escape hatch \u2014 connecting a real database is this.`
|
|
280260
|
+
)
|
|
280261
|
+
);
|
|
280262
|
+
console.log();
|
|
280263
|
+
}
|
|
280264
|
+
function printCheckItem(item) {
|
|
280265
|
+
if (item.ok) {
|
|
280266
|
+
console.log(` ${import_picocolors4.default.green("\u2713")} ${item.label}`);
|
|
280267
|
+
} else {
|
|
280268
|
+
console.log(` ${import_picocolors4.default.red("\u2717")} ${item.label}`);
|
|
280269
|
+
if (item.fix) {
|
|
280270
|
+
for (const line of item.fix.split("\n")) console.log(` ${import_picocolors4.default.red("\u2022")} ${line}`);
|
|
280271
|
+
}
|
|
280272
|
+
}
|
|
280273
|
+
}
|
|
280274
|
+
async function probeReadiness(sql, opts = {}) {
|
|
280275
|
+
const publication = opts.publication ?? ABLO_PUBLICATION;
|
|
280276
|
+
const items = [];
|
|
280277
|
+
const walRows = await sql.unsafe(`SHOW wal_level`);
|
|
280278
|
+
const walLevel = walRows[0]?.setting ?? "unknown";
|
|
280279
|
+
items.push(
|
|
280280
|
+
walLevel === "logical" ? { ok: true, label: `wal_level is ${import_picocolors4.default.bold("logical")}` } : {
|
|
280281
|
+
ok: false,
|
|
280282
|
+
label: `wal_level is ${import_picocolors4.default.bold(walLevel)} (need ${import_picocolors4.default.bold("logical")})`,
|
|
280283
|
+
fix: `ALTER SYSTEM SET wal_level = 'logical'; then RESTART Postgres.
|
|
280284
|
+
On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.`
|
|
280285
|
+
}
|
|
280286
|
+
);
|
|
280287
|
+
const pubRows = await sql.unsafe(
|
|
280288
|
+
`SELECT puballtables FROM pg_publication WHERE pubname = $1`,
|
|
280289
|
+
[publication]
|
|
280290
|
+
);
|
|
280291
|
+
items.push(
|
|
280292
|
+
pubRows.length > 0 ? {
|
|
280293
|
+
ok: true,
|
|
280294
|
+
label: `publication ${import_picocolors4.default.bold(publication)} exists ${import_picocolors4.default.dim(pubRows[0].puballtables ? "(all tables)" : "(table subset)")}`
|
|
280295
|
+
} : {
|
|
280296
|
+
ok: false,
|
|
280297
|
+
label: `publication ${import_picocolors4.default.bold(publication)} not found`,
|
|
280298
|
+
fix: `CREATE PUBLICATION ${quoteIdent(publication)} FOR ALL TABLES;`
|
|
280299
|
+
}
|
|
280300
|
+
);
|
|
280301
|
+
const roleRows = await sql.unsafe(
|
|
280302
|
+
`SELECT rolreplication, rolsuper FROM pg_roles WHERE rolname = current_user`
|
|
280303
|
+
);
|
|
280304
|
+
const role = roleRows[0];
|
|
280305
|
+
const hasReplication = Boolean(role && (role.rolreplication || role.rolsuper));
|
|
280306
|
+
items.push(
|
|
280307
|
+
hasReplication ? { ok: true, label: `DATABASE_URL role can stream replication ${import_picocolors4.default.dim("(REPLICATION)")}` } : {
|
|
280308
|
+
ok: false,
|
|
280309
|
+
label: `DATABASE_URL role lacks the ${import_picocolors4.default.bold("REPLICATION")} attribute`,
|
|
280310
|
+
fix: `ALTER ROLE current_user WITH REPLICATION;
|
|
280311
|
+
On RDS: GRANT rds_replication TO <your_role>;`
|
|
280312
|
+
}
|
|
280313
|
+
);
|
|
280314
|
+
if (pubRows.length > 0) {
|
|
280315
|
+
const badRows = await sql.unsafe(
|
|
280316
|
+
`SELECT c.relname AS table_name, c.relreplident
|
|
280317
|
+
FROM pg_publication_tables pt
|
|
280318
|
+
JOIN pg_class c ON c.relname = pt.tablename
|
|
280319
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
|
|
280320
|
+
WHERE pt.pubname = $1
|
|
280321
|
+
AND (
|
|
280322
|
+
c.relreplident = 'n'
|
|
280323
|
+
OR (
|
|
280324
|
+
c.relreplident = 'd'
|
|
280325
|
+
AND NOT EXISTS (
|
|
280326
|
+
SELECT 1 FROM pg_index i
|
|
280327
|
+
WHERE i.indrelid = c.oid AND i.indisprimary
|
|
280328
|
+
)
|
|
280329
|
+
)
|
|
280330
|
+
)`,
|
|
280331
|
+
[publication]
|
|
280332
|
+
);
|
|
280333
|
+
items.push(
|
|
280334
|
+
badRows.length === 0 ? { ok: true, label: `all published tables have a usable REPLICA IDENTITY` } : {
|
|
280335
|
+
ok: false,
|
|
280336
|
+
label: `${badRows.length} published table${badRows.length === 1 ? "" : "s"} cannot replicate UPDATE/DELETE`,
|
|
280337
|
+
fix: badRows.map(
|
|
280338
|
+
(r2) => `${r2.table_name}: add a PRIMARY KEY, or ALTER TABLE ${quoteIdent(r2.table_name)} REPLICA IDENTITY FULL;`
|
|
280339
|
+
).join("\n")
|
|
280340
|
+
}
|
|
280341
|
+
);
|
|
280342
|
+
}
|
|
280343
|
+
return items;
|
|
280344
|
+
}
|
|
280345
|
+
async function runCheck() {
|
|
280346
|
+
const dbUrl = readProjectDatabaseUrl();
|
|
280347
|
+
if (!dbUrl) {
|
|
280348
|
+
console.error(
|
|
280349
|
+
import_picocolors4.default.red(" No DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors4.default.dim(` Set it to the Postgres you want Ablo to read, then re-run ${import_picocolors4.default.bold("ablo connect --check")}.`)
|
|
280350
|
+
);
|
|
280351
|
+
process.exit(1);
|
|
280352
|
+
}
|
|
280353
|
+
console.log(`
|
|
280354
|
+
${brand("ablo")} ${import_picocolors4.default.dim("connect --check")} ${import_picocolors4.default.dim("logical-replication readiness")}
|
|
280355
|
+
`);
|
|
280356
|
+
const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
|
|
280357
|
+
} });
|
|
280358
|
+
let items;
|
|
280359
|
+
try {
|
|
280360
|
+
items = await probeReadiness(sql);
|
|
280361
|
+
} catch (err) {
|
|
280362
|
+
const pg = err ?? {};
|
|
280363
|
+
console.error(import_picocolors4.default.red(` Couldn't read the database: ${pg.message ?? String(err)}`));
|
|
280364
|
+
await sql.end({ timeout: 2 });
|
|
280365
|
+
process.exit(1);
|
|
280366
|
+
}
|
|
280367
|
+
await sql.end({ timeout: 2 });
|
|
280368
|
+
for (const item of items) printCheckItem(item);
|
|
280369
|
+
const failures = items.filter((i) => !i.ok).length;
|
|
280370
|
+
console.log();
|
|
280371
|
+
if (failures === 0) {
|
|
280372
|
+
console.log(` ${import_picocolors4.default.green("\u2713")} Ready \u2014 Ablo can connect and tail this database's WAL.
|
|
280373
|
+
`);
|
|
280374
|
+
process.exit(0);
|
|
280375
|
+
}
|
|
280376
|
+
console.log(
|
|
280377
|
+
` ${import_picocolors4.default.red(`${failures} item${failures === 1 ? "" : "s"} to fix`)} ${import_picocolors4.default.dim(`\u2014 apply the fixes above, then re-run ${import_picocolors4.default.bold("ablo connect --check")}.`)}
|
|
280378
|
+
`
|
|
280379
|
+
);
|
|
280380
|
+
process.exit(1);
|
|
280381
|
+
}
|
|
280382
|
+
async function connect(argv) {
|
|
280383
|
+
let args;
|
|
280384
|
+
try {
|
|
280385
|
+
args = parseConnectArgs(argv);
|
|
280386
|
+
} catch (err) {
|
|
280387
|
+
console.error(import_picocolors4.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
280388
|
+
process.exit(1);
|
|
280389
|
+
}
|
|
280390
|
+
if (args.check) {
|
|
280391
|
+
await runCheck();
|
|
280392
|
+
return;
|
|
280393
|
+
}
|
|
280394
|
+
printConnectRecipe(args);
|
|
280395
|
+
}
|
|
280396
|
+
var CONNECT_USAGE = ` ablo connect \u2014 connect a real database to Ablo via logical replication (the one way)
|
|
280397
|
+
|
|
280398
|
+
Ablo READS your WAL and never runs DDL, owns, or migrates your schema. Your app
|
|
280399
|
+
keeps writing through your own backend.
|
|
280400
|
+
|
|
280401
|
+
Usage:
|
|
280402
|
+
npx ablo connect Print the exact setup SQL for your Postgres
|
|
280403
|
+
npx ablo connect --tables a,b,c Publish only these tables (default: all tables)
|
|
280404
|
+
npx ablo connect --role <name> Name the replication role (default: ablo_replicator)
|
|
280405
|
+
npx ablo connect --check Validate DATABASE_URL is replication-ready`;
|
|
280406
|
+
|
|
280256
280407
|
// src/cli/generate.ts
|
|
280257
280408
|
init_cjs_shims();
|
|
280258
280409
|
var import_fs6 = require("fs");
|
|
@@ -280313,21 +280464,6 @@ var import_picocolors6 = __toESM(require_picocolors(), 1);
|
|
|
280313
280464
|
var import_fs7 = require("fs");
|
|
280314
280465
|
var import_path5 = require("path");
|
|
280315
280466
|
var import_schema5 = require("@abloatai/ablo/schema");
|
|
280316
|
-
|
|
280317
|
-
// src/cli/theme.ts
|
|
280318
|
-
init_cjs_shims();
|
|
280319
|
-
var RESET = "\x1B[0m";
|
|
280320
|
-
var PAPER_BG = "\x1B[48;2;250;250;250m";
|
|
280321
|
-
var BLACK_FG = "\x1B[38;2;0;0;0m";
|
|
280322
|
-
function colorEnabled() {
|
|
280323
|
-
return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
280324
|
-
}
|
|
280325
|
-
function brand(label = "ablo") {
|
|
280326
|
-
if (!colorEnabled()) return label;
|
|
280327
|
-
return `${PAPER_BG}${BLACK_FG} ${label} ${RESET}`;
|
|
280328
|
-
}
|
|
280329
|
-
|
|
280330
|
-
// src/cli/dev.ts
|
|
280331
280467
|
function parseDevArgs(argv) {
|
|
280332
280468
|
let schemaPath = DEFAULT_SCHEMA_PATH;
|
|
280333
280469
|
let exportName = DEFAULT_EXPORT;
|
|
@@ -280437,11 +280573,27 @@ async function runPush(schema, args) {
|
|
|
280437
280573
|
if (status2 === 409) {
|
|
280438
280574
|
const unexecutable = Array.isArray(body.unexecutable) ? body.unexecutable : [];
|
|
280439
280575
|
const warnings = Array.isArray(body.warnings) ? body.warnings : [];
|
|
280576
|
+
const hasShadowed = [...unexecutable, ...warnings].some(
|
|
280577
|
+
(s) => s.shadowed != null
|
|
280578
|
+
);
|
|
280440
280579
|
const lines = [
|
|
280441
|
-
"Incompatible schema change \u2014 not safe to apply as-is.",
|
|
280580
|
+
import_picocolors6.default.bold("Incompatible schema change \u2014 not safe to apply as-is."),
|
|
280581
|
+
"",
|
|
280442
280582
|
...unexecutable.map((u2) => import_picocolors6.default.red(fmtSignal(u2))),
|
|
280443
280583
|
...warnings.map((w2) => import_picocolors6.default.yellow(fmtSignal(w2))),
|
|
280444
|
-
|
|
280584
|
+
"",
|
|
280585
|
+
...hasShadowed ? [
|
|
280586
|
+
import_picocolors6.default.dim(
|
|
280587
|
+
" These models exist in the baseline above but not in your push. Sandbox readers"
|
|
280588
|
+
),
|
|
280589
|
+
import_picocolors6.default.dim(
|
|
280590
|
+
" fall back to the production schema until you push your own, so applying this drops them."
|
|
280591
|
+
),
|
|
280592
|
+
""
|
|
280593
|
+
] : [],
|
|
280594
|
+
import_picocolors6.default.dim(
|
|
280595
|
+
` Fix: ${import_picocolors6.default.bold("ablo push --force")} to apply anyway, or ${import_picocolors6.default.bold("--rename old:new")} if you renamed a model.`
|
|
280596
|
+
)
|
|
280445
280597
|
];
|
|
280446
280598
|
return { ok: false, message: lines.join("\n") };
|
|
280447
280599
|
}
|
|
@@ -280473,8 +280625,6 @@ async function dev(argv) {
|
|
|
280473
280625
|
console.log(`
|
|
280474
280626
|
${brand("ablo")} ${import_picocolors6.default.dim("push")} ${import_picocolors6.default.dim("(sandbox)")}
|
|
280475
280627
|
`);
|
|
280476
|
-
const projectDbUrl = readProjectDatabaseUrl();
|
|
280477
|
-
if (projectDbUrl) await ensureScopedRoleInteractive(projectDbUrl);
|
|
280478
280628
|
const schema = await loadSchema(args.schemaPath, args.exportName);
|
|
280479
280629
|
const modelCount = Object.keys(schema.models).length;
|
|
280480
280630
|
console.log(
|
|
@@ -282355,6 +282505,7 @@ var LOGO = `
|
|
|
282355
282505
|
${brand("ablo")} ${import_picocolors18.default.dim("sync engine")}
|
|
282356
282506
|
`;
|
|
282357
282507
|
var SUBCOMMAND_USAGE = {
|
|
282508
|
+
connect: CONNECT_USAGE,
|
|
282358
282509
|
migrate: MIGRATE_USAGE
|
|
282359
282510
|
};
|
|
282360
282511
|
async function main() {
|
|
@@ -282402,6 +282553,8 @@ async function main() {
|
|
|
282402
282553
|
} else {
|
|
282403
282554
|
await pull(rest);
|
|
282404
282555
|
}
|
|
282556
|
+
} else if (command === "connect") {
|
|
282557
|
+
await connect(process.argv.slice(3));
|
|
282405
282558
|
} else if (command === "migrate") {
|
|
282406
282559
|
await migrate(process.argv.slice(3));
|
|
282407
282560
|
} else if (command === "push") {
|
|
@@ -282465,7 +282618,9 @@ async function main() {
|
|
|
282465
282618
|
console.log(` npx ablo pull prisma [path] Generate schema.ts from a Prisma schema (keeps enums + relations)`);
|
|
282466
282619
|
console.log(` npx ablo pull drizzle <module> Generate schema.ts from a Drizzle schema (keeps enums + relations)`);
|
|
282467
282620
|
console.log(` npx ablo check Check your existing database fits the schema (read-only, creates no tables)`);
|
|
282468
|
-
console.log(` npx ablo
|
|
282621
|
+
console.log(` npx ablo connect Connect a real database \u2014 prints the logical-replication setup SQL (the one way)`);
|
|
282622
|
+
console.log(` npx ablo connect --check Validate DATABASE_URL is replication-ready (wal_level, publication, role, replica identity)`);
|
|
282623
|
+
console.log(` npx ablo migrate Provision your synced-model tables in your own Postgres (optional escape hatch \u2014 \`connect\` is the way)`);
|
|
282469
282624
|
console.log(` npx ablo migrate --dry-run Print the SQL without executing (preview)`);
|
|
282470
282625
|
console.log(` npx ablo push Upload your schema definition to Ablo (metadata only \u2014 rows stay in your DB)`);
|
|
282471
282626
|
console.log(` npx ablo push --force Allow destructive/unexecutable changes`);
|