@abloatai/ablo 0.16.3 → 0.18.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 +67 -0
- package/dist/BaseSyncedStore.d.ts +8 -5
- package/dist/BaseSyncedStore.js +65 -18
- package/dist/NetworkMonitor.js +3 -1
- package/dist/cli.cjs +572 -256
- package/dist/client/Ablo.d.ts +33 -1
- package/dist/client/Ablo.js +42 -12
- package/dist/client/auth.js +1 -1
- package/dist/core/StoreManager.js +3 -1
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/policy/types.d.ts +24 -9
- package/dist/policy/types.js +32 -11
- package/dist/schema/diff.d.ts +14 -0
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +2 -0
- package/dist/sync/SyncWebSocket.js +1 -1
- package/dist/sync/awaitClaimGrant.js +9 -2
- package/dist/sync/createClaimStream.js +32 -0
- package/dist/wire/errorEnvelope.d.ts +13 -0
- package/docs/data-sources.md +164 -287
- package/docs/quickstart.md +51 -63
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -276700,7 +276700,7 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
|
|
|
276700
276700
|
};
|
|
276701
276701
|
|
|
276702
276702
|
// src/cli/index.ts
|
|
276703
|
-
var
|
|
276703
|
+
var import_picocolors19 = __toESM(require_picocolors(), 1);
|
|
276704
276704
|
var import_fs12 = require("fs");
|
|
276705
276705
|
var import_path7 = require("path");
|
|
276706
276706
|
var import_child_process2 = require("child_process");
|
|
@@ -277009,6 +277009,18 @@ var ERROR_CODES = {
|
|
|
277009
277009
|
invalid_schema: wire("validation", 400, false, "The submitted schema could not be parsed."),
|
|
277010
277010
|
incompatible_change: wire("conflict", 409, false, "The schema change is incompatible with the current schema.")
|
|
277011
277011
|
};
|
|
277012
|
+
function errorCodeSpec(code) {
|
|
277013
|
+
return ERROR_CODES[code];
|
|
277014
|
+
}
|
|
277015
|
+
function classifyRecovery(code) {
|
|
277016
|
+
const spec = errorCodeSpec(code);
|
|
277017
|
+
if (!spec) return "none";
|
|
277018
|
+
if (spec.recovery) return spec.recovery;
|
|
277019
|
+
if (spec.retryable) return "transient";
|
|
277020
|
+
if (spec.httpStatus === 403) return "permission";
|
|
277021
|
+
if (spec.category === "auth") return "auth_blocked";
|
|
277022
|
+
return "none";
|
|
277023
|
+
}
|
|
277012
277024
|
|
|
277013
277025
|
// src/coordination/schema.ts
|
|
277014
277026
|
init_cjs_shims();
|
|
@@ -277331,6 +277343,20 @@ var AbloError = class extends Error {
|
|
|
277331
277343
|
...this.details ?? {}
|
|
277332
277344
|
};
|
|
277333
277345
|
}
|
|
277346
|
+
/**
|
|
277347
|
+
* A single, leak-proof line for logs and `String(err)` / template
|
|
277348
|
+
* interpolation: `AbloValidationError [code]: message (see docs) [request_id: …]`.
|
|
277349
|
+
*
|
|
277350
|
+
* Deliberately does NOT dump `details`, `cause`, or the stack — the thing that
|
|
277351
|
+
* makes `console.error(richError)` an unreadable wall of text. The structured
|
|
277352
|
+
* payload stays available via {@link toJSON}; this is the human one-liner.
|
|
277353
|
+
*/
|
|
277354
|
+
toString() {
|
|
277355
|
+
const code = this.code ? ` [${this.code}]` : "";
|
|
277356
|
+
const docs = this.docUrl ? ` (see ${this.docUrl})` : "";
|
|
277357
|
+
const req = this.requestId ? ` [request_id: ${this.requestId}]` : "";
|
|
277358
|
+
return `${this.name}${code}: ${this.message}${docs}${req}`;
|
|
277359
|
+
}
|
|
277334
277360
|
};
|
|
277335
277361
|
function docUrlForCode(code) {
|
|
277336
277362
|
return `https://docs.abloatai.com/errors#${code}`;
|
|
@@ -277378,7 +277404,7 @@ var ErrorBodyShapeSchema = import_zod4.z.object({
|
|
|
277378
277404
|
}).passthrough();
|
|
277379
277405
|
|
|
277380
277406
|
// src/cli/migrate.ts
|
|
277381
|
-
var
|
|
277407
|
+
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
277382
277408
|
var import_fs5 = require("fs");
|
|
277383
277409
|
|
|
277384
277410
|
// node_modules/postgres/src/index.js
|
|
@@ -278098,7 +278124,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
278098
278124
|
async function cancel({ pid, secret }, resolve6, reject) {
|
|
278099
278125
|
try {
|
|
278100
278126
|
cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
|
|
278101
|
-
await
|
|
278127
|
+
await connect2();
|
|
278102
278128
|
socket.once("error", reject);
|
|
278103
278129
|
socket.once("close", resolve6);
|
|
278104
278130
|
} catch (error2) {
|
|
@@ -278231,7 +278257,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
278231
278257
|
incomings = null;
|
|
278232
278258
|
}
|
|
278233
278259
|
}
|
|
278234
|
-
async function
|
|
278260
|
+
async function connect2() {
|
|
278235
278261
|
terminated = false;
|
|
278236
278262
|
backendParameters = {};
|
|
278237
278263
|
socket || (socket = await createSocket());
|
|
@@ -278250,7 +278276,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
278250
278276
|
hostIndex = (hostIndex + 1) % port.length;
|
|
278251
278277
|
}
|
|
278252
278278
|
function reconnect() {
|
|
278253
|
-
setTimeout(
|
|
278279
|
+
setTimeout(connect2, closedTime ? Math.max(0, closedTime + delay - import_perf_hooks.performance.now()) : 0);
|
|
278254
278280
|
}
|
|
278255
278281
|
function connected() {
|
|
278256
278282
|
try {
|
|
@@ -279252,7 +279278,7 @@ function Postgres(a, b4) {
|
|
|
279252
279278
|
const c = open.length ? open.shift() : await new Promise((resolve6, reject) => {
|
|
279253
279279
|
const query = { reserve: resolve6, reject };
|
|
279254
279280
|
queries.push(query);
|
|
279255
|
-
closed.length &&
|
|
279281
|
+
closed.length && connect2(closed.shift(), query);
|
|
279256
279282
|
});
|
|
279257
279283
|
move(c, reserved);
|
|
279258
279284
|
c.reserved = () => queue.length ? c.execute(queue.shift()) : move(c, reserved);
|
|
@@ -279339,7 +279365,7 @@ function Postgres(a, b4) {
|
|
|
279339
279365
|
if (open.length)
|
|
279340
279366
|
return go(open.shift(), query);
|
|
279341
279367
|
if (closed.length)
|
|
279342
|
-
return
|
|
279368
|
+
return connect2(closed.shift(), query);
|
|
279343
279369
|
busy.length ? go(busy.shift(), query) : queries.push(query);
|
|
279344
279370
|
}
|
|
279345
279371
|
function go(c, query) {
|
|
@@ -279372,7 +279398,7 @@ function Postgres(a, b4) {
|
|
|
279372
279398
|
queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
|
|
279373
279399
|
resolve6();
|
|
279374
279400
|
}
|
|
279375
|
-
function
|
|
279401
|
+
function connect2(c, query) {
|
|
279376
279402
|
move(c, connecting);
|
|
279377
279403
|
c.connect(query);
|
|
279378
279404
|
return c;
|
|
@@ -279397,7 +279423,7 @@ function Postgres(a, b4) {
|
|
|
279397
279423
|
c.reserved = null;
|
|
279398
279424
|
c.onclose && (c.onclose(e2), c.onclose = null);
|
|
279399
279425
|
options.onclose && options.onclose(c.id);
|
|
279400
|
-
queries.length &&
|
|
279426
|
+
queries.length && connect2(c, queries.shift());
|
|
279401
279427
|
}
|
|
279402
279428
|
}
|
|
279403
279429
|
function parseOptions(a, b4) {
|
|
@@ -279517,158 +279543,8 @@ function osUsername() {
|
|
|
279517
279543
|
|
|
279518
279544
|
// src/cli/dbRole.ts
|
|
279519
279545
|
init_cjs_shims();
|
|
279520
|
-
var import_crypto2 = require("crypto");
|
|
279521
|
-
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
279522
279546
|
var import_fs2 = require("fs");
|
|
279523
279547
|
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
279548
|
function readProjectDatabaseUrl(cwd = process.cwd()) {
|
|
279673
279549
|
const fromEnv = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
|
|
279674
279550
|
if (fromEnv) return fromEnv;
|
|
@@ -279680,6 +279556,16 @@ function readProjectDatabaseUrl(cwd = process.cwd()) {
|
|
|
279680
279556
|
}
|
|
279681
279557
|
return null;
|
|
279682
279558
|
}
|
|
279559
|
+
function readProjectApiKey(cwd = process.cwd()) {
|
|
279560
|
+
if (process.env.ABLO_API_KEY) return { key: process.env.ABLO_API_KEY, source: "env" };
|
|
279561
|
+
for (const name of [".env.local", ".env"]) {
|
|
279562
|
+
const path = (0, import_path.resolve)(cwd, name);
|
|
279563
|
+
if (!(0, import_fs2.existsSync)(path)) continue;
|
|
279564
|
+
const match = (0, import_fs2.readFileSync)(path, "utf8").match(/^ABLO_API_KEY=(.+)$/m);
|
|
279565
|
+
if (match?.[1]) return { key: match[1].trim().replace(/^["']|["']$/g, ""), source: name };
|
|
279566
|
+
}
|
|
279567
|
+
return null;
|
|
279568
|
+
}
|
|
279683
279569
|
|
|
279684
279570
|
// src/cli/migrate.ts
|
|
279685
279571
|
var import_schema3 = require("@abloatai/ablo/schema");
|
|
@@ -279687,7 +279573,7 @@ var import_source = require("@abloatai/ablo/source");
|
|
|
279687
279573
|
|
|
279688
279574
|
// src/cli/push.ts
|
|
279689
279575
|
init_cjs_shims();
|
|
279690
|
-
var
|
|
279576
|
+
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
279691
279577
|
|
|
279692
279578
|
// src/auth/credentialPolicy.ts
|
|
279693
279579
|
init_cjs_shims();
|
|
@@ -279929,7 +279815,16 @@ var DEFAULT_URL = "https://api.abloatai.com";
|
|
|
279929
279815
|
function fmtSignal(s) {
|
|
279930
279816
|
const sig = s;
|
|
279931
279817
|
const where = sig.field ? `${sig.model}.${sig.field}` : sig.model;
|
|
279932
|
-
|
|
279818
|
+
let line = ` \u2022 ${import_picocolors2.default.bold(where ?? "?")} \u2014 ${sig.detail ?? ""}`;
|
|
279819
|
+
if (sig.shadowed) {
|
|
279820
|
+
const env = sig.shadowed.environment ?? "production";
|
|
279821
|
+
const ver = sig.shadowed.version != null ? `v${sig.shadowed.version}` : "active";
|
|
279822
|
+
const when = sig.shadowed.pushedAt ? new Date(sig.shadowed.pushedAt).toISOString().slice(0, 10) : "unknown date";
|
|
279823
|
+
const by = sig.shadowed.pushedBy ? ` by ${sig.shadowed.pushedBy}` : "";
|
|
279824
|
+
line += `
|
|
279825
|
+
${import_picocolors2.default.dim(`\u21B3 baseline: ${env} ${ver}, pushed ${when}${by}`)}`;
|
|
279826
|
+
}
|
|
279827
|
+
return line;
|
|
279933
279828
|
}
|
|
279934
279829
|
async function pushSchema(schema, args) {
|
|
279935
279830
|
const schemaJson = JSON.parse((0, import_schema2.serializeSchema)(schema));
|
|
@@ -280010,7 +279905,7 @@ async function loadSchema(schemaPath, exportName) {
|
|
|
280010
279905
|
const abs = (0, import_path3.resolve)(process.cwd(), schemaPath);
|
|
280011
279906
|
if (!(0, import_fs4.existsSync)(abs)) {
|
|
280012
279907
|
throw new AbloValidationError(
|
|
280013
|
-
`schema not found at ${
|
|
279908
|
+
`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
279909
|
{ code: "cli_invalid_arguments" }
|
|
280015
279910
|
);
|
|
280016
279911
|
}
|
|
@@ -280021,25 +279916,50 @@ async function loadSchema(schemaPath, exportName) {
|
|
|
280021
279916
|
const schema = mod[exportName] ?? nested?.[exportName];
|
|
280022
279917
|
if (!schema || typeof schema !== "object" || !("models" in schema)) {
|
|
280023
279918
|
throw new AbloValidationError(
|
|
280024
|
-
`${
|
|
279919
|
+
`${import_picocolors2.default.bold(schemaPath)} has no \`${exportName}\` export that looks like a Schema. Did you \`export const ${exportName} = defineSchema({ ... })\`?`,
|
|
280025
279920
|
{ code: "cli_invalid_arguments" }
|
|
280026
279921
|
);
|
|
280027
279922
|
}
|
|
280028
279923
|
return schema;
|
|
280029
279924
|
}
|
|
279925
|
+
function maskKey(key) {
|
|
279926
|
+
return key ? `${key.slice(0, 12)}\u2026` : "(none)";
|
|
279927
|
+
}
|
|
279928
|
+
function describeKeySource(source) {
|
|
279929
|
+
switch (source) {
|
|
279930
|
+
case "env":
|
|
279931
|
+
return "ABLO_API_KEY (environment)";
|
|
279932
|
+
case ".env.local":
|
|
279933
|
+
return ".env.local";
|
|
279934
|
+
case ".env":
|
|
279935
|
+
return ".env";
|
|
279936
|
+
case "login":
|
|
279937
|
+
return "`ablo login` (stored sandbox config)";
|
|
279938
|
+
}
|
|
279939
|
+
}
|
|
280030
279940
|
async function push(argv) {
|
|
280031
279941
|
let args;
|
|
280032
279942
|
try {
|
|
280033
279943
|
args = parsePushArgs(argv);
|
|
280034
279944
|
} catch (err) {
|
|
280035
|
-
console.error(
|
|
279945
|
+
console.error(import_picocolors2.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
280036
279946
|
process.exit(1);
|
|
280037
279947
|
}
|
|
280038
|
-
|
|
279948
|
+
let keySource = "env";
|
|
279949
|
+
if (!args.apiKey) {
|
|
279950
|
+
const fromProject = readProjectApiKey();
|
|
279951
|
+
if (fromProject) {
|
|
279952
|
+
args.apiKey = fromProject.key;
|
|
279953
|
+
keySource = fromProject.source;
|
|
279954
|
+
} else {
|
|
279955
|
+
args.apiKey = resolveApiKey();
|
|
279956
|
+
keySource = "login";
|
|
279957
|
+
}
|
|
279958
|
+
}
|
|
280039
279959
|
if (!args.apiKey) {
|
|
280040
279960
|
console.error(
|
|
280041
|
-
|
|
280042
|
-
` Run ${
|
|
279961
|
+
import_picocolors2.default.red(` No API key.`) + import_picocolors2.default.dim(
|
|
279962
|
+
` 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
279963
|
)
|
|
280044
279964
|
);
|
|
280045
279965
|
process.exit(1);
|
|
@@ -280047,17 +279967,17 @@ async function push(argv) {
|
|
|
280047
279967
|
const schema = await loadSchema(args.schemaPath, args.exportName);
|
|
280048
279968
|
const hash = (0, import_schema2.schemaHash)(schema);
|
|
280049
279969
|
console.log(
|
|
280050
|
-
` Pushing ${
|
|
279970
|
+
` 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
279971
|
);
|
|
280052
279972
|
const { ok: resOk, status: status2, body, bodyText } = await pushSchema(schema, args);
|
|
280053
279973
|
if (resOk) {
|
|
280054
279974
|
if (body.unchanged) {
|
|
280055
|
-
console.log(` ${
|
|
279975
|
+
console.log(` ${import_picocolors2.default.dim("\u25CB")} No changes \u2014 schema already active (v${body.version}).`);
|
|
280056
279976
|
} else {
|
|
280057
|
-
console.log(` ${
|
|
279977
|
+
console.log(` ${import_picocolors2.default.green("\u2713")} Activated ${import_picocolors2.default.bold(`v${body.version}`)} ${import_picocolors2.default.dim(`(hash ${body.hash})`)}`);
|
|
280058
279978
|
if (Array.isArray(body.warnings) && body.warnings.length > 0) {
|
|
280059
|
-
console.log(
|
|
280060
|
-
for (const w2 of body.warnings) console.log(
|
|
279979
|
+
console.log(import_picocolors2.default.yellow(` Applied ${body.warnings.length} destructive change(s):`));
|
|
279980
|
+
for (const w2 of body.warnings) console.log(import_picocolors2.default.yellow(fmtSignal(w2)));
|
|
280061
279981
|
}
|
|
280062
279982
|
}
|
|
280063
279983
|
return;
|
|
@@ -280065,27 +279985,63 @@ async function push(argv) {
|
|
|
280065
279985
|
if (status2 === 409) {
|
|
280066
279986
|
const unexecutable = Array.isArray(body.unexecutable) ? body.unexecutable : [];
|
|
280067
279987
|
const warnings = Array.isArray(body.warnings) ? body.warnings : [];
|
|
280068
|
-
console.error(
|
|
279988
|
+
console.error(import_picocolors2.default.red(" Incompatible change \u2014 this push is not safe to apply as-is."));
|
|
280069
279989
|
if (unexecutable.length > 0) {
|
|
280070
|
-
console.error(
|
|
280071
|
-
for (const u2 of unexecutable) console.error(
|
|
279990
|
+
console.error(import_picocolors2.default.red(` Unexecutable (would fail on existing rows):`));
|
|
279991
|
+
for (const u2 of unexecutable) console.error(import_picocolors2.default.red(fmtSignal(u2)));
|
|
280072
279992
|
}
|
|
280073
279993
|
if (warnings.length > 0) {
|
|
280074
|
-
console.error(
|
|
280075
|
-
for (const w2 of warnings) console.error(
|
|
279994
|
+
console.error(import_picocolors2.default.yellow(` Destructive (data loss):`));
|
|
279995
|
+
for (const w2 of warnings) console.error(import_picocolors2.default.yellow(fmtSignal(w2)));
|
|
279996
|
+
}
|
|
279997
|
+
const hasShadowed = [...unexecutable, ...warnings].some(
|
|
279998
|
+
(s) => s.shadowed != null
|
|
279999
|
+
);
|
|
280000
|
+
if (hasShadowed) {
|
|
280001
|
+
console.error(
|
|
280002
|
+
import_picocolors2.default.dim(
|
|
280003
|
+
" These models exist in the baseline above but not in your push. Sandbox readers fall"
|
|
280004
|
+
)
|
|
280005
|
+
);
|
|
280006
|
+
console.error(
|
|
280007
|
+
import_picocolors2.default.dim(
|
|
280008
|
+
" back to the production schema until you push your own, so applying this drops them."
|
|
280009
|
+
)
|
|
280010
|
+
);
|
|
280076
280011
|
}
|
|
280077
|
-
console.error(
|
|
280012
|
+
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
280013
|
} else if (status2 === 403) {
|
|
280079
|
-
|
|
280080
|
-
|
|
280014
|
+
const code = body.code ?? body.reason;
|
|
280015
|
+
const serverMsg = body.message ?? body.reason;
|
|
280016
|
+
console.error(import_picocolors2.default.red(` Forbidden${code ? ` [${code}]` : ""}: ${serverMsg ?? "permission denied"}`));
|
|
280017
|
+
console.error(import_picocolors2.default.dim(` Push used ${import_picocolors2.default.bold(maskKey(args.apiKey))} from ${describeKeySource(keySource)}.`));
|
|
280018
|
+
if (code === "database_role_cannot_enforce_rls") {
|
|
280019
|
+
console.error(
|
|
280020
|
+
import_picocolors2.default.dim(
|
|
280021
|
+
` 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.`
|
|
280022
|
+
)
|
|
280023
|
+
);
|
|
280024
|
+
} else if (code === "database_tables_unforced_rls") {
|
|
280025
|
+
console.error(
|
|
280026
|
+
import_picocolors2.default.dim(
|
|
280027
|
+
` 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.`
|
|
280028
|
+
)
|
|
280029
|
+
);
|
|
280030
|
+
} else if (args.apiKey != null && classifyCredentialKind(args.apiKey) === "restricted") {
|
|
280031
|
+
console.error(
|
|
280032
|
+
import_picocolors2.default.dim(
|
|
280033
|
+
` 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")}).`
|
|
280034
|
+
)
|
|
280035
|
+
);
|
|
280036
|
+
} else {
|
|
280081
280037
|
console.error(
|
|
280082
|
-
|
|
280083
|
-
`
|
|
280038
|
+
import_picocolors2.default.dim(
|
|
280039
|
+
` This key isn't authorized to push schema (needs ${import_picocolors2.default.bold("schema:push")}). ` + (keySource === "login" ? `It's your stored ${import_picocolors2.default.bold("ablo login")} sandbox key \u2014 a key in ${import_picocolors2.default.bold(".env.local")} or ${import_picocolors2.default.bold("ABLO_API_KEY")} takes precedence, so put a schema:push key there (sandbox ${import_picocolors2.default.bold("sk_test_")} or production ${import_picocolors2.default.bold("sk_live_")}) and re-push. ` : `Use a schema:push key \u2014 a sandbox ${import_picocolors2.default.bold("sk_test_")} or production ${import_picocolors2.default.bold("sk_live_")}. `) + `Manage keys at https://abloatai.com`
|
|
280084
280040
|
)
|
|
280085
280041
|
);
|
|
280086
280042
|
}
|
|
280087
280043
|
} else {
|
|
280088
|
-
console.error(
|
|
280044
|
+
console.error(import_picocolors2.default.red(` Push failed (${status2}): ${body.message ?? body.reason ?? bodyText}`));
|
|
280089
280045
|
}
|
|
280090
280046
|
process.exit(1);
|
|
280091
280047
|
}
|
|
@@ -280093,6 +280049,10 @@ async function push(argv) {
|
|
|
280093
280049
|
// src/cli/migrate.ts
|
|
280094
280050
|
var MIGRATE_USAGE = ` ablo migrate \u2014 provision your schema's tables in your own Postgres (DATABASE_URL)
|
|
280095
280051
|
|
|
280052
|
+
To connect a real database, run \`ablo connect\` \u2014 Ablo reads your WAL via logical
|
|
280053
|
+
replication, the one way. \`migrate\` is the optional escape hatch for provisioning
|
|
280054
|
+
tables when you don't already have them.
|
|
280055
|
+
|
|
280096
280056
|
Usage:
|
|
280097
280057
|
npx ablo migrate Create the synced-model tables (with row-level security)
|
|
280098
280058
|
npx ablo migrate --dry-run Print the SQL without executing it
|
|
@@ -280141,8 +280101,8 @@ function planFor(schema, targetSchema = "public") {
|
|
|
280141
280101
|
}
|
|
280142
280102
|
var log = {
|
|
280143
280103
|
info: (msg, fields) => console.log(`[migrate] ${msg}`, fields),
|
|
280144
|
-
warn: (msg, fields) => console.warn(
|
|
280145
|
-
error: (msg, fields) => console.error(
|
|
280104
|
+
warn: (msg, fields) => console.warn(import_picocolors3.default.yellow(`[migrate] ${msg}`), fields),
|
|
280105
|
+
error: (msg, fields) => console.error(import_picocolors3.default.red(`[migrate] ${msg}`), fields)
|
|
280146
280106
|
};
|
|
280147
280107
|
var PG_LOCK_NOT_AVAILABLE = "55P03";
|
|
280148
280108
|
var LOCK_TIMEOUT = process.env.ABLO_SCHEMA_LOCK_TIMEOUT ?? process.env.ABLO_DDL_LOCK_TIMEOUT ?? "5s";
|
|
@@ -280217,7 +280177,7 @@ async function migrate(argv) {
|
|
|
280217
280177
|
try {
|
|
280218
280178
|
args = parseMigrateArgs(argv);
|
|
280219
280179
|
} catch (err) {
|
|
280220
|
-
console.error(
|
|
280180
|
+
console.error(import_picocolors3.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
280221
280181
|
process.exit(1);
|
|
280222
280182
|
}
|
|
280223
280183
|
const schema = await loadSchema(args.schemaPath, args.exportName);
|
|
@@ -280228,31 +280188,290 @@ async function migrate(argv) {
|
|
|
280228
280188
|
].join("\n");
|
|
280229
280189
|
const totalStatements = plan.statements.length + plan.concurrent.length;
|
|
280230
280190
|
console.log(
|
|
280231
|
-
` ${
|
|
280191
|
+
` ${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
280192
|
);
|
|
280233
280193
|
if (args.outputFile) {
|
|
280234
280194
|
(0, import_fs5.writeFileSync)(args.outputFile, sql + "\n");
|
|
280235
|
-
console.log(` ${
|
|
280195
|
+
console.log(` ${import_picocolors3.default.green("\u2713")} SQL written to ${import_picocolors3.default.bold(args.outputFile)}`);
|
|
280236
280196
|
return;
|
|
280237
280197
|
}
|
|
280238
280198
|
if (args.dryRun) {
|
|
280239
280199
|
console.log("\n" + sql + "\n");
|
|
280240
280200
|
return;
|
|
280241
280201
|
}
|
|
280242
|
-
const dbUrl =
|
|
280202
|
+
const dbUrl = readProjectDatabaseUrl();
|
|
280243
280203
|
if (!dbUrl) {
|
|
280244
|
-
console.error(
|
|
280204
|
+
console.error(
|
|
280205
|
+
import_picocolors3.default.red(
|
|
280206
|
+
" No DATABASE_URL found (checked process env, .env.local, .env). Set it to apply, or use --dry-run to preview."
|
|
280207
|
+
)
|
|
280208
|
+
);
|
|
280245
280209
|
process.exit(1);
|
|
280246
280210
|
}
|
|
280247
|
-
const effectiveUrl = await ensureScopedRoleInteractive(dbUrl);
|
|
280248
280211
|
try {
|
|
280249
|
-
await applyStatements(
|
|
280250
|
-
console.log(` ${
|
|
280212
|
+
await applyStatements(dbUrl, args.targetSchema, plan.statements, plan.concurrent);
|
|
280213
|
+
console.log(` ${import_picocolors3.default.green("\u2713")} Migration complete`);
|
|
280251
280214
|
} catch {
|
|
280252
280215
|
process.exit(1);
|
|
280253
280216
|
}
|
|
280254
280217
|
}
|
|
280255
280218
|
|
|
280219
|
+
// src/cli/connect.ts
|
|
280220
|
+
init_cjs_shims();
|
|
280221
|
+
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
280222
|
+
|
|
280223
|
+
// src/cli/theme.ts
|
|
280224
|
+
init_cjs_shims();
|
|
280225
|
+
var RESET = "\x1B[0m";
|
|
280226
|
+
var PAPER_BG = "\x1B[48;2;250;250;250m";
|
|
280227
|
+
var BLACK_FG = "\x1B[38;2;0;0;0m";
|
|
280228
|
+
function colorEnabled() {
|
|
280229
|
+
return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
280230
|
+
}
|
|
280231
|
+
function brand(label = "ablo") {
|
|
280232
|
+
if (!colorEnabled()) return label;
|
|
280233
|
+
return `${PAPER_BG}${BLACK_FG} ${label} ${RESET}`;
|
|
280234
|
+
}
|
|
280235
|
+
|
|
280236
|
+
// src/cli/connect.ts
|
|
280237
|
+
var ABLO_PUBLICATION = "ablo_publication";
|
|
280238
|
+
var ABLO_REPLICATION_ROLE = "ablo_replicator";
|
|
280239
|
+
function parseConnectArgs(argv) {
|
|
280240
|
+
let check2 = false;
|
|
280241
|
+
let tables = [];
|
|
280242
|
+
let role = ABLO_REPLICATION_ROLE;
|
|
280243
|
+
for (let i = 0; i < argv.length; i++) {
|
|
280244
|
+
const arg = argv[i];
|
|
280245
|
+
switch (arg) {
|
|
280246
|
+
case "--check":
|
|
280247
|
+
check2 = true;
|
|
280248
|
+
break;
|
|
280249
|
+
case "--tables": {
|
|
280250
|
+
const value = argv[++i] ?? "";
|
|
280251
|
+
tables = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
280252
|
+
break;
|
|
280253
|
+
}
|
|
280254
|
+
case "--role":
|
|
280255
|
+
role = argv[++i] ?? role;
|
|
280256
|
+
break;
|
|
280257
|
+
default:
|
|
280258
|
+
throw new AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
|
|
280259
|
+
}
|
|
280260
|
+
}
|
|
280261
|
+
return { check: check2, tables, role };
|
|
280262
|
+
}
|
|
280263
|
+
function quoteIdent(id) {
|
|
280264
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
280265
|
+
}
|
|
280266
|
+
function connectSetupSql(input) {
|
|
280267
|
+
const role = input.role && input.role.length > 0 ? input.role : ABLO_REPLICATION_ROLE;
|
|
280268
|
+
const tables = input.tables ?? [];
|
|
280269
|
+
const publicationTarget = tables.length > 0 ? `FOR TABLE ${tables.map(quoteIdent).join(", ")}` : "FOR ALL TABLES";
|
|
280270
|
+
return [
|
|
280271
|
+
// 1. Turn on logical decoding. Requires a restart (it's not reloadable).
|
|
280272
|
+
`ALTER SYSTEM SET wal_level = 'logical';`,
|
|
280273
|
+
// 2. Publish the tables Ablo should read.
|
|
280274
|
+
`CREATE PUBLICATION ${quoteIdent(ABLO_PUBLICATION)} ${publicationTarget};`,
|
|
280275
|
+
// 3. A least-privilege role: it can stream replication and SELECT, nothing more.
|
|
280276
|
+
`CREATE ROLE ${quoteIdent(role)} WITH REPLICATION LOGIN PASSWORD '<password>';`,
|
|
280277
|
+
`GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${quoteIdent(role)};`,
|
|
280278
|
+
// Future tables get SELECT automatically, so the publication doesn't outgrow the grant.
|
|
280279
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${quoteIdent(role)};`
|
|
280280
|
+
];
|
|
280281
|
+
}
|
|
280282
|
+
function printConnectRecipe(args) {
|
|
280283
|
+
const sql = connectSetupSql({ tables: args.tables, role: args.role });
|
|
280284
|
+
console.log(`
|
|
280285
|
+
${brand("ablo")} ${import_picocolors4.default.dim("connect")} ${import_picocolors4.default.dim("logical replication \u2014 the one way to connect a real database")}
|
|
280286
|
+
`);
|
|
280287
|
+
console.log(
|
|
280288
|
+
` Ablo READS your write-ahead log (WAL) and ${import_picocolors4.default.bold("never")} runs DDL, owns, or migrates your
|
|
280289
|
+
schema. Your app keeps writing through your own backend \u2014 Ablo only tails the changes.
|
|
280290
|
+
Run this once against your Postgres ${import_picocolors4.default.dim("(as a superuser / the DB owner)")}:
|
|
280291
|
+
`
|
|
280292
|
+
);
|
|
280293
|
+
console.log(` ${import_picocolors4.default.bold("1.")} Enable logical decoding ${import_picocolors4.default.dim("(then RESTART Postgres \u2014 wal_level is not reloadable)")}`);
|
|
280294
|
+
console.log(` ${import_picocolors4.default.cyan(sql[0])}`);
|
|
280295
|
+
console.log(
|
|
280296
|
+
import_picocolors4.default.dim(
|
|
280297
|
+
` On Amazon RDS / Aurora you can't ALTER SYSTEM: set ${import_picocolors4.default.bold("rds.logical_replication = 1")} in the
|
|
280298
|
+
instance's parameter group instead, then reboot.`
|
|
280299
|
+
)
|
|
280300
|
+
);
|
|
280301
|
+
console.log(`
|
|
280302
|
+
${import_picocolors4.default.bold("2.")} Publish the tables Ablo should read`);
|
|
280303
|
+
console.log(` ${import_picocolors4.default.cyan(sql[1])}`);
|
|
280304
|
+
if (args.tables.length === 0) {
|
|
280305
|
+
console.log(import_picocolors4.default.dim(` (Scope it with ${import_picocolors4.default.bold("ablo connect --tables a,b,c")} to publish a subset.)`));
|
|
280306
|
+
}
|
|
280307
|
+
console.log(`
|
|
280308
|
+
${import_picocolors4.default.bold("3.")} Create a least-privilege replication role ${import_picocolors4.default.dim("(pick your own password)")}`);
|
|
280309
|
+
console.log(` ${import_picocolors4.default.cyan(sql[2])}`);
|
|
280310
|
+
console.log(` ${import_picocolors4.default.cyan(sql[3])}`);
|
|
280311
|
+
console.log(` ${import_picocolors4.default.cyan(sql[4])}`);
|
|
280312
|
+
console.log(
|
|
280313
|
+
import_picocolors4.default.dim(
|
|
280314
|
+
` On Amazon RDS, the REPLICATION attribute is granted, not set directly:
|
|
280315
|
+
${import_picocolors4.default.bold(`GRANT rds_replication TO ${quoteIdent(args.role)};`)}`
|
|
280316
|
+
)
|
|
280317
|
+
);
|
|
280318
|
+
console.log(
|
|
280319
|
+
`
|
|
280320
|
+
${import_picocolors4.default.bold("4.")} Put the role's connection string in ${import_picocolors4.default.bold("DATABASE_URL")}, then verify:
|
|
280321
|
+
${import_picocolors4.default.cyan("npx ablo connect --check")}
|
|
280322
|
+
`
|
|
280323
|
+
);
|
|
280324
|
+
console.log(
|
|
280325
|
+
import_picocolors4.default.dim(
|
|
280326
|
+
` Reminder: Ablo never writes to your database on this path. Provisioning tables with
|
|
280327
|
+
${import_picocolors4.default.bold("ablo migrate")} is a separate, optional escape hatch \u2014 connecting a real database is this.`
|
|
280328
|
+
)
|
|
280329
|
+
);
|
|
280330
|
+
console.log();
|
|
280331
|
+
}
|
|
280332
|
+
function printCheckItem(item) {
|
|
280333
|
+
if (item.ok) {
|
|
280334
|
+
console.log(` ${import_picocolors4.default.green("\u2713")} ${item.label}`);
|
|
280335
|
+
} else {
|
|
280336
|
+
console.log(` ${import_picocolors4.default.red("\u2717")} ${item.label}`);
|
|
280337
|
+
if (item.fix) {
|
|
280338
|
+
for (const line of item.fix.split("\n")) console.log(` ${import_picocolors4.default.red("\u2022")} ${line}`);
|
|
280339
|
+
}
|
|
280340
|
+
}
|
|
280341
|
+
}
|
|
280342
|
+
async function probeReadiness(sql, opts = {}) {
|
|
280343
|
+
const publication = opts.publication ?? ABLO_PUBLICATION;
|
|
280344
|
+
const items = [];
|
|
280345
|
+
const walRows = await sql.unsafe(`SHOW wal_level`);
|
|
280346
|
+
const walLevel = walRows[0]?.setting ?? "unknown";
|
|
280347
|
+
items.push(
|
|
280348
|
+
walLevel === "logical" ? { ok: true, label: `wal_level is ${import_picocolors4.default.bold("logical")}` } : {
|
|
280349
|
+
ok: false,
|
|
280350
|
+
label: `wal_level is ${import_picocolors4.default.bold(walLevel)} (need ${import_picocolors4.default.bold("logical")})`,
|
|
280351
|
+
fix: `ALTER SYSTEM SET wal_level = 'logical'; then RESTART Postgres.
|
|
280352
|
+
On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.`
|
|
280353
|
+
}
|
|
280354
|
+
);
|
|
280355
|
+
const pubRows = await sql.unsafe(
|
|
280356
|
+
`SELECT puballtables FROM pg_publication WHERE pubname = $1`,
|
|
280357
|
+
[publication]
|
|
280358
|
+
);
|
|
280359
|
+
items.push(
|
|
280360
|
+
pubRows.length > 0 ? {
|
|
280361
|
+
ok: true,
|
|
280362
|
+
label: `publication ${import_picocolors4.default.bold(publication)} exists ${import_picocolors4.default.dim(pubRows[0].puballtables ? "(all tables)" : "(table subset)")}`
|
|
280363
|
+
} : {
|
|
280364
|
+
ok: false,
|
|
280365
|
+
label: `publication ${import_picocolors4.default.bold(publication)} not found`,
|
|
280366
|
+
fix: `CREATE PUBLICATION ${quoteIdent(publication)} FOR ALL TABLES;`
|
|
280367
|
+
}
|
|
280368
|
+
);
|
|
280369
|
+
const roleRows = await sql.unsafe(
|
|
280370
|
+
`SELECT rolreplication, rolsuper FROM pg_roles WHERE rolname = current_user`
|
|
280371
|
+
);
|
|
280372
|
+
const role = roleRows[0];
|
|
280373
|
+
const hasReplication = Boolean(role && (role.rolreplication || role.rolsuper));
|
|
280374
|
+
items.push(
|
|
280375
|
+
hasReplication ? { ok: true, label: `DATABASE_URL role can stream replication ${import_picocolors4.default.dim("(REPLICATION)")}` } : {
|
|
280376
|
+
ok: false,
|
|
280377
|
+
label: `DATABASE_URL role lacks the ${import_picocolors4.default.bold("REPLICATION")} attribute`,
|
|
280378
|
+
fix: `ALTER ROLE current_user WITH REPLICATION;
|
|
280379
|
+
On RDS: GRANT rds_replication TO <your_role>;`
|
|
280380
|
+
}
|
|
280381
|
+
);
|
|
280382
|
+
if (pubRows.length > 0) {
|
|
280383
|
+
const badRows = await sql.unsafe(
|
|
280384
|
+
`SELECT c.relname AS table_name, c.relreplident
|
|
280385
|
+
FROM pg_publication_tables pt
|
|
280386
|
+
JOIN pg_class c ON c.relname = pt.tablename
|
|
280387
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
|
|
280388
|
+
WHERE pt.pubname = $1
|
|
280389
|
+
AND (
|
|
280390
|
+
c.relreplident = 'n'
|
|
280391
|
+
OR (
|
|
280392
|
+
c.relreplident = 'd'
|
|
280393
|
+
AND NOT EXISTS (
|
|
280394
|
+
SELECT 1 FROM pg_index i
|
|
280395
|
+
WHERE i.indrelid = c.oid AND i.indisprimary
|
|
280396
|
+
)
|
|
280397
|
+
)
|
|
280398
|
+
)`,
|
|
280399
|
+
[publication]
|
|
280400
|
+
);
|
|
280401
|
+
items.push(
|
|
280402
|
+
badRows.length === 0 ? { ok: true, label: `all published tables have a usable REPLICA IDENTITY` } : {
|
|
280403
|
+
ok: false,
|
|
280404
|
+
label: `${badRows.length} published table${badRows.length === 1 ? "" : "s"} cannot replicate UPDATE/DELETE`,
|
|
280405
|
+
fix: badRows.map(
|
|
280406
|
+
(r2) => `${r2.table_name}: add a PRIMARY KEY, or ALTER TABLE ${quoteIdent(r2.table_name)} REPLICA IDENTITY FULL;`
|
|
280407
|
+
).join("\n")
|
|
280408
|
+
}
|
|
280409
|
+
);
|
|
280410
|
+
}
|
|
280411
|
+
return items;
|
|
280412
|
+
}
|
|
280413
|
+
async function runCheck() {
|
|
280414
|
+
const dbUrl = readProjectDatabaseUrl();
|
|
280415
|
+
if (!dbUrl) {
|
|
280416
|
+
console.error(
|
|
280417
|
+
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")}.`)
|
|
280418
|
+
);
|
|
280419
|
+
process.exit(1);
|
|
280420
|
+
}
|
|
280421
|
+
console.log(`
|
|
280422
|
+
${brand("ablo")} ${import_picocolors4.default.dim("connect --check")} ${import_picocolors4.default.dim("logical-replication readiness")}
|
|
280423
|
+
`);
|
|
280424
|
+
const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
|
|
280425
|
+
} });
|
|
280426
|
+
let items;
|
|
280427
|
+
try {
|
|
280428
|
+
items = await probeReadiness(sql);
|
|
280429
|
+
} catch (err) {
|
|
280430
|
+
const pg = err ?? {};
|
|
280431
|
+
console.error(import_picocolors4.default.red(` Couldn't read the database: ${pg.message ?? String(err)}`));
|
|
280432
|
+
await sql.end({ timeout: 2 });
|
|
280433
|
+
process.exit(1);
|
|
280434
|
+
}
|
|
280435
|
+
await sql.end({ timeout: 2 });
|
|
280436
|
+
for (const item of items) printCheckItem(item);
|
|
280437
|
+
const failures = items.filter((i) => !i.ok).length;
|
|
280438
|
+
console.log();
|
|
280439
|
+
if (failures === 0) {
|
|
280440
|
+
console.log(` ${import_picocolors4.default.green("\u2713")} Ready \u2014 Ablo can connect and tail this database's WAL.
|
|
280441
|
+
`);
|
|
280442
|
+
process.exit(0);
|
|
280443
|
+
}
|
|
280444
|
+
console.log(
|
|
280445
|
+
` ${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")}.`)}
|
|
280446
|
+
`
|
|
280447
|
+
);
|
|
280448
|
+
process.exit(1);
|
|
280449
|
+
}
|
|
280450
|
+
async function connect(argv) {
|
|
280451
|
+
let args;
|
|
280452
|
+
try {
|
|
280453
|
+
args = parseConnectArgs(argv);
|
|
280454
|
+
} catch (err) {
|
|
280455
|
+
console.error(import_picocolors4.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
280456
|
+
process.exit(1);
|
|
280457
|
+
}
|
|
280458
|
+
if (args.check) {
|
|
280459
|
+
await runCheck();
|
|
280460
|
+
return;
|
|
280461
|
+
}
|
|
280462
|
+
printConnectRecipe(args);
|
|
280463
|
+
}
|
|
280464
|
+
var CONNECT_USAGE = ` ablo connect \u2014 connect a real database to Ablo via logical replication (the one way)
|
|
280465
|
+
|
|
280466
|
+
Ablo READS your WAL and never runs DDL, owns, or migrates your schema. Your app
|
|
280467
|
+
keeps writing through your own backend.
|
|
280468
|
+
|
|
280469
|
+
Usage:
|
|
280470
|
+
npx ablo connect Print the exact setup SQL for your Postgres
|
|
280471
|
+
npx ablo connect --tables a,b,c Publish only these tables (default: all tables)
|
|
280472
|
+
npx ablo connect --role <name> Name the replication role (default: ablo_replicator)
|
|
280473
|
+
npx ablo connect --check Validate DATABASE_URL is replication-ready`;
|
|
280474
|
+
|
|
280256
280475
|
// src/cli/generate.ts
|
|
280257
280476
|
init_cjs_shims();
|
|
280258
280477
|
var import_fs6 = require("fs");
|
|
@@ -280313,21 +280532,6 @@ var import_picocolors6 = __toESM(require_picocolors(), 1);
|
|
|
280313
280532
|
var import_fs7 = require("fs");
|
|
280314
280533
|
var import_path5 = require("path");
|
|
280315
280534
|
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
280535
|
function parseDevArgs(argv) {
|
|
280332
280536
|
let schemaPath = DEFAULT_SCHEMA_PATH;
|
|
280333
280537
|
let exportName = DEFAULT_EXPORT;
|
|
@@ -280437,11 +280641,27 @@ async function runPush(schema, args) {
|
|
|
280437
280641
|
if (status2 === 409) {
|
|
280438
280642
|
const unexecutable = Array.isArray(body.unexecutable) ? body.unexecutable : [];
|
|
280439
280643
|
const warnings = Array.isArray(body.warnings) ? body.warnings : [];
|
|
280644
|
+
const hasShadowed = [...unexecutable, ...warnings].some(
|
|
280645
|
+
(s) => s.shadowed != null
|
|
280646
|
+
);
|
|
280440
280647
|
const lines = [
|
|
280441
|
-
"Incompatible schema change \u2014 not safe to apply as-is.",
|
|
280648
|
+
import_picocolors6.default.bold("Incompatible schema change \u2014 not safe to apply as-is."),
|
|
280649
|
+
"",
|
|
280442
280650
|
...unexecutable.map((u2) => import_picocolors6.default.red(fmtSignal(u2))),
|
|
280443
280651
|
...warnings.map((w2) => import_picocolors6.default.yellow(fmtSignal(w2))),
|
|
280444
|
-
|
|
280652
|
+
"",
|
|
280653
|
+
...hasShadowed ? [
|
|
280654
|
+
import_picocolors6.default.dim(
|
|
280655
|
+
" These models exist in the baseline above but not in your push. Sandbox readers"
|
|
280656
|
+
),
|
|
280657
|
+
import_picocolors6.default.dim(
|
|
280658
|
+
" fall back to the production schema until you push your own, so applying this drops them."
|
|
280659
|
+
),
|
|
280660
|
+
""
|
|
280661
|
+
] : [],
|
|
280662
|
+
import_picocolors6.default.dim(
|
|
280663
|
+
` Fix: ${import_picocolors6.default.bold("ablo push --force")} to apply anyway, or ${import_picocolors6.default.bold("--rename old:new")} if you renamed a model.`
|
|
280664
|
+
)
|
|
280445
280665
|
];
|
|
280446
280666
|
return { ok: false, message: lines.join("\n") };
|
|
280447
280667
|
}
|
|
@@ -280473,8 +280693,6 @@ async function dev(argv) {
|
|
|
280473
280693
|
console.log(`
|
|
280474
280694
|
${brand("ablo")} ${import_picocolors6.default.dim("push")} ${import_picocolors6.default.dim("(sandbox)")}
|
|
280475
280695
|
`);
|
|
280476
|
-
const projectDbUrl = readProjectDatabaseUrl();
|
|
280477
|
-
if (projectDbUrl) await ensureScopedRoleInteractive(projectDbUrl);
|
|
280478
280696
|
const schema = await loadSchema(args.schemaPath, args.exportName);
|
|
280479
280697
|
const modelCount = Object.keys(schema.models).length;
|
|
280480
280698
|
console.log(
|
|
@@ -282350,11 +282568,89 @@ async function drizzlePull(argv) {
|
|
|
282350
282568
|
);
|
|
282351
282569
|
}
|
|
282352
282570
|
|
|
282571
|
+
// src/cli/renderError.ts
|
|
282572
|
+
init_cjs_shims();
|
|
282573
|
+
var import_picocolors18 = __toESM(require_picocolors(), 1);
|
|
282574
|
+
var RECOVERY_HINT = {
|
|
282575
|
+
transient: "This looks transient \u2014 retry in a moment.",
|
|
282576
|
+
permission: "Your key isn't allowed to do this \u2014 check its scopes or role.",
|
|
282577
|
+
session_expiry: "Your session expired \u2014 sign in again.",
|
|
282578
|
+
access_credential_expiry: "Your access credential expired \u2014 refresh it and retry.",
|
|
282579
|
+
auth_blocked: "Authentication was blocked."
|
|
282580
|
+
};
|
|
282581
|
+
function titleForType(type) {
|
|
282582
|
+
const core = type.replace(/^Ablo/, "").replace(/Error$/, "");
|
|
282583
|
+
const spaced = core.replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
|
|
282584
|
+
if (!spaced) return "Error";
|
|
282585
|
+
return /error$/i.test(spaced) ? spaced : `${spaced} error`;
|
|
282586
|
+
}
|
|
282587
|
+
function isStringArray(v) {
|
|
282588
|
+
return Array.isArray(v) && v.every((x2) => typeof x2 === "string");
|
|
282589
|
+
}
|
|
282590
|
+
function renderKnownDetails(details, line) {
|
|
282591
|
+
if (!details) return;
|
|
282592
|
+
const { retryAfterSeconds, missingIds, requiredCapability, unexecutable, errors } = details;
|
|
282593
|
+
if (typeof retryAfterSeconds === "number") line(` ${import_picocolors18.default.dim("retry")} after ${retryAfterSeconds}s`);
|
|
282594
|
+
if (isStringArray(missingIds) && missingIds.length > 0) {
|
|
282595
|
+
const shown = missingIds.slice(0, 5).join(", ");
|
|
282596
|
+
const more = missingIds.length > 5 ? ` (+${missingIds.length - 5} more)` : "";
|
|
282597
|
+
line(` ${import_picocolors18.default.dim("missing")} ${shown}${more}`);
|
|
282598
|
+
}
|
|
282599
|
+
if (typeof requiredCapability === "string") line(` ${import_picocolors18.default.dim("needs")} ${requiredCapability}`);
|
|
282600
|
+
if (Array.isArray(unexecutable) && unexecutable.length > 0) {
|
|
282601
|
+
line(` ${import_picocolors18.default.dim("blocked")} ${unexecutable.length} change(s) can't be applied \u2014 see \`unexecutable\` (--verbose)`);
|
|
282602
|
+
}
|
|
282603
|
+
if (Array.isArray(errors)) {
|
|
282604
|
+
for (const e2 of errors.slice(0, 8)) {
|
|
282605
|
+
if (e2 && typeof e2 === "object") {
|
|
282606
|
+
const rec = e2;
|
|
282607
|
+
const where = typeof rec.param === "string" ? `${rec.param}: ` : "";
|
|
282608
|
+
const msg = typeof rec.message === "string" ? rec.message : "";
|
|
282609
|
+
if (msg) line(` ${import_picocolors18.default.dim("\xB7")} ${where}${msg}`);
|
|
282610
|
+
}
|
|
282611
|
+
}
|
|
282612
|
+
}
|
|
282613
|
+
}
|
|
282614
|
+
function renderCliError(err, opts = {}) {
|
|
282615
|
+
const line = opts.write ?? ((l2) => console.error(l2));
|
|
282616
|
+
const verbose = opts.verbose ?? (process.argv.includes("--verbose") || process.env.ABLO_VERBOSE === "1");
|
|
282617
|
+
if (err instanceof AbloError) {
|
|
282618
|
+
const codeTag = err.code ? ` ${import_picocolors18.default.dim(`[${err.code}]`)}` : "";
|
|
282619
|
+
line("");
|
|
282620
|
+
line(` ${brand("ablo")} ${import_picocolors18.default.red("\u2717")} ${import_picocolors18.default.bold(titleForType(err.type))}${codeTag}`);
|
|
282621
|
+
line("");
|
|
282622
|
+
line(` ${err.message}`);
|
|
282623
|
+
if (err.param) line(` ${import_picocolors18.default.dim("field")} ${err.param}`);
|
|
282624
|
+
renderKnownDetails(err.details, line);
|
|
282625
|
+
const hint = err.code ? RECOVERY_HINT[classifyRecovery(err.code)] : void 0;
|
|
282626
|
+
if (hint) line(` ${import_picocolors18.default.dim(hint)}`);
|
|
282627
|
+
if (err.docUrl) line(` ${import_picocolors18.default.dim("docs")} ${err.docUrl}`);
|
|
282628
|
+
if (err.requestId) line(` ${import_picocolors18.default.dim("ref")} ${err.requestId}`);
|
|
282629
|
+
if (verbose) {
|
|
282630
|
+
if (err.details && Object.keys(err.details).length > 0) {
|
|
282631
|
+
line(` ${import_picocolors18.default.dim("details")} ${JSON.stringify(err.details)}`);
|
|
282632
|
+
}
|
|
282633
|
+
if (err.stack) line(import_picocolors18.default.dim(err.stack));
|
|
282634
|
+
}
|
|
282635
|
+
line("");
|
|
282636
|
+
process.exitCode = 1;
|
|
282637
|
+
return;
|
|
282638
|
+
}
|
|
282639
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
282640
|
+
line("");
|
|
282641
|
+
line(` ${brand("ablo")} ${import_picocolors18.default.red("\u2717")} ${message}`);
|
|
282642
|
+
if (verbose && err instanceof Error && err.stack) line(import_picocolors18.default.dim(err.stack));
|
|
282643
|
+
else line(` ${import_picocolors18.default.dim("Run with --verbose for the full error.")}`);
|
|
282644
|
+
line("");
|
|
282645
|
+
process.exitCode = 1;
|
|
282646
|
+
}
|
|
282647
|
+
|
|
282353
282648
|
// src/cli/index.ts
|
|
282354
282649
|
var LOGO = `
|
|
282355
|
-
${brand("ablo")} ${
|
|
282650
|
+
${brand("ablo")} ${import_picocolors19.default.dim("sync engine")}
|
|
282356
282651
|
`;
|
|
282357
282652
|
var SUBCOMMAND_USAGE = {
|
|
282653
|
+
connect: CONNECT_USAGE,
|
|
282358
282654
|
migrate: MIGRATE_USAGE
|
|
282359
282655
|
};
|
|
282360
282656
|
async function main() {
|
|
@@ -282386,7 +282682,7 @@ async function main() {
|
|
|
282386
282682
|
const devArgs = process.argv.slice(3);
|
|
282387
282683
|
const oneShot = devArgs.includes("--no-watch");
|
|
282388
282684
|
console.log(
|
|
282389
|
-
|
|
282685
|
+
import_picocolors19.default.dim(
|
|
282390
282686
|
oneShot ? " `ablo dev --no-watch` is `ablo push` (push once, no watcher) \u2014 running that." : " `ablo dev` is now `ablo push --watch` \u2014 running that."
|
|
282391
282687
|
)
|
|
282392
282688
|
);
|
|
@@ -282402,6 +282698,8 @@ async function main() {
|
|
|
282402
282698
|
} else {
|
|
282403
282699
|
await pull(rest);
|
|
282404
282700
|
}
|
|
282701
|
+
} else if (command === "connect") {
|
|
282702
|
+
await connect(process.argv.slice(3));
|
|
282405
282703
|
} else if (command === "migrate") {
|
|
282406
282704
|
await migrate(process.argv.slice(3));
|
|
282407
282705
|
} else if (command === "push") {
|
|
@@ -282411,14 +282709,14 @@ async function main() {
|
|
|
282411
282709
|
const guard = guardActiveProjectKey();
|
|
282412
282710
|
if (!guard.ok && guard.available.length > 0 && !rest.includes("--url")) {
|
|
282413
282711
|
console.error(
|
|
282414
|
-
` ${
|
|
282712
|
+
` ${import_picocolors19.default.yellow("\u26A0")} active project ${import_picocolors19.default.bold(guard.activeProfile)} has no stored key ${import_picocolors19.default.dim(
|
|
282415
282713
|
`(you have keys for: ${guard.available.join(", ")})`
|
|
282416
282714
|
)}`
|
|
282417
282715
|
);
|
|
282418
282716
|
const loginCmd = guard.activeProfile === "default" ? "ablo login" : `ablo login --project ${guard.activeProfile}`;
|
|
282419
282717
|
console.error(
|
|
282420
|
-
|
|
282421
|
-
` Mint one with ${
|
|
282718
|
+
import_picocolors19.default.dim(
|
|
282719
|
+
` Mint one with ${import_picocolors19.default.bold(loginCmd)}, or switch with ${import_picocolors19.default.bold("ablo projects use <slug>")}.`
|
|
282422
282720
|
)
|
|
282423
282721
|
);
|
|
282424
282722
|
process.exitCode = 1;
|
|
@@ -282436,13 +282734,13 @@ async function main() {
|
|
|
282436
282734
|
await generate(process.argv.slice(3));
|
|
282437
282735
|
} else if (command === "schema") {
|
|
282438
282736
|
console.error(
|
|
282439
|
-
` ${
|
|
282737
|
+
` ${import_picocolors19.default.red("\u2717")} \`ablo schema push\` was renamed to \`${brand("ablo push")}\`.`
|
|
282440
282738
|
);
|
|
282441
282739
|
console.error(` Run \`ablo push${process.argv.slice(4).join(" ") ? " " + process.argv.slice(4).join(" ") : ""}\` instead.`);
|
|
282442
282740
|
process.exitCode = 1;
|
|
282443
282741
|
} else {
|
|
282444
282742
|
console.log(LOGO);
|
|
282445
|
-
console.log(` ${
|
|
282743
|
+
console.log(` ${import_picocolors19.default.bold("Usage:")}`);
|
|
282446
282744
|
console.log(` npx ablo init Scaffold ablo/ directory + starter schema`);
|
|
282447
282745
|
console.log(` npx ablo init --yes [--framework nextjs] Non-interactive (agents/CI): no prompts, flag-driven`);
|
|
282448
282746
|
console.log(` [--auth apikey] [--storage direct|endpoint] [--project <slug>] [--no-project]`);
|
|
@@ -282465,7 +282763,9 @@ async function main() {
|
|
|
282465
282763
|
console.log(` npx ablo pull prisma [path] Generate schema.ts from a Prisma schema (keeps enums + relations)`);
|
|
282466
282764
|
console.log(` npx ablo pull drizzle <module> Generate schema.ts from a Drizzle schema (keeps enums + relations)`);
|
|
282467
282765
|
console.log(` npx ablo check Check your existing database fits the schema (read-only, creates no tables)`);
|
|
282468
|
-
console.log(` npx ablo
|
|
282766
|
+
console.log(` npx ablo connect Connect a real database \u2014 prints the logical-replication setup SQL (the one way)`);
|
|
282767
|
+
console.log(` npx ablo connect --check Validate DATABASE_URL is replication-ready (wal_level, publication, role, replica identity)`);
|
|
282768
|
+
console.log(` npx ablo migrate Provision your synced-model tables in your own Postgres (optional escape hatch \u2014 \`connect\` is the way)`);
|
|
282469
282769
|
console.log(` npx ablo migrate --dry-run Print the SQL without executing (preview)`);
|
|
282470
282770
|
console.log(` npx ablo push Upload your schema definition to Ablo (metadata only \u2014 rows stay in your DB)`);
|
|
282471
282771
|
console.log(` npx ablo push --force Allow destructive/unexecutable changes`);
|
|
@@ -282475,10 +282775,10 @@ async function main() {
|
|
|
282475
282775
|
console.log(` npx ablo generate Emit TypeScript types from your schema`);
|
|
282476
282776
|
console.log(` npx ablo generate --out path.ts Write generated types to a path`);
|
|
282477
282777
|
console.log();
|
|
282478
|
-
console.log(` ${
|
|
282778
|
+
console.log(` ${import_picocolors19.default.bold("Schema workflow:")}`);
|
|
282479
282779
|
console.log(` The server holds its own copy of your schema \u2014 edit ${brand("ablo/schema.ts")}, then`);
|
|
282480
282780
|
console.log(` run ${brand("ablo push")} (or keep ${brand("ablo dev")} running) before the server will accept`);
|
|
282481
|
-
console.log(` writes to new or changed models. Skip it and writes fail with ${
|
|
282781
|
+
console.log(` writes to new or changed models. Skip it and writes fail with ${import_picocolors19.default.yellow("server_execute_unknown_model")}.`);
|
|
282482
282782
|
console.log();
|
|
282483
282783
|
}
|
|
282484
282784
|
}
|
|
@@ -282529,7 +282829,7 @@ async function ensureInitProject(opts) {
|
|
|
282529
282829
|
const ensured = await ensureProject(slug);
|
|
282530
282830
|
if (ensured) {
|
|
282531
282831
|
console.log(
|
|
282532
|
-
` ${
|
|
282832
|
+
` ${import_picocolors19.default.green("\u2713")} ${ensured.created ? "Created" : "Using"} project ${import_picocolors19.default.bold(ensured.slug)} ${import_picocolors19.default.dim(`(${ensured.id})`)} \u2014 keys you mint for it are isolated from the org's other apps.`
|
|
282533
282833
|
);
|
|
282534
282834
|
}
|
|
282535
282835
|
}
|
|
@@ -282571,7 +282871,7 @@ async function chooseBool(flagValue, fallback, interactive, prompt) {
|
|
|
282571
282871
|
async function init(args = []) {
|
|
282572
282872
|
const opts = parseInitArgs(args);
|
|
282573
282873
|
const interactive = Boolean(process.stdin.isTTY) && !opts.yes && !process.env.CI;
|
|
282574
|
-
Ie(`${brand("ablo")} ${
|
|
282874
|
+
Ie(`${brand("ablo")} ${import_picocolors19.default.dim("sync engine")}`);
|
|
282575
282875
|
if (!(0, import_fs12.existsSync)("package.json")) {
|
|
282576
282876
|
xe("No package.json found. Run this from your project root.");
|
|
282577
282877
|
process.exit(1);
|
|
@@ -282656,7 +282956,7 @@ async function init(args = []) {
|
|
|
282656
282956
|
if (pullExisting) {
|
|
282657
282957
|
const dbUrl = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
|
|
282658
282958
|
if (!dbUrl) {
|
|
282659
|
-
schemaNote =
|
|
282959
|
+
schemaNote = import_picocolors19.default.dim(" (no DATABASE_URL \u2014 wrote starter; run `ablo pull` later)");
|
|
282660
282960
|
} else {
|
|
282661
282961
|
try {
|
|
282662
282962
|
const pulled = await buildSchemaSourceFromDb({
|
|
@@ -282666,12 +282966,12 @@ async function init(args = []) {
|
|
|
282666
282966
|
});
|
|
282667
282967
|
if (pulled.models.length > 0) {
|
|
282668
282968
|
schemaSource = pulled.source;
|
|
282669
|
-
schemaNote =
|
|
282969
|
+
schemaNote = import_picocolors19.default.dim(` (pulled ${pulled.models.length} models)`);
|
|
282670
282970
|
} else {
|
|
282671
|
-
schemaNote =
|
|
282971
|
+
schemaNote = import_picocolors19.default.dim(" (no adoptable tables \u2014 wrote starter)");
|
|
282672
282972
|
}
|
|
282673
282973
|
} catch {
|
|
282674
|
-
schemaNote =
|
|
282974
|
+
schemaNote = import_picocolors19.default.dim(" (pull failed \u2014 wrote starter)");
|
|
282675
282975
|
}
|
|
282676
282976
|
}
|
|
282677
282977
|
}
|
|
@@ -282697,14 +282997,14 @@ async function init(args = []) {
|
|
|
282697
282997
|
const existing = (0, import_fs12.readFileSync)(envFile, "utf-8");
|
|
282698
282998
|
if (!existing.includes("ABLO_")) {
|
|
282699
282999
|
(0, import_fs12.writeFileSync)(envFile, existing + "\n" + envBody);
|
|
282700
|
-
created.push(`${envFile} ${
|
|
283000
|
+
created.push(`${envFile} ${import_picocolors19.default.dim("(appended)")}`);
|
|
282701
283001
|
} else {
|
|
282702
|
-
created.push(`${envFile} ${
|
|
283002
|
+
created.push(`${envFile} ${import_picocolors19.default.dim("(already configured)")}`);
|
|
282703
283003
|
}
|
|
282704
283004
|
}
|
|
282705
283005
|
if (wireRealKey && resolvedKey) {
|
|
282706
283006
|
wireEnvLocal(resolvedKey);
|
|
282707
|
-
created.push(`.env.local ${
|
|
283007
|
+
created.push(`.env.local ${import_picocolors19.default.dim("(ABLO_API_KEY set from your login)")}`);
|
|
282708
283008
|
}
|
|
282709
283009
|
if (agent) {
|
|
282710
283010
|
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "agent.ts"), generateAgent());
|
|
@@ -282719,17 +283019,17 @@ async function init(args = []) {
|
|
|
282719
283019
|
}
|
|
282720
283020
|
const providersPath = (0, import_path7.join)(layout.appBase, "providers.tsx");
|
|
282721
283021
|
(0, import_fs12.writeFileSync)(providersPath, generateProviders());
|
|
282722
|
-
created.push(`${providersPath} ${
|
|
283022
|
+
created.push(`${providersPath} ${import_picocolors19.default.dim(`(wrap ${(0, import_path7.join)(layout.appBase, "layout.tsx")} in <Providers>)`)}`);
|
|
282723
283023
|
const sessionDir = (0, import_path7.join)(layout.appBase, "api", "ablo-session");
|
|
282724
283024
|
(0, import_fs12.mkdirSync)(sessionDir, { recursive: true });
|
|
282725
283025
|
(0, import_fs12.writeFileSync)((0, import_path7.join)(sessionDir, "route.ts"), generateSessionRoute());
|
|
282726
|
-
created.push(`${(0, import_path7.join)(sessionDir, "route.ts")} ${
|
|
283026
|
+
created.push(`${(0, import_path7.join)(sessionDir, "route.ts")} ${import_picocolors19.default.dim("(wire your auth)")}`);
|
|
282727
283027
|
}
|
|
282728
283028
|
if (framework !== "vanilla") {
|
|
282729
283029
|
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "TaskList.tsx"), generateComponent());
|
|
282730
283030
|
created.push(`${abloDir}/TaskList.tsx`);
|
|
282731
283031
|
}
|
|
282732
|
-
Me(created.map((f) => `${
|
|
283032
|
+
Me(created.map((f) => `${import_picocolors19.default.green("\u2713")} ${f}`).join("\n"), "Created");
|
|
282733
283033
|
const pm = detectPackageManager();
|
|
282734
283034
|
if (opts.install) {
|
|
282735
283035
|
const s = Y2();
|
|
@@ -282738,45 +283038,45 @@ async function init(args = []) {
|
|
|
282738
283038
|
(0, import_child_process2.execSync)(`${pm} add @abloatai/ablo`, { stdio: "ignore" });
|
|
282739
283039
|
s.stop("Installed @abloatai/ablo");
|
|
282740
283040
|
} catch {
|
|
282741
|
-
s.stop(`${
|
|
283041
|
+
s.stop(`${import_picocolors19.default.yellow("!")} Couldn't auto-install \u2014 run ${import_picocolors19.default.bold(`${pm} install @abloatai/ablo`)}`);
|
|
282742
283042
|
}
|
|
282743
283043
|
}
|
|
282744
283044
|
const steps = [
|
|
282745
|
-
`Get a ${
|
|
282746
|
-
`Run ${
|
|
282747
|
-
`Set ${
|
|
282748
|
-
`Run ${
|
|
283045
|
+
`Get a ${import_picocolors19.default.bold("sk_test_")} key at ${import_picocolors19.default.cyan("https://abloatai.com")}`,
|
|
283046
|
+
`Run ${import_picocolors19.default.bold("npx ablo login")} (or add ${import_picocolors19.default.bold("ABLO_API_KEY")} to ${import_picocolors19.default.bold(envFile)})`,
|
|
283047
|
+
`Set ${import_picocolors19.default.bold("DATABASE_URL")} in ${import_picocolors19.default.bold(envFile)} \u2014 your Postgres is the system of record; rows live there, never with Ablo`,
|
|
283048
|
+
`Run ${import_picocolors19.default.bold("npx ablo dev")} \u2014 pushes your schema definition and watches for changes`,
|
|
282749
283049
|
...storage === "direct" ? [
|
|
282750
|
-
`Provision your DB: ${
|
|
283050
|
+
`Provision your DB: ${import_picocolors19.default.bold("npx ablo migrate")} (creates your synced-model tables with row-level security; keep your own migrations for everything else)`
|
|
282751
283051
|
] : [
|
|
282752
|
-
`Provision your DB: ${
|
|
283052
|
+
`Provision your DB: ${import_picocolors19.default.bold("npx ablo migrate")} (creates your Ablo-model tables + the adapter tables; keep your own migrations for everything else), then mount ${import_picocolors19.default.bold(`${abloDir}/data-source.ts`)} at ${import_picocolors19.default.bold("/api/ablo/source")}`
|
|
282753
283053
|
],
|
|
282754
283054
|
...framework === "nextjs" ? [
|
|
282755
|
-
`Wrap ${
|
|
283055
|
+
`Wrap ${import_picocolors19.default.bold((0, import_path7.join)(layout.appBase, "layout.tsx"))} in ${import_picocolors19.default.bold("<Providers>")} (${(0, import_path7.join)(layout.appBase, "providers.tsx")}) and add your auth to ${import_picocolors19.default.bold((0, import_path7.join)(layout.appBase, "api", "ablo-session", "route.ts"))}`
|
|
282756
283056
|
] : [],
|
|
282757
|
-
`Run ${
|
|
283057
|
+
`Run ${import_picocolors19.default.bold(`${pm} run dev`)} and open two browser tabs \u2014 changes sync in real-time`,
|
|
282758
283058
|
...agent ? [
|
|
282759
|
-
`Run ${
|
|
282760
|
-
`Run ${
|
|
283059
|
+
`Run ${import_picocolors19.default.bold(`npx tsx ${abloDir}/agent.ts`)} \u2014 an AI teammate edits the same tasks`,
|
|
283060
|
+
`Run ${import_picocolors19.default.bold("npx ablo logs")} to watch human + agent commits stream by`
|
|
282761
283061
|
] : []
|
|
282762
283062
|
];
|
|
282763
283063
|
Me(steps.map((s, i) => `${i + 1}. ${s}`).join("\n"), "Next steps");
|
|
282764
283064
|
const existingKey = resolveApiKey("sandbox");
|
|
282765
283065
|
if (existingKey) {
|
|
282766
283066
|
await ensureInitProject(opts);
|
|
282767
|
-
Se(`Already authorized ${
|
|
283067
|
+
Se(`Already authorized ${import_picocolors19.default.dim(`(${existingKey.slice(0, 11)}\u2026)`)} \u2014 run ${import_picocolors19.default.bold("npx ablo push")} next. ${import_picocolors19.default.dim("Docs:")} https://abloatai.com/docs`);
|
|
282768
283068
|
return;
|
|
282769
283069
|
}
|
|
282770
283070
|
if (interactive && opts.login) {
|
|
282771
283071
|
const loginNow = await ye({ message: "Log in now? (opens your browser)", initialValue: true });
|
|
282772
283072
|
if (!pD(loginNow) && loginNow) {
|
|
282773
|
-
Se(`${
|
|
283073
|
+
Se(`${import_picocolors19.default.dim("Docs:")} https://abloatai.com/docs`);
|
|
282774
283074
|
await login();
|
|
282775
283075
|
await ensureInitProject(opts);
|
|
282776
283076
|
return;
|
|
282777
283077
|
}
|
|
282778
283078
|
}
|
|
282779
|
-
Se(`Run ${
|
|
283079
|
+
Se(`Run ${import_picocolors19.default.bold("npx ablo login")} when ready. ${import_picocolors19.default.dim("Docs:")} https://abloatai.com/docs`);
|
|
282780
283080
|
}
|
|
282781
283081
|
function generateSchema() {
|
|
282782
283082
|
return `import { defineSchema, model, relation, z } from '@abloatai/ablo/schema';
|
|
@@ -283054,7 +283354,9 @@ async function main() {
|
|
|
283054
283354
|
}
|
|
283055
283355
|
|
|
283056
283356
|
main().catch((err) => {
|
|
283057
|
-
|
|
283357
|
+
// Ablo errors stringify to one clean line (code + message + docs link),
|
|
283358
|
+
// never a stack/object dump \u2014 see AbloError.toString().
|
|
283359
|
+
console.error(String(err));
|
|
283058
283360
|
process.exit(1);
|
|
283059
283361
|
});
|
|
283060
283362
|
`;
|
|
@@ -283141,22 +283443,33 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
283141
283443
|
`;
|
|
283142
283444
|
}
|
|
283143
283445
|
function generateSessionRoute() {
|
|
283144
|
-
return `import {
|
|
283446
|
+
return `import { headers } from 'next/headers';
|
|
283447
|
+
import { sync } from '@/ablo';
|
|
283448
|
+
import { auth } from '@/lib/auth'; // your server-side betterAuth({ ... }) instance
|
|
283145
283449
|
|
|
283146
283450
|
// Mints the browser's session token. The browser never sees your sk_ key \u2014 it
|
|
283147
|
-
// only gets this token,
|
|
283148
|
-
//
|
|
283451
|
+
// only gets this short-lived token, already scoped to your org + the user you
|
|
283452
|
+
// assert here. The browser fetches THIS route at a relative URL
|
|
283453
|
+
// ('/api/ablo-session') \u2014 that's correct + best practice (same-origin, cookies
|
|
283454
|
+
// flow automatically); the SDK refreshes it in the browser only.
|
|
283149
283455
|
export async function POST(): Promise<Response> {
|
|
283150
|
-
const user = await getCurrentUser();
|
|
283456
|
+
const user = await getCurrentUser();
|
|
283151
283457
|
if (!user) return new Response('Unauthorized', { status: 401 });
|
|
283152
283458
|
|
|
283153
283459
|
const { token } = await sync.sessions.create({ user: { id: user.id } });
|
|
283154
283460
|
return Response.json({ token });
|
|
283155
283461
|
}
|
|
283156
283462
|
|
|
283157
|
-
//
|
|
283463
|
+
// Validate the signed-in user SERVER-SIDE. This ships wired for Better Auth;
|
|
283464
|
+
// \`getSession\` is the server method \u2014 pass the request headers (async in current
|
|
283465
|
+
// Next.js), it does NOT read cookies implicitly. Using a different provider?
|
|
283466
|
+
// Replace the body:
|
|
283467
|
+
// \u2022 NextAuth: const s = await auth(); return s?.user ? { id: s.user.id } : null;
|
|
283468
|
+
// \u2022 Clerk: const { userId } = await auth(); return userId ? { id: userId } : null;
|
|
283469
|
+
// \u2022 Supabase: const { data } = await supabase.auth.getUser(); return data.user ? { id: data.user.id } : null;
|
|
283158
283470
|
async function getCurrentUser(): Promise<{ id: string } | null> {
|
|
283159
|
-
|
|
283471
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
283472
|
+
return session?.user ? { id: session.user.id } : null;
|
|
283160
283473
|
}
|
|
283161
283474
|
`;
|
|
283162
283475
|
}
|
|
@@ -283166,7 +283479,10 @@ function detectPackageManager() {
|
|
|
283166
283479
|
if ((0, import_fs12.existsSync)("bun.lockb")) return "bun";
|
|
283167
283480
|
return "npm";
|
|
283168
283481
|
}
|
|
283169
|
-
main().catch(
|
|
283482
|
+
main().catch((err) => {
|
|
283483
|
+
renderCliError(err);
|
|
283484
|
+
process.exit(process.exitCode ?? 1);
|
|
283485
|
+
});
|
|
283170
283486
|
/*! Bundled license information:
|
|
283171
283487
|
|
|
283172
283488
|
@ts-morph/common/dist/typescript.js:
|