@abloatai/ablo 0.30.2 → 0.32.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/AGENTS.md +3 -3
- package/CHANGELOG.md +53 -12
- package/README.md +12 -14
- package/dist/cli.cjs +1787 -1329
- package/dist/coordination/schema.d.ts +4 -2
- package/dist/coordination/schema.js +23 -9
- package/dist/errorCodes.d.ts +1 -0
- package/dist/errorCodes.js +2 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/schema.d.ts +57 -0
- package/dist/schema/schema.js +53 -0
- package/dist/schema/select.js +1 -0
- package/dist/schema/serialize.d.ts +3 -1
- package/dist/schema/serialize.js +7 -1
- package/dist/source/adapters/drizzle.js +4 -3
- package/dist/source/adapters/kysely.js +4 -4
- package/dist/source/adapters/prisma.js +3 -3
- package/dist/source/idempotency.d.ts +19 -4
- package/dist/source/idempotency.js +19 -4
- package/dist/source/migrations.d.ts +4 -1
- package/dist/source/migrations.js +36 -5
- package/docs/agent-messaging.md +1 -2
- package/docs/api.md +4 -4
- package/docs/audit.md +35 -23
- package/docs/client-behavior.md +7 -4
- package/docs/concurrency-convention.md +3 -3
- package/docs/data-sources.md +141 -102
- package/docs/examples/agent-human.md +2 -2
- package/docs/examples/ai-sdk-tool.md +1 -1
- package/docs/examples/nextjs.md +1 -1
- package/docs/examples/server-agent.md +2 -2
- package/docs/groups.md +160 -0
- package/docs/guarantees.md +9 -7
- package/docs/how-it-works.md +110 -0
- package/docs/integration-guide.md +8 -7
- package/docs/interaction-model.md +3 -3
- package/docs/quickstart.md +38 -31
- package/docs/schema-contract.md +7 -5
- package/docs/webhooks.md +23 -22
- package/llms.txt +10 -4
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -787,8 +787,8 @@ ${import_picocolors2.default.gray(d2)} ${t}
|
|
|
787
787
|
if (i) process.stdout.write(`${I2} ${l2}...`);
|
|
788
788
|
else if (t === "timer") process.stdout.write(`${I2} ${l2} ${O2(g2)}`);
|
|
789
789
|
else {
|
|
790
|
-
const
|
|
791
|
-
process.stdout.write(`${I2} ${l2}${
|
|
790
|
+
const z8 = ".".repeat(Math.floor(w2)).slice(0, 3);
|
|
791
|
+
process.stdout.write(`${I2} ${l2}${z8}`);
|
|
792
792
|
}
|
|
793
793
|
h2 = h2 + 1 < n.length ? h2 + 1 : 0, w2 = w2 < n.length ? w2 + 0.125 : 0;
|
|
794
794
|
}, r2);
|
|
@@ -959,6 +959,7 @@ var init_errorCodes = __esm({
|
|
|
959
959
|
entity_not_found: wire("not_found", 404, false, "No row exists with the requested id. It may have been deleted, or the id may belong to a different environment."),
|
|
960
960
|
model_not_found: wire("not_found", 404, false, "No row of this model exists with the requested id. It may have been deleted, or the id may belong to a different environment."),
|
|
961
961
|
mutate_update_entity_not_found: wire("not_found", 404, false, "The row targeted by this update does not exist \u2014 it may have been deleted since you read it. Re-read before retrying."),
|
|
962
|
+
no_data_source_registered: wire("not_found", 404, false, "No database is connected to this plane yet, so there is nothing to check. Connect one with `ablo connect apply`, then run the check again."),
|
|
962
963
|
task_id_missing: wire("server", 502, true, "The task-create response arrived without a task id, so the result cannot be used. Retry the request."),
|
|
963
964
|
// ── data integrity / database constraints ──────────────────────────
|
|
964
965
|
// Emitted when a database integrity constraint rejects a write. None are
|
|
@@ -1122,7 +1123,7 @@ var init_errorCodes = __esm({
|
|
|
1122
1123
|
query_invalid_boolean: wire("validation", 400, false, "The query compared a boolean column against an invalid boolean literal."),
|
|
1123
1124
|
protocol_version_unsupported: wire("transport", 426, false, "The client sync-protocol version is outside the range this server supports \u2014 upgrade the SDK (or the server was rolled back mid-fleet)."),
|
|
1124
1125
|
database_unreachable: wire("validation", 400, false, "Ablo could not reach this database to check that it can stream replication. The connection string may be wrong, the host may not be reachable from Ablo's servers, or the credentials may not be accepted."),
|
|
1125
|
-
database_not_replication_ready: wire("validation", 400, false, "This database is not set up for logical replication yet. Every failing item \u2014 wal_level, the publication, the replication grant, a replica identity \u2014 is listed in the error details with its exact fix. `ablo connect` prints the one-time setup; `ablo connect
|
|
1126
|
+
database_not_replication_ready: wire("validation", 400, false, "This database is not set up for logical replication yet. Every failing item \u2014 wal_level, the publication, the replication grant, a replica identity \u2014 is listed in the error details with its exact fix. `ablo connect` prints the one-time setup; `ablo connect check` verifies it."),
|
|
1126
1127
|
replication_publication_drift: wire("validation", 400, false, "Your schema maps to tables that are not members of the replication publication, so their changes silently never stream and the source looks frozen. The missing tables and the exact `ALTER PUBLICATION \u2026 ADD TABLE \u2026` to add them are in the error details \u2014 Ablo never alters your database for you."),
|
|
1127
1128
|
query_unknown_relation: wire("validation", 400, false, "The query references a relation the model does not define. Check the relation name against the schema."),
|
|
1128
1129
|
query_relation_target_unknown: wire("schema", 500, false, "A relation in the query targets a model the schema does not define."),
|
|
@@ -1198,7 +1199,7 @@ var init_roles = __esm({
|
|
|
1198
1199
|
});
|
|
1199
1200
|
|
|
1200
1201
|
// src/coordination/schema.ts
|
|
1201
|
-
var import_zod3, targetRangeSchema, participantKindSchema, wireParticipantKindSchema, targetRefSchema, onStaleModeSchema, writeGuardSchema, staleNotificationSchema, readDependencySchema, claimStatusSchema, wireClaimBaseSchema, wireClaimSummarySchema, claimErrorSchema, wireClaimSchema, claimRejectionSchema, claimLostSchema, modelTargetSchema, modelClaimSchema, claimBeginPayloadSchema, claimAbandonPayloadSchema, claimReorderPayloadSchema, claimHeartbeatFieldsSchema, claimHeartbeatPayloadSchema, claimHeartbeatAckPayloadSchema, claimHeartbeatBatchPayloadSchema, claimHeartbeatBatchAckPayloadSchema, updateSubscriptionPayloadSchema, subscriptionAckPayloadSchema, commitOperationTypeSchema, commitOperationSchema, presenceKindSchema, presenceActivitySchema, presenceUpdateFrameSchema;
|
|
1202
|
+
var import_zod3, targetRangeSchema, participantKindSchema, wireParticipantKindSchema, targetRefSchema, onStaleModeSchema, writeGuardSchema, staleNotificationSchema, readDependencySchema, claimStatusSchema, grantStampFields, wireClaimBaseSchema, wireClaimSummarySchema, claimErrorSchema, wireClaimSchema, claimRejectionSchema, claimLostSchema, modelTargetSchema, modelClaimSchema, claimBeginPayloadSchema, claimAbandonPayloadSchema, claimReorderPayloadSchema, claimHeartbeatFieldsSchema, claimHeartbeatPayloadSchema, claimHeartbeatAckPayloadSchema, claimHeartbeatBatchPayloadSchema, claimHeartbeatBatchAckPayloadSchema, updateSubscriptionPayloadSchema, subscriptionAckPayloadSchema, commitOperationTypeSchema, commitOperationSchema, presenceKindSchema, presenceActivitySchema, presenceUpdateFrameSchema;
|
|
1202
1203
|
var init_schema = __esm({
|
|
1203
1204
|
"src/coordination/schema.ts"() {
|
|
1204
1205
|
"use strict";
|
|
@@ -1289,6 +1290,21 @@ var init_schema = __esm({
|
|
|
1289
1290
|
"expired",
|
|
1290
1291
|
"canceled"
|
|
1291
1292
|
]);
|
|
1293
|
+
grantStampFields = {
|
|
1294
|
+
/**
|
|
1295
|
+
* The monotonic fencing token minted for this grant (Option B). Strictly
|
|
1296
|
+
* increasing per entity across successive grants, so a write that carries it
|
|
1297
|
+
* is rejected at commit if a later holder already advanced the entity's
|
|
1298
|
+
* high-water. A token-less write is simply not fence-checked.
|
|
1299
|
+
*/
|
|
1300
|
+
fenceToken: import_zod3.z.number().optional(),
|
|
1301
|
+
/**
|
|
1302
|
+
* Lease origin (epoch ms): when THIS holding was acquired. The cumulative-
|
|
1303
|
+
* hold ceiling measures a holder's fair share from here — and because it
|
|
1304
|
+
* survives a re-announce, a reconnect cannot rewind the clock.
|
|
1305
|
+
*/
|
|
1306
|
+
acquiredAt: import_zod3.z.number().optional()
|
|
1307
|
+
};
|
|
1292
1308
|
wireClaimBaseSchema = targetRefSchema.extend({
|
|
1293
1309
|
claimId: import_zod3.z.string(),
|
|
1294
1310
|
/**
|
|
@@ -1302,15 +1318,7 @@ var init_schema = __esm({
|
|
|
1302
1318
|
/** Server-computed TTL deadline (epoch ms). Readers treat as advisory. */
|
|
1303
1319
|
expiresAt: import_zod3.z.number(),
|
|
1304
1320
|
status: claimStatusSchema.optional(),
|
|
1305
|
-
|
|
1306
|
-
* The monotonic fencing token the server minted for this grant (Option B).
|
|
1307
|
-
* Strictly increasing per entity across successive grants, so a write that
|
|
1308
|
-
* carries it can be rejected at commit if a later holder already advanced the
|
|
1309
|
-
* entity's high-water. Server-stamped on grant and never client-supplied;
|
|
1310
|
-
* optional so every existing frame stays valid and a token-less write is
|
|
1311
|
-
* simply not fence-checked.
|
|
1312
|
-
*/
|
|
1313
|
-
fenceToken: import_zod3.z.number().optional()
|
|
1321
|
+
...grantStampFields
|
|
1314
1322
|
});
|
|
1315
1323
|
wireClaimSummarySchema = wireClaimBaseSchema.pick({
|
|
1316
1324
|
claimId: true,
|
|
@@ -1755,9 +1763,9 @@ var init_query = __esm({
|
|
|
1755
1763
|
CLOSE = {};
|
|
1756
1764
|
Query = class extends Promise {
|
|
1757
1765
|
constructor(strings, args, handler, canceller, options = {}) {
|
|
1758
|
-
let
|
|
1766
|
+
let resolve6, reject;
|
|
1759
1767
|
super((a, b4) => {
|
|
1760
|
-
|
|
1768
|
+
resolve6 = a;
|
|
1761
1769
|
reject = b4;
|
|
1762
1770
|
});
|
|
1763
1771
|
this.tagged = Array.isArray(strings.raw);
|
|
@@ -1768,7 +1776,7 @@ var init_query = __esm({
|
|
|
1768
1776
|
this.options = options;
|
|
1769
1777
|
this.state = null;
|
|
1770
1778
|
this.statement = null;
|
|
1771
|
-
this.resolve = (x2) => (this.active = false,
|
|
1779
|
+
this.resolve = (x2) => (this.active = false, resolve6(x2));
|
|
1772
1780
|
this.reject = (x2) => (this.active = false, reject(x2));
|
|
1773
1781
|
this.active = false;
|
|
1774
1782
|
this.cancelled = null;
|
|
@@ -1816,12 +1824,12 @@ var init_query = __esm({
|
|
|
1816
1824
|
if (this.executed && !this.active)
|
|
1817
1825
|
return { done: true };
|
|
1818
1826
|
prev && prev();
|
|
1819
|
-
const promise = new Promise((
|
|
1827
|
+
const promise = new Promise((resolve6, reject) => {
|
|
1820
1828
|
this.cursorFn = (value) => {
|
|
1821
|
-
|
|
1829
|
+
resolve6({ value, done: false });
|
|
1822
1830
|
return new Promise((r2) => prev = r2);
|
|
1823
1831
|
};
|
|
1824
|
-
this.resolve = () => (this.active = false,
|
|
1832
|
+
this.resolve = () => (this.active = false, resolve6({ done: true }));
|
|
1825
1833
|
this.reject = (x2) => (this.active = false, reject(x2));
|
|
1826
1834
|
});
|
|
1827
1835
|
this.execute();
|
|
@@ -2419,12 +2427,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
2419
2427
|
x2.on("drain", drain);
|
|
2420
2428
|
return x2;
|
|
2421
2429
|
}
|
|
2422
|
-
async function cancel({ pid, secret },
|
|
2430
|
+
async function cancel({ pid, secret }, resolve6, reject) {
|
|
2423
2431
|
try {
|
|
2424
2432
|
cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
|
|
2425
2433
|
await connect2();
|
|
2426
2434
|
socket.once("error", reject);
|
|
2427
|
-
socket.once("close",
|
|
2435
|
+
socket.once("close", resolve6);
|
|
2428
2436
|
} catch (error2) {
|
|
2429
2437
|
reject(error2);
|
|
2430
2438
|
}
|
|
@@ -3447,7 +3455,7 @@ var init_subscribe = __esm({
|
|
|
3447
3455
|
|
|
3448
3456
|
// node_modules/postgres/src/large.js
|
|
3449
3457
|
function largeObject(sql, oid, mode2 = 131072 | 262144) {
|
|
3450
|
-
return new Promise(async (
|
|
3458
|
+
return new Promise(async (resolve6, reject) => {
|
|
3451
3459
|
await sql.begin(async (sql2) => {
|
|
3452
3460
|
let finish;
|
|
3453
3461
|
!oid && ([{ oid }] = await sql2`select lo_creat(-1) as oid`);
|
|
@@ -3473,7 +3481,7 @@ function largeObject(sql, oid, mode2 = 131072 | 262144) {
|
|
|
3473
3481
|
) seek
|
|
3474
3482
|
`
|
|
3475
3483
|
};
|
|
3476
|
-
|
|
3484
|
+
resolve6(lo);
|
|
3477
3485
|
return new Promise(async (r2) => finish = r2);
|
|
3478
3486
|
async function readable({
|
|
3479
3487
|
highWaterMark = 2048 * 8,
|
|
@@ -3635,8 +3643,8 @@ function Postgres(a, b4) {
|
|
|
3635
3643
|
}
|
|
3636
3644
|
async function reserve() {
|
|
3637
3645
|
const queue = queue_default();
|
|
3638
|
-
const c = open.length ? open.shift() : await new Promise((
|
|
3639
|
-
const query = { reserve:
|
|
3646
|
+
const c = open.length ? open.shift() : await new Promise((resolve6, reject) => {
|
|
3647
|
+
const query = { reserve: resolve6, reject };
|
|
3640
3648
|
queries.push(query);
|
|
3641
3649
|
closed.length && connect2(closed.shift(), query);
|
|
3642
3650
|
});
|
|
@@ -3673,9 +3681,9 @@ function Postgres(a, b4) {
|
|
|
3673
3681
|
let uncaughtError, result;
|
|
3674
3682
|
name && await sql2`savepoint ${sql2(name)}`;
|
|
3675
3683
|
try {
|
|
3676
|
-
result = await new Promise((
|
|
3684
|
+
result = await new Promise((resolve6, reject) => {
|
|
3677
3685
|
const x2 = fn2(sql2);
|
|
3678
|
-
Promise.resolve(Array.isArray(x2) ? Promise.all(x2) : x2).then(
|
|
3686
|
+
Promise.resolve(Array.isArray(x2) ? Promise.all(x2) : x2).then(resolve6, reject);
|
|
3679
3687
|
});
|
|
3680
3688
|
if (uncaughtError)
|
|
3681
3689
|
throw uncaughtError;
|
|
@@ -3732,8 +3740,8 @@ function Postgres(a, b4) {
|
|
|
3732
3740
|
return c.execute(query) ? move(c, busy) : move(c, full);
|
|
3733
3741
|
}
|
|
3734
3742
|
function cancel(query) {
|
|
3735
|
-
return new Promise((
|
|
3736
|
-
query.state ? query.active ? connection_default(options).cancel(query.state,
|
|
3743
|
+
return new Promise((resolve6, reject) => {
|
|
3744
|
+
query.state ? query.active ? connection_default(options).cancel(query.state, resolve6, reject) : query.cancelled = { resolve: resolve6, reject } : (queries.remove(query), query.cancelled = true, query.reject(Errors.generic("57014", "canceling statement due to user request")), resolve6());
|
|
3737
3745
|
});
|
|
3738
3746
|
}
|
|
3739
3747
|
async function end({ timeout = null } = {}) {
|
|
@@ -3752,11 +3760,11 @@ function Postgres(a, b4) {
|
|
|
3752
3760
|
async function close() {
|
|
3753
3761
|
await Promise.all(connections.map((c) => c.end()));
|
|
3754
3762
|
}
|
|
3755
|
-
async function destroy(
|
|
3763
|
+
async function destroy(resolve6) {
|
|
3756
3764
|
await Promise.all(connections.map((c) => c.terminate()));
|
|
3757
3765
|
while (queries.length)
|
|
3758
3766
|
queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
|
|
3759
|
-
|
|
3767
|
+
resolve6();
|
|
3760
3768
|
}
|
|
3761
3769
|
function connect2(c, query) {
|
|
3762
3770
|
move(c, connecting);
|
|
@@ -3955,10 +3963,22 @@ function rewriteDatabaseUrl(ownerUrl, role, password) {
|
|
|
3955
3963
|
url.password = password;
|
|
3956
3964
|
return url.toString();
|
|
3957
3965
|
}
|
|
3966
|
+
function readProjectReplicationUrlWithSource(cwd = process.cwd()) {
|
|
3967
|
+
for (const variable of REPLICATION_URL_VARS) {
|
|
3968
|
+
const value = process.env[variable];
|
|
3969
|
+
if (value) return { url: value, variable };
|
|
3970
|
+
}
|
|
3971
|
+
for (const variable of REPLICATION_URL_VARS) {
|
|
3972
|
+
const value = readProjectEnvValue(variable, cwd);
|
|
3973
|
+
if (value) return { url: value, variable };
|
|
3974
|
+
}
|
|
3975
|
+
return null;
|
|
3976
|
+
}
|
|
3958
3977
|
function readProjectDatabaseUrl(cwd = process.cwd()) {
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3978
|
+
return readProjectReplicationUrlWithSource(cwd)?.url ?? null;
|
|
3979
|
+
}
|
|
3980
|
+
function readProjectAdminDatabaseUrl(cwd = process.cwd()) {
|
|
3981
|
+
return process.env.DATABASE_URL ?? readProjectEnvValue("DATABASE_URL", cwd);
|
|
3962
3982
|
}
|
|
3963
3983
|
function readProjectWriteDatabaseUrl(cwd = process.cwd()) {
|
|
3964
3984
|
const fromEnv = process.env.ABLO_WRITE_DATABASE_URL;
|
|
@@ -3984,7 +4004,7 @@ function readProjectApiKey(cwd = process.cwd()) {
|
|
|
3984
4004
|
}
|
|
3985
4005
|
return null;
|
|
3986
4006
|
}
|
|
3987
|
-
var import_crypto2, import_fs2, import_path;
|
|
4007
|
+
var import_crypto2, import_fs2, import_path, REPLICATION_URL_VARS;
|
|
3988
4008
|
var init_dbRole = __esm({
|
|
3989
4009
|
"src/cli/dbRole.ts"() {
|
|
3990
4010
|
"use strict";
|
|
@@ -3994,6 +4014,7 @@ var init_dbRole = __esm({
|
|
|
3994
4014
|
init_dist2();
|
|
3995
4015
|
import_fs2 = require("fs");
|
|
3996
4016
|
import_path = require("path");
|
|
4017
|
+
REPLICATION_URL_VARS = ["ABLO_REPLICATION_DATABASE_URL"];
|
|
3997
4018
|
}
|
|
3998
4019
|
});
|
|
3999
4020
|
|
|
@@ -4816,6 +4837,9 @@ function coerceBackfill(raw) {
|
|
|
4816
4837
|
if (raw !== "" && !Number.isNaN(Number(raw))) return Number(raw);
|
|
4817
4838
|
return raw;
|
|
4818
4839
|
}
|
|
4840
|
+
function apiBaseUrl() {
|
|
4841
|
+
return (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
4842
|
+
}
|
|
4819
4843
|
function fmtSignal(s) {
|
|
4820
4844
|
const sig = s;
|
|
4821
4845
|
const where = sig.field ? `${sig.model}.${sig.field}` : sig.model;
|
|
@@ -5220,7 +5244,7 @@ async function push(argv) {
|
|
|
5220
5244
|
} else if (code === "schema_provisioning_forbidden") {
|
|
5221
5245
|
console.error(
|
|
5222
5246
|
import_picocolors4.default.dim(
|
|
5223
|
-
` This is not a key problem \u2014 the push was authorized, but the target database refused to let the engine create tables (Postgres 42501). On the replication read path Ablo never runs DDL: register your database as a data source with ${import_picocolors4.default.bold("npx ablo connect
|
|
5247
|
+
` This is not a key problem \u2014 the push was authorized, but the target database refused to let the engine create tables (Postgres 42501). On the replication read path Ablo never runs DDL: register your database as a data source with ${import_picocolors4.default.bold("npx ablo connect register")}, and pushes to that environment record the schema as metadata only \u2014 no tables are created anywhere.`
|
|
5224
5248
|
)
|
|
5225
5249
|
);
|
|
5226
5250
|
} else if (args.apiKey != null && classifyCredentialKind(args.apiKey) === "restricted") {
|
|
@@ -5264,129 +5288,19 @@ var init_push = __esm({
|
|
|
5264
5288
|
}
|
|
5265
5289
|
});
|
|
5266
5290
|
|
|
5267
|
-
// src/cli/remoteValidation.ts
|
|
5268
|
-
function dialFailureReason(err) {
|
|
5269
|
-
if (err === null || typeof err !== "object") return null;
|
|
5270
|
-
const coded = err;
|
|
5271
|
-
const code = typeof coded.code === "string" ? coded.code : null;
|
|
5272
|
-
if (code && DIAL_FAILURE_CODES.has(code)) {
|
|
5273
|
-
return typeof coded.message === "string" && coded.message.length > 0 ? coded.message : code;
|
|
5274
|
-
}
|
|
5275
|
-
if (Array.isArray(coded.errors)) {
|
|
5276
|
-
for (const member of coded.errors) {
|
|
5277
|
-
const reason = dialFailureReason(member);
|
|
5278
|
-
if (reason) return reason;
|
|
5279
|
-
}
|
|
5280
|
-
}
|
|
5281
|
-
return null;
|
|
5282
|
-
}
|
|
5283
|
-
function validateEndpoint(baseUrl2) {
|
|
5284
|
-
return `${baseUrl2.replace(/\/+$/, "")}/api/v1/datasources/validate`;
|
|
5285
|
-
}
|
|
5286
|
-
function describeRemoteFailure(failure) {
|
|
5287
|
-
switch (failure.item) {
|
|
5288
|
-
case "wal_level":
|
|
5289
|
-
return {
|
|
5290
|
-
label: failure.actual ? `wal_level is ${failure.actual} (need logical)` : `wal_level must be logical`,
|
|
5291
|
-
fix: failure.fix
|
|
5292
|
-
};
|
|
5293
|
-
case "publication":
|
|
5294
|
-
return { label: "the Ablo publication does not exist", fix: failure.fix };
|
|
5295
|
-
case "replication_role":
|
|
5296
|
-
return { label: "the DATABASE_URL role lacks the REPLICATION attribute", fix: failure.fix };
|
|
5297
|
-
case "replica_identity":
|
|
5298
|
-
return {
|
|
5299
|
-
label: `published tables cannot replicate UPDATE/DELETE${failure.actual ? ` (${failure.actual})` : ""}`,
|
|
5300
|
-
fix: failure.fix
|
|
5301
|
-
};
|
|
5302
|
-
case "write_role":
|
|
5303
|
-
return { label: "ABLO_WRITE_DATABASE_URL is not a scoped DML role", fix: failure.fix };
|
|
5304
|
-
case "row_security":
|
|
5305
|
-
return { label: "the direct-write role does not enforce row_security", fix: failure.fix };
|
|
5306
|
-
case "schema_privileges":
|
|
5307
|
-
return { label: "the direct-write role has unsafe schema privileges", fix: failure.fix };
|
|
5308
|
-
case "idempotency_ledger":
|
|
5309
|
-
return { label: "ablo_idempotency is missing or has unsafe grants", fix: failure.fix };
|
|
5310
|
-
case "table_privileges":
|
|
5311
|
-
return {
|
|
5312
|
-
label: `the direct-write role lacks application DML${failure.actual ? ` (${failure.actual})` : ""}`,
|
|
5313
|
-
fix: failure.fix
|
|
5314
|
-
};
|
|
5315
|
-
case "logical_marker":
|
|
5316
|
-
return { label: "the direct-write role cannot emit the correlation marker", fix: failure.fix };
|
|
5317
|
-
default:
|
|
5318
|
-
return { label: failure.item, fix: failure.fix };
|
|
5319
|
-
}
|
|
5320
|
-
}
|
|
5321
|
-
function parseWireFailures(value) {
|
|
5322
|
-
if (!Array.isArray(value)) return [];
|
|
5323
|
-
const failures = [];
|
|
5324
|
-
for (const entry of value) {
|
|
5325
|
-
if (entry === null || typeof entry !== "object") continue;
|
|
5326
|
-
const { item, actual, fix } = entry;
|
|
5327
|
-
if (typeof item !== "string" || typeof fix !== "string") continue;
|
|
5328
|
-
failures.push({ item, fix, ...typeof actual === "string" ? { actual } : {} });
|
|
5329
|
-
}
|
|
5330
|
-
return failures;
|
|
5331
|
-
}
|
|
5332
|
-
async function requestRemoteValidation(input) {
|
|
5333
|
-
const doFetch = input.fetchImpl ?? fetch;
|
|
5334
|
-
let res;
|
|
5335
|
-
try {
|
|
5336
|
-
res = await doFetch(validateEndpoint(input.apiUrl), {
|
|
5337
|
-
method: "POST",
|
|
5338
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${input.apiKey}` },
|
|
5339
|
-
body: JSON.stringify({
|
|
5340
|
-
connectionString: input.connectionString,
|
|
5341
|
-
...input.writeConnectionString ? { writeConnectionString: input.writeConnectionString } : {}
|
|
5342
|
-
})
|
|
5343
|
-
});
|
|
5344
|
-
} catch (err) {
|
|
5345
|
-
return {
|
|
5346
|
-
ok: false,
|
|
5347
|
-
status: 0,
|
|
5348
|
-
message: `couldn't reach ${input.apiUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
5349
|
-
};
|
|
5350
|
-
}
|
|
5351
|
-
const body = await res.json().catch(() => ({}));
|
|
5352
|
-
if (!res.ok) {
|
|
5353
|
-
const code = typeof body.code === "string" ? body.code : body.error?.code;
|
|
5354
|
-
const message = typeof body.message === "string" ? body.message : typeof body.error?.message === "string" ? body.error.message : `HTTP ${res.status}`;
|
|
5355
|
-
return {
|
|
5356
|
-
ok: false,
|
|
5357
|
-
status: res.status,
|
|
5358
|
-
message,
|
|
5359
|
-
...typeof code === "string" ? { code } : {}
|
|
5360
|
-
};
|
|
5361
|
-
}
|
|
5362
|
-
const reachable = body.reachable === true;
|
|
5363
|
-
return {
|
|
5364
|
-
ok: true,
|
|
5365
|
-
reachable,
|
|
5366
|
-
ready: body.ready === true,
|
|
5367
|
-
...typeof body.reason === "string" ? { reason: body.reason } : {},
|
|
5368
|
-
failures: parseWireFailures(body.failures)
|
|
5369
|
-
};
|
|
5370
|
-
}
|
|
5371
|
-
var DIAL_FAILURE_CODES;
|
|
5372
|
-
var init_remoteValidation = __esm({
|
|
5373
|
-
"src/cli/remoteValidation.ts"() {
|
|
5374
|
-
"use strict";
|
|
5375
|
-
init_cjs_shims();
|
|
5376
|
-
DIAL_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
5377
|
-
"ENOTFOUND",
|
|
5378
|
-
"EAI_AGAIN",
|
|
5379
|
-
"ECONNREFUSED",
|
|
5380
|
-
"ECONNRESET",
|
|
5381
|
-
"ETIMEDOUT",
|
|
5382
|
-
"ENETUNREACH",
|
|
5383
|
-
"EHOSTUNREACH",
|
|
5384
|
-
"CONNECT_TIMEOUT"
|
|
5385
|
-
]);
|
|
5386
|
-
}
|
|
5387
|
-
});
|
|
5388
|
-
|
|
5389
5291
|
// src/source/migrations.ts
|
|
5292
|
+
function addColumnIfAbsent(column, definition) {
|
|
5293
|
+
return `DO $$
|
|
5294
|
+
DECLARE ledger regclass := to_regclass('ablo_idempotency');
|
|
5295
|
+
BEGIN
|
|
5296
|
+
IF ledger IS NOT NULL AND NOT EXISTS (
|
|
5297
|
+
SELECT 1 FROM pg_attribute
|
|
5298
|
+
WHERE attrelid = ledger AND attname = '${column}' AND attnum > 0 AND NOT attisdropped
|
|
5299
|
+
) THEN
|
|
5300
|
+
ALTER TABLE ablo_idempotency ADD COLUMN ${column} ${definition};
|
|
5301
|
+
END IF;
|
|
5302
|
+
END $$;`;
|
|
5303
|
+
}
|
|
5390
5304
|
function idempotencyLedgerMigrations() {
|
|
5391
5305
|
return [
|
|
5392
5306
|
{
|
|
@@ -5404,13 +5318,19 @@ function idempotencyLedgerMigrations() {
|
|
|
5404
5318
|
// Nullable only for rows created by older adapter versions. New writes
|
|
5405
5319
|
// always populate it; replaying a legacy NULL row fails closed because
|
|
5406
5320
|
// the adapter cannot prove that the intent matches.
|
|
5407
|
-
|
|
5408
|
-
|
|
5321
|
+
//
|
|
5322
|
+
// The column already exists on any current table (the CREATE above
|
|
5323
|
+
// includes it), so this only matters when upgrading a pre-existing
|
|
5324
|
+
// ledger. `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` checks ownership
|
|
5325
|
+
// BEFORE it evaluates IF NOT EXISTS, so on a ledger that predates the
|
|
5326
|
+
// setup — owned by another role — the no-op still errors "must be owner".
|
|
5327
|
+
// Guard on the catalog (readable regardless of ownership) so the ALTER
|
|
5328
|
+
// runs only when the column is genuinely absent, keeping re-runs safe.
|
|
5329
|
+
up: addColumnIfAbsent("request_hash", "TEXT")
|
|
5409
5330
|
},
|
|
5410
5331
|
{
|
|
5411
5332
|
name: "ablo_idempotency_permanent_retention",
|
|
5412
|
-
up: `
|
|
5413
|
-
ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ NOT NULL DEFAULT 'infinity';`
|
|
5333
|
+
up: addColumnIfAbsent("expires_at", `TIMESTAMPTZ NOT NULL DEFAULT 'infinity'`)
|
|
5414
5334
|
}
|
|
5415
5335
|
];
|
|
5416
5336
|
}
|
|
@@ -5421,13 +5341,460 @@ var init_migrations = __esm({
|
|
|
5421
5341
|
}
|
|
5422
5342
|
});
|
|
5423
5343
|
|
|
5344
|
+
// src/cli/connectSetup.ts
|
|
5345
|
+
function quoteIdent(id) {
|
|
5346
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
5347
|
+
}
|
|
5348
|
+
function quoteLiteral(value) {
|
|
5349
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
5350
|
+
}
|
|
5351
|
+
function scopedSequenceGrant(tables, writeRole) {
|
|
5352
|
+
const names = tables.map(quoteLiteral).join(", ");
|
|
5353
|
+
return `DO $$
|
|
5354
|
+
DECLARE seq regclass;
|
|
5355
|
+
BEGIN
|
|
5356
|
+
FOR seq IN
|
|
5357
|
+
SELECT DISTINCT d.objid::regclass
|
|
5358
|
+
FROM pg_depend d
|
|
5359
|
+
JOIN pg_class s ON s.oid = d.objid AND s.relkind = 'S'
|
|
5360
|
+
JOIN pg_class t ON t.oid = d.refobjid
|
|
5361
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
5362
|
+
WHERE d.deptype IN ('a', 'i') AND n.nspname = 'public' AND t.relname IN (${names})
|
|
5363
|
+
LOOP
|
|
5364
|
+
EXECUTE format('GRANT USAGE, SELECT ON SEQUENCE %s TO ${quoteIdent(writeRole)}', seq);
|
|
5365
|
+
END LOOP;
|
|
5366
|
+
END $$;`;
|
|
5367
|
+
}
|
|
5368
|
+
function connectSetupSql(input) {
|
|
5369
|
+
const role = input.role && input.role.length > 0 ? input.role : ABLO_REPLICATION_ROLE;
|
|
5370
|
+
const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : ABLO_WRITE_ROLE;
|
|
5371
|
+
const tables = input.tables ?? [];
|
|
5372
|
+
const publicationTarget = tables.length > 0 ? `FOR TABLE ${tables.map(quoteIdent).join(", ")}` : "FOR ALL TABLES";
|
|
5373
|
+
const tableList = tables.map(quoteIdent).join(", ");
|
|
5374
|
+
const scoped = tables.length > 0;
|
|
5375
|
+
const applicationGrant = scoped ? `GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE ${tableList} TO ${quoteIdent(writeRole)};` : `GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO ${quoteIdent(writeRole)};`;
|
|
5376
|
+
const replicationReadGrants = scoped ? [`GRANT SELECT ON TABLE ${tableList} TO ${quoteIdent(role)};`] : [
|
|
5377
|
+
`GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${quoteIdent(role)};`,
|
|
5378
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${quoteIdent(role)};`
|
|
5379
|
+
];
|
|
5380
|
+
const writerSequenceGrants = scoped ? [scopedSequenceGrant(tables, writeRole)] : [`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${quoteIdent(writeRole)};`];
|
|
5381
|
+
const ledger = idempotencyLedgerMigrations().map((migration) => migration.up);
|
|
5382
|
+
return [
|
|
5383
|
+
// 1. Turn on logical decoding. Requires a restart (it's not reloadable).
|
|
5384
|
+
`ALTER SYSTEM SET wal_level = 'logical';`,
|
|
5385
|
+
// 2. Publish the tables Ablo should read.
|
|
5386
|
+
`CREATE PUBLICATION ${quoteIdent(ABLO_PUBLICATION)} ${publicationTarget};`,
|
|
5387
|
+
// 3. A least-privilege role: it can stream replication and SELECT the
|
|
5388
|
+
// published tables, nothing more.
|
|
5389
|
+
`CREATE ROLE ${quoteIdent(role)} WITH REPLICATION LOGIN PASSWORD '<password>';`,
|
|
5390
|
+
...replicationReadGrants,
|
|
5391
|
+
// 4. A distinct DML role: no replication, role administration, ownership,
|
|
5392
|
+
// schema creation, or DDL. It runs NOBYPASSRLS with row_security on, so on a
|
|
5393
|
+
// table that HAS row-level-security policies they govern its writes and it
|
|
5394
|
+
// can't bypass them; on a table without policies there is nothing to apply
|
|
5395
|
+
// and the table grants alone bound it.
|
|
5396
|
+
`CREATE ROLE ${quoteIdent(writeRole)} WITH LOGIN PASSWORD '<write-password>' NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE NOREPLICATION NOINHERIT;`,
|
|
5397
|
+
`ALTER ROLE ${quoteIdent(writeRole)} SET row_security = on;`,
|
|
5398
|
+
`GRANT USAGE ON SCHEMA public TO ${quoteIdent(writeRole)};`,
|
|
5399
|
+
applicationGrant,
|
|
5400
|
+
...writerSequenceGrants,
|
|
5401
|
+
...scoped ? [] : [
|
|
5402
|
+
// "All tables" mode only: keep future tables/sequences writable so the
|
|
5403
|
+
// publication doesn't outgrow the grant.
|
|
5404
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${quoteIdent(writeRole)};`,
|
|
5405
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${quoteIdent(writeRole)};`
|
|
5406
|
+
],
|
|
5407
|
+
// 5. Direct uses the durable replay ledger but deliberately no outbox.
|
|
5408
|
+
...ledger,
|
|
5409
|
+
`REVOKE ALL ON TABLE public.ablo_idempotency FROM PUBLIC;`,
|
|
5410
|
+
`GRANT SELECT, INSERT, UPDATE ON TABLE public.ablo_idempotency TO ${quoteIdent(writeRole)};`,
|
|
5411
|
+
`REVOKE DELETE ON TABLE public.ablo_idempotency FROM ${quoteIdent(writeRole)};`,
|
|
5412
|
+
// The writer emits a transactional marker on your WAL so Ablo can correlate
|
|
5413
|
+
// the committed row back to the originating write and confirm it — this
|
|
5414
|
+
// EXECUTE grant is what makes that confirmation possible. Granted by lookup
|
|
5415
|
+
// across every pg_logical_emit_message variant instead of one literal
|
|
5416
|
+
// signature: PostgreSQL 17 adds an optional fourth `flush` parameter, so the
|
|
5417
|
+
// historical three-argument form no longer exists there and a signature-
|
|
5418
|
+
// pinned GRANT fails on an otherwise healthy database.
|
|
5419
|
+
`-- required: grants the writer Ablo's WAL write-confirmation marker function
|
|
5420
|
+
DO $$
|
|
5421
|
+
DECLARE fn regprocedure;
|
|
5422
|
+
BEGIN
|
|
5423
|
+
FOR fn IN
|
|
5424
|
+
SELECT p.oid::regprocedure
|
|
5425
|
+
FROM pg_proc p
|
|
5426
|
+
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
5427
|
+
WHERE n.nspname = 'pg_catalog' AND p.proname = 'pg_logical_emit_message'
|
|
5428
|
+
LOOP
|
|
5429
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO ${quoteIdent(writeRole)}', fn);
|
|
5430
|
+
END LOOP;
|
|
5431
|
+
END $$;`
|
|
5432
|
+
];
|
|
5433
|
+
}
|
|
5434
|
+
async function probeReadiness(sql, opts = {}) {
|
|
5435
|
+
const publication = opts.publication ?? ABLO_PUBLICATION;
|
|
5436
|
+
const items = [];
|
|
5437
|
+
const walRows = await sql.unsafe(
|
|
5438
|
+
`SELECT setting FROM pg_settings WHERE name = 'wal_level'`
|
|
5439
|
+
);
|
|
5440
|
+
const walLevel = walRows[0]?.setting ?? "unknown";
|
|
5441
|
+
items.push(
|
|
5442
|
+
walLevel === "logical" ? { ok: true, label: `wal_level is ${import_picocolors6.default.bold("logical")}` } : {
|
|
5443
|
+
ok: false,
|
|
5444
|
+
label: `wal_level is ${import_picocolors6.default.bold(walLevel)} (need ${import_picocolors6.default.bold("logical")})`,
|
|
5445
|
+
fix: `ALTER SYSTEM SET wal_level = 'logical'; then RESTART Postgres.
|
|
5446
|
+
On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.
|
|
5447
|
+
On Neon enable Logical Replication in the project (Console \u2192 Settings \u2192 Logical Replication, or the API) \u2014 Neon forbids ALTER SYSTEM; the toggle sets wal_level=logical.`
|
|
5448
|
+
}
|
|
5449
|
+
);
|
|
5450
|
+
const pubRows = await sql.unsafe(
|
|
5451
|
+
`SELECT puballtables FROM pg_publication WHERE pubname = $1`,
|
|
5452
|
+
[publication]
|
|
5453
|
+
);
|
|
5454
|
+
const pubRow = pubRows[0];
|
|
5455
|
+
items.push(
|
|
5456
|
+
pubRow ? {
|
|
5457
|
+
ok: true,
|
|
5458
|
+
label: `publication ${import_picocolors6.default.bold(publication)} exists ${import_picocolors6.default.dim(pubRow.puballtables ? "(all tables)" : "(table subset)")}`
|
|
5459
|
+
} : {
|
|
5460
|
+
ok: false,
|
|
5461
|
+
label: `publication ${import_picocolors6.default.bold(publication)} not found`,
|
|
5462
|
+
fix: `CREATE PUBLICATION ${quoteIdent(publication)} FOR ALL TABLES;`
|
|
5463
|
+
}
|
|
5464
|
+
);
|
|
5465
|
+
const roleRows = await sql.unsafe(
|
|
5466
|
+
`SELECT rolreplication, rolsuper FROM pg_roles WHERE rolname = current_user`
|
|
5467
|
+
);
|
|
5468
|
+
const role = roleRows[0];
|
|
5469
|
+
const hasReplication = Boolean(role && (role.rolreplication || role.rolsuper));
|
|
5470
|
+
items.push(
|
|
5471
|
+
hasReplication ? {
|
|
5472
|
+
ok: true,
|
|
5473
|
+
label: `the replication role can stream replication ${import_picocolors6.default.dim("(REPLICATION)")}`
|
|
5474
|
+
} : {
|
|
5475
|
+
ok: false,
|
|
5476
|
+
label: `the replication role lacks the ${import_picocolors6.default.bold("REPLICATION")} attribute`,
|
|
5477
|
+
fix: `ALTER ROLE current_user WITH REPLICATION;
|
|
5478
|
+
On RDS: GRANT rds_replication TO <your_role>;`
|
|
5479
|
+
}
|
|
5480
|
+
);
|
|
5481
|
+
if (pubRows.length > 0) {
|
|
5482
|
+
const badRows = await sql.unsafe(
|
|
5483
|
+
`SELECT c.relname AS table_name, c.relreplident
|
|
5484
|
+
FROM pg_publication_tables pt
|
|
5485
|
+
JOIN pg_class c ON c.relname = pt.tablename
|
|
5486
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
|
|
5487
|
+
WHERE pt.pubname = $1
|
|
5488
|
+
AND (
|
|
5489
|
+
c.relreplident = 'n'
|
|
5490
|
+
OR (
|
|
5491
|
+
c.relreplident = 'd'
|
|
5492
|
+
AND NOT EXISTS (
|
|
5493
|
+
SELECT 1 FROM pg_index i
|
|
5494
|
+
WHERE i.indrelid = c.oid AND i.indisprimary
|
|
5495
|
+
)
|
|
5496
|
+
)
|
|
5497
|
+
)`,
|
|
5498
|
+
[publication]
|
|
5499
|
+
);
|
|
5500
|
+
items.push(
|
|
5501
|
+
badRows.length === 0 ? { ok: true, label: `all published tables have a usable REPLICA IDENTITY` } : {
|
|
5502
|
+
ok: false,
|
|
5503
|
+
label: `${badRows.length} published table${badRows.length === 1 ? "" : "s"} cannot replicate UPDATE/DELETE`,
|
|
5504
|
+
fix: badRows.map(
|
|
5505
|
+
(r2) => `${r2.table_name}: add a PRIMARY KEY, or ALTER TABLE ${quoteIdent(r2.table_name)} REPLICA IDENTITY FULL;`
|
|
5506
|
+
).join("\n")
|
|
5507
|
+
}
|
|
5508
|
+
);
|
|
5509
|
+
}
|
|
5510
|
+
return items;
|
|
5511
|
+
}
|
|
5512
|
+
function registerEndpoint(baseUrl2) {
|
|
5513
|
+
return `${baseUrl2.replace(/\/+$/, "")}/api/v1/datasources`;
|
|
5514
|
+
}
|
|
5515
|
+
async function parseJsonBody(res, schema) {
|
|
5516
|
+
const parsed = schema.safeParse(await res.json().catch(() => null));
|
|
5517
|
+
return parsed.success ? parsed.data : {};
|
|
5518
|
+
}
|
|
5519
|
+
async function registerDirectDataSource(opts) {
|
|
5520
|
+
let res;
|
|
5521
|
+
try {
|
|
5522
|
+
res = await fetch(registerEndpoint(opts.apiUrl), {
|
|
5523
|
+
method: "POST",
|
|
5524
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${opts.apiKey}` },
|
|
5525
|
+
body: JSON.stringify({
|
|
5526
|
+
connection: "direct",
|
|
5527
|
+
connectionString: opts.replicationUrl,
|
|
5528
|
+
writeConnectionString: opts.writeUrl,
|
|
5529
|
+
route: opts.route
|
|
5530
|
+
})
|
|
5531
|
+
});
|
|
5532
|
+
} catch (err) {
|
|
5533
|
+
console.error(
|
|
5534
|
+
import_picocolors6.default.red(
|
|
5535
|
+
`
|
|
5536
|
+
Couldn't reach ${opts.apiUrl}: ${err instanceof Error ? err.message : String(err)}
|
|
5537
|
+
`
|
|
5538
|
+
)
|
|
5539
|
+
);
|
|
5540
|
+
return false;
|
|
5541
|
+
}
|
|
5542
|
+
if (res.ok) {
|
|
5543
|
+
const body2 = await parseJsonBody(res, DataSourceRegisterSuccess);
|
|
5544
|
+
const statusNote = body2.status === "active" ? `${opts.route}, active` : opts.route;
|
|
5545
|
+
console.log(
|
|
5546
|
+
`
|
|
5547
|
+
${import_picocolors6.default.green("\u2713")} Registered${body2.host ? ` ${import_picocolors6.default.dim(body2.host)}` : ""}${body2.id ? ` ${import_picocolors6.default.dim(`(${body2.id})`)}` : ""} as a direct DataSource (${statusNote}).
|
|
5548
|
+
Customer COMMIT is durable acceptance; correlated WAL promotes queued writes to confirmed.
|
|
5549
|
+
`
|
|
5550
|
+
);
|
|
5551
|
+
return true;
|
|
5552
|
+
}
|
|
5553
|
+
const body = await parseJsonBody(res, DataSourceRegisterError);
|
|
5554
|
+
const code = body.code ?? body.error?.code;
|
|
5555
|
+
const message = body.message ?? body.error?.message ?? `HTTP ${res.status}`;
|
|
5556
|
+
console.error(import_picocolors6.default.red(`
|
|
5557
|
+
Registration failed: ${message}`));
|
|
5558
|
+
if (code === "forbidden") {
|
|
5559
|
+
console.error(
|
|
5560
|
+
import_picocolors6.default.dim(
|
|
5561
|
+
` Registering a database needs a ${import_picocolors6.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors6.default.bold("ablo login")} for one.`
|
|
5562
|
+
)
|
|
5563
|
+
);
|
|
5564
|
+
} else if (code === "datasource_connection_unsupported") {
|
|
5565
|
+
console.error(
|
|
5566
|
+
import_picocolors6.default.dim(
|
|
5567
|
+
` This deployment can\u2019t accept connection strings \u2014 use a self-hosted/hosted engine, or the signed endpoint fallback.`
|
|
5568
|
+
)
|
|
5569
|
+
);
|
|
5570
|
+
} else if (code === "database_not_replication_ready" || code === "data_source_blocked") {
|
|
5571
|
+
for (const f of body.details?.failures ?? []) {
|
|
5572
|
+
console.error(
|
|
5573
|
+
` ${import_picocolors6.default.red("\u2717")} ${import_picocolors6.default.bold(f.item ?? "item")}${f.actual ? import_picocolors6.default.dim(` (${f.actual})`) : ""}`
|
|
5574
|
+
);
|
|
5575
|
+
if (f.fix)
|
|
5576
|
+
for (const line of f.fix.split("\n")) console.error(` ${import_picocolors6.default.red("\u2022")} ${line}`);
|
|
5577
|
+
}
|
|
5578
|
+
console.error(
|
|
5579
|
+
import_picocolors6.default.dim(`
|
|
5580
|
+
Apply the fixes, verify with ${import_picocolors6.default.bold("ablo connect check")}, then re-run.`)
|
|
5581
|
+
);
|
|
5582
|
+
} else if (code === "database_unreachable" || code === "source_unreachable") {
|
|
5583
|
+
if (body.details?.reason) console.error(import_picocolors6.default.dim(` ${body.details.reason}`));
|
|
5584
|
+
console.error(
|
|
5585
|
+
import_picocolors6.default.dim(
|
|
5586
|
+
` Ablo's servers must be able to reach this database \u2014 a localhost or private-network
|
|
5587
|
+
Postgres can't use the direct path. Establish an allowlist, PrivateLink, peering, or VPN.
|
|
5588
|
+
Only when no inbound route is possible, register the signed ${import_picocolors6.default.bold("dataSource()")} endpoint fallback.`
|
|
5589
|
+
)
|
|
5590
|
+
);
|
|
5591
|
+
}
|
|
5592
|
+
console.error();
|
|
5593
|
+
return false;
|
|
5594
|
+
}
|
|
5595
|
+
var import_picocolors6, import_zod6, ABLO_PUBLICATION, ABLO_REPLICATION_ROLE, ABLO_WRITE_ROLE, DIRECT_DATA_SOURCE_ROUTES, DataSourceRegisterSuccess, DataSourceRegisterError;
|
|
5596
|
+
var init_connectSetup = __esm({
|
|
5597
|
+
"src/cli/connectSetup.ts"() {
|
|
5598
|
+
"use strict";
|
|
5599
|
+
init_cjs_shims();
|
|
5600
|
+
import_picocolors6 = __toESM(require_picocolors(), 1);
|
|
5601
|
+
import_zod6 = require("zod");
|
|
5602
|
+
init_migrations();
|
|
5603
|
+
ABLO_PUBLICATION = "ablo_publication";
|
|
5604
|
+
ABLO_REPLICATION_ROLE = "ablo_replicator";
|
|
5605
|
+
ABLO_WRITE_ROLE = "ablo_writer";
|
|
5606
|
+
DIRECT_DATA_SOURCE_ROUTES = [
|
|
5607
|
+
"public-allowlist",
|
|
5608
|
+
"privatelink",
|
|
5609
|
+
"peering",
|
|
5610
|
+
"vpn"
|
|
5611
|
+
];
|
|
5612
|
+
DataSourceRegisterSuccess = import_zod6.z.object({
|
|
5613
|
+
id: import_zod6.z.string().optional(),
|
|
5614
|
+
host: import_zod6.z.string().optional(),
|
|
5615
|
+
status: import_zod6.z.string().optional()
|
|
5616
|
+
});
|
|
5617
|
+
DataSourceRegisterError = import_zod6.z.object({
|
|
5618
|
+
code: import_zod6.z.string().optional(),
|
|
5619
|
+
message: import_zod6.z.string().optional(),
|
|
5620
|
+
details: import_zod6.z.object({
|
|
5621
|
+
failures: import_zod6.z.array(
|
|
5622
|
+
import_zod6.z.object({
|
|
5623
|
+
item: import_zod6.z.string().optional(),
|
|
5624
|
+
actual: import_zod6.z.string().optional(),
|
|
5625
|
+
fix: import_zod6.z.string().optional()
|
|
5626
|
+
})
|
|
5627
|
+
).optional(),
|
|
5628
|
+
reason: import_zod6.z.string().optional()
|
|
5629
|
+
}).optional(),
|
|
5630
|
+
error: import_zod6.z.object({ code: import_zod6.z.string().optional(), message: import_zod6.z.string().optional() }).optional()
|
|
5631
|
+
});
|
|
5632
|
+
}
|
|
5633
|
+
});
|
|
5634
|
+
|
|
5635
|
+
// src/cli/disconnect.ts
|
|
5636
|
+
var disconnect_exports = {};
|
|
5637
|
+
__export(disconnect_exports, {
|
|
5638
|
+
DISCONNECT_USAGE: () => DISCONNECT_USAGE,
|
|
5639
|
+
disconnect: () => disconnect
|
|
5640
|
+
});
|
|
5641
|
+
function planeLabel(target) {
|
|
5642
|
+
const confirmed = target.confirmed;
|
|
5643
|
+
const project = confirmed?.project?.name ?? confirmed?.project?.slug ?? confirmed?.projectId ?? "the default project";
|
|
5644
|
+
const env = confirmed?.environment ?? target.keyEnv ?? "unknown environment";
|
|
5645
|
+
return { project, env };
|
|
5646
|
+
}
|
|
5647
|
+
async function disconnect(argv) {
|
|
5648
|
+
let skipConfirm = false;
|
|
5649
|
+
for (const arg of argv) {
|
|
5650
|
+
if (arg === "--yes" || arg === "-y") skipConfirm = true;
|
|
5651
|
+
else if (arg === "--help" || arg === "-h") {
|
|
5652
|
+
console.log(DISCONNECT_USAGE);
|
|
5653
|
+
return;
|
|
5654
|
+
} else {
|
|
5655
|
+
console.error(
|
|
5656
|
+
import_picocolors7.default.red(` Unknown flag ${import_picocolors7.default.bold(arg)}.`) + import_picocolors7.default.dim(` See ${import_picocolors7.default.bold("ablo connect deregister --help")}.`)
|
|
5657
|
+
);
|
|
5658
|
+
process.exit(1);
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
console.log(
|
|
5662
|
+
`
|
|
5663
|
+
${brand("ablo")} ${import_picocolors7.default.dim("connect deregister")} ${import_picocolors7.default.dim("remove this project's data source")}
|
|
5664
|
+
`
|
|
5665
|
+
);
|
|
5666
|
+
const resolved = resolveEffectiveApiKey();
|
|
5667
|
+
const apiKey = resolved.key;
|
|
5668
|
+
const keySource = resolved.source ?? "stored";
|
|
5669
|
+
if (!apiKey) {
|
|
5670
|
+
console.error(
|
|
5671
|
+
import_picocolors7.default.red(` Disconnecting needs an API key, and none was found.`) + import_picocolors7.default.dim(
|
|
5672
|
+
` Run ${import_picocolors7.default.bold("ablo login")} (or set ${import_picocolors7.default.bold("ABLO_API_KEY")}), then re-run ${import_picocolors7.default.bold("ablo connect deregister")}.`
|
|
5673
|
+
)
|
|
5674
|
+
);
|
|
5675
|
+
process.exit(1);
|
|
5676
|
+
}
|
|
5677
|
+
const apiUrl2 = apiBaseUrl();
|
|
5678
|
+
const target = await resolveTarget({ url: apiUrl2, apiKey, keySource });
|
|
5679
|
+
const { project, env } = planeLabel(target);
|
|
5680
|
+
const envLabel = env === "production" ? import_picocolors7.default.yellow(env) : import_picocolors7.default.dim(env);
|
|
5681
|
+
for (const mismatch of target.mismatches) {
|
|
5682
|
+
console.log(` ${import_picocolors7.default.yellow("\u26A0")} ${describeMismatch(mismatch)}
|
|
5683
|
+
`);
|
|
5684
|
+
}
|
|
5685
|
+
if (!skipConfirm) {
|
|
5686
|
+
if (!process.stdout.isTTY) {
|
|
5687
|
+
console.error(
|
|
5688
|
+
import_picocolors7.default.dim(` Re-run with ${import_picocolors7.default.bold("--yes")} to disconnect in a non-interactive session.
|
|
5689
|
+
`)
|
|
5690
|
+
);
|
|
5691
|
+
process.exit(1);
|
|
5692
|
+
}
|
|
5693
|
+
const proceed = await ye({
|
|
5694
|
+
message: `Disconnect the data source for ${import_picocolors7.default.bold(project)} in ${envLabel}?`,
|
|
5695
|
+
initialValue: true
|
|
5696
|
+
});
|
|
5697
|
+
if (pD(proceed) || !proceed) {
|
|
5698
|
+
console.log(import_picocolors7.default.dim(` Nothing changed. The data source is still connected.
|
|
5699
|
+
`));
|
|
5700
|
+
process.exit(0);
|
|
5701
|
+
}
|
|
5702
|
+
}
|
|
5703
|
+
let res;
|
|
5704
|
+
try {
|
|
5705
|
+
res = await fetch(registerEndpoint(apiUrl2), {
|
|
5706
|
+
method: "DELETE",
|
|
5707
|
+
headers: { authorization: `Bearer ${apiKey}` }
|
|
5708
|
+
});
|
|
5709
|
+
} catch (err) {
|
|
5710
|
+
console.error(
|
|
5711
|
+
import_picocolors7.default.red(`
|
|
5712
|
+
Couldn't reach ${apiUrl2}: ${err instanceof Error ? err.message : String(err)}
|
|
5713
|
+
`)
|
|
5714
|
+
);
|
|
5715
|
+
process.exit(1);
|
|
5716
|
+
}
|
|
5717
|
+
if (res.ok) {
|
|
5718
|
+
const body2 = await res.json().catch(() => ({}));
|
|
5719
|
+
const parts = [];
|
|
5720
|
+
if (body2.cleared?.direct) parts.push("the direct database registration");
|
|
5721
|
+
if (body2.cleared?.endpoints) {
|
|
5722
|
+
parts.push(
|
|
5723
|
+
`${body2.cleared.endpoints} endpoint registration${body2.cleared.endpoints === 1 ? "" : "s"}`
|
|
5724
|
+
);
|
|
5725
|
+
}
|
|
5726
|
+
const what = parts.length > 0 ? parts.join(" and ") : "the data source";
|
|
5727
|
+
console.log(
|
|
5728
|
+
`
|
|
5729
|
+
${import_picocolors7.default.green("\u2713")} Disconnected ${what} for ${import_picocolors7.default.bold(project)} in ${envLabel}. Reconnect with ${import_picocolors7.default.bold("ablo connect")}.
|
|
5730
|
+
`
|
|
5731
|
+
);
|
|
5732
|
+
return;
|
|
5733
|
+
}
|
|
5734
|
+
const body = await res.json().catch(() => ({}));
|
|
5735
|
+
const code = body.code ?? body.error?.code;
|
|
5736
|
+
const message = body.message ?? body.error?.message ?? `HTTP ${res.status}`;
|
|
5737
|
+
if (code === "entity_not_found") {
|
|
5738
|
+
console.log(import_picocolors7.default.dim(` No data source registered for ${project} in ${env}.
|
|
5739
|
+
`));
|
|
5740
|
+
return;
|
|
5741
|
+
}
|
|
5742
|
+
console.error(import_picocolors7.default.red(`
|
|
5743
|
+
Disconnect failed: ${message}`));
|
|
5744
|
+
if (code === "forbidden") {
|
|
5745
|
+
console.error(
|
|
5746
|
+
import_picocolors7.default.dim(
|
|
5747
|
+
` Disconnecting needs a ${import_picocolors7.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors7.default.bold("ablo login")} for one.`
|
|
5748
|
+
)
|
|
5749
|
+
);
|
|
5750
|
+
}
|
|
5751
|
+
console.error();
|
|
5752
|
+
process.exit(1);
|
|
5753
|
+
}
|
|
5754
|
+
var import_picocolors7, DISCONNECT_USAGE;
|
|
5755
|
+
var init_disconnect = __esm({
|
|
5756
|
+
"src/cli/disconnect.ts"() {
|
|
5757
|
+
"use strict";
|
|
5758
|
+
init_cjs_shims();
|
|
5759
|
+
import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
5760
|
+
init_dist2();
|
|
5761
|
+
init_config();
|
|
5762
|
+
init_push();
|
|
5763
|
+
init_theme();
|
|
5764
|
+
init_connectSetup();
|
|
5765
|
+
init_target();
|
|
5766
|
+
DISCONNECT_USAGE = `${brand("ablo")} connect deregister ${import_picocolors7.default.dim("remove this project's data source")}
|
|
5767
|
+
|
|
5768
|
+
Usage
|
|
5769
|
+
npx ablo connect deregister Remove the active project's data source (confirms first)
|
|
5770
|
+
npx ablo connect deregister --yes Skip the confirmation
|
|
5771
|
+
|
|
5772
|
+
Acts on one plane \u2014 the active project (${import_picocolors7.default.bold("ablo projects use")}) in the active
|
|
5773
|
+
environment (${import_picocolors7.default.bold("ablo mode")}), shown before it runs. Removes the registration and
|
|
5774
|
+
Ablo's replication state for that plane, so Ablo stops reading and writing the
|
|
5775
|
+
database. Reconnect with ${import_picocolors7.default.bold("ablo connect")}.`;
|
|
5776
|
+
}
|
|
5777
|
+
});
|
|
5778
|
+
|
|
5424
5779
|
// src/cli/connectApply.ts
|
|
5425
5780
|
var connectApply_exports = {};
|
|
5426
5781
|
__export(connectApply_exports, {
|
|
5427
5782
|
connectApplyPlan: () => connectApplyPlan,
|
|
5783
|
+
detectProvider: () => detectProvider,
|
|
5784
|
+
ledgerBlockedBy: () => ledgerBlockedBy,
|
|
5785
|
+
logicalReplicationGuidance: () => logicalReplicationGuidance,
|
|
5428
5786
|
passwordClause: () => passwordClause,
|
|
5787
|
+
postRegistrationOutcome: () => postRegistrationOutcome,
|
|
5429
5788
|
runConnectApply: () => runConnectApply
|
|
5430
5789
|
});
|
|
5790
|
+
function postRegistrationOutcome(input) {
|
|
5791
|
+
if (input.registered) return { exitCode: 0, notice: null };
|
|
5792
|
+
if (!input.rotating) return { exitCode: 1, notice: null };
|
|
5793
|
+
return {
|
|
5794
|
+
exitCode: 1,
|
|
5795
|
+
notice: "The new passwords are set in your database, but Ablo could not be updated with them.\nAblo still holds the previous password, which no longer works \u2014 writes will fail until\nyou re-run `ablo connect rotate` (each run is idempotent and rotates a fresh password)."
|
|
5796
|
+
};
|
|
5797
|
+
}
|
|
5431
5798
|
function passwordClause(password, mode2) {
|
|
5432
5799
|
return mode2 === "scram-verifier" ? scramSha256Verifier(password) : password.replace(/'/g, "''");
|
|
5433
5800
|
}
|
|
@@ -5442,19 +5809,28 @@ function connectApplyPlan(input) {
|
|
|
5442
5809
|
const role = input.role && input.role.length > 0 ? input.role : ABLO_REPLICATION_ROLE;
|
|
5443
5810
|
const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : ABLO_WRITE_ROLE;
|
|
5444
5811
|
const tables = input.tables ?? [];
|
|
5812
|
+
const provider = input.provider ?? "generic";
|
|
5445
5813
|
const recipe = connectSetupSql({ tables, role, writeRole });
|
|
5446
5814
|
const isWal = (s) => s.startsWith("ALTER SYSTEM SET wal_level");
|
|
5447
5815
|
const isPublication = (s) => s.startsWith("CREATE PUBLICATION");
|
|
5448
|
-
const isRoleCreate = (s) =>
|
|
5816
|
+
const isRoleCreate = (s) => s.startsWith("CREATE ROLE ");
|
|
5449
5817
|
const grants = recipe.filter((s) => !isWal(s) && !isPublication(s) && !isRoleCreate(s));
|
|
5450
5818
|
const publicationTarget = tables.length > 0 ? `FOR TABLE ${tables.map(quoteIdent).join(", ")}` : "FOR ALL TABLES";
|
|
5451
|
-
|
|
5452
|
-
{
|
|
5819
|
+
const walStep = input.walAlreadyLogical ? [] : [
|
|
5820
|
+
provider === "generic" ? {
|
|
5453
5821
|
key: "wal",
|
|
5454
5822
|
title: "Turn on logical replication",
|
|
5455
|
-
detail: "lets Ablo read your changes as they happen",
|
|
5823
|
+
detail: "lets Ablo read your changes as they happen (needs a restart to take effect)",
|
|
5456
5824
|
sql: [`ALTER SYSTEM SET wal_level = 'logical';`]
|
|
5457
|
-
}
|
|
5825
|
+
} : {
|
|
5826
|
+
key: "wal",
|
|
5827
|
+
title: "Turn on logical replication",
|
|
5828
|
+
detail: logicalReplicationGuidance(provider),
|
|
5829
|
+
sql: []
|
|
5830
|
+
}
|
|
5831
|
+
];
|
|
5832
|
+
return [
|
|
5833
|
+
...walStep,
|
|
5458
5834
|
{
|
|
5459
5835
|
key: "publication",
|
|
5460
5836
|
title: "Publish your tables to Ablo",
|
|
@@ -5475,7 +5851,7 @@ END $$;`
|
|
|
5475
5851
|
{
|
|
5476
5852
|
key: "write-role",
|
|
5477
5853
|
title: "Create a scoped writer role",
|
|
5478
|
-
detail: `${writeRole} \u2014 writes rows through Ablo
|
|
5854
|
+
detail: `${writeRole} \u2014 writes rows through Ablo; where a table has row-level-security policies, they govern its writes too (it can't bypass them)`,
|
|
5479
5855
|
sql: [
|
|
5480
5856
|
idempotentRole(
|
|
5481
5857
|
writeRole,
|
|
@@ -5496,53 +5872,23 @@ function isPlaintextRefusal(err) {
|
|
|
5496
5872
|
const message = err instanceof Error ? err.message : String(err);
|
|
5497
5873
|
return /plaintext password/i.test(message);
|
|
5498
5874
|
}
|
|
5499
|
-
function persistScopedUrls(vars, cwd = process.cwd()) {
|
|
5500
|
-
const targets = [".env.local", ".env"];
|
|
5501
|
-
const existing = targets.find((name) => (0, import_fs6.existsSync)((0, import_path4.resolve)(cwd, name)));
|
|
5502
|
-
const file = existing ?? ".env.local";
|
|
5503
|
-
const path = (0, import_path4.resolve)(cwd, file);
|
|
5504
|
-
let content = (0, import_fs6.existsSync)(path) ? (0, import_fs6.readFileSync)(path, "utf8") : "";
|
|
5505
|
-
for (const [name, value] of Object.entries(vars)) {
|
|
5506
|
-
const line = `${name}=${value}`;
|
|
5507
|
-
const pattern = new RegExp(`^${name}=.*$`, "m");
|
|
5508
|
-
if (pattern.test(content)) {
|
|
5509
|
-
content = content.replace(pattern, line);
|
|
5510
|
-
} else {
|
|
5511
|
-
content = `${content}${content.endsWith("\n") || content.length === 0 ? "" : "\n"}${line}
|
|
5512
|
-
`;
|
|
5513
|
-
}
|
|
5514
|
-
}
|
|
5515
|
-
if (!(0, import_fs6.existsSync)(path)) {
|
|
5516
|
-
(0, import_fs6.writeFileSync)(path, content, { mode: 384 });
|
|
5517
|
-
} else {
|
|
5518
|
-
(0, import_fs6.writeFileSync)(path, content);
|
|
5519
|
-
}
|
|
5520
|
-
const gitignorePath = (0, import_path4.resolve)(cwd, ".gitignore");
|
|
5521
|
-
const gitignore = (0, import_fs6.existsSync)(gitignorePath) ? (0, import_fs6.readFileSync)(gitignorePath, "utf8") : "";
|
|
5522
|
-
if (!/^\.env(\.local|\*|\.\*)?$/m.test(gitignore)) {
|
|
5523
|
-
(0, import_fs6.appendFileSync)(
|
|
5524
|
-
gitignorePath,
|
|
5525
|
-
`${gitignore.endsWith("\n") || gitignore.length === 0 ? "" : "\n"}.env.local
|
|
5526
|
-
`
|
|
5527
|
-
);
|
|
5528
|
-
}
|
|
5529
|
-
return file;
|
|
5530
|
-
}
|
|
5531
5875
|
function printPlan2(steps, showSql) {
|
|
5532
5876
|
console.log(` This sets up your database for Ablo:
|
|
5533
5877
|
`);
|
|
5534
5878
|
for (const step of steps) {
|
|
5535
|
-
console.log(` ${
|
|
5879
|
+
console.log(` ${import_picocolors8.default.green("\u2022")} ${step.title}`);
|
|
5536
5880
|
if (showSql) {
|
|
5537
5881
|
for (const statement of step.sql) {
|
|
5538
|
-
for (const line of statement.split("\n")) console.log(` ${
|
|
5882
|
+
for (const line of statement.split("\n")) console.log(` ${import_picocolors8.default.dim(line)}`);
|
|
5539
5883
|
}
|
|
5540
5884
|
}
|
|
5541
5885
|
}
|
|
5542
5886
|
console.log(
|
|
5543
|
-
|
|
5887
|
+
import_picocolors8.default.dim(
|
|
5888
|
+
`
|
|
5544
5889
|
Your admin password stays on this machine.${showSql ? "" : " (--show-sql for the exact statements)"}
|
|
5545
|
-
`
|
|
5890
|
+
`
|
|
5891
|
+
)
|
|
5546
5892
|
);
|
|
5547
5893
|
}
|
|
5548
5894
|
async function adminCanCreateRoles(sql) {
|
|
@@ -5551,8 +5897,54 @@ async function adminCanCreateRoles(sql) {
|
|
|
5551
5897
|
);
|
|
5552
5898
|
return rows[0] ?? null;
|
|
5553
5899
|
}
|
|
5900
|
+
function ledgerBlockedBy(row) {
|
|
5901
|
+
if (!row || row.is_owner || row.is_superuser) return null;
|
|
5902
|
+
return row.owner;
|
|
5903
|
+
}
|
|
5904
|
+
async function unmanageableLedgerOwner(sql) {
|
|
5905
|
+
const rows = await sql.unsafe(
|
|
5906
|
+
`SELECT pg_get_userbyid(c.relowner) AS owner,
|
|
5907
|
+
c.relowner = r.oid AS is_owner,
|
|
5908
|
+
r.rolsuper AS is_superuser
|
|
5909
|
+
FROM pg_class c
|
|
5910
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
5911
|
+
JOIN pg_roles r ON r.rolname = current_user
|
|
5912
|
+
WHERE c.relkind = 'r'
|
|
5913
|
+
AND c.relname = 'ablo_idempotency'
|
|
5914
|
+
AND n.nspname = 'public'`
|
|
5915
|
+
);
|
|
5916
|
+
return ledgerBlockedBy(rows[0]);
|
|
5917
|
+
}
|
|
5918
|
+
function detectProvider(hostOrTarget) {
|
|
5919
|
+
const host = hostOrTarget.toLowerCase();
|
|
5920
|
+
if (host.includes("neon.tech")) return "neon";
|
|
5921
|
+
if (host.includes("supabase.co") || host.includes("supabase.com") || host.includes("pooler.supabase")) {
|
|
5922
|
+
return "supabase";
|
|
5923
|
+
}
|
|
5924
|
+
if (host.includes("rds.amazonaws.com") || host.includes(".rds.")) return "rds";
|
|
5925
|
+
return "generic";
|
|
5926
|
+
}
|
|
5927
|
+
function logicalReplicationGuidance(provider) {
|
|
5928
|
+
switch (provider) {
|
|
5929
|
+
case "neon":
|
|
5930
|
+
return `enable logical replication in your Neon project settings \u2014 it can't be set over SQL`;
|
|
5931
|
+
case "supabase":
|
|
5932
|
+
return `raise wal_level to logical in your Supabase project's database settings`;
|
|
5933
|
+
case "rds":
|
|
5934
|
+
return `set rds.logical_replication = 1 in the instance's parameter group, then reboot`;
|
|
5935
|
+
case "generic":
|
|
5936
|
+
return `run the ALTER SYSTEM above, then restart Postgres \u2014 wal_level is not reloadable`;
|
|
5937
|
+
}
|
|
5938
|
+
}
|
|
5939
|
+
async function currentWalLevel(sql) {
|
|
5940
|
+
try {
|
|
5941
|
+
const rows = await sql.unsafe(`SHOW wal_level`);
|
|
5942
|
+
return rows[0]?.wal_level ?? "";
|
|
5943
|
+
} catch {
|
|
5944
|
+
return "";
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5554
5947
|
async function executePlan(sql, steps, rebuildPlaintext) {
|
|
5555
|
-
let walDeferred = false;
|
|
5556
5948
|
let plaintextSteps = null;
|
|
5557
5949
|
for (const step of steps) {
|
|
5558
5950
|
for (const statement of step.sql) {
|
|
@@ -5560,7 +5952,6 @@ async function executePlan(sql, steps, rebuildPlaintext) {
|
|
|
5560
5952
|
await sql.unsafe(statement);
|
|
5561
5953
|
} catch (err) {
|
|
5562
5954
|
if (step.key === "wal") {
|
|
5563
|
-
walDeferred = true;
|
|
5564
5955
|
continue;
|
|
5565
5956
|
}
|
|
5566
5957
|
if ((step.key === "replication-role" || step.key === "write-role") && isPlaintextRefusal(err)) {
|
|
@@ -5575,39 +5966,86 @@ async function executePlan(sql, steps, rebuildPlaintext) {
|
|
|
5575
5966
|
}
|
|
5576
5967
|
}
|
|
5577
5968
|
}
|
|
5578
|
-
return { walDeferred };
|
|
5579
5969
|
}
|
|
5580
5970
|
async function runConnectApply(args) {
|
|
5581
|
-
const
|
|
5971
|
+
const rotating = args.rotate;
|
|
5972
|
+
const verb = rotating ? "connect rotate" : "connect apply";
|
|
5973
|
+
const adminUrl = args.url ?? readProjectAdminDatabaseUrl();
|
|
5582
5974
|
if (!adminUrl) {
|
|
5583
5975
|
console.error(
|
|
5584
|
-
|
|
5976
|
+
import_picocolors8.default.red(" No admin connection string.") + import_picocolors8.default.dim(
|
|
5977
|
+
` Pass ${import_picocolors8.default.bold("--url <admin-conn>")} (or set ${import_picocolors8.default.bold("DATABASE_URL")}) and re-run.`
|
|
5978
|
+
)
|
|
5979
|
+
);
|
|
5980
|
+
process.exit(1);
|
|
5981
|
+
}
|
|
5982
|
+
const adminSource = args.url ? "--url" : "DATABASE_URL";
|
|
5983
|
+
let target = "your database";
|
|
5984
|
+
try {
|
|
5985
|
+
const parsed = new URL(adminUrl);
|
|
5986
|
+
target = `${parsed.host}${parsed.pathname === "/" ? "" : parsed.pathname}`;
|
|
5987
|
+
} catch {
|
|
5988
|
+
}
|
|
5989
|
+
const apiKey = resolveApiKey();
|
|
5990
|
+
if (!apiKey) {
|
|
5991
|
+
console.error(
|
|
5992
|
+
import_picocolors8.default.red(" Not logged in.") + import_picocolors8.default.dim(
|
|
5993
|
+
` Run ${import_picocolors8.default.bold("ablo login")} (or set ${import_picocolors8.default.bold("ABLO_API_KEY")}) so Ablo knows which project to register this database for.`
|
|
5994
|
+
)
|
|
5585
5995
|
);
|
|
5586
5996
|
process.exit(1);
|
|
5587
5997
|
}
|
|
5588
5998
|
console.log(
|
|
5589
5999
|
`
|
|
5590
|
-
${brand("ablo")} ${
|
|
6000
|
+
${brand("ablo")} ${import_picocolors8.default.dim(verb)} ${import_picocolors8.default.dim(rotating ? "re-key the scoped roles" : "set up your database for Ablo")}
|
|
5591
6001
|
`
|
|
5592
6002
|
);
|
|
5593
|
-
|
|
5594
|
-
} })
|
|
6003
|
+
console.log(
|
|
6004
|
+
` ${import_picocolors8.default.dim("\u2192")} ${import_picocolors8.default.bold(target)}${adminSource === "DATABASE_URL" ? import_picocolors8.default.dim(" (admin via DATABASE_URL)") : ""}
|
|
6005
|
+
`
|
|
6006
|
+
);
|
|
6007
|
+
const admin = src_default(adminUrl, {
|
|
6008
|
+
max: 1,
|
|
6009
|
+
prepare: false,
|
|
6010
|
+
connect_timeout: 10,
|
|
6011
|
+
onnotice: () => {
|
|
6012
|
+
}
|
|
6013
|
+
});
|
|
5595
6014
|
let capability;
|
|
5596
6015
|
try {
|
|
5597
6016
|
capability = await adminCanCreateRoles(admin);
|
|
5598
6017
|
} catch (err) {
|
|
5599
6018
|
await admin.end({ timeout: 2 }).catch(() => void 0);
|
|
5600
6019
|
const pg = err ?? {};
|
|
5601
|
-
console.error(
|
|
6020
|
+
console.error(import_picocolors8.default.red(` Couldn't connect: ${pg.message ?? String(err)}`));
|
|
5602
6021
|
process.exit(1);
|
|
5603
6022
|
}
|
|
5604
6023
|
if (!capability || !(capability.rolsuper || capability.rolcreaterole)) {
|
|
5605
6024
|
await admin.end({ timeout: 2 });
|
|
5606
6025
|
console.error(
|
|
5607
|
-
|
|
6026
|
+
import_picocolors8.default.red(` ${capability?.rolname ?? "This role"} can't create roles.`) + import_picocolors8.default.dim(` Point ${import_picocolors8.default.bold("--url")} at your owner/admin connection and re-run.`)
|
|
6027
|
+
);
|
|
6028
|
+
process.exit(1);
|
|
6029
|
+
}
|
|
6030
|
+
const blockingOwner = await unmanageableLedgerOwner(admin).catch(() => null);
|
|
6031
|
+
if (blockingOwner) {
|
|
6032
|
+
await admin.end({ timeout: 2 });
|
|
6033
|
+
console.error(
|
|
6034
|
+
import_picocolors8.default.red(
|
|
6035
|
+
`
|
|
6036
|
+
${import_picocolors8.default.bold("ablo_idempotency")} already exists on ${target}, owned by ${import_picocolors8.default.bold(blockingOwner)}, but you connected as ${import_picocolors8.default.bold(capability.rolname)}.`
|
|
6037
|
+
) + `
|
|
6038
|
+
Ablo's setup grants the writer role access to this ledger, and Postgres reserves that
|
|
6039
|
+
for the table's owner. Either re-run pointing ${import_picocolors8.default.bold("--url")} at ${import_picocolors8.default.bold(blockingOwner)}'s connection,
|
|
6040
|
+
or drop the existing ledger so Ablo recreates it under this admin \u2014 it holds only
|
|
6041
|
+
idempotency replay records, safe to drop when no commit is in flight:
|
|
6042
|
+
${import_picocolors8.default.cyan("DROP TABLE ablo_idempotency;")}
|
|
6043
|
+
`
|
|
5608
6044
|
);
|
|
5609
6045
|
process.exit(1);
|
|
5610
6046
|
}
|
|
6047
|
+
const provider = detectProvider(target);
|
|
6048
|
+
const walReady = await currentWalLevel(admin) === "logical";
|
|
5611
6049
|
const replicationPassword = generateRolePassword();
|
|
5612
6050
|
const writePassword = generateRolePassword();
|
|
5613
6051
|
const buildPlan = (mode2) => connectApplyPlan({
|
|
@@ -5617,7 +6055,9 @@ async function runConnectApply(args) {
|
|
|
5617
6055
|
credentials: {
|
|
5618
6056
|
replicationClause: passwordClause(replicationPassword, mode2),
|
|
5619
6057
|
writeClause: passwordClause(writePassword, mode2)
|
|
5620
|
-
}
|
|
6058
|
+
},
|
|
6059
|
+
walAlreadyLogical: walReady,
|
|
6060
|
+
provider
|
|
5621
6061
|
});
|
|
5622
6062
|
const steps = buildPlan("scram-verifier");
|
|
5623
6063
|
printPlan2(steps, args.showSql);
|
|
@@ -5625,31 +6065,32 @@ async function runConnectApply(args) {
|
|
|
5625
6065
|
if (!process.stdout.isTTY) {
|
|
5626
6066
|
await admin.end({ timeout: 2 });
|
|
5627
6067
|
console.error(
|
|
5628
|
-
|
|
6068
|
+
import_picocolors8.default.dim(` Re-run with ${import_picocolors8.default.bold("--yes")} to apply this in a non-interactive session.
|
|
5629
6069
|
`)
|
|
5630
6070
|
);
|
|
5631
6071
|
process.exit(1);
|
|
5632
6072
|
}
|
|
5633
6073
|
const proceed = await ye({
|
|
5634
|
-
message: `
|
|
6074
|
+
message: rotating ? `Re-key Ablo's roles on ${target}?` : `Provision Ablo on ${target}?`,
|
|
5635
6075
|
initialValue: true
|
|
5636
6076
|
});
|
|
5637
6077
|
if (pD(proceed) || !proceed) {
|
|
5638
6078
|
await admin.end({ timeout: 2 });
|
|
5639
|
-
console.log(
|
|
5640
|
-
`)
|
|
6079
|
+
console.log(
|
|
6080
|
+
import_picocolors8.default.dim(` Nothing applied. Run ${import_picocolors8.default.bold("ablo connect")} to see the manual recipe.
|
|
6081
|
+
`)
|
|
6082
|
+
);
|
|
5641
6083
|
process.exit(0);
|
|
5642
6084
|
}
|
|
5643
6085
|
}
|
|
5644
|
-
let walDeferred = false;
|
|
5645
6086
|
try {
|
|
5646
|
-
|
|
6087
|
+
await executePlan(admin, steps, () => buildPlan("plaintext"));
|
|
5647
6088
|
} catch (err) {
|
|
5648
6089
|
await admin.end({ timeout: 2 }).catch(() => void 0);
|
|
5649
6090
|
const pg = err ?? {};
|
|
5650
6091
|
console.error(
|
|
5651
|
-
|
|
5652
|
-
Setup stopped: ${pg.message ?? String(err)}`) +
|
|
6092
|
+
import_picocolors8.default.red(`
|
|
6093
|
+
Setup stopped: ${pg.message ?? String(err)}`) + import_picocolors8.default.dim(` Every step is safe to re-run.
|
|
5653
6094
|
`)
|
|
5654
6095
|
);
|
|
5655
6096
|
process.exit(1);
|
|
@@ -5659,787 +6100,72 @@ async function runConnectApply(args) {
|
|
|
5659
6100
|
const writeRole = args.writeRole && args.writeRole.length > 0 ? args.writeRole : ABLO_WRITE_ROLE;
|
|
5660
6101
|
const replicationUrl = rewriteDatabaseUrl(adminUrl, role, replicationPassword);
|
|
5661
6102
|
const writeUrl = rewriteDatabaseUrl(adminUrl, writeRole, writePassword);
|
|
5662
|
-
const where = persistScopedUrls({
|
|
5663
|
-
DATABASE_URL: replicationUrl,
|
|
5664
|
-
ABLO_WRITE_DATABASE_URL: writeUrl
|
|
5665
|
-
});
|
|
5666
|
-
console.log(
|
|
5667
|
-
`
|
|
5668
|
-
${import_picocolors6.default.green("\u2713")} Roles created \u2014 ${import_picocolors6.default.bold("DATABASE_URL")} + ${import_picocolors6.default.bold("ABLO_WRITE_DATABASE_URL")} updated in ${import_picocolors6.default.bold(where)}.
|
|
5669
|
-
`
|
|
5670
|
-
);
|
|
5671
|
-
const verifier = src_default(replicationUrl, { max: 1, prepare: false, connect_timeout: 10, onnotice: () => {
|
|
5672
|
-
} });
|
|
5673
|
-
let items;
|
|
5674
|
-
try {
|
|
5675
|
-
items = await probeReadiness(verifier);
|
|
5676
|
-
} catch {
|
|
5677
|
-
await verifier.end({ timeout: 2 }).catch(() => void 0);
|
|
5678
|
-
console.log(import_picocolors6.default.dim(` Verify when reachable: `) + import_picocolors6.default.cyan("npx ablo connect --check") + "\n");
|
|
5679
|
-
process.exit(0);
|
|
5680
|
-
}
|
|
5681
|
-
await verifier.end({ timeout: 2 });
|
|
5682
|
-
const failed = items.filter((i) => !i.ok);
|
|
5683
|
-
if (walDeferred) {
|
|
5684
|
-
console.log(
|
|
5685
|
-
` ${import_picocolors6.default.yellow("!")} One step left \u2014 logical replication needs a restart.
|
|
5686
|
-
` + import_picocolors6.default.dim(` RDS/Aurora: set rds.logical_replication=1, reboot. Neon: enable it in settings.
|
|
5687
|
-
`) + `
|
|
5688
|
-
Then: ${import_picocolors6.default.cyan("npx ablo connect --register")}
|
|
5689
|
-
`
|
|
5690
|
-
);
|
|
5691
|
-
process.exit(0);
|
|
5692
|
-
}
|
|
5693
|
-
if (failed.length === 0) {
|
|
5694
|
-
console.log(` ${import_picocolors6.default.green("\u2713")} Verified. Next: ${import_picocolors6.default.cyan("npx ablo connect --register")}
|
|
5695
|
-
`);
|
|
5696
|
-
} else {
|
|
5697
|
-
for (const item of failed) console.log(` ${import_picocolors6.default.yellow("!")} ${item.label}`);
|
|
5698
|
-
console.log(`
|
|
5699
|
-
${import_picocolors6.default.dim("Resolve, then")} ${import_picocolors6.default.cyan("npx ablo connect --register")}
|
|
5700
|
-
`);
|
|
5701
|
-
}
|
|
5702
|
-
process.exit(0);
|
|
5703
|
-
}
|
|
5704
|
-
var import_picocolors6, import_fs6, import_path4;
|
|
5705
|
-
var init_connectApply = __esm({
|
|
5706
|
-
"src/cli/connectApply.ts"() {
|
|
5707
|
-
"use strict";
|
|
5708
|
-
init_cjs_shims();
|
|
5709
|
-
import_picocolors6 = __toESM(require_picocolors(), 1);
|
|
5710
|
-
init_src();
|
|
5711
|
-
init_dist2();
|
|
5712
|
-
import_fs6 = require("fs");
|
|
5713
|
-
import_path4 = require("path");
|
|
5714
|
-
init_connect();
|
|
5715
|
-
init_dbRole();
|
|
5716
|
-
init_theme();
|
|
5717
|
-
}
|
|
5718
|
-
});
|
|
5719
|
-
|
|
5720
|
-
// src/cli/connect.ts
|
|
5721
|
-
function parseConnectArgs(argv) {
|
|
5722
|
-
let check2 = false;
|
|
5723
|
-
let register = false;
|
|
5724
|
-
let apply2 = false;
|
|
5725
|
-
let yes = false;
|
|
5726
|
-
let showSql = false;
|
|
5727
|
-
let auditInfra = false;
|
|
5728
|
-
let tables = [];
|
|
5729
|
-
let role = ABLO_REPLICATION_ROLE;
|
|
5730
|
-
let writeRole = ABLO_WRITE_ROLE;
|
|
5731
|
-
let route = "public-allowlist";
|
|
5732
|
-
for (let i = 0; i < argv.length; i++) {
|
|
5733
|
-
const arg = argv[i];
|
|
5734
|
-
switch (arg) {
|
|
5735
|
-
case "--check":
|
|
5736
|
-
check2 = true;
|
|
5737
|
-
break;
|
|
5738
|
-
case "--register":
|
|
5739
|
-
register = true;
|
|
5740
|
-
break;
|
|
5741
|
-
case "--apply":
|
|
5742
|
-
apply2 = true;
|
|
5743
|
-
break;
|
|
5744
|
-
case "--yes":
|
|
5745
|
-
case "-y":
|
|
5746
|
-
yes = true;
|
|
5747
|
-
break;
|
|
5748
|
-
case "--show-sql":
|
|
5749
|
-
showSql = true;
|
|
5750
|
-
break;
|
|
5751
|
-
case "--audit-infra":
|
|
5752
|
-
auditInfra = true;
|
|
5753
|
-
break;
|
|
5754
|
-
case "--tables": {
|
|
5755
|
-
const value = argv[++i] ?? "";
|
|
5756
|
-
tables = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
5757
|
-
break;
|
|
5758
|
-
}
|
|
5759
|
-
case "--role":
|
|
5760
|
-
role = argv[++i] ?? role;
|
|
5761
|
-
break;
|
|
5762
|
-
case "--write-role":
|
|
5763
|
-
writeRole = argv[++i] ?? writeRole;
|
|
5764
|
-
break;
|
|
5765
|
-
case "--route": {
|
|
5766
|
-
const value = argv[++i] ?? "";
|
|
5767
|
-
if (!DIRECT_DATA_SOURCE_ROUTES.includes(value)) {
|
|
5768
|
-
throw new AbloValidationError(
|
|
5769
|
-
`invalid direct route: ${value || "(missing)"} (expected ${DIRECT_DATA_SOURCE_ROUTES.join(", ")})`,
|
|
5770
|
-
{ code: "cli_invalid_arguments" }
|
|
5771
|
-
);
|
|
5772
|
-
}
|
|
5773
|
-
route = value;
|
|
5774
|
-
break;
|
|
5775
|
-
}
|
|
5776
|
-
default:
|
|
5777
|
-
throw new AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
|
|
5778
|
-
}
|
|
5779
|
-
}
|
|
5780
|
-
if (role === writeRole) {
|
|
5781
|
-
throw new AbloValidationError("replication and write roles must be different", {
|
|
5782
|
-
code: "cli_invalid_arguments"
|
|
5783
|
-
});
|
|
5784
|
-
}
|
|
5785
|
-
return { check: check2, register, apply: apply2, yes, showSql, auditInfra, tables, role, writeRole, route };
|
|
5786
|
-
}
|
|
5787
|
-
function quoteIdent(id) {
|
|
5788
|
-
return `"${id.replace(/"/g, '""')}"`;
|
|
5789
|
-
}
|
|
5790
|
-
function connectSetupSql(input) {
|
|
5791
|
-
const role = input.role && input.role.length > 0 ? input.role : ABLO_REPLICATION_ROLE;
|
|
5792
|
-
const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : ABLO_WRITE_ROLE;
|
|
5793
|
-
const tables = input.tables ?? [];
|
|
5794
|
-
const publicationTarget = tables.length > 0 ? `FOR TABLE ${tables.map(quoteIdent).join(", ")}` : "FOR ALL TABLES";
|
|
5795
|
-
const applicationGrant = tables.length > 0 ? `GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE ${tables.map(quoteIdent).join(", ")} TO ${quoteIdent(writeRole)};` : `GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO ${quoteIdent(writeRole)};`;
|
|
5796
|
-
const ledger = idempotencyLedgerMigrations().map((migration) => migration.up);
|
|
5797
|
-
return [
|
|
5798
|
-
// 1. Turn on logical decoding. Requires a restart (it's not reloadable).
|
|
5799
|
-
`ALTER SYSTEM SET wal_level = 'logical';`,
|
|
5800
|
-
// 2. Publish the tables Ablo should read.
|
|
5801
|
-
`CREATE PUBLICATION ${quoteIdent(ABLO_PUBLICATION)} ${publicationTarget};`,
|
|
5802
|
-
// 3. A least-privilege role: it can stream replication and SELECT, nothing more.
|
|
5803
|
-
`CREATE ROLE ${quoteIdent(role)} WITH REPLICATION LOGIN PASSWORD '<password>';`,
|
|
5804
|
-
`GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${quoteIdent(role)};`,
|
|
5805
|
-
// Future tables get SELECT automatically, so the publication doesn't outgrow the grant.
|
|
5806
|
-
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${quoteIdent(role)};`,
|
|
5807
|
-
// 4. A distinct DML role: no replication, role administration, ownership,
|
|
5808
|
-
// schema creation, or DDL. It is subject to RLS on every transaction.
|
|
5809
|
-
`CREATE ROLE ${quoteIdent(writeRole)} WITH LOGIN PASSWORD '<write-password>' NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE NOREPLICATION NOINHERIT;`,
|
|
5810
|
-
`ALTER ROLE ${quoteIdent(writeRole)} SET row_security = on;`,
|
|
5811
|
-
// PUBLIC can carry CREATE on older Postgres clusters. A role-level REVOKE
|
|
5812
|
-
// cannot override that inherited grant, so harden the schema explicitly.
|
|
5813
|
-
`REVOKE CREATE ON SCHEMA public FROM PUBLIC;`,
|
|
5814
|
-
`GRANT USAGE ON SCHEMA public TO ${quoteIdent(writeRole)};`,
|
|
5815
|
-
applicationGrant,
|
|
5816
|
-
`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${quoteIdent(writeRole)};`,
|
|
5817
|
-
...tables.length === 0 ? [
|
|
5818
|
-
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${quoteIdent(writeRole)};`,
|
|
5819
|
-
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${quoteIdent(writeRole)};`
|
|
5820
|
-
] : [],
|
|
5821
|
-
// 5. Direct uses the durable replay ledger but deliberately no outbox.
|
|
5822
|
-
...ledger,
|
|
5823
|
-
`REVOKE ALL ON TABLE public.ablo_idempotency FROM PUBLIC;`,
|
|
5824
|
-
`GRANT SELECT, INSERT, UPDATE ON TABLE public.ablo_idempotency TO ${quoteIdent(writeRole)};`,
|
|
5825
|
-
`REVOKE DELETE ON TABLE public.ablo_idempotency FROM ${quoteIdent(writeRole)};`,
|
|
5826
|
-
// Grant every pg_logical_emit_message variant by lookup instead of one
|
|
5827
|
-
// literal signature: PostgreSQL 17 adds an optional fourth `flush`
|
|
5828
|
-
// parameter, so the historical three-argument form no longer exists there
|
|
5829
|
-
// and a signature-pinned GRANT fails on an otherwise healthy database.
|
|
5830
|
-
`DO $$
|
|
5831
|
-
DECLARE fn regprocedure;
|
|
5832
|
-
BEGIN
|
|
5833
|
-
FOR fn IN
|
|
5834
|
-
SELECT p.oid::regprocedure
|
|
5835
|
-
FROM pg_proc p
|
|
5836
|
-
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
5837
|
-
WHERE n.nspname = 'pg_catalog' AND p.proname = 'pg_logical_emit_message'
|
|
5838
|
-
LOOP
|
|
5839
|
-
EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO ${quoteIdent(writeRole)}', fn);
|
|
5840
|
-
END LOOP;
|
|
5841
|
-
END $$;`
|
|
5842
|
-
];
|
|
5843
|
-
}
|
|
5844
|
-
function printConnectRecipe(args) {
|
|
5845
|
-
const sql = connectSetupSql({
|
|
5846
|
-
tables: args.tables,
|
|
5847
|
-
role: args.role,
|
|
5848
|
-
writeRole: args.writeRole
|
|
5849
|
-
});
|
|
5850
6103
|
console.log(`
|
|
5851
|
-
${
|
|
6104
|
+
${import_picocolors8.default.green("\u2713")} Roles ${rotating ? "re-keyed" : "created"}.
|
|
5852
6105
|
`);
|
|
5853
|
-
|
|
5854
|
-
` Ablo applies coordinated writes directly to your Postgres with a scoped DML role. WAL
|
|
5855
|
-
observes what committed, orders it with external writes, and confirms it in the sync log.
|
|
5856
|
-
Your database stays authoritative; Ablo never owns or migrates your application tables.
|
|
5857
|
-
Run this once against your Postgres ${import_picocolors7.default.dim("(as a superuser / DB owner)")}:
|
|
5858
|
-
`
|
|
5859
|
-
);
|
|
5860
|
-
console.log(` ${import_picocolors7.default.bold("1.")} Enable logical decoding ${import_picocolors7.default.dim("(then RESTART Postgres \u2014 wal_level is not reloadable)")}`);
|
|
5861
|
-
console.log(` ${import_picocolors7.default.cyan(sql[0])}`);
|
|
5862
|
-
console.log(
|
|
5863
|
-
import_picocolors7.default.dim(
|
|
5864
|
-
` On Amazon RDS / Aurora you can't ALTER SYSTEM: set ${import_picocolors7.default.bold("rds.logical_replication = 1")} in the
|
|
5865
|
-
instance's parameter group instead, then reboot.`
|
|
5866
|
-
)
|
|
5867
|
-
);
|
|
5868
|
-
console.log(`
|
|
5869
|
-
${import_picocolors7.default.bold("2.")} Publish the tables Ablo should read`);
|
|
5870
|
-
console.log(` ${import_picocolors7.default.cyan(sql[1])}`);
|
|
5871
|
-
if (args.tables.length === 0) {
|
|
5872
|
-
console.log(import_picocolors7.default.dim(` (Scope it with ${import_picocolors7.default.bold("ablo connect --tables a,b,c")} to publish a subset.)`));
|
|
5873
|
-
}
|
|
5874
|
-
console.log(`
|
|
5875
|
-
${import_picocolors7.default.bold("3.")} Create the scoped replication role ${import_picocolors7.default.dim("(pick your own replication password)")}`);
|
|
5876
|
-
console.log(` ${import_picocolors7.default.cyan(sql[2])}`);
|
|
5877
|
-
console.log(` ${import_picocolors7.default.cyan(sql[3])}`);
|
|
5878
|
-
console.log(` ${import_picocolors7.default.cyan(sql[4])}`);
|
|
5879
|
-
console.log(
|
|
5880
|
-
import_picocolors7.default.dim(
|
|
5881
|
-
` On Amazon RDS, the REPLICATION attribute is granted, not set directly:
|
|
5882
|
-
${import_picocolors7.default.bold(`GRANT rds_replication TO ${quoteIdent(args.role)};`)}`
|
|
5883
|
-
)
|
|
5884
|
-
);
|
|
5885
|
-
console.log(
|
|
5886
|
-
`
|
|
5887
|
-
${import_picocolors7.default.bold("4.")} Create the separate DML role and permanent idempotency ledger ` + import_picocolors7.default.dim("(pick your own write password)")
|
|
5888
|
-
);
|
|
5889
|
-
for (const statement of sql.slice(5)) {
|
|
5890
|
-
for (const line of statement.split("\n")) console.log(` ${import_picocolors7.default.cyan(line)}`);
|
|
5891
|
-
}
|
|
5892
|
-
console.log(
|
|
5893
|
-
import_picocolors7.default.dim(
|
|
5894
|
-
` The writer gets row DML + ledger access only. It has no REPLICATION, schema CREATE,
|
|
5895
|
-
role administration, database creation, ownership, or customer-table DDL. Direct uses
|
|
5896
|
-
${import_picocolors7.default.bold("ablo_idempotency")} but no outbox; WAL carries the committed row changes.`
|
|
5897
|
-
)
|
|
5898
|
-
);
|
|
5899
|
-
console.log(
|
|
5900
|
-
`
|
|
5901
|
-
${import_picocolors7.default.bold("5.")} Configure both scoped connections, then verify:
|
|
5902
|
-
${import_picocolors7.default.bold("DATABASE_URL")} ${import_picocolors7.default.dim(`\u2192 ${args.role} (replication only)`)}
|
|
5903
|
-
${import_picocolors7.default.bold("ABLO_WRITE_DATABASE_URL")} ${import_picocolors7.default.dim(`\u2192 ${args.writeRole} (DML only)`)}
|
|
5904
|
-
${import_picocolors7.default.cyan("npx ablo connect --check")}
|
|
5905
|
-
`
|
|
5906
|
-
);
|
|
5907
|
-
console.log(
|
|
5908
|
-
import_picocolors7.default.dim(
|
|
5909
|
-
` Reachable databases use this direct path. If no inbound route can be established, use the
|
|
5910
|
-
signed ${import_picocolors7.default.bold("dataSource()")} endpoint fallback; its correlated events confirm writes
|
|
5911
|
-
without an Ablo-side customer database socket.`
|
|
5912
|
-
)
|
|
5913
|
-
);
|
|
5914
|
-
console.log();
|
|
5915
|
-
}
|
|
5916
|
-
function printCheckItem(item) {
|
|
5917
|
-
if (item.ok) {
|
|
5918
|
-
console.log(` ${import_picocolors7.default.green("\u2713")} ${item.label}`);
|
|
5919
|
-
} else {
|
|
5920
|
-
console.log(` ${import_picocolors7.default.red("\u2717")} ${item.label}`);
|
|
5921
|
-
if (item.fix) {
|
|
5922
|
-
for (const line of item.fix.split("\n")) console.log(` ${import_picocolors7.default.red("\u2022")} ${line}`);
|
|
5923
|
-
}
|
|
5924
|
-
}
|
|
5925
|
-
}
|
|
5926
|
-
async function probeReadiness(sql, opts = {}) {
|
|
5927
|
-
const publication = opts.publication ?? ABLO_PUBLICATION;
|
|
5928
|
-
const items = [];
|
|
5929
|
-
const walRows = await sql.unsafe(
|
|
5930
|
-
`SELECT setting FROM pg_settings WHERE name = 'wal_level'`
|
|
5931
|
-
);
|
|
5932
|
-
const walLevel = walRows[0]?.setting ?? "unknown";
|
|
5933
|
-
items.push(
|
|
5934
|
-
walLevel === "logical" ? { ok: true, label: `wal_level is ${import_picocolors7.default.bold("logical")}` } : {
|
|
5935
|
-
ok: false,
|
|
5936
|
-
label: `wal_level is ${import_picocolors7.default.bold(walLevel)} (need ${import_picocolors7.default.bold("logical")})`,
|
|
5937
|
-
fix: `ALTER SYSTEM SET wal_level = 'logical'; then RESTART Postgres.
|
|
5938
|
-
On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.
|
|
5939
|
-
On Neon enable Logical Replication in the project (Console \u2192 Settings \u2192 Logical Replication, or the API) \u2014 Neon forbids ALTER SYSTEM; the toggle sets wal_level=logical.`
|
|
5940
|
-
}
|
|
5941
|
-
);
|
|
5942
|
-
const pubRows = await sql.unsafe(
|
|
5943
|
-
`SELECT puballtables FROM pg_publication WHERE pubname = $1`,
|
|
5944
|
-
[publication]
|
|
5945
|
-
);
|
|
5946
|
-
const pubRow = pubRows[0];
|
|
5947
|
-
items.push(
|
|
5948
|
-
pubRow ? {
|
|
5949
|
-
ok: true,
|
|
5950
|
-
label: `publication ${import_picocolors7.default.bold(publication)} exists ${import_picocolors7.default.dim(pubRow.puballtables ? "(all tables)" : "(table subset)")}`
|
|
5951
|
-
} : {
|
|
5952
|
-
ok: false,
|
|
5953
|
-
label: `publication ${import_picocolors7.default.bold(publication)} not found`,
|
|
5954
|
-
fix: `CREATE PUBLICATION ${quoteIdent(publication)} FOR ALL TABLES;`
|
|
5955
|
-
}
|
|
5956
|
-
);
|
|
5957
|
-
const roleRows = await sql.unsafe(
|
|
5958
|
-
`SELECT rolreplication, rolsuper FROM pg_roles WHERE rolname = current_user`
|
|
5959
|
-
);
|
|
5960
|
-
const role = roleRows[0];
|
|
5961
|
-
const hasReplication = Boolean(role && (role.rolreplication || role.rolsuper));
|
|
5962
|
-
items.push(
|
|
5963
|
-
hasReplication ? { ok: true, label: `DATABASE_URL role can stream replication ${import_picocolors7.default.dim("(REPLICATION)")}` } : {
|
|
5964
|
-
ok: false,
|
|
5965
|
-
label: `DATABASE_URL role lacks the ${import_picocolors7.default.bold("REPLICATION")} attribute`,
|
|
5966
|
-
fix: `ALTER ROLE current_user WITH REPLICATION;
|
|
5967
|
-
On RDS: GRANT rds_replication TO <your_role>;`
|
|
5968
|
-
}
|
|
5969
|
-
);
|
|
5970
|
-
if (pubRows.length > 0) {
|
|
5971
|
-
const badRows = await sql.unsafe(
|
|
5972
|
-
`SELECT c.relname AS table_name, c.relreplident
|
|
5973
|
-
FROM pg_publication_tables pt
|
|
5974
|
-
JOIN pg_class c ON c.relname = pt.tablename
|
|
5975
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
|
|
5976
|
-
WHERE pt.pubname = $1
|
|
5977
|
-
AND (
|
|
5978
|
-
c.relreplident = 'n'
|
|
5979
|
-
OR (
|
|
5980
|
-
c.relreplident = 'd'
|
|
5981
|
-
AND NOT EXISTS (
|
|
5982
|
-
SELECT 1 FROM pg_index i
|
|
5983
|
-
WHERE i.indrelid = c.oid AND i.indisprimary
|
|
5984
|
-
)
|
|
5985
|
-
)
|
|
5986
|
-
)`,
|
|
5987
|
-
[publication]
|
|
5988
|
-
);
|
|
5989
|
-
items.push(
|
|
5990
|
-
badRows.length === 0 ? { ok: true, label: `all published tables have a usable REPLICA IDENTITY` } : {
|
|
5991
|
-
ok: false,
|
|
5992
|
-
label: `${badRows.length} published table${badRows.length === 1 ? "" : "s"} cannot replicate UPDATE/DELETE`,
|
|
5993
|
-
fix: badRows.map(
|
|
5994
|
-
(r2) => `${r2.table_name}: add a PRIMARY KEY, or ALTER TABLE ${quoteIdent(r2.table_name)} REPLICA IDENTITY FULL;`
|
|
5995
|
-
).join("\n")
|
|
5996
|
-
}
|
|
5997
|
-
);
|
|
5998
|
-
}
|
|
5999
|
-
return items;
|
|
6000
|
-
}
|
|
6001
|
-
async function probeDirectWriteReadiness(sql, opts = {}) {
|
|
6002
|
-
const schema = opts.schema ?? "public";
|
|
6003
|
-
const publication = opts.publication ?? ABLO_PUBLICATION;
|
|
6004
|
-
const items = [];
|
|
6005
|
-
const roleRows = await sql.unsafe(
|
|
6006
|
-
`SELECT rolname, rolsuper, rolbypassrls, rolcreatedb, rolcreaterole, rolreplication
|
|
6007
|
-
FROM pg_roles WHERE rolname = current_user`
|
|
6008
|
-
);
|
|
6009
|
-
const role = roleRows[0];
|
|
6010
|
-
const dangerous = Boolean(
|
|
6011
|
-
!role || role.rolsuper || role.rolbypassrls || role.rolcreatedb || role.rolcreaterole || role.rolreplication
|
|
6012
|
-
);
|
|
6013
|
-
items.push(
|
|
6014
|
-
dangerous ? {
|
|
6015
|
-
ok: false,
|
|
6016
|
-
label: `ABLO_WRITE_DATABASE_URL is not a scoped DML role`,
|
|
6017
|
-
fix: `Use ${ABLO_WRITE_ROLE} from the setup recipe: NOSUPERUSER, NOBYPASSRLS, NOCREATEDB, NOCREATEROLE, NOREPLICATION.`
|
|
6018
|
-
} : { ok: true, label: `write role ${import_picocolors7.default.bold(role?.rolname ?? ABLO_WRITE_ROLE)} is DML-only` }
|
|
6019
|
-
);
|
|
6020
|
-
const schemaRows = await sql.unsafe(
|
|
6021
|
-
`SELECT
|
|
6022
|
-
has_schema_privilege(current_user, $1, 'USAGE') AS usage,
|
|
6023
|
-
has_schema_privilege(current_user, $1, 'CREATE') AS create,
|
|
6024
|
-
current_setting('row_security') AS row_security`,
|
|
6025
|
-
[schema]
|
|
6026
|
-
);
|
|
6027
|
-
const schemaRow = schemaRows[0];
|
|
6028
|
-
items.push(
|
|
6029
|
-
schemaRow?.usage && !schemaRow.create && schemaRow.row_security === "on" ? { ok: true, label: `writer uses ${schema} with RLS on and no schema CREATE` } : {
|
|
6030
|
-
ok: false,
|
|
6031
|
-
label: `writer schema/RLS privileges are not least-privilege`,
|
|
6032
|
-
fix: `REVOKE CREATE ON SCHEMA ${quoteIdent(schema)} FROM PUBLIC; GRANT USAGE ON SCHEMA ${quoteIdent(schema)} TO ${quoteIdent(role?.rolname ?? ABLO_WRITE_ROLE)}; ALTER ROLE ${quoteIdent(role?.rolname ?? ABLO_WRITE_ROLE)} SET row_security = on;`
|
|
6033
|
-
}
|
|
6034
|
-
);
|
|
6035
|
-
const ledgerName = `${quoteIdent(schema)}.${quoteIdent("ablo_idempotency")}`;
|
|
6036
|
-
const ledgerRows = await sql.unsafe(
|
|
6037
|
-
`SELECT
|
|
6038
|
-
to_regclass($1) IS NOT NULL AS present,
|
|
6039
|
-
CASE WHEN to_regclass($1) IS NULL THEN false ELSE has_table_privilege(current_user, $1, 'SELECT,INSERT,UPDATE') END AS writes,
|
|
6040
|
-
CASE WHEN to_regclass($1) IS NULL THEN false ELSE has_table_privilege(current_user, $1, 'DELETE') END AS deletes`,
|
|
6041
|
-
[ledgerName]
|
|
6042
|
-
);
|
|
6043
|
-
const ledger = ledgerRows[0];
|
|
6044
|
-
items.push(
|
|
6045
|
-
ledger?.present && ledger.writes && !ledger.deletes ? { ok: true, label: `${import_picocolors7.default.bold("ablo_idempotency")} is durable and protected from DELETE` } : {
|
|
6046
|
-
ok: false,
|
|
6047
|
-
label: `${import_picocolors7.default.bold("ablo_idempotency")} is missing or has unsafe grants`,
|
|
6048
|
-
fix: `Apply the DML-role and idempotency-ledger statements printed by ${import_picocolors7.default.bold("ablo connect")}.`
|
|
6049
|
-
}
|
|
6050
|
-
);
|
|
6051
|
-
const tableRows = await sql.unsafe(
|
|
6052
|
-
`SELECT format('%I.%I', schemaname, tablename) AS relation
|
|
6053
|
-
FROM pg_publication_tables
|
|
6054
|
-
WHERE pubname = $1 AND schemaname = $2 AND tablename <> 'ablo_idempotency'
|
|
6055
|
-
AND NOT has_table_privilege(
|
|
6056
|
-
current_user,
|
|
6057
|
-
format('%I.%I', schemaname, tablename),
|
|
6058
|
-
'SELECT,INSERT,UPDATE,DELETE'
|
|
6059
|
-
)`,
|
|
6060
|
-
[publication, schema]
|
|
6061
|
-
);
|
|
6062
|
-
items.push(
|
|
6063
|
-
tableRows.length === 0 ? { ok: true, label: `writer can apply DML to every published application table` } : {
|
|
6064
|
-
ok: false,
|
|
6065
|
-
label: `writer lacks DML on ${tableRows.length} published table${tableRows.length === 1 ? "" : "s"}`,
|
|
6066
|
-
fix: `Grant SELECT, INSERT, UPDATE, DELETE on: ${tableRows.map((row) => row.relation).join(", ")}`
|
|
6067
|
-
}
|
|
6068
|
-
);
|
|
6069
|
-
return items;
|
|
6070
|
-
}
|
|
6071
|
-
async function auditTenantSyncInfra(sql) {
|
|
6072
|
-
const artifacts = [];
|
|
6073
|
-
for (const name of SYNC_INFRA_RELATIONS) {
|
|
6074
|
-
const rows = await sql.unsafe(
|
|
6075
|
-
`SELECT to_regclass($1)::text AS reg`,
|
|
6076
|
-
[`public.${name}`]
|
|
6077
|
-
);
|
|
6078
|
-
artifacts.push({ kind: "relation", name, present: rows[0]?.reg != null });
|
|
6079
|
-
}
|
|
6080
|
-
for (const name of SYNC_INFRA_TYPES) {
|
|
6081
|
-
const rows = await sql.unsafe(
|
|
6082
|
-
`SELECT to_regtype($1)::text AS reg`,
|
|
6083
|
-
[`public.${name}`]
|
|
6084
|
-
);
|
|
6085
|
-
artifacts.push({ kind: "type", name, present: rows[0]?.reg != null });
|
|
6086
|
-
}
|
|
6087
|
-
return artifacts;
|
|
6088
|
-
}
|
|
6089
|
-
function requireDatabaseUrl(verb) {
|
|
6090
|
-
const dbUrl = readProjectDatabaseUrl();
|
|
6091
|
-
if (!dbUrl) {
|
|
6092
|
-
console.error(
|
|
6093
|
-
import_picocolors7.default.red(" No DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors7.default.dim(` Set it to the Postgres you want Ablo to read, then re-run ${import_picocolors7.default.bold(`ablo connect ${verb}`)}.`)
|
|
6094
|
-
);
|
|
6095
|
-
process.exit(1);
|
|
6096
|
-
}
|
|
6097
|
-
return dbUrl;
|
|
6098
|
-
}
|
|
6099
|
-
function requireWriteDatabaseUrl(verb) {
|
|
6100
|
-
const dbUrl = readProjectWriteDatabaseUrl();
|
|
6101
|
-
if (!dbUrl) {
|
|
6106
|
+
if (!walReady) {
|
|
6102
6107
|
console.error(
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6108
|
+
` ${import_picocolors8.default.yellow("!")} One step left \u2014 logical replication isn't on yet.
|
|
6109
|
+
${logicalReplicationGuidance(provider)}.
|
|
6110
|
+
|
|
6111
|
+
Then re-run: ${import_picocolors8.default.cyan(`npx ablo ${verb}`)}
|
|
6112
|
+
`
|
|
6106
6113
|
);
|
|
6107
6114
|
process.exit(1);
|
|
6108
6115
|
}
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6116
|
+
const verifier = src_default(replicationUrl, {
|
|
6117
|
+
max: 1,
|
|
6118
|
+
prepare: false,
|
|
6119
|
+
connect_timeout: 10,
|
|
6120
|
+
onnotice: () => {
|
|
6121
|
+
}
|
|
6122
|
+
});
|
|
6114
6123
|
let items;
|
|
6115
6124
|
try {
|
|
6116
|
-
items =
|
|
6117
|
-
} catch
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
const pg = err ?? {};
|
|
6122
|
-
console.error(import_picocolors7.default.red(` Couldn't read the database: ${pg.message ?? String(err)}`));
|
|
6123
|
-
process.exit(1);
|
|
6125
|
+
items = await probeReadiness(verifier);
|
|
6126
|
+
} catch {
|
|
6127
|
+
items = null;
|
|
6128
|
+
console.log(import_picocolors8.default.dim(` Couldn't verify from here; Ablo will validate from its own network.
|
|
6129
|
+
`));
|
|
6124
6130
|
}
|
|
6125
|
-
await
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
}
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
That is not the verdict: direct writes and replication run from Ablo's infrastructure.
|
|
6133
|
-
Asking Ablo to check both roles from its side\u2026
|
|
6134
|
-
`
|
|
6135
|
-
);
|
|
6136
|
-
const apiKey = resolveApiKey();
|
|
6137
|
-
if (!apiKey) {
|
|
6138
|
-
console.error(
|
|
6139
|
-
import_picocolors7.default.red(` The engine-side check needs an API key, and none was found.`) + import_picocolors7.default.dim(
|
|
6140
|
-
` Run ${import_picocolors7.default.bold("ablo login")} (or set ${import_picocolors7.default.bold("ABLO_API_KEY")}), then re-run ${import_picocolors7.default.bold("ablo connect --check")}.`
|
|
6141
|
-
)
|
|
6142
|
-
);
|
|
6131
|
+
await verifier.end({ timeout: 2 }).catch(() => void 0);
|
|
6132
|
+
const failed = items?.filter((i) => !i.ok) ?? [];
|
|
6133
|
+
if (failed.length > 0) {
|
|
6134
|
+
for (const item of failed) console.log(` ${import_picocolors8.default.yellow("!")} ${item.label}`);
|
|
6135
|
+
console.log(`
|
|
6136
|
+
${import_picocolors8.default.dim("Resolve, then re-run")} ${import_picocolors8.default.cyan(`npx ablo ${verb}`)}
|
|
6137
|
+
`);
|
|
6143
6138
|
process.exit(1);
|
|
6144
6139
|
}
|
|
6145
|
-
const apiUrl2 = (
|
|
6146
|
-
const
|
|
6140
|
+
const apiUrl2 = apiBaseUrl();
|
|
6141
|
+
const registered = await registerDirectDataSource({
|
|
6147
6142
|
apiUrl: apiUrl2,
|
|
6148
6143
|
apiKey,
|
|
6149
|
-
|
|
6150
|
-
|
|
6144
|
+
replicationUrl,
|
|
6145
|
+
writeUrl,
|
|
6146
|
+
route: args.route
|
|
6151
6147
|
});
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
import_picocolors7.default.dim(` Checking a database from Ablo's side needs a ${import_picocolors7.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors7.default.bold("ablo login")} for one.`)
|
|
6157
|
-
);
|
|
6158
|
-
}
|
|
6159
|
-
console.error();
|
|
6160
|
-
process.exit(1);
|
|
6161
|
-
}
|
|
6162
|
-
if (!result.reachable) {
|
|
6163
|
-
console.error(
|
|
6164
|
-
` ${import_picocolors7.default.red("\u2717")} Ablo's infrastructure can't reach both direct connections${result.reason ? ` ${import_picocolors7.default.dim(`(${result.reason})`)}` : ""}.`
|
|
6165
|
-
);
|
|
6166
|
-
console.error(
|
|
6167
|
-
import_picocolors7.default.dim(
|
|
6168
|
-
` Direct needs a route Ablo's servers can dial \u2014 public allowlist, PrivateLink, peering,
|
|
6169
|
-
or VPN. Only when no inbound route can exist, use the signed ${import_picocolors7.default.bold("dataSource()")} endpoint fallback.
|
|
6170
|
-
`
|
|
6171
|
-
)
|
|
6172
|
-
);
|
|
6173
|
-
process.exit(1);
|
|
6174
|
-
}
|
|
6175
|
-
for (const failure of result.failures) {
|
|
6176
|
-
const { label, fix } = describeRemoteFailure(failure);
|
|
6177
|
-
printCheckItem({ ok: false, label, fix });
|
|
6178
|
-
}
|
|
6179
|
-
console.log();
|
|
6180
|
-
if (result.ready) {
|
|
6181
|
-
console.log(
|
|
6182
|
-
` ${import_picocolors7.default.green("\u2713")} Ready \u2014 checked from Ablo's infrastructure. Both direct DML and WAL settlement are available.
|
|
6183
|
-
`
|
|
6184
|
-
);
|
|
6185
|
-
process.exit(0);
|
|
6186
|
-
}
|
|
6187
|
-
const count = result.failures.length;
|
|
6188
|
-
console.log(
|
|
6189
|
-
` ${import_picocolors7.default.red(`${count} item${count === 1 ? "" : "s"} to fix`)} ${import_picocolors7.default.dim(`\u2014 found by Ablo's infrastructure. Apply the fixes above, then re-run ${import_picocolors7.default.bold("ablo connect --check")}.`)}
|
|
6190
|
-
`
|
|
6191
|
-
);
|
|
6192
|
-
process.exit(1);
|
|
6193
|
-
}
|
|
6194
|
-
async function runCheck() {
|
|
6195
|
-
const dbUrl = requireDatabaseUrl("--check");
|
|
6196
|
-
const writeDbUrl = requireWriteDatabaseUrl("--check");
|
|
6197
|
-
console.log(`
|
|
6198
|
-
${brand("ablo")} ${import_picocolors7.default.dim("connect --check")} ${import_picocolors7.default.dim("direct-write + WAL readiness")}
|
|
6199
|
-
`);
|
|
6200
|
-
console.log(` ${import_picocolors7.default.bold("Replication role")}
|
|
6201
|
-
`);
|
|
6202
|
-
const replication = await probeAndReport(dbUrl, "replication");
|
|
6203
|
-
console.log(`
|
|
6204
|
-
${import_picocolors7.default.bold("Direct-write role")}
|
|
6148
|
+
const outcome = postRegistrationOutcome({ rotating, registered });
|
|
6149
|
+
if (outcome.notice) {
|
|
6150
|
+
console.error(`
|
|
6151
|
+
${import_picocolors8.default.red(outcome.notice.split("\n").join("\n "))}
|
|
6205
6152
|
`);
|
|
6206
|
-
const write = await probeAndReport(writeDbUrl, "write");
|
|
6207
|
-
if (replication.kind === "no-dial" || write.kind === "no-dial") {
|
|
6208
|
-
const reasons = [
|
|
6209
|
-
replication.kind === "no-dial" ? `replication: ${replication.reason}` : null,
|
|
6210
|
-
write.kind === "no-dial" ? `write: ${write.reason}` : null
|
|
6211
|
-
].filter((reason) => reason !== null);
|
|
6212
|
-
return runRemoteCheck(dbUrl, writeDbUrl, reasons.join("; "));
|
|
6213
|
-
}
|
|
6214
|
-
const failures = replication.failures + write.failures;
|
|
6215
|
-
console.log();
|
|
6216
|
-
if (failures === 0) {
|
|
6217
|
-
console.log(` ${import_picocolors7.default.green("\u2713")} Ready \u2014 Ablo can apply scoped DML and settle it from WAL.
|
|
6218
|
-
`);
|
|
6219
|
-
process.exit(0);
|
|
6220
6153
|
}
|
|
6221
|
-
|
|
6222
|
-
` ${import_picocolors7.default.red(`${failures} item${failures === 1 ? "" : "s"} to fix`)} ${import_picocolors7.default.dim(`\u2014 apply the fixes above, then re-run ${import_picocolors7.default.bold("ablo connect --check")}.`)}
|
|
6223
|
-
`
|
|
6224
|
-
);
|
|
6225
|
-
process.exit(1);
|
|
6226
|
-
}
|
|
6227
|
-
function registerEndpoint(baseUrl2) {
|
|
6228
|
-
return `${baseUrl2.replace(/\/+$/, "")}/api/v1/datasources`;
|
|
6229
|
-
}
|
|
6230
|
-
async function runRegister(args) {
|
|
6231
|
-
const dbUrl = requireDatabaseUrl("--register");
|
|
6232
|
-
const writeDbUrl = requireWriteDatabaseUrl("--register");
|
|
6233
|
-
const apiKey = resolveApiKey();
|
|
6234
|
-
if (!apiKey) {
|
|
6235
|
-
console.error(
|
|
6236
|
-
import_picocolors7.default.red(" Not logged in.") + import_picocolors7.default.dim(` Run ${import_picocolors7.default.bold("ablo login")} (or set ${import_picocolors7.default.bold("ABLO_API_KEY")}) so Ablo knows which project to register this database for.`)
|
|
6237
|
-
);
|
|
6238
|
-
process.exit(1);
|
|
6239
|
-
}
|
|
6240
|
-
console.log(`
|
|
6241
|
-
${brand("ablo")} ${import_picocolors7.default.dim("connect --register")} ${import_picocolors7.default.dim("register a direct DataSource")}
|
|
6242
|
-
`);
|
|
6243
|
-
console.log(` ${import_picocolors7.default.bold("Replication role")}
|
|
6244
|
-
`);
|
|
6245
|
-
const replication = await probeAndReport(dbUrl, "replication");
|
|
6246
|
-
console.log(`
|
|
6247
|
-
${import_picocolors7.default.bold("Direct-write role")}
|
|
6248
|
-
`);
|
|
6249
|
-
const write = await probeAndReport(writeDbUrl, "write");
|
|
6250
|
-
const noDial = [
|
|
6251
|
-
replication.kind === "no-dial" ? `replication: ${replication.reason}` : null,
|
|
6252
|
-
write.kind === "no-dial" ? `write: ${write.reason}` : null
|
|
6253
|
-
].filter((reason) => reason !== null);
|
|
6254
|
-
const failures = (replication.kind === "probed" ? replication.failures : 0) + (write.kind === "probed" ? write.failures : 0);
|
|
6255
|
-
if (noDial.length > 0) {
|
|
6256
|
-
console.log(
|
|
6257
|
-
` This machine can't reach one or both scoped connections (${import_picocolors7.default.dim(noDial.join("; "))}) \u2014 continuing anyway.
|
|
6258
|
-
Ablo validates both credentials from the infrastructure that will use them and refuses
|
|
6259
|
-
registration unless replication and direct DML are both ready.
|
|
6260
|
-
`
|
|
6261
|
-
);
|
|
6262
|
-
}
|
|
6263
|
-
if (failures > 0) {
|
|
6264
|
-
console.log(
|
|
6265
|
-
`
|
|
6266
|
-
${import_picocolors7.default.red(`${failures} item${failures === 1 ? "" : "s"} to fix`)} ${import_picocolors7.default.dim("\u2014 direct registration requires both scoped roles. Fix the above, then re-run.")}
|
|
6267
|
-
`
|
|
6268
|
-
);
|
|
6269
|
-
process.exit(1);
|
|
6270
|
-
}
|
|
6271
|
-
const apiUrl2 = (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
6272
|
-
let res;
|
|
6273
|
-
try {
|
|
6274
|
-
res = await fetch(registerEndpoint(apiUrl2), {
|
|
6275
|
-
method: "POST",
|
|
6276
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
6277
|
-
body: JSON.stringify({
|
|
6278
|
-
connection: "direct",
|
|
6279
|
-
connectionString: dbUrl,
|
|
6280
|
-
writeConnectionString: writeDbUrl,
|
|
6281
|
-
route: args.route
|
|
6282
|
-
})
|
|
6283
|
-
});
|
|
6284
|
-
} catch (err) {
|
|
6285
|
-
console.error(import_picocolors7.default.red(`
|
|
6286
|
-
Couldn't reach ${apiUrl2}: ${err instanceof Error ? err.message : String(err)}
|
|
6287
|
-
`));
|
|
6288
|
-
process.exit(1);
|
|
6289
|
-
}
|
|
6290
|
-
if (res.ok) {
|
|
6291
|
-
const body2 = await res.json().catch(() => ({}));
|
|
6292
|
-
const statusNote = body2.status === "active" ? `${args.route}, active` : args.route;
|
|
6293
|
-
console.log(
|
|
6294
|
-
`
|
|
6295
|
-
${import_picocolors7.default.green("\u2713")} Registered${body2.host ? ` ${import_picocolors7.default.dim(body2.host)}` : ""}${body2.id ? ` ${import_picocolors7.default.dim(`(${body2.id})`)}` : ""} as a direct DataSource (${statusNote}).
|
|
6296
|
-
Customer COMMIT is durable acceptance; correlated WAL promotes queued writes to confirmed.
|
|
6297
|
-
`
|
|
6298
|
-
);
|
|
6299
|
-
process.exit(0);
|
|
6300
|
-
}
|
|
6301
|
-
const body = await res.json().catch(() => ({}));
|
|
6302
|
-
const code = body.code ?? body.error?.code;
|
|
6303
|
-
const message = body.message ?? body.error?.message ?? `HTTP ${res.status}`;
|
|
6304
|
-
console.error(import_picocolors7.default.red(`
|
|
6305
|
-
Registration failed: ${message}`));
|
|
6306
|
-
if (code === "forbidden") {
|
|
6307
|
-
console.error(import_picocolors7.default.dim(` Registering a database needs a ${import_picocolors7.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors7.default.bold("ablo login")} for one.`));
|
|
6308
|
-
} else if (code === "datasource_connection_unsupported") {
|
|
6309
|
-
console.error(
|
|
6310
|
-
import_picocolors7.default.dim(` This deployment can\u2019t accept connection strings \u2014 use a self-hosted/hosted engine, or the signed endpoint fallback.`)
|
|
6311
|
-
);
|
|
6312
|
-
} else if (code === "database_not_replication_ready" || code === "data_source_blocked") {
|
|
6313
|
-
for (const f of body.details?.failures ?? []) {
|
|
6314
|
-
console.error(` ${import_picocolors7.default.red("\u2717")} ${import_picocolors7.default.bold(f.item ?? "item")}${f.actual ? import_picocolors7.default.dim(` (${f.actual})`) : ""}`);
|
|
6315
|
-
if (f.fix) for (const line of f.fix.split("\n")) console.error(` ${import_picocolors7.default.red("\u2022")} ${line}`);
|
|
6316
|
-
}
|
|
6317
|
-
console.error(import_picocolors7.default.dim(`
|
|
6318
|
-
Apply the fixes, verify with ${import_picocolors7.default.bold("ablo connect --check")}, then re-run.`));
|
|
6319
|
-
} else if (code === "database_unreachable" || code === "source_unreachable") {
|
|
6320
|
-
if (body.details?.reason) console.error(import_picocolors7.default.dim(` ${body.details.reason}`));
|
|
6321
|
-
console.error(
|
|
6322
|
-
import_picocolors7.default.dim(
|
|
6323
|
-
` Ablo's servers must be able to reach this database \u2014 a localhost or private-network
|
|
6324
|
-
Postgres can't use the direct path. Establish an allowlist, PrivateLink, peering, or VPN.
|
|
6325
|
-
Only when no inbound route is possible, register the signed ${import_picocolors7.default.bold("dataSource()")} endpoint fallback.`
|
|
6326
|
-
)
|
|
6327
|
-
);
|
|
6328
|
-
}
|
|
6329
|
-
console.error();
|
|
6330
|
-
process.exit(1);
|
|
6331
|
-
}
|
|
6332
|
-
async function runAuditInfra() {
|
|
6333
|
-
const dbUrl = requireDatabaseUrl("--audit-infra");
|
|
6334
|
-
const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
|
|
6335
|
-
} });
|
|
6336
|
-
let artifacts;
|
|
6337
|
-
try {
|
|
6338
|
-
artifacts = await auditTenantSyncInfra(sql);
|
|
6339
|
-
} catch (err) {
|
|
6340
|
-
const pg = err ?? {};
|
|
6341
|
-
console.error(import_picocolors7.default.red(` Couldn't audit the database: ${pg.message ?? String(err)}`));
|
|
6342
|
-
await sql.end({ timeout: 2 });
|
|
6343
|
-
process.exit(1);
|
|
6344
|
-
}
|
|
6345
|
-
await sql.end({ timeout: 2 });
|
|
6346
|
-
console.log(`
|
|
6347
|
-
${brand("ablo")} ${import_picocolors7.default.dim("connect --audit-infra")} ${import_picocolors7.default.dim("Stage 5 tenant DB sync-infra audit")}
|
|
6348
|
-
`);
|
|
6349
|
-
const present = artifacts.filter((a) => a.present);
|
|
6350
|
-
if (present.length === 0) {
|
|
6351
|
-
console.log(` ${import_picocolors7.default.green("\u2713")} No deprecated Ablo sync infrastructure found in public.
|
|
6352
|
-
`);
|
|
6353
|
-
process.exit(0);
|
|
6354
|
-
}
|
|
6355
|
-
for (const artifact of present) {
|
|
6356
|
-
const label = artifact.kind === "type" ? "type" : "relation";
|
|
6357
|
-
console.log(` ${import_picocolors7.default.yellow("!")} ${label} ${import_picocolors7.default.bold(`public.${artifact.name}`)} exists`);
|
|
6358
|
-
}
|
|
6359
|
-
console.log(
|
|
6360
|
-
`
|
|
6361
|
-
${import_picocolors7.default.yellow(`${present.length} artifact${present.length === 1 ? "" : "s"} found`)} ` + import_picocolors7.default.dim("\u2014 do not drop automatically. Confirm the org/environment is log-authoritative, then follow ") + import_picocolors7.default.bold("docs/runbooks/wal-stage5-customer-db-infra-cleanup.md") + import_picocolors7.default.dim(".\n")
|
|
6362
|
-
);
|
|
6363
|
-
process.exit(1);
|
|
6364
|
-
}
|
|
6365
|
-
async function connect(argv) {
|
|
6366
|
-
let args;
|
|
6367
|
-
try {
|
|
6368
|
-
args = parseConnectArgs(argv);
|
|
6369
|
-
} catch (err) {
|
|
6370
|
-
console.error(import_picocolors7.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
6371
|
-
process.exit(1);
|
|
6372
|
-
}
|
|
6373
|
-
if (args.apply) {
|
|
6374
|
-
const { runConnectApply: runConnectApply2 } = await Promise.resolve().then(() => (init_connectApply(), connectApply_exports));
|
|
6375
|
-
await runConnectApply2(args);
|
|
6376
|
-
return;
|
|
6377
|
-
}
|
|
6378
|
-
if (args.check) {
|
|
6379
|
-
await runCheck();
|
|
6380
|
-
return;
|
|
6381
|
-
}
|
|
6382
|
-
if (args.register) {
|
|
6383
|
-
await runRegister(args);
|
|
6384
|
-
return;
|
|
6385
|
-
}
|
|
6386
|
-
if (args.auditInfra) {
|
|
6387
|
-
await runAuditInfra();
|
|
6388
|
-
return;
|
|
6389
|
-
}
|
|
6390
|
-
printConnectRecipe(args);
|
|
6154
|
+
process.exit(outcome.exitCode);
|
|
6391
6155
|
}
|
|
6392
|
-
var
|
|
6393
|
-
var
|
|
6394
|
-
"src/cli/
|
|
6156
|
+
var import_picocolors8;
|
|
6157
|
+
var init_connectApply = __esm({
|
|
6158
|
+
"src/cli/connectApply.ts"() {
|
|
6395
6159
|
"use strict";
|
|
6396
6160
|
init_cjs_shims();
|
|
6397
|
-
|
|
6398
|
-
import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
6161
|
+
import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
6399
6162
|
init_src();
|
|
6163
|
+
init_dist2();
|
|
6164
|
+
init_connectSetup();
|
|
6400
6165
|
init_dbRole();
|
|
6401
6166
|
init_config();
|
|
6402
6167
|
init_push();
|
|
6403
6168
|
init_theme();
|
|
6404
|
-
init_remoteValidation();
|
|
6405
|
-
init_migrations();
|
|
6406
|
-
ABLO_PUBLICATION = "ablo_publication";
|
|
6407
|
-
ABLO_REPLICATION_ROLE = "ablo_replicator";
|
|
6408
|
-
ABLO_WRITE_ROLE = "ablo_writer";
|
|
6409
|
-
DIRECT_DATA_SOURCE_ROUTES = [
|
|
6410
|
-
"public-allowlist",
|
|
6411
|
-
"privatelink",
|
|
6412
|
-
"peering",
|
|
6413
|
-
"vpn"
|
|
6414
|
-
];
|
|
6415
|
-
SYNC_INFRA_RELATIONS = [
|
|
6416
|
-
"sync_deltas",
|
|
6417
|
-
"sync_id_seq",
|
|
6418
|
-
"sync_metadata",
|
|
6419
|
-
"mutation_log"
|
|
6420
|
-
];
|
|
6421
|
-
SYNC_INFRA_TYPES = [
|
|
6422
|
-
"participant_kind",
|
|
6423
|
-
"backfill_provenance",
|
|
6424
|
-
"confirmation_state"
|
|
6425
|
-
];
|
|
6426
|
-
CONNECT_USAGE = ` ablo connect \u2014 direct writes, settled by logical replication
|
|
6427
|
-
|
|
6428
|
-
Ablo applies coordinated DML with a scoped writer role. WAL observes what committed,
|
|
6429
|
-
orders it with external changes, and confirms it. Your Postgres remains authoritative.
|
|
6430
|
-
|
|
6431
|
-
Usage:
|
|
6432
|
-
npx ablo connect --apply Set it up for you: create the roles, publish, verify
|
|
6433
|
-
npx ablo connect Print the exact setup SQL instead of running it
|
|
6434
|
-
npx ablo connect --tables a,b,c Publish only these tables (default: all tables)
|
|
6435
|
-
npx ablo connect --role <name> Name the replication role (default: ablo_replicator)
|
|
6436
|
-
npx ablo connect --write-role <name> Name the DML role (default: ablo_writer)
|
|
6437
|
-
npx ablo connect --route <route> public-allowlist | privatelink | peering | vpn
|
|
6438
|
-
npx ablo connect --check Validate DATABASE_URL + ABLO_WRITE_DATABASE_URL
|
|
6439
|
-
npx ablo connect --register Register both scoped credentials as one direct DataSource
|
|
6440
|
-
npx ablo connect --audit-infra Read-only Stage 5 audit for deprecated Ablo sync tables/types
|
|
6441
|
-
|
|
6442
|
-
--apply also takes --yes (skip the confirm) and --show-sql (show the exact statements).`;
|
|
6443
6169
|
}
|
|
6444
6170
|
});
|
|
6445
6171
|
|
|
@@ -131336,7 +131062,7 @@ ${lanes.join("\n")}
|
|
|
131336
131062
|
}
|
|
131337
131063
|
}
|
|
131338
131064
|
function createImportCallExpressionAMD(arg, containsLexicalThis) {
|
|
131339
|
-
const
|
|
131065
|
+
const resolve6 = factory2.createUniqueName("resolve");
|
|
131340
131066
|
const reject = factory2.createUniqueName("reject");
|
|
131341
131067
|
const parameters = [
|
|
131342
131068
|
factory2.createParameterDeclaration(
|
|
@@ -131345,7 +131071,7 @@ ${lanes.join("\n")}
|
|
|
131345
131071
|
/*dotDotDotToken*/
|
|
131346
131072
|
void 0,
|
|
131347
131073
|
/*name*/
|
|
131348
|
-
|
|
131074
|
+
resolve6
|
|
131349
131075
|
),
|
|
131350
131076
|
factory2.createParameterDeclaration(
|
|
131351
131077
|
/*modifiers*/
|
|
@@ -131362,7 +131088,7 @@ ${lanes.join("\n")}
|
|
|
131362
131088
|
factory2.createIdentifier("require"),
|
|
131363
131089
|
/*typeArguments*/
|
|
131364
131090
|
void 0,
|
|
131365
|
-
[factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]),
|
|
131091
|
+
[factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]), resolve6, reject]
|
|
131366
131092
|
)
|
|
131367
131093
|
)
|
|
131368
131094
|
]);
|
|
@@ -217971,8 +217697,8 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
|
|
|
217971
217697
|
installPackage(options) {
|
|
217972
217698
|
this.packageInstallId++;
|
|
217973
217699
|
const request2 = { kind: "installPackage", ...options, id: this.packageInstallId };
|
|
217974
|
-
const promise = new Promise((
|
|
217975
|
-
(this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map())).set(this.packageInstallId, { resolve:
|
|
217700
|
+
const promise = new Promise((resolve6, reject) => {
|
|
217701
|
+
(this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map())).set(this.packageInstallId, { resolve: resolve6, reject });
|
|
217976
217702
|
});
|
|
217977
217703
|
this.installer.send(request2);
|
|
217978
217704
|
return promise;
|
|
@@ -218460,11 +218186,11 @@ var require_commonjs2 = __commonJS({
|
|
|
218460
218186
|
if (pad) {
|
|
218461
218187
|
const need = width - c.length;
|
|
218462
218188
|
if (need > 0) {
|
|
218463
|
-
const
|
|
218189
|
+
const z8 = new Array(need + 1).join("0");
|
|
218464
218190
|
if (i < 0) {
|
|
218465
|
-
c = "-" +
|
|
218191
|
+
c = "-" + z8 + c.slice(1);
|
|
218466
218192
|
} else {
|
|
218467
|
-
c =
|
|
218193
|
+
c = z8 + c;
|
|
218468
218194
|
}
|
|
218469
218195
|
}
|
|
218470
218196
|
}
|
|
@@ -220245,7 +219971,7 @@ var require_path_browserify = __commonJS({
|
|
|
220245
219971
|
}
|
|
220246
219972
|
var posix = {
|
|
220247
219973
|
// path.resolve([from ...], to)
|
|
220248
|
-
resolve: function
|
|
219974
|
+
resolve: function resolve6() {
|
|
220249
219975
|
var resolvedPath = "";
|
|
220250
219976
|
var resolvedAbsolute = false;
|
|
220251
219977
|
var cwd;
|
|
@@ -225145,41 +224871,41 @@ var require_queue = __commonJS({
|
|
|
225145
224871
|
queue.drained = drained;
|
|
225146
224872
|
return queue;
|
|
225147
224873
|
function push2(value) {
|
|
225148
|
-
var p2 = new Promise(function(
|
|
224874
|
+
var p2 = new Promise(function(resolve6, reject) {
|
|
225149
224875
|
pushCb(value, function(err, result) {
|
|
225150
224876
|
if (err) {
|
|
225151
224877
|
reject(err);
|
|
225152
224878
|
return;
|
|
225153
224879
|
}
|
|
225154
|
-
|
|
224880
|
+
resolve6(result);
|
|
225155
224881
|
});
|
|
225156
224882
|
});
|
|
225157
224883
|
p2.catch(noop3);
|
|
225158
224884
|
return p2;
|
|
225159
224885
|
}
|
|
225160
224886
|
function unshift(value) {
|
|
225161
|
-
var p2 = new Promise(function(
|
|
224887
|
+
var p2 = new Promise(function(resolve6, reject) {
|
|
225162
224888
|
unshiftCb(value, function(err, result) {
|
|
225163
224889
|
if (err) {
|
|
225164
224890
|
reject(err);
|
|
225165
224891
|
return;
|
|
225166
224892
|
}
|
|
225167
|
-
|
|
224893
|
+
resolve6(result);
|
|
225168
224894
|
});
|
|
225169
224895
|
});
|
|
225170
224896
|
p2.catch(noop3);
|
|
225171
224897
|
return p2;
|
|
225172
224898
|
}
|
|
225173
224899
|
function drained() {
|
|
225174
|
-
var p2 = new Promise(function(
|
|
224900
|
+
var p2 = new Promise(function(resolve6) {
|
|
225175
224901
|
process.nextTick(function() {
|
|
225176
224902
|
if (queue.idle()) {
|
|
225177
|
-
|
|
224903
|
+
resolve6();
|
|
225178
224904
|
} else {
|
|
225179
224905
|
var previousDrain = queue.drain;
|
|
225180
224906
|
queue.drain = function() {
|
|
225181
224907
|
if (typeof previousDrain === "function") previousDrain();
|
|
225182
|
-
|
|
224908
|
+
resolve6();
|
|
225183
224909
|
queue.drain = previousDrain;
|
|
225184
224910
|
};
|
|
225185
224911
|
}
|
|
@@ -225676,9 +225402,9 @@ var require_stream3 = __commonJS({
|
|
|
225676
225402
|
});
|
|
225677
225403
|
}
|
|
225678
225404
|
_getStat(filepath) {
|
|
225679
|
-
return new Promise((
|
|
225405
|
+
return new Promise((resolve6, reject) => {
|
|
225680
225406
|
this._stat(filepath, this._fsStatSettings, (error, stats) => {
|
|
225681
|
-
return error === null ?
|
|
225407
|
+
return error === null ? resolve6(stats) : reject(error);
|
|
225682
225408
|
});
|
|
225683
225409
|
});
|
|
225684
225410
|
}
|
|
@@ -225703,10 +225429,10 @@ var require_async5 = __commonJS({
|
|
|
225703
225429
|
this._readerStream = new stream_1.default(this._settings);
|
|
225704
225430
|
}
|
|
225705
225431
|
dynamic(root, options) {
|
|
225706
|
-
return new Promise((
|
|
225432
|
+
return new Promise((resolve6, reject) => {
|
|
225707
225433
|
this._walkAsync(root, options, (error, entries) => {
|
|
225708
225434
|
if (error === null) {
|
|
225709
|
-
|
|
225435
|
+
resolve6(entries);
|
|
225710
225436
|
} else {
|
|
225711
225437
|
reject(error);
|
|
225712
225438
|
}
|
|
@@ -225716,10 +225442,10 @@ var require_async5 = __commonJS({
|
|
|
225716
225442
|
async static(patterns, options) {
|
|
225717
225443
|
const entries = [];
|
|
225718
225444
|
const stream = this._readerStream.static(patterns, options);
|
|
225719
|
-
return new Promise((
|
|
225445
|
+
return new Promise((resolve6, reject) => {
|
|
225720
225446
|
stream.once("error", reject);
|
|
225721
225447
|
stream.on("data", (entry) => entries.push(entry));
|
|
225722
|
-
stream.once("end", () =>
|
|
225448
|
+
stream.once("end", () => resolve6(entries));
|
|
225723
225449
|
});
|
|
225724
225450
|
}
|
|
225725
225451
|
};
|
|
@@ -258625,12 +258351,12 @@ type XMLHttpRequestResponseType = "" | "arraybuffer" | "blob" | "document" | "js
|
|
|
258625
258351
|
};
|
|
258626
258352
|
var NodeRuntimeFileSystem = class {
|
|
258627
258353
|
delete(path2) {
|
|
258628
|
-
return new Promise((
|
|
258354
|
+
return new Promise((resolve6, reject) => {
|
|
258629
258355
|
fs__namespace.rm(path2, { recursive: true }, (err) => {
|
|
258630
258356
|
if (err)
|
|
258631
258357
|
reject(err);
|
|
258632
258358
|
else
|
|
258633
|
-
|
|
258359
|
+
resolve6();
|
|
258634
258360
|
});
|
|
258635
258361
|
});
|
|
258636
258362
|
}
|
|
@@ -258649,12 +258375,12 @@ type XMLHttpRequestResponseType = "" | "arraybuffer" | "blob" | "document" | "js
|
|
|
258649
258375
|
}));
|
|
258650
258376
|
}
|
|
258651
258377
|
readFile(filePath, encoding = "utf-8") {
|
|
258652
|
-
return new Promise((
|
|
258378
|
+
return new Promise((resolve6, reject) => {
|
|
258653
258379
|
fs__namespace.readFile(filePath, encoding, (err, data) => {
|
|
258654
258380
|
if (err)
|
|
258655
258381
|
reject(err);
|
|
258656
258382
|
else
|
|
258657
|
-
|
|
258383
|
+
resolve6(data);
|
|
258658
258384
|
});
|
|
258659
258385
|
});
|
|
258660
258386
|
}
|
|
@@ -258662,12 +258388,12 @@ type XMLHttpRequestResponseType = "" | "arraybuffer" | "blob" | "document" | "js
|
|
|
258662
258388
|
return fs__namespace.readFileSync(filePath, encoding);
|
|
258663
258389
|
}
|
|
258664
258390
|
async writeFile(filePath, fileText) {
|
|
258665
|
-
await new Promise((
|
|
258391
|
+
await new Promise((resolve6, reject) => {
|
|
258666
258392
|
fs__namespace.writeFile(filePath, fileText, (err) => {
|
|
258667
258393
|
if (err)
|
|
258668
258394
|
reject(err);
|
|
258669
258395
|
else
|
|
258670
|
-
|
|
258396
|
+
resolve6();
|
|
258671
258397
|
});
|
|
258672
258398
|
});
|
|
258673
258399
|
}
|
|
@@ -258681,12 +258407,12 @@ type XMLHttpRequestResponseType = "" | "arraybuffer" | "blob" | "document" | "js
|
|
|
258681
258407
|
fs__namespace.mkdirSync(dirPath, { recursive: true });
|
|
258682
258408
|
}
|
|
258683
258409
|
move(srcPath, destPath) {
|
|
258684
|
-
return new Promise((
|
|
258410
|
+
return new Promise((resolve6, reject) => {
|
|
258685
258411
|
fs__namespace.rename(srcPath, destPath, (err) => {
|
|
258686
258412
|
if (err)
|
|
258687
258413
|
reject(err);
|
|
258688
258414
|
else
|
|
258689
|
-
|
|
258415
|
+
resolve6();
|
|
258690
258416
|
});
|
|
258691
258417
|
});
|
|
258692
258418
|
}
|
|
@@ -258694,12 +258420,12 @@ type XMLHttpRequestResponseType = "" | "arraybuffer" | "blob" | "document" | "js
|
|
|
258694
258420
|
fs__namespace.renameSync(srcPath, destPath);
|
|
258695
258421
|
}
|
|
258696
258422
|
copy(srcPath, destPath) {
|
|
258697
|
-
return new Promise((
|
|
258423
|
+
return new Promise((resolve6, reject) => {
|
|
258698
258424
|
fs__namespace.copyFile(srcPath, destPath, (err) => {
|
|
258699
258425
|
if (err)
|
|
258700
258426
|
reject(err);
|
|
258701
258427
|
else
|
|
258702
|
-
|
|
258428
|
+
resolve6();
|
|
258703
258429
|
});
|
|
258704
258430
|
});
|
|
258705
258431
|
}
|
|
@@ -258707,15 +258433,15 @@ type XMLHttpRequestResponseType = "" | "arraybuffer" | "blob" | "document" | "js
|
|
|
258707
258433
|
fs__namespace.copyFileSync(srcPath, destPath);
|
|
258708
258434
|
}
|
|
258709
258435
|
stat(path2) {
|
|
258710
|
-
return new Promise((
|
|
258436
|
+
return new Promise((resolve6, reject) => {
|
|
258711
258437
|
fs__namespace.stat(path2, (err, stat) => {
|
|
258712
258438
|
if (err) {
|
|
258713
258439
|
if (err.code === "ENOENT" || err.code === "ENOTDIR")
|
|
258714
|
-
|
|
258440
|
+
resolve6(void 0);
|
|
258715
258441
|
else
|
|
258716
258442
|
reject(err);
|
|
258717
258443
|
} else {
|
|
258718
|
-
|
|
258444
|
+
resolve6(stat);
|
|
258719
258445
|
}
|
|
258720
258446
|
});
|
|
258721
258447
|
});
|
|
@@ -282393,9 +282119,9 @@ Node text: ${this.#forgottenText}`;
|
|
|
282393
282119
|
// src/cli/index.ts
|
|
282394
282120
|
init_cjs_shims();
|
|
282395
282121
|
init_dist2();
|
|
282396
|
-
var
|
|
282397
|
-
var
|
|
282398
|
-
var
|
|
282122
|
+
var import_picocolors23 = __toESM(require_picocolors(), 1);
|
|
282123
|
+
var import_fs12 = require("fs");
|
|
282124
|
+
var import_path7 = require("path");
|
|
282399
282125
|
var import_child_process3 = require("child_process");
|
|
282400
282126
|
|
|
282401
282127
|
// src/cli/migrate.ts
|
|
@@ -282511,7 +282237,7 @@ async function applyStatements(dbUrl, targetSchema, statements, concurrent = [])
|
|
|
282511
282237
|
if (pg.code === import_schema3.PG_LOCK_NOT_AVAILABLE && attempt < maxLockAttempts) {
|
|
282512
282238
|
const backoffMs = (0, import_schema3.ddlLockRetryBackoffMs)(attempt);
|
|
282513
282239
|
log.warn("schema change blocked by a lock; backing off and retrying", { targetSchema, attempt, backoffMs });
|
|
282514
|
-
await new Promise((
|
|
282240
|
+
await new Promise((resolve6) => setTimeout(resolve6, backoffMs));
|
|
282515
282241
|
continue;
|
|
282516
282242
|
}
|
|
282517
282243
|
throw err;
|
|
@@ -282581,16 +282307,747 @@ async function migrate(argv) {
|
|
|
282581
282307
|
}
|
|
282582
282308
|
}
|
|
282583
282309
|
|
|
282310
|
+
// src/cli/connect.ts
|
|
282311
|
+
init_cjs_shims();
|
|
282312
|
+
init_errors();
|
|
282313
|
+
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
282314
|
+
init_src();
|
|
282315
|
+
init_dbRole();
|
|
282316
|
+
init_config();
|
|
282317
|
+
init_push();
|
|
282318
|
+
init_theme();
|
|
282319
|
+
|
|
282320
|
+
// src/cli/remoteValidation.ts
|
|
282321
|
+
init_cjs_shims();
|
|
282322
|
+
var DIAL_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
282323
|
+
"ENOTFOUND",
|
|
282324
|
+
"EAI_AGAIN",
|
|
282325
|
+
"ECONNREFUSED",
|
|
282326
|
+
"ECONNRESET",
|
|
282327
|
+
"ETIMEDOUT",
|
|
282328
|
+
"ENETUNREACH",
|
|
282329
|
+
"EHOSTUNREACH",
|
|
282330
|
+
"CONNECT_TIMEOUT"
|
|
282331
|
+
]);
|
|
282332
|
+
function dialFailureReason(err) {
|
|
282333
|
+
if (err === null || typeof err !== "object") return null;
|
|
282334
|
+
const coded = err;
|
|
282335
|
+
const code = typeof coded.code === "string" ? coded.code : null;
|
|
282336
|
+
if (code && DIAL_FAILURE_CODES.has(code)) {
|
|
282337
|
+
return typeof coded.message === "string" && coded.message.length > 0 ? coded.message : code;
|
|
282338
|
+
}
|
|
282339
|
+
if (Array.isArray(coded.errors)) {
|
|
282340
|
+
for (const member of coded.errors) {
|
|
282341
|
+
const reason = dialFailureReason(member);
|
|
282342
|
+
if (reason) return reason;
|
|
282343
|
+
}
|
|
282344
|
+
}
|
|
282345
|
+
return null;
|
|
282346
|
+
}
|
|
282347
|
+
function validateEndpoint(baseUrl2) {
|
|
282348
|
+
return `${baseUrl2.replace(/\/+$/, "")}/api/v1/datasources/validate`;
|
|
282349
|
+
}
|
|
282350
|
+
function describeRemoteFailure(failure) {
|
|
282351
|
+
switch (failure.item) {
|
|
282352
|
+
case "wal_level":
|
|
282353
|
+
return {
|
|
282354
|
+
label: failure.actual ? `wal_level is ${failure.actual} (need logical)` : `wal_level must be logical`,
|
|
282355
|
+
fix: failure.fix
|
|
282356
|
+
};
|
|
282357
|
+
case "publication":
|
|
282358
|
+
return { label: "the Ablo publication does not exist", fix: failure.fix };
|
|
282359
|
+
case "replication_role":
|
|
282360
|
+
return { label: "the DATABASE_URL role lacks the REPLICATION attribute", fix: failure.fix };
|
|
282361
|
+
case "replica_identity":
|
|
282362
|
+
return {
|
|
282363
|
+
label: `published tables cannot replicate UPDATE/DELETE${failure.actual ? ` (${failure.actual})` : ""}`,
|
|
282364
|
+
fix: failure.fix
|
|
282365
|
+
};
|
|
282366
|
+
case "write_role":
|
|
282367
|
+
return { label: "ABLO_WRITE_DATABASE_URL is not a scoped DML role", fix: failure.fix };
|
|
282368
|
+
case "row_security":
|
|
282369
|
+
return { label: "the direct-write role does not enforce row_security", fix: failure.fix };
|
|
282370
|
+
case "schema_privileges":
|
|
282371
|
+
return { label: "the direct-write role has unsafe schema privileges", fix: failure.fix };
|
|
282372
|
+
case "idempotency_ledger":
|
|
282373
|
+
return { label: "ablo_idempotency is missing or has unsafe grants", fix: failure.fix };
|
|
282374
|
+
case "table_privileges":
|
|
282375
|
+
return {
|
|
282376
|
+
label: `the direct-write role lacks application DML${failure.actual ? ` (${failure.actual})` : ""}`,
|
|
282377
|
+
fix: failure.fix
|
|
282378
|
+
};
|
|
282379
|
+
case "logical_marker":
|
|
282380
|
+
return { label: "the direct-write role cannot emit the correlation marker", fix: failure.fix };
|
|
282381
|
+
default:
|
|
282382
|
+
return { label: failure.item, fix: failure.fix };
|
|
282383
|
+
}
|
|
282384
|
+
}
|
|
282385
|
+
function parseWireFailures(value) {
|
|
282386
|
+
if (!Array.isArray(value)) return [];
|
|
282387
|
+
const failures = [];
|
|
282388
|
+
for (const entry of value) {
|
|
282389
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
282390
|
+
const { item, actual, fix } = entry;
|
|
282391
|
+
if (typeof item !== "string" || typeof fix !== "string") continue;
|
|
282392
|
+
failures.push({ item, fix, ...typeof actual === "string" ? { actual } : {} });
|
|
282393
|
+
}
|
|
282394
|
+
return failures;
|
|
282395
|
+
}
|
|
282396
|
+
async function requestRemoteValidation(input) {
|
|
282397
|
+
const doFetch = input.fetchImpl ?? fetch;
|
|
282398
|
+
let res;
|
|
282399
|
+
try {
|
|
282400
|
+
res = await doFetch(validateEndpoint(input.apiUrl), {
|
|
282401
|
+
method: "POST",
|
|
282402
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${input.apiKey}` },
|
|
282403
|
+
body: JSON.stringify({
|
|
282404
|
+
...input.connectionString ? { connectionString: input.connectionString } : {},
|
|
282405
|
+
...input.writeConnectionString ? { writeConnectionString: input.writeConnectionString } : {}
|
|
282406
|
+
})
|
|
282407
|
+
});
|
|
282408
|
+
} catch (err) {
|
|
282409
|
+
return {
|
|
282410
|
+
ok: false,
|
|
282411
|
+
status: 0,
|
|
282412
|
+
message: `couldn't reach ${input.apiUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
282413
|
+
};
|
|
282414
|
+
}
|
|
282415
|
+
const body = await res.json().catch(() => ({}));
|
|
282416
|
+
if (!res.ok) {
|
|
282417
|
+
const code = typeof body.code === "string" ? body.code : body.error?.code;
|
|
282418
|
+
const message = typeof body.message === "string" ? body.message : typeof body.error?.message === "string" ? body.error.message : `HTTP ${res.status}`;
|
|
282419
|
+
return {
|
|
282420
|
+
ok: false,
|
|
282421
|
+
status: res.status,
|
|
282422
|
+
message,
|
|
282423
|
+
...typeof code === "string" ? { code } : {}
|
|
282424
|
+
};
|
|
282425
|
+
}
|
|
282426
|
+
const reachable = body.reachable === true;
|
|
282427
|
+
return {
|
|
282428
|
+
ok: true,
|
|
282429
|
+
reachable,
|
|
282430
|
+
ready: body.ready === true,
|
|
282431
|
+
...typeof body.reason === "string" ? { reason: body.reason } : {},
|
|
282432
|
+
failures: parseWireFailures(body.failures)
|
|
282433
|
+
};
|
|
282434
|
+
}
|
|
282435
|
+
|
|
282436
|
+
// src/cli/connect.ts
|
|
282437
|
+
init_connectSetup();
|
|
282438
|
+
function parseConnectArgs(argv) {
|
|
282439
|
+
let check2 = false;
|
|
282440
|
+
let register = false;
|
|
282441
|
+
let apply2 = false;
|
|
282442
|
+
let rotate = false;
|
|
282443
|
+
let url;
|
|
282444
|
+
let yes = false;
|
|
282445
|
+
let showSql = false;
|
|
282446
|
+
let scan = false;
|
|
282447
|
+
let tables = [];
|
|
282448
|
+
let role = ABLO_REPLICATION_ROLE;
|
|
282449
|
+
let writeRole = ABLO_WRITE_ROLE;
|
|
282450
|
+
let route = "public-allowlist";
|
|
282451
|
+
let start = 0;
|
|
282452
|
+
const lead = argv[0];
|
|
282453
|
+
if (lead !== void 0 && !lead.startsWith("-")) {
|
|
282454
|
+
switch (lead) {
|
|
282455
|
+
case "register":
|
|
282456
|
+
register = true;
|
|
282457
|
+
break;
|
|
282458
|
+
case "check":
|
|
282459
|
+
check2 = true;
|
|
282460
|
+
break;
|
|
282461
|
+
case "apply":
|
|
282462
|
+
apply2 = true;
|
|
282463
|
+
break;
|
|
282464
|
+
case "rotate":
|
|
282465
|
+
rotate = true;
|
|
282466
|
+
break;
|
|
282467
|
+
case "scan":
|
|
282468
|
+
scan = true;
|
|
282469
|
+
break;
|
|
282470
|
+
default:
|
|
282471
|
+
throw new AbloValidationError(
|
|
282472
|
+
`unknown connect subcommand: ${lead} (expected register, deregister, check, apply, rotate, scan)`,
|
|
282473
|
+
{ code: "cli_invalid_arguments" }
|
|
282474
|
+
);
|
|
282475
|
+
}
|
|
282476
|
+
start = 1;
|
|
282477
|
+
}
|
|
282478
|
+
for (let i = start; i < argv.length; i++) {
|
|
282479
|
+
const arg = argv[i];
|
|
282480
|
+
switch (arg) {
|
|
282481
|
+
case "--url":
|
|
282482
|
+
url = argv[++i] ?? url;
|
|
282483
|
+
break;
|
|
282484
|
+
case "--yes":
|
|
282485
|
+
case "-y":
|
|
282486
|
+
yes = true;
|
|
282487
|
+
break;
|
|
282488
|
+
case "--show-sql":
|
|
282489
|
+
showSql = true;
|
|
282490
|
+
break;
|
|
282491
|
+
case "--tables": {
|
|
282492
|
+
const value = argv[++i] ?? "";
|
|
282493
|
+
tables = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
282494
|
+
break;
|
|
282495
|
+
}
|
|
282496
|
+
case "--role":
|
|
282497
|
+
role = argv[++i] ?? role;
|
|
282498
|
+
break;
|
|
282499
|
+
case "--write-role":
|
|
282500
|
+
writeRole = argv[++i] ?? writeRole;
|
|
282501
|
+
break;
|
|
282502
|
+
case "--route": {
|
|
282503
|
+
const value = argv[++i] ?? "";
|
|
282504
|
+
if (!DIRECT_DATA_SOURCE_ROUTES.includes(value)) {
|
|
282505
|
+
throw new AbloValidationError(
|
|
282506
|
+
`invalid direct route: ${value || "(missing)"} (expected ${DIRECT_DATA_SOURCE_ROUTES.join(", ")})`,
|
|
282507
|
+
{ code: "cli_invalid_arguments" }
|
|
282508
|
+
);
|
|
282509
|
+
}
|
|
282510
|
+
route = value;
|
|
282511
|
+
break;
|
|
282512
|
+
}
|
|
282513
|
+
default:
|
|
282514
|
+
throw new AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
|
|
282515
|
+
}
|
|
282516
|
+
}
|
|
282517
|
+
if (role === writeRole) {
|
|
282518
|
+
throw new AbloValidationError("replication and write roles must be different", {
|
|
282519
|
+
code: "cli_invalid_arguments"
|
|
282520
|
+
});
|
|
282521
|
+
}
|
|
282522
|
+
return {
|
|
282523
|
+
check: check2,
|
|
282524
|
+
register,
|
|
282525
|
+
apply: apply2,
|
|
282526
|
+
rotate,
|
|
282527
|
+
url,
|
|
282528
|
+
yes,
|
|
282529
|
+
showSql,
|
|
282530
|
+
scan,
|
|
282531
|
+
tables,
|
|
282532
|
+
role,
|
|
282533
|
+
writeRole,
|
|
282534
|
+
route
|
|
282535
|
+
};
|
|
282536
|
+
}
|
|
282537
|
+
function printConnectRecipe(args) {
|
|
282538
|
+
const sql = connectSetupSql({
|
|
282539
|
+
tables: args.tables,
|
|
282540
|
+
role: args.role,
|
|
282541
|
+
writeRole: args.writeRole
|
|
282542
|
+
});
|
|
282543
|
+
console.log(
|
|
282544
|
+
`
|
|
282545
|
+
${brand("ablo")} ${import_picocolors9.default.dim("connect")} ${import_picocolors9.default.dim("direct writes + WAL-settled sync")}
|
|
282546
|
+
`
|
|
282547
|
+
);
|
|
282548
|
+
console.log(
|
|
282549
|
+
` Ablo applies coordinated writes directly to your Postgres with a scoped DML role. WAL
|
|
282550
|
+
observes what committed, orders it with external writes, and confirms it in the sync log.
|
|
282551
|
+
Your database stays authoritative; Ablo never owns or migrates your application tables.
|
|
282552
|
+
|
|
282553
|
+
${import_picocolors9.default.bold("ablo connect apply")} runs every step below for you from a one-time admin
|
|
282554
|
+
connection and leaves your app holding only ${import_picocolors9.default.bold("ABLO_API_KEY")}. To do it by hand,
|
|
282555
|
+
run this once against your Postgres ${import_picocolors9.default.dim("(as a superuser / DB owner)")}:
|
|
282556
|
+
`
|
|
282557
|
+
);
|
|
282558
|
+
console.log(
|
|
282559
|
+
` ${import_picocolors9.default.bold("1.")} Enable logical decoding ${import_picocolors9.default.dim("(then RESTART Postgres \u2014 wal_level is not reloadable)")}`
|
|
282560
|
+
);
|
|
282561
|
+
console.log(` ${import_picocolors9.default.cyan(sql[0])}`);
|
|
282562
|
+
console.log(
|
|
282563
|
+
import_picocolors9.default.dim(
|
|
282564
|
+
` Managed hosts don't take ALTER SYSTEM: on Amazon RDS / Aurora set ${import_picocolors9.default.bold("rds.logical_replication = 1")}
|
|
282565
|
+
in the parameter group and reboot; on Neon or Supabase enable logical replication in the
|
|
282566
|
+
project settings. ${import_picocolors9.default.bold("ablo connect apply")} detects the host and does the right thing.`
|
|
282567
|
+
)
|
|
282568
|
+
);
|
|
282569
|
+
console.log(`
|
|
282570
|
+
${import_picocolors9.default.bold("2.")} Publish the tables Ablo should read`);
|
|
282571
|
+
console.log(` ${import_picocolors9.default.cyan(sql[1])}`);
|
|
282572
|
+
if (args.tables.length === 0) {
|
|
282573
|
+
console.log(
|
|
282574
|
+
import_picocolors9.default.dim(
|
|
282575
|
+
` (Scope it with ${import_picocolors9.default.bold("ablo connect --tables a,b,c")} to publish a subset.)`
|
|
282576
|
+
)
|
|
282577
|
+
);
|
|
282578
|
+
}
|
|
282579
|
+
const writerStart = sql.findIndex((s) => s.includes("NOSUPERUSER NOBYPASSRLS"));
|
|
282580
|
+
const replicationStatements = sql.slice(2, writerStart);
|
|
282581
|
+
const writerStatements = sql.slice(writerStart);
|
|
282582
|
+
console.log(
|
|
282583
|
+
`
|
|
282584
|
+
${import_picocolors9.default.bold("3.")} Create the scoped replication role ${import_picocolors9.default.dim("(pick your own replication password)")}`
|
|
282585
|
+
);
|
|
282586
|
+
for (const statement of replicationStatements) {
|
|
282587
|
+
for (const line of statement.split("\n")) console.log(` ${import_picocolors9.default.cyan(line)}`);
|
|
282588
|
+
}
|
|
282589
|
+
console.log(
|
|
282590
|
+
import_picocolors9.default.dim(
|
|
282591
|
+
` On Amazon RDS, the REPLICATION attribute is granted, not set directly:
|
|
282592
|
+
${import_picocolors9.default.bold(`GRANT rds_replication TO ${quoteIdent(args.role)};`)}`
|
|
282593
|
+
)
|
|
282594
|
+
);
|
|
282595
|
+
console.log(
|
|
282596
|
+
`
|
|
282597
|
+
${import_picocolors9.default.bold("4.")} Create the separate DML role and the idempotency ledger ` + import_picocolors9.default.dim("(pick your own write password)")
|
|
282598
|
+
);
|
|
282599
|
+
for (const statement of writerStatements) {
|
|
282600
|
+
for (const line of statement.split("\n")) console.log(` ${import_picocolors9.default.cyan(line)}`);
|
|
282601
|
+
}
|
|
282602
|
+
console.log(
|
|
282603
|
+
import_picocolors9.default.dim(
|
|
282604
|
+
` The writer gets row DML + ledger access only. It has no REPLICATION, schema CREATE,
|
|
282605
|
+
role administration, database creation, ownership, or customer-table DDL. Direct uses
|
|
282606
|
+
${import_picocolors9.default.bold("ablo_idempotency")} but no outbox; WAL carries the committed row changes.
|
|
282607
|
+
Each ledger row carries an ${import_picocolors9.default.bold("expires_at")}; the writer can't DELETE (tamper-
|
|
282608
|
+
resistance), so prune it from your own admin/cron when convenient:
|
|
282609
|
+
${import_picocolors9.default.bold("DELETE FROM ablo_idempotency WHERE expires_at < now();")}`
|
|
282610
|
+
)
|
|
282611
|
+
);
|
|
282612
|
+
console.log(
|
|
282613
|
+
`
|
|
282614
|
+
${import_picocolors9.default.bold("5.")} Register the two roles with Ablo. Set them just long enough to register \u2014
|
|
282615
|
+
Ablo holds them from here, so your app keeps only ${import_picocolors9.default.bold("ABLO_API_KEY")}:
|
|
282616
|
+
${import_picocolors9.default.bold("ABLO_REPLICATION_DATABASE_URL")} ${import_picocolors9.default.dim(`\u2192 ${args.role} (replication only)`)}
|
|
282617
|
+
${import_picocolors9.default.bold("ABLO_WRITE_DATABASE_URL")} ${import_picocolors9.default.dim(`\u2192 ${args.writeRole} (DML only)`)}
|
|
282618
|
+
${import_picocolors9.default.cyan("npx ablo connect register")}
|
|
282619
|
+
`
|
|
282620
|
+
);
|
|
282621
|
+
console.log(
|
|
282622
|
+
` ${import_picocolors9.default.bold("6.")} Verify readiness ${import_picocolors9.default.dim("(checked from Ablo\u2019s side \u2014 needs only ABLO_API_KEY):")}
|
|
282623
|
+
${import_picocolors9.default.cyan("npx ablo connect check")}
|
|
282624
|
+
`
|
|
282625
|
+
);
|
|
282626
|
+
console.log(
|
|
282627
|
+
import_picocolors9.default.dim(
|
|
282628
|
+
` Reachable databases use this direct path. If no inbound route can be established, use the
|
|
282629
|
+
signed ${import_picocolors9.default.bold("dataSource()")} endpoint fallback; its correlated events confirm writes
|
|
282630
|
+
without an Ablo-side customer database socket.`
|
|
282631
|
+
)
|
|
282632
|
+
);
|
|
282633
|
+
console.log();
|
|
282634
|
+
}
|
|
282635
|
+
var SYNC_INFRA_RELATIONS = [
|
|
282636
|
+
"sync_deltas",
|
|
282637
|
+
"sync_id_seq",
|
|
282638
|
+
"sync_metadata",
|
|
282639
|
+
"mutation_log"
|
|
282640
|
+
];
|
|
282641
|
+
var SYNC_INFRA_TYPES = ["participant_kind", "backfill_provenance", "confirmation_state"];
|
|
282642
|
+
function printCheckItem(item) {
|
|
282643
|
+
if (item.ok) {
|
|
282644
|
+
console.log(` ${import_picocolors9.default.green("\u2713")} ${item.label}`);
|
|
282645
|
+
} else {
|
|
282646
|
+
console.log(` ${import_picocolors9.default.red("\u2717")} ${item.label}`);
|
|
282647
|
+
if (item.fix) {
|
|
282648
|
+
for (const line of item.fix.split("\n")) console.log(` ${import_picocolors9.default.red("\u2022")} ${line}`);
|
|
282649
|
+
}
|
|
282650
|
+
}
|
|
282651
|
+
}
|
|
282652
|
+
async function probeDirectWriteReadiness(sql, opts = {}) {
|
|
282653
|
+
const schema = opts.schema ?? "public";
|
|
282654
|
+
const publication = opts.publication ?? ABLO_PUBLICATION;
|
|
282655
|
+
const items = [];
|
|
282656
|
+
const roleRows = await sql.unsafe(
|
|
282657
|
+
`SELECT rolname, rolsuper, rolbypassrls, rolcreatedb, rolcreaterole, rolreplication
|
|
282658
|
+
FROM pg_roles WHERE rolname = current_user`
|
|
282659
|
+
);
|
|
282660
|
+
const role = roleRows[0];
|
|
282661
|
+
const dangerous = Boolean(
|
|
282662
|
+
!role || role.rolsuper || role.rolbypassrls || role.rolcreatedb || role.rolcreaterole || role.rolreplication
|
|
282663
|
+
);
|
|
282664
|
+
items.push(
|
|
282665
|
+
dangerous ? {
|
|
282666
|
+
ok: false,
|
|
282667
|
+
label: `ABLO_WRITE_DATABASE_URL is not a scoped DML role`,
|
|
282668
|
+
fix: `Use ${ABLO_WRITE_ROLE} from the setup recipe: NOSUPERUSER, NOBYPASSRLS, NOCREATEDB, NOCREATEROLE, NOREPLICATION.`
|
|
282669
|
+
} : { ok: true, label: `write role ${import_picocolors9.default.bold(role?.rolname ?? ABLO_WRITE_ROLE)} is DML-only` }
|
|
282670
|
+
);
|
|
282671
|
+
const schemaRows = await sql.unsafe(
|
|
282672
|
+
`SELECT
|
|
282673
|
+
has_schema_privilege(current_user, $1, 'USAGE') AS usage,
|
|
282674
|
+
has_schema_privilege(current_user, $1, 'CREATE') AS create,
|
|
282675
|
+
current_setting('row_security') AS row_security`,
|
|
282676
|
+
[schema]
|
|
282677
|
+
);
|
|
282678
|
+
const schemaRow = schemaRows[0];
|
|
282679
|
+
items.push(
|
|
282680
|
+
schemaRow?.usage && !schemaRow.create && schemaRow.row_security === "on" ? { ok: true, label: `writer uses ${schema} with row_security on and no schema CREATE` } : {
|
|
282681
|
+
ok: false,
|
|
282682
|
+
label: `writer schema/RLS privileges are not least-privilege`,
|
|
282683
|
+
fix: `REVOKE CREATE ON SCHEMA ${quoteIdent(schema)} FROM PUBLIC; GRANT USAGE ON SCHEMA ${quoteIdent(schema)} TO ${quoteIdent(role?.rolname ?? ABLO_WRITE_ROLE)}; ALTER ROLE ${quoteIdent(role?.rolname ?? ABLO_WRITE_ROLE)} SET row_security = on;`
|
|
282684
|
+
}
|
|
282685
|
+
);
|
|
282686
|
+
const ledgerName = `${quoteIdent(schema)}.${quoteIdent("ablo_idempotency")}`;
|
|
282687
|
+
const ledgerRows = await sql.unsafe(
|
|
282688
|
+
`SELECT
|
|
282689
|
+
to_regclass($1) IS NOT NULL AS present,
|
|
282690
|
+
CASE WHEN to_regclass($1) IS NULL THEN false ELSE has_table_privilege(current_user, $1, 'SELECT,INSERT,UPDATE') END AS writes,
|
|
282691
|
+
CASE WHEN to_regclass($1) IS NULL THEN false ELSE has_table_privilege(current_user, $1, 'DELETE') END AS deletes`,
|
|
282692
|
+
[ledgerName]
|
|
282693
|
+
);
|
|
282694
|
+
const ledger = ledgerRows[0];
|
|
282695
|
+
items.push(
|
|
282696
|
+
ledger?.present && ledger.writes && !ledger.deletes ? { ok: true, label: `${import_picocolors9.default.bold("ablo_idempotency")} is durable and protected from DELETE` } : {
|
|
282697
|
+
ok: false,
|
|
282698
|
+
label: `${import_picocolors9.default.bold("ablo_idempotency")} is missing or has unsafe grants`,
|
|
282699
|
+
fix: `Apply the DML-role and idempotency-ledger statements printed by ${import_picocolors9.default.bold("ablo connect")}.`
|
|
282700
|
+
}
|
|
282701
|
+
);
|
|
282702
|
+
const tableRows = await sql.unsafe(
|
|
282703
|
+
`SELECT format('%I.%I', schemaname, tablename) AS relation
|
|
282704
|
+
FROM pg_publication_tables
|
|
282705
|
+
WHERE pubname = $1 AND schemaname = $2 AND tablename <> 'ablo_idempotency'
|
|
282706
|
+
AND NOT has_table_privilege(
|
|
282707
|
+
current_user,
|
|
282708
|
+
format('%I.%I', schemaname, tablename),
|
|
282709
|
+
'SELECT,INSERT,UPDATE,DELETE'
|
|
282710
|
+
)`,
|
|
282711
|
+
[publication, schema]
|
|
282712
|
+
);
|
|
282713
|
+
items.push(
|
|
282714
|
+
tableRows.length === 0 ? { ok: true, label: `writer can apply DML to every published application table` } : {
|
|
282715
|
+
ok: false,
|
|
282716
|
+
label: `writer lacks DML on ${tableRows.length} published table${tableRows.length === 1 ? "" : "s"}`,
|
|
282717
|
+
fix: `Grant SELECT, INSERT, UPDATE, DELETE on: ${tableRows.map((row) => row.relation).join(", ")}`
|
|
282718
|
+
}
|
|
282719
|
+
);
|
|
282720
|
+
const rlsCoverageRows = await sql.unsafe(
|
|
282721
|
+
`SELECT
|
|
282722
|
+
count(*)::int AS total,
|
|
282723
|
+
(count(*) FILTER (WHERE c.relrowsecurity))::int AS with_rls
|
|
282724
|
+
FROM pg_publication_tables pt
|
|
282725
|
+
JOIN pg_class c ON c.relname = pt.tablename
|
|
282726
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
|
|
282727
|
+
WHERE pt.pubname = $1 AND pt.schemaname = $2 AND pt.tablename <> 'ablo_idempotency'`,
|
|
282728
|
+
[publication, schema]
|
|
282729
|
+
);
|
|
282730
|
+
const coverage = rlsCoverageRows[0];
|
|
282731
|
+
if (coverage && coverage.total > 0) {
|
|
282732
|
+
const { total, with_rls: withRls } = coverage;
|
|
282733
|
+
const plural = total === 1 ? "" : "s";
|
|
282734
|
+
items.push(
|
|
282735
|
+
withRls === total ? {
|
|
282736
|
+
ok: true,
|
|
282737
|
+
label: `row-level security governs the writer on all ${total} published table${plural}`
|
|
282738
|
+
} : {
|
|
282739
|
+
ok: true,
|
|
282740
|
+
label: `row-level security governs the writer on ${withRls} of ${total} published tables \u2014 the other ${total - withRls} have no policies, so the writer's table grants alone bound it`
|
|
282741
|
+
}
|
|
282742
|
+
);
|
|
282743
|
+
}
|
|
282744
|
+
const ownerBypassRows = await sql.unsafe(
|
|
282745
|
+
`SELECT format('%I.%I', n.nspname, c.relname) AS relation
|
|
282746
|
+
FROM pg_publication_tables pt
|
|
282747
|
+
JOIN pg_class c ON c.relname = pt.tablename
|
|
282748
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
|
|
282749
|
+
WHERE pt.pubname = $1 AND pt.schemaname = $2 AND pt.tablename <> 'ablo_idempotency'
|
|
282750
|
+
AND c.relowner = (SELECT oid FROM pg_roles WHERE rolname = current_user)
|
|
282751
|
+
AND c.relrowsecurity = true
|
|
282752
|
+
AND c.relforcerowsecurity = false`,
|
|
282753
|
+
[publication, schema]
|
|
282754
|
+
);
|
|
282755
|
+
items.push(
|
|
282756
|
+
ownerBypassRows.length === 0 ? {
|
|
282757
|
+
ok: true,
|
|
282758
|
+
label: `row-level security is enforced on the writer \u2014 it owns no published table that could bypass its own policies`
|
|
282759
|
+
} : {
|
|
282760
|
+
ok: false,
|
|
282761
|
+
label: `writer owns ${ownerBypassRows.length} published table${ownerBypassRows.length === 1 ? "" : "s"} whose RLS is enabled but not forced \u2014 its policies are silently skipped`,
|
|
282762
|
+
fix: `A table owner bypasses row-level security unless the table forces it. Reassign these tables to another owner, or force it: ${ownerBypassRows.map((r2) => `ALTER TABLE ${r2.relation} FORCE ROW LEVEL SECURITY;`).join(" ")}`
|
|
282763
|
+
}
|
|
282764
|
+
);
|
|
282765
|
+
return items;
|
|
282766
|
+
}
|
|
282767
|
+
async function auditTenantSyncInfra(sql) {
|
|
282768
|
+
const artifacts = [];
|
|
282769
|
+
for (const name of SYNC_INFRA_RELATIONS) {
|
|
282770
|
+
const rows = await sql.unsafe(`SELECT to_regclass($1)::text AS reg`, [
|
|
282771
|
+
`public.${name}`
|
|
282772
|
+
]);
|
|
282773
|
+
artifacts.push({ kind: "relation", name, present: rows[0]?.reg != null });
|
|
282774
|
+
}
|
|
282775
|
+
for (const name of SYNC_INFRA_TYPES) {
|
|
282776
|
+
const rows = await sql.unsafe(`SELECT to_regtype($1)::text AS reg`, [
|
|
282777
|
+
`public.${name}`
|
|
282778
|
+
]);
|
|
282779
|
+
artifacts.push({ kind: "type", name, present: rows[0]?.reg != null });
|
|
282780
|
+
}
|
|
282781
|
+
return artifacts;
|
|
282782
|
+
}
|
|
282783
|
+
function requireScopedUrl(kind, verb) {
|
|
282784
|
+
if (kind === "replication") {
|
|
282785
|
+
const resolved = readProjectReplicationUrlWithSource();
|
|
282786
|
+
if (resolved) return resolved.url;
|
|
282787
|
+
console.error(
|
|
282788
|
+
import_picocolors9.default.red(" No replication connection found (checked process env, .env.local, .env).") + import_picocolors9.default.dim(
|
|
282789
|
+
` Set ${import_picocolors9.default.bold("ABLO_REPLICATION_DATABASE_URL")} to the ${ABLO_REPLICATION_ROLE} connection printed by ${import_picocolors9.default.bold("ablo connect")}, then re-run ${import_picocolors9.default.bold(`ablo connect ${verb}`)}.`
|
|
282790
|
+
)
|
|
282791
|
+
);
|
|
282792
|
+
process.exit(1);
|
|
282793
|
+
}
|
|
282794
|
+
const dbUrl = readProjectWriteDatabaseUrl();
|
|
282795
|
+
if (dbUrl) return dbUrl;
|
|
282796
|
+
console.error(
|
|
282797
|
+
import_picocolors9.default.red(" No ABLO_WRITE_DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors9.default.dim(
|
|
282798
|
+
` Set it to the ${ABLO_WRITE_ROLE} connection printed by ${import_picocolors9.default.bold("ablo connect")}, then re-run ${import_picocolors9.default.bold(`ablo connect ${verb}`)}. The replication credential is never reused for DML.`
|
|
282799
|
+
)
|
|
282800
|
+
);
|
|
282801
|
+
process.exit(1);
|
|
282802
|
+
}
|
|
282803
|
+
async function probeAndReport(dbUrl, kind = "replication") {
|
|
282804
|
+
const sql = src_default(dbUrl, { max: 1, prepare: false, connect_timeout: 10, onnotice: () => {
|
|
282805
|
+
} });
|
|
282806
|
+
let items;
|
|
282807
|
+
try {
|
|
282808
|
+
items = kind === "replication" ? await probeReadiness(sql) : await probeDirectWriteReadiness(sql);
|
|
282809
|
+
} catch (err) {
|
|
282810
|
+
await sql.end({ timeout: 2 }).catch(() => void 0);
|
|
282811
|
+
const dial = dialFailureReason(err);
|
|
282812
|
+
if (dial) return { kind: "no-dial", reason: dial };
|
|
282813
|
+
const pg = err ?? {};
|
|
282814
|
+
console.error(import_picocolors9.default.red(` Couldn't read the database: ${pg.message ?? String(err)}`));
|
|
282815
|
+
process.exit(1);
|
|
282816
|
+
}
|
|
282817
|
+
await sql.end({ timeout: 2 });
|
|
282818
|
+
for (const item of items) printCheckItem(item);
|
|
282819
|
+
return { kind: "probed", failures: items.filter((i) => !i.ok).length };
|
|
282820
|
+
}
|
|
282821
|
+
async function runCheck() {
|
|
282822
|
+
console.log(
|
|
282823
|
+
`
|
|
282824
|
+
${brand("ablo")} ${import_picocolors9.default.dim("connect check")} ${import_picocolors9.default.dim("direct-write + WAL readiness")}
|
|
282825
|
+
`
|
|
282826
|
+
);
|
|
282827
|
+
const apiKey = resolveApiKey();
|
|
282828
|
+
if (!apiKey) {
|
|
282829
|
+
console.error(
|
|
282830
|
+
import_picocolors9.default.red(" No API key found.") + import_picocolors9.default.dim(
|
|
282831
|
+
` Run ${import_picocolors9.default.bold("ablo login")} (or set ${import_picocolors9.default.bold("ABLO_API_KEY")}), then re-run ${import_picocolors9.default.bold("ablo connect check")}.`
|
|
282832
|
+
)
|
|
282833
|
+
);
|
|
282834
|
+
process.exit(1);
|
|
282835
|
+
}
|
|
282836
|
+
const apiUrl2 = apiBaseUrl();
|
|
282837
|
+
const result = await requestRemoteValidation({ apiUrl: apiUrl2, apiKey });
|
|
282838
|
+
if (!result.ok) {
|
|
282839
|
+
if (result.code === "no_data_source_registered") {
|
|
282840
|
+
console.error(
|
|
282841
|
+
` ${import_picocolors9.default.yellow("\u2014")} No database is connected to this plane yet, so there's nothing to check.
|
|
282842
|
+
` + import_picocolors9.default.dim(
|
|
282843
|
+
` Connect one with ${import_picocolors9.default.bold("ablo connect apply")}, then re-run ${import_picocolors9.default.bold("ablo connect check")}.
|
|
282844
|
+
`
|
|
282845
|
+
)
|
|
282846
|
+
);
|
|
282847
|
+
process.exit(1);
|
|
282848
|
+
}
|
|
282849
|
+
console.error(import_picocolors9.default.red(` The check failed: ${result.message}`));
|
|
282850
|
+
if (result.code === "forbidden") {
|
|
282851
|
+
console.error(
|
|
282852
|
+
import_picocolors9.default.dim(
|
|
282853
|
+
` Checking a connected database needs a ${import_picocolors9.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors9.default.bold("ablo login")} for one.`
|
|
282854
|
+
)
|
|
282855
|
+
);
|
|
282856
|
+
}
|
|
282857
|
+
console.error();
|
|
282858
|
+
process.exit(1);
|
|
282859
|
+
}
|
|
282860
|
+
if (!result.reachable) {
|
|
282861
|
+
console.error(
|
|
282862
|
+
` ${import_picocolors9.default.red("\u2717")} Ablo's infrastructure can't reach your database${result.reason ? ` ${import_picocolors9.default.dim(`(${result.reason})`)}` : ""}.`
|
|
282863
|
+
);
|
|
282864
|
+
console.error(
|
|
282865
|
+
import_picocolors9.default.dim(
|
|
282866
|
+
` Direct needs a route Ablo's servers can dial \u2014 public allowlist, PrivateLink, peering,
|
|
282867
|
+
or VPN. Only when no inbound route can exist, use the signed ${import_picocolors9.default.bold("dataSource()")} endpoint fallback.
|
|
282868
|
+
`
|
|
282869
|
+
)
|
|
282870
|
+
);
|
|
282871
|
+
process.exit(1);
|
|
282872
|
+
}
|
|
282873
|
+
for (const failure of result.failures) {
|
|
282874
|
+
const { label, fix } = describeRemoteFailure(failure);
|
|
282875
|
+
printCheckItem({ ok: false, label, fix });
|
|
282876
|
+
}
|
|
282877
|
+
console.log();
|
|
282878
|
+
if (result.ready) {
|
|
282879
|
+
console.log(
|
|
282880
|
+
` ${import_picocolors9.default.green("\u2713")} Ready \u2014 checked from Ablo's infrastructure. Ablo can apply scoped DML and settle it from WAL.
|
|
282881
|
+
`
|
|
282882
|
+
);
|
|
282883
|
+
process.exit(0);
|
|
282884
|
+
}
|
|
282885
|
+
const count = result.failures.length;
|
|
282886
|
+
console.log(
|
|
282887
|
+
` ${import_picocolors9.default.red(`${count} item${count === 1 ? "" : "s"} to fix`)} ${import_picocolors9.default.dim(`\u2014 apply the fixes above, then re-run ${import_picocolors9.default.bold("ablo connect check")}.`)}
|
|
282888
|
+
`
|
|
282889
|
+
);
|
|
282890
|
+
process.exit(1);
|
|
282891
|
+
}
|
|
282892
|
+
async function runRegister(args) {
|
|
282893
|
+
const dbUrl = requireScopedUrl("replication", "register");
|
|
282894
|
+
const writeDbUrl = requireScopedUrl("write", "register");
|
|
282895
|
+
const apiKey = resolveApiKey();
|
|
282896
|
+
if (!apiKey) {
|
|
282897
|
+
console.error(
|
|
282898
|
+
import_picocolors9.default.red(" Not logged in.") + import_picocolors9.default.dim(
|
|
282899
|
+
` Run ${import_picocolors9.default.bold("ablo login")} (or set ${import_picocolors9.default.bold("ABLO_API_KEY")}) so Ablo knows which project to register this database for.`
|
|
282900
|
+
)
|
|
282901
|
+
);
|
|
282902
|
+
process.exit(1);
|
|
282903
|
+
}
|
|
282904
|
+
console.log(
|
|
282905
|
+
`
|
|
282906
|
+
${brand("ablo")} ${import_picocolors9.default.dim("connect register")} ${import_picocolors9.default.dim("register a direct DataSource")}
|
|
282907
|
+
`
|
|
282908
|
+
);
|
|
282909
|
+
console.log(` ${import_picocolors9.default.bold("Replication role")}
|
|
282910
|
+
`);
|
|
282911
|
+
const replication = await probeAndReport(dbUrl, "replication");
|
|
282912
|
+
console.log(`
|
|
282913
|
+
${import_picocolors9.default.bold("Direct-write role")}
|
|
282914
|
+
`);
|
|
282915
|
+
const write = await probeAndReport(writeDbUrl, "write");
|
|
282916
|
+
const noDial = [
|
|
282917
|
+
replication.kind === "no-dial" ? `replication: ${replication.reason}` : null,
|
|
282918
|
+
write.kind === "no-dial" ? `write: ${write.reason}` : null
|
|
282919
|
+
].filter((reason) => reason !== null);
|
|
282920
|
+
const failures = (replication.kind === "probed" ? replication.failures : 0) + (write.kind === "probed" ? write.failures : 0);
|
|
282921
|
+
if (noDial.length > 0) {
|
|
282922
|
+
console.log(
|
|
282923
|
+
` This machine can't reach one or both scoped connections (${import_picocolors9.default.dim(noDial.join("; "))}) \u2014 continuing anyway.
|
|
282924
|
+
Ablo validates both credentials from the infrastructure that will use them and refuses
|
|
282925
|
+
registration unless replication and direct DML are both ready.
|
|
282926
|
+
`
|
|
282927
|
+
);
|
|
282928
|
+
}
|
|
282929
|
+
if (failures > 0) {
|
|
282930
|
+
console.log(
|
|
282931
|
+
`
|
|
282932
|
+
${import_picocolors9.default.red(`${failures} item${failures === 1 ? "" : "s"} to fix`)} ${import_picocolors9.default.dim("\u2014 direct registration requires both scoped roles. Fix the above, then re-run.")}
|
|
282933
|
+
`
|
|
282934
|
+
);
|
|
282935
|
+
process.exit(1);
|
|
282936
|
+
}
|
|
282937
|
+
const apiUrl2 = apiBaseUrl();
|
|
282938
|
+
const registered = await registerDirectDataSource({
|
|
282939
|
+
apiUrl: apiUrl2,
|
|
282940
|
+
apiKey,
|
|
282941
|
+
replicationUrl: dbUrl,
|
|
282942
|
+
writeUrl: writeDbUrl,
|
|
282943
|
+
route: args.route
|
|
282944
|
+
});
|
|
282945
|
+
process.exit(registered ? 0 : 1);
|
|
282946
|
+
}
|
|
282947
|
+
async function runScan() {
|
|
282948
|
+
const dbUrl = requireScopedUrl("replication", "scan");
|
|
282949
|
+
const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
|
|
282950
|
+
} });
|
|
282951
|
+
let artifacts;
|
|
282952
|
+
try {
|
|
282953
|
+
artifacts = await auditTenantSyncInfra(sql);
|
|
282954
|
+
} catch (err) {
|
|
282955
|
+
const pg = err ?? {};
|
|
282956
|
+
console.error(import_picocolors9.default.red(` Couldn't audit the database: ${pg.message ?? String(err)}`));
|
|
282957
|
+
await sql.end({ timeout: 2 });
|
|
282958
|
+
process.exit(1);
|
|
282959
|
+
}
|
|
282960
|
+
await sql.end({ timeout: 2 });
|
|
282961
|
+
console.log(
|
|
282962
|
+
`
|
|
282963
|
+
${brand("ablo")} ${import_picocolors9.default.dim("connect scan")} ${import_picocolors9.default.dim("audit for leftover Ablo sync infrastructure")}
|
|
282964
|
+
`
|
|
282965
|
+
);
|
|
282966
|
+
const present = artifacts.filter((a) => a.present);
|
|
282967
|
+
if (present.length === 0) {
|
|
282968
|
+
console.log(` ${import_picocolors9.default.green("\u2713")} No deprecated Ablo sync infrastructure found in public.
|
|
282969
|
+
`);
|
|
282970
|
+
process.exit(0);
|
|
282971
|
+
}
|
|
282972
|
+
for (const artifact of present) {
|
|
282973
|
+
const label = artifact.kind === "type" ? "type" : "relation";
|
|
282974
|
+
console.log(` ${import_picocolors9.default.yellow("!")} ${label} ${import_picocolors9.default.bold(`public.${artifact.name}`)} exists`);
|
|
282975
|
+
}
|
|
282976
|
+
console.log(
|
|
282977
|
+
`
|
|
282978
|
+
${import_picocolors9.default.yellow(`${present.length} artifact${present.length === 1 ? "" : "s"} found`)} ` + import_picocolors9.default.dim(
|
|
282979
|
+
"\u2014 do not drop automatically. Confirm the org/environment is log-authoritative, then follow "
|
|
282980
|
+
) + import_picocolors9.default.bold("docs/runbooks/wal-stage5-customer-db-infra-cleanup.md") + import_picocolors9.default.dim(".\n")
|
|
282981
|
+
);
|
|
282982
|
+
process.exit(1);
|
|
282983
|
+
}
|
|
282984
|
+
async function connect(argv) {
|
|
282985
|
+
if (argv[0] === "deregister") {
|
|
282986
|
+
const { disconnect: disconnect2 } = await Promise.resolve().then(() => (init_disconnect(), disconnect_exports));
|
|
282987
|
+
await disconnect2(argv.slice(1));
|
|
282988
|
+
return;
|
|
282989
|
+
}
|
|
282990
|
+
let args;
|
|
282991
|
+
try {
|
|
282992
|
+
args = parseConnectArgs(argv);
|
|
282993
|
+
} catch (err) {
|
|
282994
|
+
console.error(import_picocolors9.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
282995
|
+
process.exit(1);
|
|
282996
|
+
}
|
|
282997
|
+
if (args.apply || args.rotate) {
|
|
282998
|
+
const { runConnectApply: runConnectApply2 } = await Promise.resolve().then(() => (init_connectApply(), connectApply_exports));
|
|
282999
|
+
await runConnectApply2(args);
|
|
283000
|
+
return;
|
|
283001
|
+
}
|
|
283002
|
+
if (args.check) {
|
|
283003
|
+
await runCheck();
|
|
283004
|
+
return;
|
|
283005
|
+
}
|
|
283006
|
+
if (args.register) {
|
|
283007
|
+
await runRegister(args);
|
|
283008
|
+
return;
|
|
283009
|
+
}
|
|
283010
|
+
if (args.scan) {
|
|
283011
|
+
await runScan();
|
|
283012
|
+
return;
|
|
283013
|
+
}
|
|
283014
|
+
printConnectRecipe(args);
|
|
283015
|
+
}
|
|
283016
|
+
var CONNECT_USAGE = ` ablo connect \u2014 direct writes, settled by logical replication
|
|
283017
|
+
|
|
283018
|
+
Ablo applies coordinated DML with a scoped writer role. WAL observes what committed,
|
|
283019
|
+
orders it with external changes, and confirms it. Your Postgres remains authoritative.
|
|
283020
|
+
|
|
283021
|
+
Usage:
|
|
283022
|
+
npx ablo connect Print the exact setup SQL instead of running it
|
|
283023
|
+
npx ablo connect apply Set it up end to end: create the roles, publish, register
|
|
283024
|
+
npx ablo connect register Register both scoped credentials as one direct DataSource (hand-run-SQL path)
|
|
283025
|
+
npx ablo connect deregister Remove this project's data source \u2014 Ablo stops reading/writing it
|
|
283026
|
+
npx ablo connect check Verify the connected database from Ablo's side (needs only ABLO_API_KEY)
|
|
283027
|
+
npx ablo connect rotate New passwords for both scoped roles, then re-register
|
|
283028
|
+
npx ablo connect scan Read-only audit for leftover Ablo sync tables/types (never drops)
|
|
283029
|
+
|
|
283030
|
+
Modifiers:
|
|
283031
|
+
--url <admin-conn> Admin connection for apply/rotate (else DATABASE_URL)
|
|
283032
|
+
--tables a,b,c Publish only these tables (default: all tables)
|
|
283033
|
+
--role <name> Name the replication role (default: ablo_replicator)
|
|
283034
|
+
--write-role <name> Name the DML role (default: ablo_writer)
|
|
283035
|
+
--route <route> public-allowlist | privatelink | peering | vpn
|
|
283036
|
+
--yes Skip the apply confirmation (non-interactive)
|
|
283037
|
+
--show-sql Show the exact statements in the apply plan
|
|
283038
|
+
|
|
283039
|
+
apply registers with Ablo directly; the admin credential is used only on this
|
|
283040
|
+
machine and never persisted. Your app holds only ABLO_API_KEY.`;
|
|
283041
|
+
|
|
282584
283042
|
// src/cli/index.ts
|
|
282585
|
-
init_connect();
|
|
282586
283043
|
init_push();
|
|
282587
283044
|
|
|
282588
283045
|
// src/cli/generate.ts
|
|
282589
283046
|
init_cjs_shims();
|
|
282590
283047
|
init_errors();
|
|
282591
|
-
var
|
|
282592
|
-
var
|
|
282593
|
-
var
|
|
283048
|
+
var import_fs6 = require("fs");
|
|
283049
|
+
var import_path4 = require("path");
|
|
283050
|
+
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
282594
283051
|
var import_schema4 = require("@abloatai/ablo/schema");
|
|
282595
283052
|
init_push();
|
|
282596
283053
|
var DEFAULT_SCHEMA_PATH3 = "ablo/schema.ts";
|
|
@@ -282623,7 +283080,7 @@ async function generate(argv) {
|
|
|
282623
283080
|
try {
|
|
282624
283081
|
args = parseGenerateArgs(argv);
|
|
282625
283082
|
} catch (err) {
|
|
282626
|
-
console.error(
|
|
283083
|
+
console.error(import_picocolors10.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
282627
283084
|
process.exit(1);
|
|
282628
283085
|
}
|
|
282629
283086
|
let source;
|
|
@@ -282632,23 +283089,23 @@ async function generate(argv) {
|
|
|
282632
283089
|
const schemaJson = JSON.parse((0, import_schema4.serializeSchema)(schema));
|
|
282633
283090
|
source = (0, import_schema4.generateTypes)(schemaJson);
|
|
282634
283091
|
} catch (err) {
|
|
282635
|
-
console.error(
|
|
283092
|
+
console.error(import_picocolors10.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
282636
283093
|
process.exit(1);
|
|
282637
283094
|
}
|
|
282638
|
-
const abs = (0,
|
|
282639
|
-
(0,
|
|
282640
|
-
(0,
|
|
282641
|
-
console.log(` ${
|
|
283095
|
+
const abs = (0, import_path4.resolve)(process.cwd(), args.out);
|
|
283096
|
+
(0, import_fs6.mkdirSync)((0, import_path4.dirname)(abs), { recursive: true });
|
|
283097
|
+
(0, import_fs6.writeFileSync)(abs, source);
|
|
283098
|
+
console.log(` ${import_picocolors10.default.green("\u2713")} Generated types \u2192 ${import_picocolors10.default.bold(args.out)}`);
|
|
282642
283099
|
}
|
|
282643
283100
|
|
|
282644
283101
|
// src/cli/dev.ts
|
|
282645
283102
|
init_cjs_shims();
|
|
282646
283103
|
init_errors();
|
|
282647
283104
|
init_credentialPolicy();
|
|
282648
|
-
var
|
|
283105
|
+
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
282649
283106
|
init_dist2();
|
|
282650
|
-
var
|
|
282651
|
-
var
|
|
283107
|
+
var import_fs7 = require("fs");
|
|
283108
|
+
var import_path5 = require("path");
|
|
282652
283109
|
var import_schema5 = require("@abloatai/ablo/schema");
|
|
282653
283110
|
init_push();
|
|
282654
283111
|
init_config();
|
|
@@ -282687,62 +283144,62 @@ function classifyKey(apiKey, activeMode) {
|
|
|
282687
283144
|
if (!apiKey) {
|
|
282688
283145
|
return {
|
|
282689
283146
|
ok: false,
|
|
282690
|
-
reason: `No API key. Run ${
|
|
283147
|
+
reason: `No API key. Run ${import_picocolors11.default.bold("npx ablo login")} for the sandbox dev loop \u2014 or set ${import_picocolors11.default.bold("ABLO_API_KEY")} (${import_picocolors11.default.bold("sk_test_")} = sandbox; ${import_picocolors11.default.bold("sk_live_")} = deliberate production deploy). ` + import_picocolors11.default.dim(`Mode is currently '${activeMode}'.`)
|
|
282691
283148
|
};
|
|
282692
283149
|
}
|
|
282693
283150
|
if (apiKey.startsWith("sk_test_")) return { ok: true };
|
|
282694
283151
|
if (apiKey.startsWith("sk_live_")) {
|
|
282695
283152
|
return {
|
|
282696
283153
|
ok: false,
|
|
282697
|
-
reason: `Production schema deploys run one-shot: ${
|
|
283154
|
+
reason: `Production schema deploys run one-shot: ${import_picocolors11.default.bold("ABLO_API_KEY=sk_live_\u2026 npx ablo push")} (or ${import_picocolors11.default.bold("ablo mode production")}). ${import_picocolors11.default.bold("--watch")} is sandbox-only.`
|
|
282698
283155
|
};
|
|
282699
283156
|
}
|
|
282700
283157
|
if (classifyCredentialKind(apiKey) === "restricted") {
|
|
282701
283158
|
return {
|
|
282702
283159
|
ok: false,
|
|
282703
|
-
reason: `Restricted (${
|
|
283160
|
+
reason: `Restricted (${import_picocolors11.default.bold("rk_")}) keys can't push schema. Use a secret key: ${import_picocolors11.default.bold("sk_test_")} for the sandbox dev loop, or ${import_picocolors11.default.bold("sk_live_")} with ${import_picocolors11.default.bold("npx ablo push")} for a production deploy.`
|
|
282704
283161
|
};
|
|
282705
283162
|
}
|
|
282706
283163
|
return {
|
|
282707
283164
|
ok: false,
|
|
282708
|
-
reason: `${
|
|
283165
|
+
reason: `${import_picocolors11.default.bold("ABLO_API_KEY")} is not an Ablo key \u2014 expected ${import_picocolors11.default.bold("sk_test_\u2026")} (sandbox) or ${import_picocolors11.default.bold("sk_live_\u2026")} (production deploy via ${import_picocolors11.default.bold("npx ablo push")}).`
|
|
282709
283166
|
};
|
|
282710
283167
|
}
|
|
282711
283168
|
function wireEnvLocal(apiKey, cwd = process.cwd()) {
|
|
282712
|
-
const envPath = (0,
|
|
283169
|
+
const envPath = (0, import_path5.resolve)(cwd, ".env.local");
|
|
282713
283170
|
const line = `ABLO_API_KEY=${apiKey}`;
|
|
282714
283171
|
let action;
|
|
282715
|
-
if (!(0,
|
|
282716
|
-
(0,
|
|
283172
|
+
if (!(0, import_fs7.existsSync)(envPath)) {
|
|
283173
|
+
(0, import_fs7.writeFileSync)(envPath, `${line}
|
|
282717
283174
|
`, { mode: 384 });
|
|
282718
|
-
action = `Created ${
|
|
283175
|
+
action = `Created ${import_picocolors11.default.bold(".env.local")} with ${import_picocolors11.default.bold("ABLO_API_KEY")}`;
|
|
282719
283176
|
} else {
|
|
282720
|
-
const content = (0,
|
|
283177
|
+
const content = (0, import_fs7.readFileSync)(envPath, "utf8");
|
|
282721
283178
|
const match = /^ABLO_API_KEY=(.*)$/m.exec(content);
|
|
282722
283179
|
const existing = match?.[1] ?? "";
|
|
282723
283180
|
if (!match) {
|
|
282724
|
-
(0,
|
|
283181
|
+
(0, import_fs7.appendFileSync)(envPath, `${content.endsWith("\n") || content.length === 0 ? "" : "\n"}${line}
|
|
282725
283182
|
`);
|
|
282726
|
-
action = `Added ${
|
|
283183
|
+
action = `Added ${import_picocolors11.default.bold("ABLO_API_KEY")} to ${import_picocolors11.default.bold(".env.local")}`;
|
|
282727
283184
|
} else if (existing === apiKey) {
|
|
282728
|
-
action = `${
|
|
283185
|
+
action = `${import_picocolors11.default.bold(".env.local")} already has this key`;
|
|
282729
283186
|
} else {
|
|
282730
|
-
(0,
|
|
282731
|
-
action = `Updated ${
|
|
283187
|
+
(0, import_fs7.writeFileSync)(envPath, content.replace(/^ABLO_API_KEY=.*$/m, line));
|
|
283188
|
+
action = `Updated ${import_picocolors11.default.bold("ABLO_API_KEY")} in ${import_picocolors11.default.bold(".env.local")} ${import_picocolors11.default.dim(`(was ${existing.slice(0, 12)}\u2026)`)}`;
|
|
282732
283189
|
}
|
|
282733
283190
|
}
|
|
282734
|
-
const gitignorePath = (0,
|
|
282735
|
-
const gitignore = (0,
|
|
283191
|
+
const gitignorePath = (0, import_path5.resolve)(cwd, ".gitignore");
|
|
283192
|
+
const gitignore = (0, import_fs7.existsSync)(gitignorePath) ? (0, import_fs7.readFileSync)(gitignorePath, "utf8") : "";
|
|
282736
283193
|
const ignored = /^(\.env\.local|\.env\*|\.env\.\*|\.env.*)$/m.test(gitignore);
|
|
282737
283194
|
let gitignoreNote = "";
|
|
282738
283195
|
if (!ignored) {
|
|
282739
|
-
(0,
|
|
283196
|
+
(0, import_fs7.writeFileSync)(
|
|
282740
283197
|
gitignorePath,
|
|
282741
283198
|
`${gitignore.endsWith("\n") || gitignore.length === 0 ? gitignore : `${gitignore}
|
|
282742
283199
|
`}.env.local
|
|
282743
283200
|
`
|
|
282744
283201
|
);
|
|
282745
|
-
gitignoreNote = ` Added ${
|
|
283202
|
+
gitignoreNote = ` Added ${import_picocolors11.default.bold(".env.local")} to ${import_picocolors11.default.bold(".gitignore")} so the key can't be committed.`;
|
|
282746
283203
|
}
|
|
282747
283204
|
return `${action}.${gitignoreNote}`;
|
|
282748
283205
|
}
|
|
@@ -282757,7 +283214,7 @@ async function runPush(schema, args) {
|
|
|
282757
283214
|
if (ok) {
|
|
282758
283215
|
return {
|
|
282759
283216
|
ok: true,
|
|
282760
|
-
message: body.unchanged ? `schema unchanged ${
|
|
283217
|
+
message: body.unchanged ? `schema unchanged ${import_picocolors11.default.dim(`(v${body.version})`)}` : `schema pushed (sandbox) ${import_picocolors11.default.dim(`(v${body.version}, hash ${body.hash})`)}`
|
|
282761
283218
|
};
|
|
282762
283219
|
}
|
|
282763
283220
|
if (status2 === 409) {
|
|
@@ -282767,33 +283224,33 @@ async function runPush(schema, args) {
|
|
|
282767
283224
|
(s) => s.shadowed != null
|
|
282768
283225
|
);
|
|
282769
283226
|
const lines = [
|
|
282770
|
-
|
|
283227
|
+
import_picocolors11.default.bold("Incompatible schema change \u2014 not safe to apply as-is."),
|
|
282771
283228
|
"",
|
|
282772
|
-
...unexecutable.map((u2) =>
|
|
282773
|
-
...warnings.map((w2) =>
|
|
283229
|
+
...unexecutable.map((u2) => import_picocolors11.default.red(fmtSignal(u2))),
|
|
283230
|
+
...warnings.map((w2) => import_picocolors11.default.yellow(fmtSignal(w2))),
|
|
282774
283231
|
"",
|
|
282775
283232
|
...hasShadowed ? [
|
|
282776
|
-
|
|
283233
|
+
import_picocolors11.default.dim(
|
|
282777
283234
|
" These models exist in the baseline above but not in your push. Sandbox readers"
|
|
282778
283235
|
),
|
|
282779
|
-
|
|
283236
|
+
import_picocolors11.default.dim(
|
|
282780
283237
|
" fall back to the production schema until you push your own, so applying this drops them."
|
|
282781
283238
|
),
|
|
282782
283239
|
""
|
|
282783
283240
|
] : [],
|
|
282784
|
-
|
|
282785
|
-
` Fix: ${
|
|
283241
|
+
import_picocolors11.default.dim(
|
|
283242
|
+
` Fix: ${import_picocolors11.default.bold("ablo push --force")} to apply anyway, or ${import_picocolors11.default.bold("--rename old:new")} if you renamed a model.`
|
|
282786
283243
|
)
|
|
282787
283244
|
];
|
|
282788
283245
|
return { ok: false, message: lines.join("\n") };
|
|
282789
283246
|
}
|
|
282790
283247
|
if (status2 === 403) {
|
|
282791
283248
|
const serverSays = body.message ?? body.reason;
|
|
282792
|
-
const hint = body.code === "database_role_cannot_enforce_rls" ? `Run ${
|
|
283249
|
+
const hint = body.code === "database_role_cannot_enforce_rls" ? `Run ${import_picocolors11.default.bold("npx ablo migrate")} \u2014 it creates the scoped role for you (your DB credential never leaves this machine).` : `Schema authoring needs a ${import_picocolors11.default.bold("sandbox")} key with ${import_picocolors11.default.bold("schema:push")} \u2014 manage keys at ${import_picocolors11.default.cyan("https://abloatai.com")}.`;
|
|
282793
283250
|
return {
|
|
282794
283251
|
ok: false,
|
|
282795
283252
|
message: `${serverSays ?? "This key can't author schema (missing schema:push scope)."}
|
|
282796
|
-
` +
|
|
283253
|
+
` + import_picocolors11.default.dim(hint)
|
|
282797
283254
|
};
|
|
282798
283255
|
}
|
|
282799
283256
|
return { ok: false, message: `Push failed (${status2}): ${body.message ?? body.reason ?? bodyText}` };
|
|
@@ -282803,25 +283260,25 @@ async function dev(argv) {
|
|
|
282803
283260
|
try {
|
|
282804
283261
|
args = parseDevArgs(argv);
|
|
282805
283262
|
} catch (err) {
|
|
282806
|
-
console.error(
|
|
283263
|
+
console.error(import_picocolors11.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
282807
283264
|
process.exit(1);
|
|
282808
283265
|
}
|
|
282809
283266
|
if (!args.apiKey) args.apiKey = resolveEffectiveApiKey("sandbox").key;
|
|
282810
283267
|
const key = classifyKey(args.apiKey, getMode());
|
|
282811
283268
|
if (!key.ok) {
|
|
282812
|
-
console.error(
|
|
283269
|
+
console.error(import_picocolors11.default.red(` ${key.reason}`));
|
|
282813
283270
|
process.exit(1);
|
|
282814
283271
|
}
|
|
282815
283272
|
console.log(`
|
|
282816
|
-
${brand("ablo")} ${
|
|
283273
|
+
${brand("ablo")} ${import_picocolors11.default.dim("push")} ${import_picocolors11.default.dim("(sandbox)")}
|
|
282817
283274
|
`);
|
|
282818
283275
|
const schema = await loadSchema(args.schemaPath, args.exportName);
|
|
282819
283276
|
const modelCount = Object.keys(schema.models).length;
|
|
282820
283277
|
console.log(
|
|
282821
|
-
` ${
|
|
283278
|
+
` ${import_picocolors11.default.dim("schema")} ${import_picocolors11.default.bold(args.schemaPath)} ${import_picocolors11.default.dim(`(${modelCount} models, hash ${(0, import_schema5.schemaHash)(schema)})`)}`
|
|
282822
283279
|
);
|
|
282823
|
-
console.log(` ${
|
|
282824
|
-
console.log(` ${
|
|
283280
|
+
console.log(` ${import_picocolors11.default.dim("key")} ${args.apiKey.slice(0, 12)}\u2026`);
|
|
283281
|
+
console.log(` ${import_picocolors11.default.dim("api")} ${args.url}
|
|
282825
283282
|
`);
|
|
282826
283283
|
const s = Y2();
|
|
282827
283284
|
s.start("Pushing schema definition (sandbox)");
|
|
@@ -282830,20 +283287,20 @@ async function dev(argv) {
|
|
|
282830
283287
|
if (!first.ok) process.exit(1);
|
|
282831
283288
|
if (process.env.ABLO_API_KEY) {
|
|
282832
283289
|
console.log(`
|
|
282833
|
-
${
|
|
283290
|
+
${import_picocolors11.default.green("\u2713")} ${import_picocolors11.default.bold("ABLO_API_KEY")} is set in this shell \u2014 the SDK reads it directly.`);
|
|
282834
283291
|
} else {
|
|
282835
283292
|
console.log(`
|
|
282836
|
-
${
|
|
282837
|
-
console.log(` ${
|
|
283293
|
+
${import_picocolors11.default.green("\u2713")} ${wireEnvLocal(args.apiKey)}`);
|
|
283294
|
+
console.log(` ${import_picocolors11.default.dim("Frameworks load it automatically; plain Node: node --env-file=.env.local app.ts")}`);
|
|
282838
283295
|
}
|
|
282839
283296
|
console.log(` Your app is wired for the sandbox.`);
|
|
282840
283297
|
if (!args.watch) return;
|
|
282841
|
-
const abs = (0,
|
|
282842
|
-
console.log(` ${
|
|
283298
|
+
const abs = (0, import_path5.resolve)(process.cwd(), args.schemaPath);
|
|
283299
|
+
console.log(` ${import_picocolors11.default.dim(`watching ${args.schemaPath} \u2026 (Ctrl-C to stop)`)}
|
|
282843
283300
|
`);
|
|
282844
283301
|
let timer2 = null;
|
|
282845
283302
|
let pushing = false;
|
|
282846
|
-
const watcher = (0,
|
|
283303
|
+
const watcher = (0, import_fs7.watch)(abs, () => {
|
|
282847
283304
|
if (timer2) clearTimeout(timer2);
|
|
282848
283305
|
timer2 = setTimeout(() => {
|
|
282849
283306
|
void rePush();
|
|
@@ -282859,7 +283316,7 @@ async function dev(argv) {
|
|
|
282859
283316
|
const r2 = await runPush(next, args);
|
|
282860
283317
|
s2.stop(r2.message, r2.ok ? 0 : 1);
|
|
282861
283318
|
} catch (err) {
|
|
282862
|
-
s2.stop(
|
|
283319
|
+
s2.stop(import_picocolors11.default.red(`schema reload failed: ${err instanceof Error ? err.message : String(err)}`), 1);
|
|
282863
283320
|
} finally {
|
|
282864
283321
|
pushing = false;
|
|
282865
283322
|
}
|
|
@@ -282867,7 +283324,7 @@ async function dev(argv) {
|
|
|
282867
283324
|
const stop = () => {
|
|
282868
283325
|
watcher.close();
|
|
282869
283326
|
console.log(`
|
|
282870
|
-
${
|
|
283327
|
+
${import_picocolors11.default.dim("stopped.")}`);
|
|
282871
283328
|
process.exit(0);
|
|
282872
283329
|
};
|
|
282873
283330
|
process.on("SIGINT", stop);
|
|
@@ -282879,7 +283336,7 @@ async function dev(argv) {
|
|
|
282879
283336
|
// src/cli/login.ts
|
|
282880
283337
|
init_cjs_shims();
|
|
282881
283338
|
var import_child_process2 = require("child_process");
|
|
282882
|
-
var
|
|
283339
|
+
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
282883
283340
|
init_dist2();
|
|
282884
283341
|
init_config();
|
|
282885
283342
|
init_theme();
|
|
@@ -282940,9 +283397,9 @@ async function deviceLogin(argv, deps = {}) {
|
|
|
282940
283397
|
const code = await codeRes.json();
|
|
282941
283398
|
const approvePath = `/cli?user_code=${code.user_code}`;
|
|
282942
283399
|
const url = account === "signup" ? `${DASHBOARD_URL}/signup?next=${encodeURIComponent(approvePath)}` : `${DASHBOARD_URL}${approvePath}`;
|
|
282943
|
-
Me(`${
|
|
283400
|
+
Me(`${import_picocolors12.default.bold(code.user_code)}
|
|
282944
283401
|
|
|
282945
|
-
${
|
|
283402
|
+
${import_picocolors12.default.dim(url)}`, "Approve in your browser");
|
|
282946
283403
|
openUrl(url);
|
|
282947
283404
|
const s = Y2();
|
|
282948
283405
|
s.start("Waiting for approval\u2026");
|
|
@@ -283009,7 +283466,7 @@ ${import_picocolors10.default.dim(url)}`, "Approve in your browser");
|
|
|
283009
283466
|
if (reason) M2.error(reason);
|
|
283010
283467
|
else if (provRes) M2.error(`Key provisioning returned ${provRes.status} from ${DASHBOARD_URL}/api/cli/provision-key.`);
|
|
283011
283468
|
M2.error(
|
|
283012
|
-
`The browser approval succeeded but the key handoff failed. Try again, or grab a ${
|
|
283469
|
+
`The browser approval succeeded but the key handoff failed. Try again, or grab a ${import_picocolors12.default.bold("sk_test_")} key from the dashboard and set ${import_picocolors12.default.bold("ABLO_API_KEY")}.`
|
|
283013
283470
|
);
|
|
283014
283471
|
process.exit(1);
|
|
283015
283472
|
}
|
|
@@ -283029,9 +283486,9 @@ ${import_picocolors10.default.dim(url)}`, "Approve in your browser");
|
|
|
283029
283486
|
{ mode: "sandbox", activeProject: prov.project ?? void 0 }
|
|
283030
283487
|
);
|
|
283031
283488
|
s.stop(`Saved keys to ${path}`);
|
|
283032
|
-
const where = prov.project ? ` ${
|
|
283489
|
+
const where = prov.project ? ` ${import_picocolors12.default.dim(`(project ${prov.project.slug})`)}` : "";
|
|
283033
283490
|
Se(
|
|
283034
|
-
`${
|
|
283491
|
+
`${import_picocolors12.default.green("\u2713")} Logged in ${import_picocolors12.default.dim("(sandbox)")}${where}. Run ${import_picocolors12.default.bold("npx ablo push")} to push your schema.`
|
|
283035
283492
|
);
|
|
283036
283493
|
}
|
|
283037
283494
|
async function login(argv = [], deps = {}) {
|
|
@@ -283040,13 +283497,13 @@ async function login(argv = [], deps = {}) {
|
|
|
283040
283497
|
function logout() {
|
|
283041
283498
|
const removed = clearCredential();
|
|
283042
283499
|
if (removed) {
|
|
283043
|
-
console.log(` ${
|
|
283500
|
+
console.log(` ${import_picocolors12.default.green("\u2713")} Logged out ${import_picocolors12.default.dim(`(credentials removed from ${configDir()})`)}`);
|
|
283044
283501
|
} else {
|
|
283045
|
-
console.log(` ${
|
|
283502
|
+
console.log(` ${import_picocolors12.default.dim("\u25CB")} Not logged in \u2014 nothing to remove.`);
|
|
283046
283503
|
}
|
|
283047
283504
|
if (process.env.ABLO_API_KEY) {
|
|
283048
283505
|
console.log(
|
|
283049
|
-
|
|
283506
|
+
import_picocolors12.default.dim(` Note: ${import_picocolors12.default.bold("ABLO_API_KEY")} is still set in this shell and takes precedence.`)
|
|
283050
283507
|
);
|
|
283051
283508
|
}
|
|
283052
283509
|
}
|
|
@@ -283056,7 +283513,7 @@ init_config();
|
|
|
283056
283513
|
|
|
283057
283514
|
// src/cli/mode.ts
|
|
283058
283515
|
init_cjs_shims();
|
|
283059
|
-
var
|
|
283516
|
+
var import_picocolors13 = __toESM(require_picocolors(), 1);
|
|
283060
283517
|
init_dist2();
|
|
283061
283518
|
init_config();
|
|
283062
283519
|
var PREFIX = { sandbox: "sk_test_", production: "rk_live_" };
|
|
@@ -283068,10 +283525,10 @@ function hintFor(m2, current) {
|
|
|
283068
283525
|
}
|
|
283069
283526
|
function apply(m2) {
|
|
283070
283527
|
setMode(m2);
|
|
283071
|
-
console.log(` ${
|
|
283528
|
+
console.log(` ${import_picocolors13.default.green("\u2713")} now in ${import_picocolors13.default.bold(m2)}`);
|
|
283072
283529
|
if (!getKeyEntry(m2)) {
|
|
283073
283530
|
console.log(
|
|
283074
|
-
|
|
283531
|
+
import_picocolors13.default.dim(` No ${m2} key stored \u2014 run ${import_picocolors13.default.bold("ablo login")} or ${import_picocolors13.default.bold(`ablo login --api-key ${PREFIX[m2]}\u2026`)}.`)
|
|
283075
283532
|
);
|
|
283076
283533
|
}
|
|
283077
283534
|
}
|
|
@@ -283084,14 +283541,14 @@ async function mode(argv) {
|
|
|
283084
283541
|
}
|
|
283085
283542
|
if (arg) {
|
|
283086
283543
|
console.error(
|
|
283087
|
-
|
|
283544
|
+
import_picocolors13.default.red(` unknown mode: ${arg}`) + import_picocolors13.default.dim(` (expected ${import_picocolors13.default.bold("sandbox")} or ${import_picocolors13.default.bold("production")})`)
|
|
283088
283545
|
);
|
|
283089
283546
|
process.exit(1);
|
|
283090
283547
|
}
|
|
283091
283548
|
const current = getMode();
|
|
283092
283549
|
if (!process.stdin.isTTY || process.env.CI) {
|
|
283093
283550
|
console.error(
|
|
283094
|
-
|
|
283551
|
+
import_picocolors13.default.red(" `ablo mode` needs an argument without a TTY: ") + import_picocolors13.default.bold("ablo mode sandbox") + import_picocolors13.default.dim(" | ") + import_picocolors13.default.bold("ablo mode production") + import_picocolors13.default.dim(` (current: ${current})`)
|
|
283095
283552
|
);
|
|
283096
283553
|
process.exit(1);
|
|
283097
283554
|
}
|
|
@@ -283115,7 +283572,7 @@ init_projects();
|
|
|
283115
283572
|
|
|
283116
283573
|
// src/cli/status.ts
|
|
283117
283574
|
init_cjs_shims();
|
|
283118
|
-
var
|
|
283575
|
+
var import_picocolors14 = __toESM(require_picocolors(), 1);
|
|
283119
283576
|
init_config();
|
|
283120
283577
|
init_target();
|
|
283121
283578
|
init_theme();
|
|
@@ -283123,9 +283580,9 @@ init_push();
|
|
|
283123
283580
|
function expiryLabel(iso) {
|
|
283124
283581
|
const ms = Date.parse(iso) - Date.now();
|
|
283125
283582
|
if (Number.isNaN(ms)) return "";
|
|
283126
|
-
if (ms <= 0) return
|
|
283583
|
+
if (ms <= 0) return import_picocolors14.default.red("expired");
|
|
283127
283584
|
const days = Math.floor(ms / (24 * 60 * 60 * 1e3));
|
|
283128
|
-
return
|
|
283585
|
+
return import_picocolors14.default.dim(days > 0 ? `expires in ${days}d` : "expires <1d");
|
|
283129
283586
|
}
|
|
283130
283587
|
async function ping(apiUrl2) {
|
|
283131
283588
|
const ctrl = new AbortController();
|
|
@@ -283209,31 +283666,31 @@ function formatConflict(conflict) {
|
|
|
283209
283666
|
function printTargetLines(target, localProject) {
|
|
283210
283667
|
const confirmed = target?.confirmed ?? null;
|
|
283211
283668
|
if (confirmed?.organizationId) {
|
|
283212
|
-
console.log(` ${
|
|
283669
|
+
console.log(` ${import_picocolors14.default.dim("org")} ${import_picocolors14.default.dim(confirmed.organizationId)}`);
|
|
283213
283670
|
}
|
|
283214
283671
|
let projectLine;
|
|
283215
283672
|
if (confirmed?.project) {
|
|
283216
283673
|
const p2 = confirmed.project;
|
|
283217
|
-
projectLine = p2.isDefault ? `${
|
|
283674
|
+
projectLine = p2.isDefault ? `${import_picocolors14.default.bold("default")} ${import_picocolors14.default.dim("(org-default)")}` : `${import_picocolors14.default.bold(p2.slug)} ${import_picocolors14.default.dim(`(${p2.id})`)}`;
|
|
283218
283675
|
} else if (confirmed) {
|
|
283219
|
-
projectLine = `${
|
|
283676
|
+
projectLine = `${import_picocolors14.default.bold("default")} ${import_picocolors14.default.dim("(org-default)")}`;
|
|
283220
283677
|
} else if (localProject) {
|
|
283221
|
-
projectLine = `${
|
|
283678
|
+
projectLine = `${import_picocolors14.default.bold(localProject.slug)} ${import_picocolors14.default.dim(`(${localProject.id})`)} ${import_picocolors14.default.yellow("(unconfirmed)")}`;
|
|
283222
283679
|
} else {
|
|
283223
|
-
projectLine = `${
|
|
283680
|
+
projectLine = `${import_picocolors14.default.bold("default")} ${target ? import_picocolors14.default.yellow("(unconfirmed)") : import_picocolors14.default.dim("(org-default)")}`;
|
|
283224
283681
|
}
|
|
283225
|
-
console.log(` ${
|
|
283682
|
+
console.log(` ${import_picocolors14.default.dim("project")} ${projectLine}`);
|
|
283226
283683
|
const env = confirmed?.environment ?? target?.keyEnv ?? null;
|
|
283227
283684
|
if (env) {
|
|
283228
|
-
const suffix = confirmed ? "" : ` ${
|
|
283229
|
-
console.log(` ${
|
|
283685
|
+
const suffix = confirmed ? "" : ` ${import_picocolors14.default.yellow("(unconfirmed)")}`;
|
|
283686
|
+
console.log(` ${import_picocolors14.default.dim("env")} ${import_picocolors14.default.bold(env)}${suffix}`);
|
|
283230
283687
|
}
|
|
283231
283688
|
for (const m2 of target?.mismatches ?? []) {
|
|
283232
|
-
console.log(` ${
|
|
283689
|
+
console.log(` ${import_picocolors14.default.yellow("\u26A0")} ${import_picocolors14.default.yellow(describeMismatch(m2))}`);
|
|
283233
283690
|
}
|
|
283234
283691
|
}
|
|
283235
283692
|
async function status(args = []) {
|
|
283236
|
-
const apiUrl2 = (
|
|
283693
|
+
const apiUrl2 = apiBaseUrl();
|
|
283237
283694
|
const cfg = readConfig();
|
|
283238
283695
|
const mode2 = getMode();
|
|
283239
283696
|
const effective = resolveEffectiveApiKey();
|
|
@@ -283300,80 +283757,80 @@ async function status(args = []) {
|
|
|
283300
283757
|
return;
|
|
283301
283758
|
}
|
|
283302
283759
|
console.log(`
|
|
283303
|
-
${brand("ablo")} ${
|
|
283760
|
+
${brand("ablo")} ${import_picocolors14.default.dim("status")}
|
|
283304
283761
|
`);
|
|
283305
283762
|
if (effective.key && effective.source && effective.source !== "stored") {
|
|
283306
283763
|
const label = effective.source === "env" ? "ABLO_API_KEY env" : effective.source;
|
|
283307
283764
|
console.log(
|
|
283308
|
-
` ${
|
|
283765
|
+
` ${import_picocolors14.default.dim("key")} ${effective.key.slice(0, 12)}\u2026 ${import_picocolors14.default.dim(`(${label} \u2014 overrides stored)`)}`
|
|
283309
283766
|
);
|
|
283310
283767
|
} else if (!cfg) {
|
|
283311
|
-
console.log(` ${
|
|
283768
|
+
console.log(` ${import_picocolors14.default.yellow("!")} Not logged in \u2014 run ${import_picocolors14.default.bold("ablo login")}.`);
|
|
283312
283769
|
}
|
|
283313
|
-
console.log(` ${
|
|
283770
|
+
console.log(` ${import_picocolors14.default.dim("mode")} ${import_picocolors14.default.bold(mode2)}`);
|
|
283314
283771
|
const activeEntry = getKeyEntry(mode2);
|
|
283315
283772
|
const key = describeEffectiveKey(mode2, process.env.ABLO_API_KEY, activeEntry);
|
|
283316
283773
|
if (key.keyMismatch) {
|
|
283317
|
-
console.log(` ${
|
|
283774
|
+
console.log(` ${import_picocolors14.default.yellow("!")} ${import_picocolors14.default.yellow(key.keyMismatch.message)}`);
|
|
283318
283775
|
}
|
|
283319
283776
|
const activeProject = getActiveProject();
|
|
283320
283777
|
printTargetLines(target, activeProject);
|
|
283321
283778
|
for (const m2 of ["sandbox", "production"]) {
|
|
283322
283779
|
const entry = getKeyEntry(m2);
|
|
283323
|
-
const marker = m2 === mode2 ?
|
|
283780
|
+
const marker = m2 === mode2 ? import_picocolors14.default.green("\u25CF") : import_picocolors14.default.dim("\u25CB");
|
|
283324
283781
|
if (entry) {
|
|
283325
283782
|
const exp = entry.expiresAt ? ` ${expiryLabel(entry.expiresAt)}` : "";
|
|
283326
|
-
console.log(` ${marker} ${m2.padEnd(10)} ${
|
|
283783
|
+
console.log(` ${marker} ${m2.padEnd(10)} ${import_picocolors14.default.dim(`${entry.apiKey.slice(0, 12)}\u2026`)}${exp}`);
|
|
283327
283784
|
} else {
|
|
283328
|
-
console.log(` ${marker} ${m2.padEnd(10)} ${
|
|
283785
|
+
console.log(` ${marker} ${m2.padEnd(10)} ${import_picocolors14.default.dim("\u2014 no key")}`);
|
|
283329
283786
|
}
|
|
283330
283787
|
}
|
|
283331
283788
|
const org = target?.confirmed?.organizationId ?? activeEntry?.organizationId;
|
|
283332
283789
|
const plan = resolvePushPlan();
|
|
283333
283790
|
console.log(
|
|
283334
|
-
` ${
|
|
283791
|
+
` ${import_picocolors14.default.dim("push")} ${plan.apiKey ? `${import_picocolors14.default.bold(plan.flow)} ${import_picocolors14.default.dim(`with ${plan.apiKey.slice(0, 12)}\u2026 (${plan.source})`)}` : `${import_picocolors14.default.bold(plan.flow)} ${import_picocolors14.default.yellow("\u2014 no credential")} ${import_picocolors14.default.dim(`(run ${import_picocolors14.default.bold("ablo login")} or set ${import_picocolors14.default.bold("ABLO_API_KEY")})`)}`}`
|
|
283335
283792
|
);
|
|
283336
|
-
process.stdout.write(` ${
|
|
283793
|
+
process.stdout.write(` ${import_picocolors14.default.dim("api")} ${apiUrl2} `);
|
|
283337
283794
|
const reachable = await ping(apiUrl2);
|
|
283338
|
-
console.log(reachable ?
|
|
283795
|
+
console.log(reachable ? import_picocolors14.default.green("reachable") : import_picocolors14.default.red("unreachable"));
|
|
283339
283796
|
if (reachable) {
|
|
283340
283797
|
const introspectKey = effective.key;
|
|
283341
283798
|
const pushed = await fetchPushedSchema(apiUrl2, introspectKey);
|
|
283342
283799
|
if (pushed?.active) {
|
|
283343
|
-
const when = pushed.pushedAt ? ` ${
|
|
283344
|
-
const ver = pushed.version != null ? ` ${
|
|
283345
|
-
const hashLabel = pushed.hash ? ` ${
|
|
283346
|
-
console.log(` ${
|
|
283800
|
+
const when = pushed.pushedAt ? ` ${import_picocolors14.default.dim(`@ ${pushed.pushedAt.slice(0, 10)}`)}` : "";
|
|
283801
|
+
const ver = pushed.version != null ? ` ${import_picocolors14.default.dim(`(rev ${pushed.version})`)}` : "";
|
|
283802
|
+
const hashLabel = pushed.hash ? ` ${import_picocolors14.default.dim(`hash ${pushed.hash}`)}` : "";
|
|
283803
|
+
console.log(` ${import_picocolors14.default.dim("schema")} ${import_picocolors14.default.bold(`${pushed.models.length} models pushed`)}${ver}${hashLabel}${when}`);
|
|
283347
283804
|
for (const m2 of pushed.models) {
|
|
283348
|
-
const tn = m2.typename === m2.key ?
|
|
283805
|
+
const tn = m2.typename === m2.key ? import_picocolors14.default.dim(`typename=${m2.typename}`) : import_picocolors14.default.yellow(`typename=${m2.typename}`);
|
|
283349
283806
|
const conflict = formatConflict(m2.conflict);
|
|
283350
|
-
const conflictStr2 = conflict ? ` ${
|
|
283351
|
-
console.log(` ${
|
|
283807
|
+
const conflictStr2 = conflict ? ` ${import_picocolors14.default.dim(`conflict=${conflict}`)}` : "";
|
|
283808
|
+
console.log(` ${import_picocolors14.default.dim("\u2022")} ${m2.key.padEnd(14)} ${tn}${conflictStr2}`);
|
|
283352
283809
|
}
|
|
283353
283810
|
} else if (pushed && !pushed.active) {
|
|
283354
|
-
console.log(` ${
|
|
283811
|
+
console.log(` ${import_picocolors14.default.dim("schema")} ${import_picocolors14.default.yellow("none pushed")} ${import_picocolors14.default.dim(`(run ${import_picocolors14.default.bold("ablo push")} or ${import_picocolors14.default.bold("ablo dev")})`)}`);
|
|
283355
283812
|
}
|
|
283356
283813
|
const firstPushedModel = pushed?.active ? pushed.models[0] : void 0;
|
|
283357
283814
|
if (firstPushedModel !== void 0) {
|
|
283358
283815
|
const probe = await probeDataPlane(apiUrl2, introspectKey, firstPushedModel.typename);
|
|
283359
283816
|
if (probe.status === "no_database") {
|
|
283360
|
-
console.log(` ${
|
|
283817
|
+
console.log(` ${import_picocolors14.default.dim("data")} ${import_picocolors14.default.red("\u2717 no database registered")}${org ? import_picocolors14.default.dim(` for org ${org}`) : ""}`);
|
|
283361
283818
|
console.log(
|
|
283362
|
-
` ${
|
|
283363
|
-
`reads/writes will fail with ${
|
|
283819
|
+
` ${import_picocolors14.default.dim(
|
|
283820
|
+
`reads/writes will fail with ${import_picocolors14.default.bold("tenant_routing_failed")}. Connect one with ${import_picocolors14.default.bold("ablo connect")}, or point ${import_picocolors14.default.bold("ABLO_API_KEY")} at an org that has a database.`
|
|
283364
283821
|
)}`
|
|
283365
283822
|
);
|
|
283366
283823
|
} else if (probe.status === "intermittent") {
|
|
283367
|
-
console.log(` ${
|
|
283824
|
+
console.log(` ${import_picocolors14.default.dim("data")} ${import_picocolors14.default.red(`\u2717 database routing is intermittent`)} ${import_picocolors14.default.dim(`(${probe.ok} ok / ${probe.failed} failed of ${probe.ok + probe.failed})`)}${org ? import_picocolors14.default.dim(` for org ${org}`) : ""}`);
|
|
283368
283825
|
console.log(
|
|
283369
|
-
` ${
|
|
283370
|
-
`some reads/writes fail with ${
|
|
283826
|
+
` ${import_picocolors14.default.dim(
|
|
283827
|
+
`some reads/writes fail with ${import_picocolors14.default.bold("tenant_routing_failed")} \u2014 the registration is unstable. Re-establish it with ${import_picocolors14.default.bold("ablo connect")} (or check for a recent server redeploy).`
|
|
283371
283828
|
)}`
|
|
283372
283829
|
);
|
|
283373
283830
|
} else if (probe.status === "forbidden") {
|
|
283374
|
-
console.log(` ${
|
|
283831
|
+
console.log(` ${import_picocolors14.default.dim("data")} ${import_picocolors14.default.yellow("? key not authorized to read")}${probe.detail ? import_picocolors14.default.dim(` (${probe.detail})`) : ""}`);
|
|
283375
283832
|
} else if (probe.status === "unknown") {
|
|
283376
|
-
console.log(` ${
|
|
283833
|
+
console.log(` ${import_picocolors14.default.dim("data")} ${import_picocolors14.default.yellow(`? data-plane check inconclusive (${probe.detail})`)}`);
|
|
283377
283834
|
}
|
|
283378
283835
|
}
|
|
283379
283836
|
}
|
|
@@ -283383,7 +283840,7 @@ async function status(args = []) {
|
|
|
283383
283840
|
// src/cli/logs.ts
|
|
283384
283841
|
init_cjs_shims();
|
|
283385
283842
|
init_errors();
|
|
283386
|
-
var
|
|
283843
|
+
var import_picocolors15 = __toESM(require_picocolors(), 1);
|
|
283387
283844
|
init_config();
|
|
283388
283845
|
init_theme();
|
|
283389
283846
|
init_push();
|
|
@@ -283450,10 +283907,10 @@ function resolveSince(since) {
|
|
|
283450
283907
|
var sleep2 = (ms) => new Promise((r2) => setTimeout(r2, ms));
|
|
283451
283908
|
function colorOp(op) {
|
|
283452
283909
|
const label = op.padEnd(6);
|
|
283453
|
-
if (op === "create") return
|
|
283454
|
-
if (op === "update") return
|
|
283455
|
-
if (op === "delete") return
|
|
283456
|
-
return
|
|
283910
|
+
if (op === "create") return import_picocolors15.default.green(label);
|
|
283911
|
+
if (op === "update") return import_picocolors15.default.yellow(label);
|
|
283912
|
+
if (op === "delete") return import_picocolors15.default.red(label);
|
|
283913
|
+
return import_picocolors15.default.dim(label);
|
|
283457
283914
|
}
|
|
283458
283915
|
function render(e2, json) {
|
|
283459
283916
|
if (json) {
|
|
@@ -283462,25 +283919,25 @@ function render(e2, json) {
|
|
|
283462
283919
|
return;
|
|
283463
283920
|
}
|
|
283464
283921
|
const t = new Date(e2.at).toLocaleTimeString();
|
|
283465
|
-
const actor = e2.actor ?
|
|
283466
|
-
console.log(` ${
|
|
283922
|
+
const actor = e2.actor ? import_picocolors15.default.dim(` ${e2.actor}`) : "";
|
|
283923
|
+
console.log(` ${import_picocolors15.default.dim(t)} ${colorOp(e2.op)} ${import_picocolors15.default.bold(e2.model)} ${import_picocolors15.default.dim(e2.recordId)}${actor}`);
|
|
283467
283924
|
}
|
|
283468
283925
|
async function logs(argv) {
|
|
283469
283926
|
let args;
|
|
283470
283927
|
try {
|
|
283471
283928
|
args = parseLogsArgs(argv);
|
|
283472
283929
|
} catch (err) {
|
|
283473
|
-
console.error(
|
|
283930
|
+
console.error(import_picocolors15.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
283474
283931
|
process.exit(1);
|
|
283475
283932
|
}
|
|
283476
283933
|
const apiKey = resolveApiKey(args.mode);
|
|
283477
283934
|
if (!apiKey) {
|
|
283478
283935
|
console.error(
|
|
283479
|
-
|
|
283936
|
+
import_picocolors15.default.red(` No API key.`) + import_picocolors15.default.dim(` Run ${import_picocolors15.default.bold("ablo login")} or set ${import_picocolors15.default.bold("ABLO_API_KEY")}.`)
|
|
283480
283937
|
);
|
|
283481
283938
|
process.exit(1);
|
|
283482
283939
|
}
|
|
283483
|
-
const baseUrl2 = (
|
|
283940
|
+
const baseUrl2 = apiBaseUrl();
|
|
283484
283941
|
const since = resolveSince(args.since);
|
|
283485
283942
|
async function fetchPage(params) {
|
|
283486
283943
|
const qs = new URLSearchParams(params).toString();
|
|
@@ -283490,7 +283947,7 @@ async function logs(argv) {
|
|
|
283490
283947
|
if (!res) return null;
|
|
283491
283948
|
if (!res.ok) {
|
|
283492
283949
|
const body = await res.json().catch(() => ({}));
|
|
283493
|
-
console.error(
|
|
283950
|
+
console.error(import_picocolors15.default.red(` logs failed (${res.status}): ${body.reason ?? body.message ?? ""}`));
|
|
283494
283951
|
process.exit(1);
|
|
283495
283952
|
}
|
|
283496
283953
|
const json = await res.json();
|
|
@@ -283501,7 +283958,7 @@ async function logs(argv) {
|
|
|
283501
283958
|
}
|
|
283502
283959
|
if (!args.json) {
|
|
283503
283960
|
console.log(`
|
|
283504
|
-
${brand("ablo")} ${
|
|
283961
|
+
${brand("ablo")} ${import_picocolors15.default.dim("logs")} ${import_picocolors15.default.dim(`(${args.mode ?? "active"} mode)`)}
|
|
283505
283962
|
`);
|
|
283506
283963
|
}
|
|
283507
283964
|
const initial = await fetchPage({
|
|
@@ -283511,13 +283968,13 @@ async function logs(argv) {
|
|
|
283511
283968
|
...args.op ? { op: args.op } : {}
|
|
283512
283969
|
});
|
|
283513
283970
|
if (!initial) {
|
|
283514
|
-
console.error(
|
|
283971
|
+
console.error(import_picocolors15.default.red(` Couldn't reach ${baseUrl2}.`));
|
|
283515
283972
|
process.exit(1);
|
|
283516
283973
|
}
|
|
283517
283974
|
for (const e2 of initial.events) render(e2, args.json);
|
|
283518
283975
|
let cursor = initial.cursor;
|
|
283519
283976
|
if (!args.follow) return;
|
|
283520
|
-
if (!args.json) console.log(` ${
|
|
283977
|
+
if (!args.json) console.log(` ${import_picocolors15.default.dim("watching for new activity \u2026 (Ctrl-C to stop)")}
|
|
283521
283978
|
`);
|
|
283522
283979
|
for (; ; ) {
|
|
283523
283980
|
await sleep2(1500);
|
|
@@ -283534,8 +283991,8 @@ async function logs(argv) {
|
|
|
283534
283991
|
|
|
283535
283992
|
// src/cli/webhooks.ts
|
|
283536
283993
|
init_cjs_shims();
|
|
283537
|
-
var
|
|
283538
|
-
var
|
|
283994
|
+
var import_fs8 = require("fs");
|
|
283995
|
+
var import_picocolors16 = __toESM(require_picocolors(), 1);
|
|
283539
283996
|
init_credentialPolicy();
|
|
283540
283997
|
init_config();
|
|
283541
283998
|
init_theme();
|
|
@@ -283568,17 +284025,17 @@ function requireKey2(mode2) {
|
|
|
283568
284025
|
const apiKey = resolveApiKey(mode2);
|
|
283569
284026
|
if (!apiKey) {
|
|
283570
284027
|
console.error(
|
|
283571
|
-
|
|
284028
|
+
import_picocolors16.default.red(" No API key.") + import_picocolors16.default.dim(` Run ${import_picocolors16.default.bold("ablo login")} or set ${import_picocolors16.default.bold("ABLO_API_KEY")}.`)
|
|
283572
284029
|
);
|
|
283573
284030
|
process.exit(1);
|
|
283574
284031
|
}
|
|
283575
284032
|
if (classifyCredentialKind(apiKey) !== "secret") {
|
|
283576
|
-
console.error(
|
|
284033
|
+
console.error(import_picocolors16.default.red(" Managing webhooks requires a secret key ") + import_picocolors16.default.dim("(sk_test_ / sk_live_)."));
|
|
283577
284034
|
process.exit(1);
|
|
283578
284035
|
}
|
|
283579
284036
|
return apiKey;
|
|
283580
284037
|
}
|
|
283581
|
-
var baseUrl = () => (
|
|
284038
|
+
var baseUrl = () => apiBaseUrl();
|
|
283582
284039
|
async function api(apiKey, method, path, body) {
|
|
283583
284040
|
const res = await fetch(`${baseUrl()}/api/v1/webhook_endpoints${path}`, {
|
|
283584
284041
|
method,
|
|
@@ -283589,22 +284046,22 @@ async function api(apiKey, method, path, body) {
|
|
|
283589
284046
|
...body ? { body: JSON.stringify(body) } : {}
|
|
283590
284047
|
}).catch(() => null);
|
|
283591
284048
|
if (!res) {
|
|
283592
|
-
console.error(
|
|
284049
|
+
console.error(import_picocolors16.default.red(` Couldn't reach ${baseUrl()}.`));
|
|
283593
284050
|
process.exit(1);
|
|
283594
284051
|
}
|
|
283595
284052
|
if (!res.ok) {
|
|
283596
284053
|
const err = await res.json().catch(() => ({}));
|
|
283597
|
-
console.error(
|
|
284054
|
+
console.error(import_picocolors16.default.red(` Request failed (${res.status}): ${err.message ?? err.reason ?? ""}`));
|
|
283598
284055
|
process.exit(1);
|
|
283599
284056
|
}
|
|
283600
284057
|
return await res.json();
|
|
283601
284058
|
}
|
|
283602
284059
|
function writeSecretToEnv(secret) {
|
|
283603
|
-
const file = (0,
|
|
284060
|
+
const file = (0, import_fs8.existsSync)(".env.local") ? ".env.local" : (0, import_fs8.existsSync)(".env") ? ".env" : ".env.local";
|
|
283604
284061
|
const line = `${ENV_KEY}=${secret}`;
|
|
283605
284062
|
let next;
|
|
283606
|
-
if ((0,
|
|
283607
|
-
const existing = (0,
|
|
284063
|
+
if ((0, import_fs8.existsSync)(file)) {
|
|
284064
|
+
const existing = (0, import_fs8.readFileSync)(file, "utf-8");
|
|
283608
284065
|
next = new RegExp(`^${ENV_KEY}=.*$`, "m").test(existing) ? existing.replace(new RegExp(`^${ENV_KEY}=.*$`, "m"), line) : `${existing.replace(/\n*$/, "")}
|
|
283609
284066
|
${line}
|
|
283610
284067
|
`;
|
|
@@ -283612,15 +284069,15 @@ ${line}
|
|
|
283612
284069
|
next = `${line}
|
|
283613
284070
|
`;
|
|
283614
284071
|
}
|
|
283615
|
-
(0,
|
|
284072
|
+
(0, import_fs8.writeFileSync)(file, next);
|
|
283616
284073
|
return file;
|
|
283617
284074
|
}
|
|
283618
284075
|
function printEndpoint(e2) {
|
|
283619
|
-
const dot = e2.status === "enabled" ?
|
|
283620
|
-
const health = e2.last_error ?
|
|
283621
|
-
console.log(` ${dot} ${
|
|
284076
|
+
const dot = e2.status === "enabled" ? import_picocolors16.default.green("\u25CF") : import_picocolors16.default.red("\u25CF");
|
|
284077
|
+
const health = e2.last_error ? import_picocolors16.default.red(` last error: ${e2.last_error}`) : "";
|
|
284078
|
+
console.log(` ${dot} ${import_picocolors16.default.bold(e2.id)} ${e2.url}`);
|
|
283622
284079
|
console.log(
|
|
283623
|
-
|
|
284080
|
+
import_picocolors16.default.dim(
|
|
283624
284081
|
` ${e2.status} \xB7 ${e2.environment} \xB7 events ${e2.enabled_events.join(",")} \xB7 cursor ${e2.cursor ?? "\u2014"}${health}`
|
|
283625
284082
|
)
|
|
283626
284083
|
);
|
|
@@ -283632,7 +284089,7 @@ async function webhooks(argv) {
|
|
|
283632
284089
|
if (sub === "create") {
|
|
283633
284090
|
const url = positional(rest);
|
|
283634
284091
|
if (!url) {
|
|
283635
|
-
console.error(
|
|
284092
|
+
console.error(import_picocolors16.default.red(" Usage: ") + brand("ablo webhooks create <url>"));
|
|
283636
284093
|
process.exit(1);
|
|
283637
284094
|
}
|
|
283638
284095
|
const apiKey = requireKey2(mode2);
|
|
@@ -283644,8 +284101,8 @@ async function webhooks(argv) {
|
|
|
283644
284101
|
});
|
|
283645
284102
|
const file = writeSecretToEnv(created.secret);
|
|
283646
284103
|
console.log(`
|
|
283647
|
-
${
|
|
283648
|
-
console.log(` ${
|
|
284104
|
+
${import_picocolors16.default.green("\u2713")} Registered ${import_picocolors16.default.bold(created.id)} \u2192 ${created.url}`);
|
|
284105
|
+
console.log(` ${import_picocolors16.default.green("\u2713")} Wrote ${import_picocolors16.default.bold(ENV_KEY)} to ${import_picocolors16.default.bold(file)} ${import_picocolors16.default.dim("(shown once)")}
|
|
283649
284106
|
`);
|
|
283650
284107
|
return;
|
|
283651
284108
|
}
|
|
@@ -283653,7 +284110,7 @@ async function webhooks(argv) {
|
|
|
283653
284110
|
const apiKey = requireKey2(mode2);
|
|
283654
284111
|
const { data } = await api(apiKey, "GET", "");
|
|
283655
284112
|
if (data.length === 0) {
|
|
283656
|
-
console.log(
|
|
284113
|
+
console.log(import_picocolors16.default.dim(" No webhook endpoints. ") + brand("ablo webhooks create <url>"));
|
|
283657
284114
|
return;
|
|
283658
284115
|
}
|
|
283659
284116
|
console.log();
|
|
@@ -283664,40 +284121,40 @@ async function webhooks(argv) {
|
|
|
283664
284121
|
if (sub === "roll") {
|
|
283665
284122
|
const id = positional(rest);
|
|
283666
284123
|
if (!id) {
|
|
283667
|
-
console.error(
|
|
284124
|
+
console.error(import_picocolors16.default.red(" Usage: ") + brand("ablo webhooks roll <id>"));
|
|
283668
284125
|
process.exit(1);
|
|
283669
284126
|
}
|
|
283670
284127
|
const apiKey = requireKey2(mode2);
|
|
283671
284128
|
const rolled = await api(apiKey, "POST", `/${id}/roll_secret`);
|
|
283672
284129
|
const file = writeSecretToEnv(rolled.secret);
|
|
283673
284130
|
console.log(`
|
|
283674
|
-
${
|
|
284131
|
+
${import_picocolors16.default.green("\u2713")} Rolled secret for ${import_picocolors16.default.bold(id)} \u2192 ${import_picocolors16.default.bold(file)} ${import_picocolors16.default.dim("(old secret now invalid)")}
|
|
283675
284132
|
`);
|
|
283676
284133
|
return;
|
|
283677
284134
|
}
|
|
283678
284135
|
if (sub === "enable") {
|
|
283679
284136
|
const id = positional(rest);
|
|
283680
284137
|
if (!id) {
|
|
283681
|
-
console.error(
|
|
284138
|
+
console.error(import_picocolors16.default.red(" Usage: ") + brand("ablo webhooks enable <id>"));
|
|
283682
284139
|
process.exit(1);
|
|
283683
284140
|
}
|
|
283684
284141
|
const apiKey = requireKey2(mode2);
|
|
283685
284142
|
const e2 = await api(apiKey, "POST", `/${id}/enable`);
|
|
283686
|
-
console.log(` ${
|
|
284143
|
+
console.log(` ${import_picocolors16.default.green("\u2713")} Re-enabled ${import_picocolors16.default.bold(e2.id)}`);
|
|
283687
284144
|
return;
|
|
283688
284145
|
}
|
|
283689
284146
|
if (sub === "rm" || sub === "delete") {
|
|
283690
284147
|
const id = positional(rest);
|
|
283691
284148
|
if (!id) {
|
|
283692
|
-
console.error(
|
|
284149
|
+
console.error(import_picocolors16.default.red(" Usage: ") + brand("ablo webhooks rm <id>"));
|
|
283693
284150
|
process.exit(1);
|
|
283694
284151
|
}
|
|
283695
284152
|
const apiKey = requireKey2(mode2);
|
|
283696
284153
|
await api(apiKey, "DELETE", `/${id}`);
|
|
283697
|
-
console.log(` ${
|
|
284154
|
+
console.log(` ${import_picocolors16.default.green("\u2713")} Removed ${import_picocolors16.default.bold(id)}`);
|
|
283698
284155
|
return;
|
|
283699
284156
|
}
|
|
283700
|
-
console.log(` ${
|
|
284157
|
+
console.log(` ${import_picocolors16.default.bold("Usage:")}`);
|
|
283701
284158
|
console.log(` ${brand("ablo webhooks create <url>")} Register an endpoint; writes ${ENV_KEY}`);
|
|
283702
284159
|
console.log(` ${brand("ablo webhooks list")} List endpoints + delivery health`);
|
|
283703
284160
|
console.log(` ${brand("ablo webhooks roll <id>")} Mint a fresh signing secret`);
|
|
@@ -283709,7 +284166,7 @@ async function webhooks(argv) {
|
|
|
283709
284166
|
// src/cli/check.ts
|
|
283710
284167
|
init_cjs_shims();
|
|
283711
284168
|
init_errors();
|
|
283712
|
-
var
|
|
284169
|
+
var import_picocolors17 = __toESM(require_picocolors(), 1);
|
|
283713
284170
|
init_src();
|
|
283714
284171
|
var import_schema6 = require("@abloatai/ablo/schema");
|
|
283715
284172
|
init_push();
|
|
@@ -283747,13 +284204,13 @@ async function check(argv) {
|
|
|
283747
284204
|
try {
|
|
283748
284205
|
args = parseCheckArgs(argv);
|
|
283749
284206
|
} catch (err) {
|
|
283750
|
-
console.error(
|
|
284207
|
+
console.error(import_picocolors17.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
283751
284208
|
process.exit(1);
|
|
283752
284209
|
}
|
|
283753
284210
|
const dbUrl = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
|
|
283754
284211
|
if (!dbUrl) {
|
|
283755
284212
|
console.error(
|
|
283756
|
-
|
|
284213
|
+
import_picocolors17.default.red(` No database.`) + import_picocolors17.default.dim(` Set ${import_picocolors17.default.bold("DATABASE_URL")} to the Postgres you want Ablo to adopt.`)
|
|
283757
284214
|
);
|
|
283758
284215
|
process.exit(1);
|
|
283759
284216
|
}
|
|
@@ -283768,7 +284225,7 @@ async function check(argv) {
|
|
|
283768
284225
|
[args.appSchema]
|
|
283769
284226
|
);
|
|
283770
284227
|
} catch (err) {
|
|
283771
|
-
console.error(
|
|
284228
|
+
console.error(import_picocolors17.default.red(` Couldn't read the database: ${err instanceof Error ? err.message : String(err)}`));
|
|
283772
284229
|
await sql.end({ timeout: 2 });
|
|
283773
284230
|
process.exit(1);
|
|
283774
284231
|
}
|
|
@@ -283783,7 +284240,7 @@ async function check(argv) {
|
|
|
283783
284240
|
set.add(r2.column_name);
|
|
283784
284241
|
}
|
|
283785
284242
|
console.log(`
|
|
283786
|
-
${brand("ablo")} ${
|
|
284243
|
+
${brand("ablo")} ${import_picocolors17.default.dim("check")} ${import_picocolors17.default.dim(`schema "${args.appSchema}"`)}
|
|
283787
284244
|
`);
|
|
283788
284245
|
const declaredTables = /* @__PURE__ */ new Set();
|
|
283789
284246
|
let errors = 0;
|
|
@@ -283793,7 +284250,7 @@ async function check(argv) {
|
|
|
283793
284250
|
declaredTables.add(table);
|
|
283794
284251
|
const present = colsByTable.get(table);
|
|
283795
284252
|
if (!present) {
|
|
283796
|
-
console.log(` ${
|
|
284253
|
+
console.log(` ${import_picocolors17.default.red("\u2717")} ${import_picocolors17.default.bold(key)} ${import_picocolors17.default.dim("\u2192")} table ${import_picocolors17.default.bold(table)} ${import_picocolors17.default.red("not found")}`);
|
|
283797
284254
|
errors++;
|
|
283798
284255
|
continue;
|
|
283799
284256
|
}
|
|
@@ -283815,26 +284272,26 @@ async function check(argv) {
|
|
|
283815
284272
|
if (!present.has(col)) problems.push(`missing column "${col}" (field ${fieldName})`);
|
|
283816
284273
|
}
|
|
283817
284274
|
if (problems.length > 0) {
|
|
283818
|
-
console.log(` ${
|
|
283819
|
-
for (const p2 of problems) console.log(` ${
|
|
283820
|
-
for (const w2 of warns) console.log(` ${
|
|
284275
|
+
console.log(` ${import_picocolors17.default.red("\u2717")} ${import_picocolors17.default.bold(key)} ${import_picocolors17.default.dim("\u2192")} ${table}`);
|
|
284276
|
+
for (const p2 of problems) console.log(` ${import_picocolors17.default.red("\u2022")} ${p2}`);
|
|
284277
|
+
for (const w2 of warns) console.log(` ${import_picocolors17.default.yellow("\u2022")} ${w2}`);
|
|
283821
284278
|
errors++;
|
|
283822
284279
|
} else if (warns.length > 0) {
|
|
283823
|
-
console.log(` ${
|
|
283824
|
-
for (const w2 of warns) console.log(` ${
|
|
284280
|
+
console.log(` ${import_picocolors17.default.yellow("!")} ${import_picocolors17.default.bold(key)} ${import_picocolors17.default.dim("\u2192")} ${table}`);
|
|
284281
|
+
for (const w2 of warns) console.log(` ${import_picocolors17.default.yellow("\u2022")} ${w2}`);
|
|
283825
284282
|
warnings++;
|
|
283826
284283
|
} else {
|
|
283827
|
-
console.log(` ${
|
|
284284
|
+
console.log(` ${import_picocolors17.default.green("\u2713")} ${import_picocolors17.default.bold(key)} ${import_picocolors17.default.dim(`\u2192 ${table} (id, ${orgCol ?? "no org"} ok)`)}`);
|
|
283828
284285
|
}
|
|
283829
284286
|
}
|
|
283830
284287
|
const modelCount = Object.keys(schemaJson.models).length;
|
|
283831
284288
|
const ignored = [...colsByTable.keys()].filter((t) => !declaredTables.has(t)).length;
|
|
283832
284289
|
console.log(
|
|
283833
284290
|
`
|
|
283834
|
-
${modelCount} model${modelCount === 1 ? "" : "s"} \xB7 ${
|
|
284291
|
+
${modelCount} model${modelCount === 1 ? "" : "s"} \xB7 ${import_picocolors17.default.green(`${modelCount - errors - warnings} ok`)}` + (warnings ? ` \xB7 ${import_picocolors17.default.yellow(`${warnings} warning${warnings === 1 ? "" : "s"}`)}` : "") + (errors ? ` \xB7 ${import_picocolors17.default.red(`${errors} error${errors === 1 ? "" : "s"}`)}` : "")
|
|
283835
284292
|
);
|
|
283836
284293
|
if (ignored > 0) {
|
|
283837
|
-
console.log(` ${
|
|
284294
|
+
console.log(` ${import_picocolors17.default.dim(`${ignored} other table${ignored === 1 ? "" : "s"} in your database \u2014 ignored by Ablo`)}`);
|
|
283838
284295
|
}
|
|
283839
284296
|
console.log();
|
|
283840
284297
|
process.exit(errors > 0 ? 1 : 0);
|
|
@@ -283842,7 +284299,7 @@ async function check(argv) {
|
|
|
283842
284299
|
|
|
283843
284300
|
// src/cli/upgrade.ts
|
|
283844
284301
|
init_cjs_shims();
|
|
283845
|
-
var
|
|
284302
|
+
var import_picocolors18 = __toESM(require_picocolors(), 1);
|
|
283846
284303
|
var import_ts_morph = __toESM(require_ts_morph(), 1);
|
|
283847
284304
|
var DEFAULT_GLOBS = ["app/**/*.{ts,tsx}", "src/**/*.{ts,tsx}", "ablo/**/*.{ts,tsx}", "lib/**/*.{ts,tsx}"];
|
|
283848
284305
|
var VERB_ARGS = {
|
|
@@ -283920,7 +284377,7 @@ async function upgrade(argv) {
|
|
|
283920
284377
|
project.addSourceFilesAtPaths(globs.length > 0 ? globs : DEFAULT_GLOBS);
|
|
283921
284378
|
const files = project.getSourceFiles();
|
|
283922
284379
|
if (files.length === 0) {
|
|
283923
|
-
console.log(
|
|
284380
|
+
console.log(import_picocolors18.default.yellow(' No .ts/.tsx files found. Pass a glob, e.g. `ablo upgrade "src/**/*.tsx"`.'));
|
|
283924
284381
|
return;
|
|
283925
284382
|
}
|
|
283926
284383
|
const edits = [];
|
|
@@ -283994,41 +284451,41 @@ async function upgrade(argv) {
|
|
|
283994
284451
|
const rel = (f) => f.replace(cwd + "/", "");
|
|
283995
284452
|
console.log();
|
|
283996
284453
|
if (edits.length === 0 && manual.length === 0) {
|
|
283997
|
-
console.log(
|
|
284454
|
+
console.log(import_picocolors18.default.green(" \u2713 Nothing to migrate \u2014 your code is already on the current API."));
|
|
283998
284455
|
return;
|
|
283999
284456
|
}
|
|
284000
284457
|
if (edits.length > 0) {
|
|
284001
|
-
console.log(
|
|
284458
|
+
console.log(import_picocolors18.default.bold(` ${write ? "Applied" : "Would apply"} ${edits.length} change${edits.length === 1 ? "" : "s"}:`));
|
|
284002
284459
|
for (const e2 of edits) {
|
|
284003
|
-
console.log(` ${
|
|
284004
|
-
console.log(` ${
|
|
284005
|
-
console.log(` ${
|
|
284460
|
+
console.log(` ${import_picocolors18.default.dim(`${rel(e2.file)}:${e2.line}`)} ${import_picocolors18.default.cyan(e2.rule)}`);
|
|
284461
|
+
console.log(` ${import_picocolors18.default.red("-")} ${e2.before}`);
|
|
284462
|
+
console.log(` ${import_picocolors18.default.green("+")} ${e2.after}`);
|
|
284006
284463
|
}
|
|
284007
284464
|
}
|
|
284008
284465
|
if (manual.length > 0) {
|
|
284009
284466
|
console.log();
|
|
284010
|
-
console.log(
|
|
284467
|
+
console.log(import_picocolors18.default.bold(import_picocolors18.default.yellow(` ${manual.length} spot${manual.length === 1 ? "" : "s"} need manual review (structural):`)));
|
|
284011
284468
|
for (const m2 of manual) {
|
|
284012
|
-
console.log(` ${
|
|
284013
|
-
console.log(` ${
|
|
284469
|
+
console.log(` ${import_picocolors18.default.dim(`${rel(m2.file)}:${m2.line}`)} ${import_picocolors18.default.yellow(m2.rule)}`);
|
|
284470
|
+
console.log(` ${import_picocolors18.default.dim(m2.snippet)}`);
|
|
284014
284471
|
console.log(` \u2192 ${m2.hint}`);
|
|
284015
284472
|
}
|
|
284016
284473
|
}
|
|
284017
284474
|
console.log();
|
|
284018
284475
|
if (write) {
|
|
284019
284476
|
await project.save();
|
|
284020
|
-
console.log(
|
|
284477
|
+
console.log(import_picocolors18.default.green(` \u2713 Wrote ${edits.length} change${edits.length === 1 ? "" : "s"}. Review the diff, run your typecheck.`));
|
|
284021
284478
|
} else {
|
|
284022
|
-
console.log(
|
|
284479
|
+
console.log(import_picocolors18.default.dim(" Dry run. Re-run with `--write` to apply the auto-fixes above (manual items are never auto-written)."));
|
|
284023
284480
|
}
|
|
284024
284481
|
}
|
|
284025
284482
|
|
|
284026
284483
|
// src/cli/pull.ts
|
|
284027
284484
|
init_cjs_shims();
|
|
284028
284485
|
init_errors();
|
|
284029
|
-
var
|
|
284486
|
+
var import_picocolors19 = __toESM(require_picocolors(), 1);
|
|
284030
284487
|
init_src();
|
|
284031
|
-
var
|
|
284488
|
+
var import_fs9 = require("fs");
|
|
284032
284489
|
init_theme();
|
|
284033
284490
|
var DEFAULT_OUT2 = "ablo/schema.ts";
|
|
284034
284491
|
var DEFAULT_IMPORT = "@abloatai/ablo/schema";
|
|
@@ -284140,45 +284597,45 @@ async function pull(argv) {
|
|
|
284140
284597
|
try {
|
|
284141
284598
|
args = parsePullArgs(argv);
|
|
284142
284599
|
} catch (err) {
|
|
284143
|
-
console.error(
|
|
284600
|
+
console.error(import_picocolors19.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
284144
284601
|
process.exit(1);
|
|
284145
284602
|
}
|
|
284146
284603
|
const dbUrl = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
|
|
284147
284604
|
if (!dbUrl) {
|
|
284148
|
-
console.error(
|
|
284605
|
+
console.error(import_picocolors19.default.red(` No database.`) + import_picocolors19.default.dim(` Set ${import_picocolors19.default.bold("DATABASE_URL")} to the Postgres to pull from.`));
|
|
284149
284606
|
process.exit(1);
|
|
284150
284607
|
}
|
|
284151
|
-
if ((0,
|
|
284608
|
+
if ((0, import_fs9.existsSync)(args.out) && !args.force) {
|
|
284152
284609
|
console.error(
|
|
284153
|
-
|
|
284610
|
+
import_picocolors19.default.red(` ${args.out} already exists.`) + import_picocolors19.default.dim(` Re-run with ${import_picocolors19.default.bold("--force")} to overwrite.`)
|
|
284154
284611
|
);
|
|
284155
284612
|
process.exit(1);
|
|
284156
284613
|
}
|
|
284157
284614
|
console.log(`
|
|
284158
|
-
${brand("ablo")} ${
|
|
284615
|
+
${brand("ablo")} ${import_picocolors19.default.dim("pull")} ${import_picocolors19.default.dim(`schema "${args.appSchema}"`)}
|
|
284159
284616
|
`);
|
|
284160
284617
|
let result;
|
|
284161
284618
|
try {
|
|
284162
284619
|
result = await buildSchemaSourceFromDb({ dbUrl, appSchema: args.appSchema, importPath: args.importPath });
|
|
284163
284620
|
} catch (err) {
|
|
284164
|
-
console.error(
|
|
284621
|
+
console.error(import_picocolors19.default.red(` Couldn't read the database: ${err instanceof Error ? err.message : String(err)}`));
|
|
284165
284622
|
process.exit(1);
|
|
284166
284623
|
}
|
|
284167
284624
|
if (result.models.length === 0) {
|
|
284168
284625
|
console.error(
|
|
284169
|
-
|
|
284626
|
+
import_picocolors19.default.yellow(` No adoptable tables found`) + import_picocolors19.default.dim(` (a model needs an ${import_picocolors19.default.bold("id")} + ${import_picocolors19.default.bold("organization_id")} column).`)
|
|
284170
284627
|
);
|
|
284171
284628
|
process.exit(1);
|
|
284172
284629
|
}
|
|
284173
|
-
(0,
|
|
284174
|
-
console.log(` ${
|
|
284175
|
-
console.log(` ${
|
|
284630
|
+
(0, import_fs9.writeFileSync)(args.out, result.source);
|
|
284631
|
+
console.log(` ${import_picocolors19.default.green("\u2713")} wrote ${import_picocolors19.default.bold(args.out)} ${import_picocolors19.default.dim(`(${result.models.length} models)`)}`);
|
|
284632
|
+
console.log(` ${import_picocolors19.default.dim(`models: ${result.models.join(", ")}`)}`);
|
|
284176
284633
|
if (result.skipped > 0) {
|
|
284177
|
-
console.log(` ${
|
|
284634
|
+
console.log(` ${import_picocolors19.default.dim(`${result.skipped} table(s) skipped \u2014 no id/organization_id`)}`);
|
|
284178
284635
|
}
|
|
284179
284636
|
console.log(
|
|
284180
284637
|
`
|
|
284181
|
-
${
|
|
284638
|
+
${import_picocolors19.default.dim("Introspection is lossy (enums, JSON shape, relations). Review the file, then")} ${import_picocolors19.default.bold("ablo check")}.
|
|
284182
284639
|
`
|
|
284183
284640
|
);
|
|
284184
284641
|
}
|
|
@@ -284186,8 +284643,8 @@ async function pull(argv) {
|
|
|
284186
284643
|
// src/cli/prismaPull.ts
|
|
284187
284644
|
init_cjs_shims();
|
|
284188
284645
|
init_errors();
|
|
284189
|
-
var
|
|
284190
|
-
var
|
|
284646
|
+
var import_picocolors20 = __toESM(require_picocolors(), 1);
|
|
284647
|
+
var import_fs10 = require("fs");
|
|
284191
284648
|
init_theme();
|
|
284192
284649
|
|
|
284193
284650
|
// src/cli/schemaIr.ts
|
|
@@ -284471,58 +284928,58 @@ async function prismaPull(argv) {
|
|
|
284471
284928
|
try {
|
|
284472
284929
|
args = parsePrismaPullArgs(argv);
|
|
284473
284930
|
} catch (err) {
|
|
284474
|
-
console.error(
|
|
284931
|
+
console.error(import_picocolors20.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
284475
284932
|
process.exit(1);
|
|
284476
284933
|
}
|
|
284477
|
-
if (!(0,
|
|
284934
|
+
if (!(0, import_fs10.existsSync)(args.schema)) {
|
|
284478
284935
|
console.error(
|
|
284479
|
-
|
|
284936
|
+
import_picocolors20.default.red(` No Prisma schema at ${import_picocolors20.default.bold(args.schema)}.`) + import_picocolors20.default.dim(` Pass a path: ${import_picocolors20.default.bold("ablo pull prisma <path>")}.`)
|
|
284480
284937
|
);
|
|
284481
284938
|
process.exit(1);
|
|
284482
284939
|
}
|
|
284483
|
-
if ((0,
|
|
284940
|
+
if ((0, import_fs10.existsSync)(args.out) && !args.force) {
|
|
284484
284941
|
console.error(
|
|
284485
|
-
|
|
284942
|
+
import_picocolors20.default.red(` ${args.out} already exists.`) + import_picocolors20.default.dim(` Re-run with ${import_picocolors20.default.bold("--force")} to overwrite.`)
|
|
284486
284943
|
);
|
|
284487
284944
|
process.exit(1);
|
|
284488
284945
|
}
|
|
284489
284946
|
console.log(`
|
|
284490
|
-
${brand("ablo")} ${
|
|
284947
|
+
${brand("ablo")} ${import_picocolors20.default.dim("pull prisma")} ${import_picocolors20.default.dim(args.schema)}
|
|
284491
284948
|
`);
|
|
284492
284949
|
let result;
|
|
284493
284950
|
try {
|
|
284494
|
-
const src = (0,
|
|
284951
|
+
const src = (0, import_fs10.readFileSync)(args.schema, "utf8");
|
|
284495
284952
|
result = buildSchemaSourceFromPrisma({ src, importPath: args.importPath });
|
|
284496
284953
|
} catch (err) {
|
|
284497
|
-
console.error(
|
|
284954
|
+
console.error(import_picocolors20.default.red(` Couldn't parse the schema: ${err instanceof Error ? err.message : String(err)}`));
|
|
284498
284955
|
process.exit(1);
|
|
284499
284956
|
}
|
|
284500
284957
|
if (result.models.length === 0) {
|
|
284501
284958
|
console.error(
|
|
284502
|
-
|
|
284959
|
+
import_picocolors20.default.yellow(` No adoptable models found`) + import_picocolors20.default.dim(` (a model needs an ${import_picocolors20.default.bold("id")} + ${import_picocolors20.default.bold("organizationId")} / ${import_picocolors20.default.bold("organization_id")}).`)
|
|
284503
284960
|
);
|
|
284504
284961
|
process.exit(1);
|
|
284505
284962
|
}
|
|
284506
|
-
(0,
|
|
284507
|
-
console.log(` ${
|
|
284508
|
-
console.log(` ${
|
|
284963
|
+
(0, import_fs10.writeFileSync)(args.out, result.source);
|
|
284964
|
+
console.log(` ${import_picocolors20.default.green("\u2713")} wrote ${import_picocolors20.default.bold(args.out)} ${import_picocolors20.default.dim(`(${result.models.length} models)`)}`);
|
|
284965
|
+
console.log(` ${import_picocolors20.default.dim(`models: ${result.models.join(", ")}`)}`);
|
|
284509
284966
|
if (result.skipped.length > 0) {
|
|
284510
|
-
console.log(` ${
|
|
284511
|
-
for (const s of result.skipped) console.log(` ${
|
|
284967
|
+
console.log(` ${import_picocolors20.default.dim(`${result.skipped.length} model(s) skipped:`)}`);
|
|
284968
|
+
for (const s of result.skipped) console.log(` ${import_picocolors20.default.dim(`- ${s.name}: ${s.reason}`)}`);
|
|
284512
284969
|
}
|
|
284513
284970
|
console.log(
|
|
284514
284971
|
`
|
|
284515
|
-
${
|
|
284972
|
+
${import_picocolors20.default.dim("Enums and relations were preserved. Review the file, then")} ${import_picocolors20.default.bold("ablo check")}.
|
|
284516
284973
|
`
|
|
284517
284974
|
);
|
|
284518
284975
|
}
|
|
284519
284976
|
|
|
284520
284977
|
// src/cli/drizzlePull.ts
|
|
284521
284978
|
init_cjs_shims();
|
|
284522
|
-
var
|
|
284979
|
+
var import_picocolors21 = __toESM(require_picocolors(), 1);
|
|
284523
284980
|
init_errors();
|
|
284524
|
-
var
|
|
284525
|
-
var
|
|
284981
|
+
var import_fs11 = require("fs");
|
|
284982
|
+
var import_path6 = require("path");
|
|
284526
284983
|
init_theme();
|
|
284527
284984
|
var DEFAULT_OUT4 = "ablo/schema.ts";
|
|
284528
284985
|
var DEFAULT_IMPORT3 = "@abloatai/ablo/schema";
|
|
@@ -284650,7 +285107,7 @@ function parseDrizzlePullArgs(argv) {
|
|
|
284650
285107
|
async function loadModule(path) {
|
|
284651
285108
|
const { createJiti } = await import("jiti");
|
|
284652
285109
|
const jiti = createJiti(process.cwd());
|
|
284653
|
-
const mod = await jiti.import((0,
|
|
285110
|
+
const mod = await jiti.import((0, import_path6.resolve)(path));
|
|
284654
285111
|
return mod;
|
|
284655
285112
|
}
|
|
284656
285113
|
async function drizzlePull(argv) {
|
|
@@ -284658,27 +285115,27 @@ async function drizzlePull(argv) {
|
|
|
284658
285115
|
try {
|
|
284659
285116
|
args = parseDrizzlePullArgs(argv);
|
|
284660
285117
|
} catch (err) {
|
|
284661
|
-
console.error(
|
|
285118
|
+
console.error(import_picocolors21.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
284662
285119
|
process.exit(1);
|
|
284663
285120
|
}
|
|
284664
285121
|
if (!args.schema) {
|
|
284665
285122
|
console.error(
|
|
284666
|
-
|
|
285123
|
+
import_picocolors21.default.red(` No Drizzle schema given.`) + import_picocolors21.default.dim(` Pass the module: ${import_picocolors21.default.bold("ablo pull drizzle src/db/schema.ts")}.`)
|
|
284667
285124
|
);
|
|
284668
285125
|
process.exit(1);
|
|
284669
285126
|
}
|
|
284670
|
-
if (!(0,
|
|
284671
|
-
console.error(
|
|
285127
|
+
if (!(0, import_fs11.existsSync)(args.schema)) {
|
|
285128
|
+
console.error(import_picocolors21.default.red(` No file at ${import_picocolors21.default.bold(args.schema)}.`));
|
|
284672
285129
|
process.exit(1);
|
|
284673
285130
|
}
|
|
284674
|
-
if ((0,
|
|
285131
|
+
if ((0, import_fs11.existsSync)(args.out) && !args.force) {
|
|
284675
285132
|
console.error(
|
|
284676
|
-
|
|
285133
|
+
import_picocolors21.default.red(` ${args.out} already exists.`) + import_picocolors21.default.dim(` Re-run with ${import_picocolors21.default.bold("--force")} to overwrite.`)
|
|
284677
285134
|
);
|
|
284678
285135
|
process.exit(1);
|
|
284679
285136
|
}
|
|
284680
285137
|
console.log(`
|
|
284681
|
-
${brand("ablo")} ${
|
|
285138
|
+
${brand("ablo")} ${import_picocolors21.default.dim("pull drizzle")} ${import_picocolors21.default.dim(args.schema)}
|
|
284682
285139
|
`);
|
|
284683
285140
|
let result;
|
|
284684
285141
|
try {
|
|
@@ -284686,26 +285143,26 @@ async function drizzlePull(argv) {
|
|
|
284686
285143
|
result = await buildSchemaSourceFromDrizzle({ mod, importPath: args.importPath });
|
|
284687
285144
|
} catch (err) {
|
|
284688
285145
|
const msg = err instanceof Error ? err.message : String(err);
|
|
284689
|
-
const hint = msg.includes("Cannot find package 'drizzle-orm'") ?
|
|
284690
|
-
console.error(
|
|
285146
|
+
const hint = msg.includes("Cannot find package 'drizzle-orm'") ? import_picocolors21.default.dim(` (install ${import_picocolors21.default.bold("drizzle-orm")} in this project)`) : "";
|
|
285147
|
+
console.error(import_picocolors21.default.red(` Couldn't load the schema: ${msg}`) + hint);
|
|
284691
285148
|
process.exit(1);
|
|
284692
285149
|
}
|
|
284693
285150
|
if (result.models.length === 0) {
|
|
284694
285151
|
console.error(
|
|
284695
|
-
|
|
285152
|
+
import_picocolors21.default.yellow(` No adoptable tables found`) + import_picocolors21.default.dim(` (a table needs an ${import_picocolors21.default.bold("id")} + ${import_picocolors21.default.bold("organization_id")} column).`)
|
|
284696
285153
|
);
|
|
284697
285154
|
process.exit(1);
|
|
284698
285155
|
}
|
|
284699
|
-
(0,
|
|
284700
|
-
console.log(` ${
|
|
284701
|
-
console.log(` ${
|
|
285156
|
+
(0, import_fs11.writeFileSync)(args.out, result.source);
|
|
285157
|
+
console.log(` ${import_picocolors21.default.green("\u2713")} wrote ${import_picocolors21.default.bold(args.out)} ${import_picocolors21.default.dim(`(${result.models.length} models)`)}`);
|
|
285158
|
+
console.log(` ${import_picocolors21.default.dim(`models: ${result.models.join(", ")}`)}`);
|
|
284702
285159
|
if (result.skipped.length > 0) {
|
|
284703
|
-
console.log(` ${
|
|
284704
|
-
for (const s of result.skipped) console.log(` ${
|
|
285160
|
+
console.log(` ${import_picocolors21.default.dim(`${result.skipped.length} table(s) skipped:`)}`);
|
|
285161
|
+
for (const s of result.skipped) console.log(` ${import_picocolors21.default.dim(`- ${s.name}: ${s.reason}`)}`);
|
|
284705
285162
|
}
|
|
284706
285163
|
console.log(
|
|
284707
285164
|
`
|
|
284708
|
-
${
|
|
285165
|
+
${import_picocolors21.default.dim("Enums and relations were preserved. Review the file, then")} ${import_picocolors21.default.bold("ablo check")}.
|
|
284709
285166
|
`
|
|
284710
285167
|
);
|
|
284711
285168
|
}
|
|
@@ -284715,7 +285172,7 @@ init_theme();
|
|
|
284715
285172
|
|
|
284716
285173
|
// src/cli/renderError.ts
|
|
284717
285174
|
init_cjs_shims();
|
|
284718
|
-
var
|
|
285175
|
+
var import_picocolors22 = __toESM(require_picocolors(), 1);
|
|
284719
285176
|
init_errors();
|
|
284720
285177
|
init_theme();
|
|
284721
285178
|
var RECOVERY_HINT = {
|
|
@@ -284737,15 +285194,15 @@ function isStringArray(v2) {
|
|
|
284737
285194
|
function renderKnownDetails(details, line) {
|
|
284738
285195
|
if (!details) return;
|
|
284739
285196
|
const { retryAfterSeconds, missingIds, requiredCapability, unexecutable, errors } = details;
|
|
284740
|
-
if (typeof retryAfterSeconds === "number") line(` ${
|
|
285197
|
+
if (typeof retryAfterSeconds === "number") line(` ${import_picocolors22.default.dim("retry")} after ${retryAfterSeconds}s`);
|
|
284741
285198
|
if (isStringArray(missingIds) && missingIds.length > 0) {
|
|
284742
285199
|
const shown = missingIds.slice(0, 5).join(", ");
|
|
284743
285200
|
const more = missingIds.length > 5 ? ` (+${missingIds.length - 5} more)` : "";
|
|
284744
|
-
line(` ${
|
|
285201
|
+
line(` ${import_picocolors22.default.dim("missing")} ${shown}${more}`);
|
|
284745
285202
|
}
|
|
284746
|
-
if (typeof requiredCapability === "string") line(` ${
|
|
285203
|
+
if (typeof requiredCapability === "string") line(` ${import_picocolors22.default.dim("needs")} ${requiredCapability}`);
|
|
284747
285204
|
if (Array.isArray(unexecutable) && unexecutable.length > 0) {
|
|
284748
|
-
line(` ${
|
|
285205
|
+
line(` ${import_picocolors22.default.dim("blocked")} ${unexecutable.length} change(s) can't be applied \u2014 see \`unexecutable\` (--verbose)`);
|
|
284749
285206
|
}
|
|
284750
285207
|
if (Array.isArray(errors)) {
|
|
284751
285208
|
for (const e2 of errors.slice(0, 8)) {
|
|
@@ -284753,7 +285210,7 @@ function renderKnownDetails(details, line) {
|
|
|
284753
285210
|
const rec = e2;
|
|
284754
285211
|
const where = typeof rec.param === "string" ? `${rec.param}: ` : "";
|
|
284755
285212
|
const msg = typeof rec.message === "string" ? rec.message : "";
|
|
284756
|
-
if (msg) line(` ${
|
|
285213
|
+
if (msg) line(` ${import_picocolors22.default.dim("\xB7")} ${where}${msg}`);
|
|
284757
285214
|
}
|
|
284758
285215
|
}
|
|
284759
285216
|
}
|
|
@@ -284764,22 +285221,22 @@ function renderCliError(err, opts = {}) {
|
|
|
284764
285221
|
});
|
|
284765
285222
|
const verbose = opts.verbose ?? (process.argv.includes("--verbose") || process.env.ABLO_VERBOSE === "1");
|
|
284766
285223
|
if (err instanceof AbloError) {
|
|
284767
|
-
const codeTag = err.code ? ` ${
|
|
285224
|
+
const codeTag = err.code ? ` ${import_picocolors22.default.dim(`[${err.code}]`)}` : "";
|
|
284768
285225
|
line("");
|
|
284769
|
-
line(` ${brand("ablo")} ${
|
|
285226
|
+
line(` ${brand("ablo")} ${import_picocolors22.default.red("\u2717")} ${import_picocolors22.default.bold(titleForType(err.type))}${codeTag}`);
|
|
284770
285227
|
line("");
|
|
284771
285228
|
line(` ${err.message}`);
|
|
284772
|
-
if (err.param) line(` ${
|
|
285229
|
+
if (err.param) line(` ${import_picocolors22.default.dim("field")} ${err.param}`);
|
|
284773
285230
|
renderKnownDetails(err.details, line);
|
|
284774
285231
|
const hint = err.code ? RECOVERY_HINT[classifyRecovery(err.code)] : void 0;
|
|
284775
|
-
if (hint) line(` ${
|
|
284776
|
-
if (err.docUrl) line(` ${
|
|
284777
|
-
if (err.requestId) line(` ${
|
|
285232
|
+
if (hint) line(` ${import_picocolors22.default.dim(hint)}`);
|
|
285233
|
+
if (err.docUrl) line(` ${import_picocolors22.default.dim("docs")} ${err.docUrl}`);
|
|
285234
|
+
if (err.requestId) line(` ${import_picocolors22.default.dim("ref")} ${err.requestId}`);
|
|
284778
285235
|
if (verbose) {
|
|
284779
285236
|
if (err.details && Object.keys(err.details).length > 0) {
|
|
284780
|
-
line(` ${
|
|
285237
|
+
line(` ${import_picocolors22.default.dim("details")} ${JSON.stringify(err.details)}`);
|
|
284781
285238
|
}
|
|
284782
|
-
if (err.stack) line(
|
|
285239
|
+
if (err.stack) line(import_picocolors22.default.dim(err.stack));
|
|
284783
285240
|
}
|
|
284784
285241
|
line("");
|
|
284785
285242
|
process.exitCode = 1;
|
|
@@ -284787,16 +285244,16 @@ function renderCliError(err, opts = {}) {
|
|
|
284787
285244
|
}
|
|
284788
285245
|
const message = err instanceof Error ? err.message : String(err);
|
|
284789
285246
|
line("");
|
|
284790
|
-
line(` ${brand("ablo")} ${
|
|
284791
|
-
if (verbose && err instanceof Error && err.stack) line(
|
|
284792
|
-
else line(` ${
|
|
285247
|
+
line(` ${brand("ablo")} ${import_picocolors22.default.red("\u2717")} ${message}`);
|
|
285248
|
+
if (verbose && err instanceof Error && err.stack) line(import_picocolors22.default.dim(err.stack));
|
|
285249
|
+
else line(` ${import_picocolors22.default.dim("Run with --verbose for the full error.")}`);
|
|
284793
285250
|
line("");
|
|
284794
285251
|
process.exitCode = 1;
|
|
284795
285252
|
}
|
|
284796
285253
|
|
|
284797
285254
|
// src/cli/index.ts
|
|
284798
285255
|
var LOGO = `
|
|
284799
|
-
${brand("ablo")} ${
|
|
285256
|
+
${brand("ablo")} ${import_picocolors23.default.dim("sync engine")}
|
|
284800
285257
|
`;
|
|
284801
285258
|
var SUBCOMMAND_USAGE = {
|
|
284802
285259
|
connect: CONNECT_USAGE,
|
|
@@ -284831,7 +285288,7 @@ async function main() {
|
|
|
284831
285288
|
const devArgs = process.argv.slice(3);
|
|
284832
285289
|
const oneShot = devArgs.includes("--no-watch");
|
|
284833
285290
|
console.log(
|
|
284834
|
-
|
|
285291
|
+
import_picocolors23.default.dim(
|
|
284835
285292
|
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."
|
|
284836
285293
|
)
|
|
284837
285294
|
);
|
|
@@ -284860,14 +285317,14 @@ async function main() {
|
|
|
284860
285317
|
const guard = guardActiveProjectKey();
|
|
284861
285318
|
if (!guard.ok && guard.available.length > 0 && !rest.includes("--url")) {
|
|
284862
285319
|
console.error(
|
|
284863
|
-
` ${
|
|
285320
|
+
` ${import_picocolors23.default.yellow("\u26A0")} active project ${import_picocolors23.default.bold(guard.activeProfile)} has no stored key ${import_picocolors23.default.dim(
|
|
284864
285321
|
`(you have keys for: ${guard.available.join(", ")})`
|
|
284865
285322
|
)}`
|
|
284866
285323
|
);
|
|
284867
285324
|
const loginCmd = guard.activeProfile === "default" ? "ablo login" : `ablo login --project ${guard.activeProfile}`;
|
|
284868
285325
|
console.error(
|
|
284869
|
-
|
|
284870
|
-
` Mint one with ${
|
|
285326
|
+
import_picocolors23.default.dim(
|
|
285327
|
+
` Mint one with ${import_picocolors23.default.bold(loginCmd)}, or switch with ${import_picocolors23.default.bold("ablo projects use <slug>")}.`
|
|
284871
285328
|
)
|
|
284872
285329
|
);
|
|
284873
285330
|
process.exitCode = 1;
|
|
@@ -284885,13 +285342,13 @@ async function main() {
|
|
|
284885
285342
|
await generate(process.argv.slice(3));
|
|
284886
285343
|
} else if (command === "schema") {
|
|
284887
285344
|
console.error(
|
|
284888
|
-
` ${
|
|
285345
|
+
` ${import_picocolors23.default.red("\u2717")} \`ablo schema push\` was renamed to \`${brand("ablo push")}\`.`
|
|
284889
285346
|
);
|
|
284890
285347
|
console.error(` Run \`ablo push${process.argv.slice(4).join(" ") ? " " + process.argv.slice(4).join(" ") : ""}\` instead.`);
|
|
284891
285348
|
process.exitCode = 1;
|
|
284892
285349
|
} else {
|
|
284893
285350
|
console.log(LOGO);
|
|
284894
|
-
console.log(` ${
|
|
285351
|
+
console.log(` ${import_picocolors23.default.bold("Usage:")}`);
|
|
284895
285352
|
console.log(` npx ablo init Scaffold ablo/ directory + starter schema`);
|
|
284896
285353
|
console.log(` npx ablo init --yes [--framework nextjs] Non-interactive (agents/CI): no prompts, flag-driven`);
|
|
284897
285354
|
console.log(` [--auth apikey] [--storage replication|endpoint] [--project <slug>] [--no-project]`);
|
|
@@ -284915,8 +285372,9 @@ async function main() {
|
|
|
284915
285372
|
console.log(` npx ablo pull drizzle <module> Generate schema.ts from a Drizzle schema (keeps enums + relations)`);
|
|
284916
285373
|
console.log(` npx ablo check Check your existing database fits the schema (read-only, creates no tables)`);
|
|
284917
285374
|
console.log(` npx ablo connect Connect a real database \u2014 prints the logical-replication setup SQL (the one way)`);
|
|
284918
|
-
console.log(` npx ablo connect
|
|
284919
|
-
console.log(` npx ablo connect
|
|
285375
|
+
console.log(` npx ablo connect check Validate DATABASE_URL is replication-ready (wal_level, publication, role, replica identity)`);
|
|
285376
|
+
console.log(` npx ablo connect scan Read-only audit for leftover Ablo sync infra in a customer DB`);
|
|
285377
|
+
console.log(` npx ablo connect deregister Remove this project's data source \u2014 Ablo stops reading/writing it`);
|
|
284920
285378
|
console.log(` npx ablo migrate Provision your synced-model tables in your own Postgres (optional escape hatch \u2014 \`connect\` is the way)`);
|
|
284921
285379
|
console.log(` npx ablo migrate --dry-run Print the SQL without executing (preview)`);
|
|
284922
285380
|
console.log(` npx ablo push Upload your schema definition to Ablo (metadata only \u2014 rows stay in your DB)`);
|
|
@@ -284927,10 +285385,10 @@ async function main() {
|
|
|
284927
285385
|
console.log(` npx ablo generate Emit TypeScript types from your schema`);
|
|
284928
285386
|
console.log(` npx ablo generate --out path.ts Write generated types to a path`);
|
|
284929
285387
|
console.log();
|
|
284930
|
-
console.log(` ${
|
|
285388
|
+
console.log(` ${import_picocolors23.default.bold("Schema workflow:")}`);
|
|
284931
285389
|
console.log(` The server holds its own copy of your schema \u2014 edit ${brand("ablo/schema.ts")}, then`);
|
|
284932
285390
|
console.log(` run ${brand("ablo push")} (or keep ${brand("ablo dev")} running) before the server will accept`);
|
|
284933
|
-
console.log(` writes to new or changed models. Skip it and writes fail with ${
|
|
285391
|
+
console.log(` writes to new or changed models. Skip it and writes fail with ${import_picocolors23.default.yellow("server_execute_unknown_model")}.`);
|
|
284934
285392
|
console.log();
|
|
284935
285393
|
}
|
|
284936
285394
|
}
|
|
@@ -284971,7 +285429,7 @@ async function ensureInitProject(opts) {
|
|
|
284971
285429
|
const slug = opts.project ?? projectSlugFromPackageName(
|
|
284972
285430
|
(() => {
|
|
284973
285431
|
try {
|
|
284974
|
-
return JSON.parse((0,
|
|
285432
|
+
return JSON.parse((0, import_fs12.readFileSync)("package.json", "utf-8")).name;
|
|
284975
285433
|
} catch {
|
|
284976
285434
|
return void 0;
|
|
284977
285435
|
}
|
|
@@ -284981,14 +285439,14 @@ async function ensureInitProject(opts) {
|
|
|
284981
285439
|
const ensured = await ensureProject(slug);
|
|
284982
285440
|
if (ensured) {
|
|
284983
285441
|
console.log(
|
|
284984
|
-
` ${
|
|
285442
|
+
` ${import_picocolors23.default.green("\u2713")} ${ensured.created ? "Created" : "Using"} project ${import_picocolors23.default.bold(ensured.slug)} ${import_picocolors23.default.dim(`(${ensured.id})`)} \u2014 keys you mint for it are isolated from the org's other apps.`
|
|
284985
285443
|
);
|
|
284986
285444
|
}
|
|
284987
285445
|
}
|
|
284988
285446
|
function detectOrm(override) {
|
|
284989
285447
|
if (override === "prisma" || override === "drizzle" || override === "none") return override;
|
|
284990
285448
|
try {
|
|
284991
|
-
const pkg = JSON.parse((0,
|
|
285449
|
+
const pkg = JSON.parse((0, import_fs12.readFileSync)("package.json", "utf-8"));
|
|
284992
285450
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
284993
285451
|
if (deps["@prisma/client"] || deps.prisma) return "prisma";
|
|
284994
285452
|
if (deps["drizzle-orm"]) return "drizzle";
|
|
@@ -284997,8 +285455,8 @@ function detectOrm(override) {
|
|
|
284997
285455
|
return "none";
|
|
284998
285456
|
}
|
|
284999
285457
|
function detectNextLayout() {
|
|
285000
|
-
const useSrc = (0,
|
|
285001
|
-
return useSrc ? { appBase: (0,
|
|
285458
|
+
const useSrc = (0, import_fs12.existsSync)((0, import_path7.join)("src", "app")) || !(0, import_fs12.existsSync)("app") && (0, import_fs12.existsSync)("src");
|
|
285459
|
+
return useSrc ? { appBase: (0, import_path7.join)("src", "app"), aliasBase: "src" } : { appBase: "app", aliasBase: "." };
|
|
285002
285460
|
}
|
|
285003
285461
|
async function chooseOption(name, flagValue, fallback, allowed, interactive, prompt) {
|
|
285004
285462
|
if (flagValue !== void 0) {
|
|
@@ -285023,8 +285481,8 @@ async function chooseBool(flagValue, fallback, interactive, prompt) {
|
|
|
285023
285481
|
async function init(args = []) {
|
|
285024
285482
|
const opts = parseInitArgs(args);
|
|
285025
285483
|
const interactive = Boolean(process.stdin.isTTY) && !opts.yes && !process.env.CI;
|
|
285026
|
-
Ie(`${brand("ablo")} ${
|
|
285027
|
-
if (!(0,
|
|
285484
|
+
Ie(`${brand("ablo")} ${import_picocolors23.default.dim("sync engine")}`);
|
|
285485
|
+
if (!(0, import_fs12.existsSync)("package.json")) {
|
|
285028
285486
|
xe("No package.json found. Run this from your project root.");
|
|
285029
285487
|
process.exit(1);
|
|
285030
285488
|
}
|
|
@@ -285093,15 +285551,15 @@ async function init(args = []) {
|
|
|
285093
285551
|
);
|
|
285094
285552
|
}
|
|
285095
285553
|
const layout = framework === "nextjs" ? detectNextLayout() : { appBase: "app", aliasBase: "." };
|
|
285096
|
-
const abloDir = (0,
|
|
285097
|
-
(0,
|
|
285554
|
+
const abloDir = (0, import_path7.join)(layout.aliasBase, "ablo");
|
|
285555
|
+
(0, import_fs12.mkdirSync)(abloDir, { recursive: true });
|
|
285098
285556
|
const created = [];
|
|
285099
285557
|
let schemaSource = generateSchema();
|
|
285100
285558
|
let schemaNote = "";
|
|
285101
285559
|
if (pullExisting) {
|
|
285102
285560
|
const dbUrl = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
|
|
285103
285561
|
if (!dbUrl) {
|
|
285104
|
-
schemaNote =
|
|
285562
|
+
schemaNote = import_picocolors23.default.dim(" (no DATABASE_URL \u2014 wrote starter; run `ablo pull` later)");
|
|
285105
285563
|
} else {
|
|
285106
285564
|
try {
|
|
285107
285565
|
const pulled = await buildSchemaSourceFromDb({
|
|
@@ -285111,70 +285569,70 @@ async function init(args = []) {
|
|
|
285111
285569
|
});
|
|
285112
285570
|
if (pulled.models.length > 0) {
|
|
285113
285571
|
schemaSource = pulled.source;
|
|
285114
|
-
schemaNote =
|
|
285572
|
+
schemaNote = import_picocolors23.default.dim(` (pulled ${pulled.models.length} models)`);
|
|
285115
285573
|
} else {
|
|
285116
|
-
schemaNote =
|
|
285574
|
+
schemaNote = import_picocolors23.default.dim(" (no adoptable tables \u2014 wrote starter)");
|
|
285117
285575
|
}
|
|
285118
285576
|
} catch {
|
|
285119
|
-
schemaNote =
|
|
285577
|
+
schemaNote = import_picocolors23.default.dim(" (pull failed \u2014 wrote starter)");
|
|
285120
285578
|
}
|
|
285121
285579
|
}
|
|
285122
285580
|
}
|
|
285123
|
-
(0,
|
|
285581
|
+
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "schema.ts"), schemaSource);
|
|
285124
285582
|
created.push(`${abloDir}/schema.ts${schemaNote}`);
|
|
285125
|
-
(0,
|
|
285583
|
+
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "index.ts"), generateSyncConfig(auth));
|
|
285126
285584
|
created.push(`${abloDir}/index.ts`);
|
|
285127
|
-
(0,
|
|
285585
|
+
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "register.ts"), generateRegister());
|
|
285128
285586
|
created.push(`${abloDir}/register.ts`);
|
|
285129
285587
|
const orm = detectOrm(opts.orm);
|
|
285130
285588
|
if (storage === "endpoint") {
|
|
285131
|
-
(0,
|
|
285589
|
+
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "data-source.ts"), generateDataSource(orm));
|
|
285132
285590
|
created.push(`${abloDir}/data-source.ts${orm === "drizzle" ? " (Drizzle)" : " (Prisma)"}`);
|
|
285133
285591
|
}
|
|
285134
285592
|
const envFile = framework === "nextjs" ? ".env.local" : ".env";
|
|
285135
285593
|
const resolvedKey = process.env.ABLO_API_KEY ? void 0 : resolveApiKey("sandbox");
|
|
285136
285594
|
const wireRealKey = envFile === ".env.local" && Boolean(resolvedKey);
|
|
285137
285595
|
const envBody = generateEnv(storage, { includeApiKey: !wireRealKey });
|
|
285138
|
-
if (!(0,
|
|
285139
|
-
(0,
|
|
285596
|
+
if (!(0, import_fs12.existsSync)(envFile)) {
|
|
285597
|
+
(0, import_fs12.writeFileSync)(envFile, envBody);
|
|
285140
285598
|
created.push(envFile);
|
|
285141
285599
|
} else {
|
|
285142
|
-
const existing = (0,
|
|
285600
|
+
const existing = (0, import_fs12.readFileSync)(envFile, "utf-8");
|
|
285143
285601
|
if (!existing.includes("ABLO_")) {
|
|
285144
|
-
(0,
|
|
285145
|
-
created.push(`${envFile} ${
|
|
285602
|
+
(0, import_fs12.writeFileSync)(envFile, existing + "\n" + envBody);
|
|
285603
|
+
created.push(`${envFile} ${import_picocolors23.default.dim("(appended)")}`);
|
|
285146
285604
|
} else {
|
|
285147
|
-
created.push(`${envFile} ${
|
|
285605
|
+
created.push(`${envFile} ${import_picocolors23.default.dim("(already configured)")}`);
|
|
285148
285606
|
}
|
|
285149
285607
|
}
|
|
285150
285608
|
if (wireRealKey && resolvedKey) {
|
|
285151
285609
|
wireEnvLocal(resolvedKey);
|
|
285152
|
-
created.push(`.env.local ${
|
|
285610
|
+
created.push(`.env.local ${import_picocolors23.default.dim("(ABLO_API_KEY set from your login)")}`);
|
|
285153
285611
|
}
|
|
285154
285612
|
if (agent) {
|
|
285155
|
-
(0,
|
|
285613
|
+
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "agent.ts"), generateAgent());
|
|
285156
285614
|
created.push(`${abloDir}/agent.ts`);
|
|
285157
285615
|
}
|
|
285158
285616
|
if (framework === "nextjs") {
|
|
285159
285617
|
if (storage === "endpoint") {
|
|
285160
|
-
const webhookDir = (0,
|
|
285161
|
-
(0,
|
|
285162
|
-
(0,
|
|
285618
|
+
const webhookDir = (0, import_path7.join)(layout.appBase, "api", "ablo", "webhooks");
|
|
285619
|
+
(0, import_fs12.mkdirSync)(webhookDir, { recursive: true });
|
|
285620
|
+
(0, import_fs12.writeFileSync)((0, import_path7.join)(webhookDir, "route.ts"), generateWebhookRoute(orm));
|
|
285163
285621
|
created.push(`${webhookDir}/route.ts${orm === "prisma" ? " (Prisma mirror)" : " (add your database write)"}`);
|
|
285164
285622
|
}
|
|
285165
|
-
const providersPath = (0,
|
|
285166
|
-
(0,
|
|
285167
|
-
created.push(`${providersPath} ${
|
|
285168
|
-
const sessionDir = (0,
|
|
285169
|
-
(0,
|
|
285170
|
-
(0,
|
|
285171
|
-
created.push(`${(0,
|
|
285623
|
+
const providersPath = (0, import_path7.join)(layout.appBase, "providers.tsx");
|
|
285624
|
+
(0, import_fs12.writeFileSync)(providersPath, generateProviders());
|
|
285625
|
+
created.push(`${providersPath} ${import_picocolors23.default.dim(`(wrap ${(0, import_path7.join)(layout.appBase, "layout.tsx")} in <Providers>)`)}`);
|
|
285626
|
+
const sessionDir = (0, import_path7.join)(layout.appBase, "api", "ablo-session");
|
|
285627
|
+
(0, import_fs12.mkdirSync)(sessionDir, { recursive: true });
|
|
285628
|
+
(0, import_fs12.writeFileSync)((0, import_path7.join)(sessionDir, "route.ts"), generateSessionRoute());
|
|
285629
|
+
created.push(`${(0, import_path7.join)(sessionDir, "route.ts")} ${import_picocolors23.default.dim("(wire your auth)")}`);
|
|
285172
285630
|
}
|
|
285173
285631
|
if (framework !== "vanilla") {
|
|
285174
|
-
(0,
|
|
285632
|
+
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "TaskList.tsx"), generateComponent());
|
|
285175
285633
|
created.push(`${abloDir}/TaskList.tsx`);
|
|
285176
285634
|
}
|
|
285177
|
-
Me(created.map((f) => `${
|
|
285635
|
+
Me(created.map((f) => `${import_picocolors23.default.green("\u2713")} ${f}`).join("\n"), "Created");
|
|
285178
285636
|
const pm = detectPackageManager();
|
|
285179
285637
|
if (opts.install) {
|
|
285180
285638
|
const s = Y2();
|
|
@@ -285183,47 +285641,47 @@ async function init(args = []) {
|
|
|
285183
285641
|
(0, import_child_process3.execSync)(`${pm} add @abloatai/ablo`, { stdio: "ignore" });
|
|
285184
285642
|
s.stop("Installed @abloatai/ablo");
|
|
285185
285643
|
} catch {
|
|
285186
|
-
s.stop(`${
|
|
285644
|
+
s.stop(`${import_picocolors23.default.yellow("!")} Couldn't auto-install \u2014 run ${import_picocolors23.default.bold(`${pm} install @abloatai/ablo`)}`);
|
|
285187
285645
|
}
|
|
285188
285646
|
}
|
|
285189
285647
|
const steps = [
|
|
285190
|
-
`Get a ${
|
|
285191
|
-
`Run ${
|
|
285192
|
-
`Set ${
|
|
285193
|
-
`Run ${
|
|
285648
|
+
`Get a ${import_picocolors23.default.bold("sk_test_")} key at ${import_picocolors23.default.cyan("https://abloatai.com")}`,
|
|
285649
|
+
`Run ${import_picocolors23.default.bold("npx ablo login")} (or add ${import_picocolors23.default.bold("ABLO_API_KEY")} to ${import_picocolors23.default.bold(envFile)})`,
|
|
285650
|
+
`Set ${import_picocolors23.default.bold("DATABASE_URL")} in ${import_picocolors23.default.bold(envFile)} \u2014 your Postgres is the system of record; rows live there, never with Ablo`,
|
|
285651
|
+
`Run ${import_picocolors23.default.bold("npx ablo dev")} \u2014 pushes your schema definition and watches for changes`,
|
|
285194
285652
|
...storage === "replication" ? [
|
|
285195
|
-
`Connect your database \u2014 ${
|
|
285196
|
-
`Verify it \u2014 ${
|
|
285197
|
-
`Register it \u2014 ${
|
|
285653
|
+
`Connect your database \u2014 ${import_picocolors23.default.bold("npx ablo connect")} prints the one-time logical-replication setup SQL to run on your Postgres`,
|
|
285654
|
+
`Verify it \u2014 ${import_picocolors23.default.bold("npx ablo connect check")} walks wal_level, the publication, the role, and replica identity, with the exact fix for anything missing`,
|
|
285655
|
+
`Register it \u2014 ${import_picocolors23.default.bold("npx ablo connect register")} tells Ablo to start replicating; your app keeps writing through your own backend while Ablo tails the WAL`
|
|
285198
285656
|
] : [
|
|
285199
|
-
`Provision your DB: ${
|
|
285657
|
+
`Provision your DB: ${import_picocolors23.default.bold("npx ablo migrate")} (creates your Ablo-model tables + the adapter tables; keep your own migrations for everything else), then mount ${import_picocolors23.default.bold(`${abloDir}/data-source.ts`)} at ${import_picocolors23.default.bold("/api/ablo/source")}`
|
|
285200
285658
|
],
|
|
285201
285659
|
...framework === "nextjs" ? [
|
|
285202
|
-
`Wrap ${
|
|
285660
|
+
`Wrap ${import_picocolors23.default.bold((0, import_path7.join)(layout.appBase, "layout.tsx"))} in ${import_picocolors23.default.bold("<Providers>")} (${(0, import_path7.join)(layout.appBase, "providers.tsx")}) and add your auth to ${import_picocolors23.default.bold((0, import_path7.join)(layout.appBase, "api", "ablo-session", "route.ts"))}`
|
|
285203
285661
|
] : [],
|
|
285204
|
-
`Run ${
|
|
285662
|
+
`Run ${import_picocolors23.default.bold(`${pm} run dev`)} and open two browser tabs \u2014 changes sync in real-time`,
|
|
285205
285663
|
...agent ? [
|
|
285206
|
-
`Run ${
|
|
285207
|
-
`Run ${
|
|
285664
|
+
`Run ${import_picocolors23.default.bold(`npx tsx ${abloDir}/agent.ts`)} \u2014 an AI teammate edits the same tasks`,
|
|
285665
|
+
`Run ${import_picocolors23.default.bold("npx ablo logs")} to watch human + agent commits stream by`
|
|
285208
285666
|
] : []
|
|
285209
285667
|
];
|
|
285210
285668
|
Me(steps.map((s, i) => `${i + 1}. ${s}`).join("\n"), "Next steps");
|
|
285211
285669
|
const existingKey = resolveApiKey("sandbox");
|
|
285212
285670
|
if (existingKey) {
|
|
285213
285671
|
await ensureInitProject(opts);
|
|
285214
|
-
Se(`Already authorized ${
|
|
285672
|
+
Se(`Already authorized ${import_picocolors23.default.dim(`(${existingKey.slice(0, 11)}\u2026)`)} \u2014 run ${import_picocolors23.default.bold("npx ablo push")} next. ${import_picocolors23.default.dim("Docs:")} https://abloatai.com/docs`);
|
|
285215
285673
|
return;
|
|
285216
285674
|
}
|
|
285217
285675
|
if (interactive && opts.login) {
|
|
285218
285676
|
const loginNow = await ye({ message: "Log in now? (opens your browser)", initialValue: true });
|
|
285219
285677
|
if (!pD(loginNow) && loginNow) {
|
|
285220
|
-
Se(`${
|
|
285678
|
+
Se(`${import_picocolors23.default.dim("Docs:")} https://abloatai.com/docs`);
|
|
285221
285679
|
await login();
|
|
285222
285680
|
await ensureInitProject(opts);
|
|
285223
285681
|
return;
|
|
285224
285682
|
}
|
|
285225
285683
|
}
|
|
285226
|
-
Se(`Run ${
|
|
285684
|
+
Se(`Run ${import_picocolors23.default.bold("npx ablo login")} when ready. ${import_picocolors23.default.dim("Docs:")} https://abloatai.com/docs`);
|
|
285227
285685
|
}
|
|
285228
285686
|
function generateSchema() {
|
|
285229
285687
|
return `import { defineSchema, model, relation, z } from '@abloatai/ablo/schema';
|
|
@@ -285607,9 +286065,9 @@ async function getCurrentUser(): Promise<{ id: string } | null> {
|
|
|
285607
286065
|
`;
|
|
285608
286066
|
}
|
|
285609
286067
|
function detectPackageManager() {
|
|
285610
|
-
if ((0,
|
|
285611
|
-
if ((0,
|
|
285612
|
-
if ((0,
|
|
286068
|
+
if ((0, import_fs12.existsSync)("pnpm-lock.yaml")) return "pnpm";
|
|
286069
|
+
if ((0, import_fs12.existsSync)("yarn.lock")) return "yarn";
|
|
286070
|
+
if ((0, import_fs12.existsSync)("bun.lockb")) return "bun";
|
|
285613
286071
|
return "npm";
|
|
285614
286072
|
}
|
|
285615
286073
|
main().catch((err) => {
|