@abloatai/ablo 0.31.0 → 0.33.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 +50 -0
- package/README.md +25 -13
- package/dist/cli.cjs +1686 -1219
- 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 +55 -1
- package/dist/schema/schema.js +47 -0
- package/dist/schema/select.js +1 -0
- package/dist/schema/serialize.d.ts +3 -1
- package/dist/schema/serialize.js +9 -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/coordination.md +13 -1
- package/docs/data-sources.md +34 -32
- 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 +2 -2
- package/docs/interaction-model.md +3 -3
- package/docs/quickstart.md +6 -6
- 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,
|
|
@@ -3996,7 +4004,7 @@ function readProjectApiKey(cwd = process.cwd()) {
|
|
|
3996
4004
|
}
|
|
3997
4005
|
return null;
|
|
3998
4006
|
}
|
|
3999
|
-
var import_crypto2, import_fs2, import_path, REPLICATION_URL_VARS
|
|
4007
|
+
var import_crypto2, import_fs2, import_path, REPLICATION_URL_VARS;
|
|
4000
4008
|
var init_dbRole = __esm({
|
|
4001
4009
|
"src/cli/dbRole.ts"() {
|
|
4002
4010
|
"use strict";
|
|
@@ -4006,8 +4014,7 @@ var init_dbRole = __esm({
|
|
|
4006
4014
|
init_dist2();
|
|
4007
4015
|
import_fs2 = require("fs");
|
|
4008
4016
|
import_path = require("path");
|
|
4009
|
-
REPLICATION_URL_VARS = ["ABLO_REPLICATION_DATABASE_URL"
|
|
4010
|
-
DATABASE_URL_REMOVAL_VERSION = "0.32.0";
|
|
4017
|
+
REPLICATION_URL_VARS = ["ABLO_REPLICATION_DATABASE_URL"];
|
|
4011
4018
|
}
|
|
4012
4019
|
});
|
|
4013
4020
|
|
|
@@ -4830,6 +4837,9 @@ function coerceBackfill(raw) {
|
|
|
4830
4837
|
if (raw !== "" && !Number.isNaN(Number(raw))) return Number(raw);
|
|
4831
4838
|
return raw;
|
|
4832
4839
|
}
|
|
4840
|
+
function apiBaseUrl() {
|
|
4841
|
+
return (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
4842
|
+
}
|
|
4833
4843
|
function fmtSignal(s) {
|
|
4834
4844
|
const sig = s;
|
|
4835
4845
|
const where = sig.field ? `${sig.model}.${sig.field}` : sig.model;
|
|
@@ -5234,7 +5244,7 @@ async function push(argv) {
|
|
|
5234
5244
|
} else if (code === "schema_provisioning_forbidden") {
|
|
5235
5245
|
console.error(
|
|
5236
5246
|
import_picocolors4.default.dim(
|
|
5237
|
-
` 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.`
|
|
5238
5248
|
)
|
|
5239
5249
|
);
|
|
5240
5250
|
} else if (args.apiKey != null && classifyCredentialKind(args.apiKey) === "restricted") {
|
|
@@ -5278,129 +5288,19 @@ var init_push = __esm({
|
|
|
5278
5288
|
}
|
|
5279
5289
|
});
|
|
5280
5290
|
|
|
5281
|
-
// src/cli/remoteValidation.ts
|
|
5282
|
-
function dialFailureReason(err) {
|
|
5283
|
-
if (err === null || typeof err !== "object") return null;
|
|
5284
|
-
const coded = err;
|
|
5285
|
-
const code = typeof coded.code === "string" ? coded.code : null;
|
|
5286
|
-
if (code && DIAL_FAILURE_CODES.has(code)) {
|
|
5287
|
-
return typeof coded.message === "string" && coded.message.length > 0 ? coded.message : code;
|
|
5288
|
-
}
|
|
5289
|
-
if (Array.isArray(coded.errors)) {
|
|
5290
|
-
for (const member of coded.errors) {
|
|
5291
|
-
const reason = dialFailureReason(member);
|
|
5292
|
-
if (reason) return reason;
|
|
5293
|
-
}
|
|
5294
|
-
}
|
|
5295
|
-
return null;
|
|
5296
|
-
}
|
|
5297
|
-
function validateEndpoint(baseUrl2) {
|
|
5298
|
-
return `${baseUrl2.replace(/\/+$/, "")}/api/v1/datasources/validate`;
|
|
5299
|
-
}
|
|
5300
|
-
function describeRemoteFailure(failure) {
|
|
5301
|
-
switch (failure.item) {
|
|
5302
|
-
case "wal_level":
|
|
5303
|
-
return {
|
|
5304
|
-
label: failure.actual ? `wal_level is ${failure.actual} (need logical)` : `wal_level must be logical`,
|
|
5305
|
-
fix: failure.fix
|
|
5306
|
-
};
|
|
5307
|
-
case "publication":
|
|
5308
|
-
return { label: "the Ablo publication does not exist", fix: failure.fix };
|
|
5309
|
-
case "replication_role":
|
|
5310
|
-
return { label: "the DATABASE_URL role lacks the REPLICATION attribute", fix: failure.fix };
|
|
5311
|
-
case "replica_identity":
|
|
5312
|
-
return {
|
|
5313
|
-
label: `published tables cannot replicate UPDATE/DELETE${failure.actual ? ` (${failure.actual})` : ""}`,
|
|
5314
|
-
fix: failure.fix
|
|
5315
|
-
};
|
|
5316
|
-
case "write_role":
|
|
5317
|
-
return { label: "ABLO_WRITE_DATABASE_URL is not a scoped DML role", fix: failure.fix };
|
|
5318
|
-
case "row_security":
|
|
5319
|
-
return { label: "the direct-write role does not enforce row_security", fix: failure.fix };
|
|
5320
|
-
case "schema_privileges":
|
|
5321
|
-
return { label: "the direct-write role has unsafe schema privileges", fix: failure.fix };
|
|
5322
|
-
case "idempotency_ledger":
|
|
5323
|
-
return { label: "ablo_idempotency is missing or has unsafe grants", fix: failure.fix };
|
|
5324
|
-
case "table_privileges":
|
|
5325
|
-
return {
|
|
5326
|
-
label: `the direct-write role lacks application DML${failure.actual ? ` (${failure.actual})` : ""}`,
|
|
5327
|
-
fix: failure.fix
|
|
5328
|
-
};
|
|
5329
|
-
case "logical_marker":
|
|
5330
|
-
return { label: "the direct-write role cannot emit the correlation marker", fix: failure.fix };
|
|
5331
|
-
default:
|
|
5332
|
-
return { label: failure.item, fix: failure.fix };
|
|
5333
|
-
}
|
|
5334
|
-
}
|
|
5335
|
-
function parseWireFailures(value) {
|
|
5336
|
-
if (!Array.isArray(value)) return [];
|
|
5337
|
-
const failures = [];
|
|
5338
|
-
for (const entry of value) {
|
|
5339
|
-
if (entry === null || typeof entry !== "object") continue;
|
|
5340
|
-
const { item, actual, fix } = entry;
|
|
5341
|
-
if (typeof item !== "string" || typeof fix !== "string") continue;
|
|
5342
|
-
failures.push({ item, fix, ...typeof actual === "string" ? { actual } : {} });
|
|
5343
|
-
}
|
|
5344
|
-
return failures;
|
|
5345
|
-
}
|
|
5346
|
-
async function requestRemoteValidation(input) {
|
|
5347
|
-
const doFetch = input.fetchImpl ?? fetch;
|
|
5348
|
-
let res;
|
|
5349
|
-
try {
|
|
5350
|
-
res = await doFetch(validateEndpoint(input.apiUrl), {
|
|
5351
|
-
method: "POST",
|
|
5352
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${input.apiKey}` },
|
|
5353
|
-
body: JSON.stringify({
|
|
5354
|
-
connectionString: input.connectionString,
|
|
5355
|
-
...input.writeConnectionString ? { writeConnectionString: input.writeConnectionString } : {}
|
|
5356
|
-
})
|
|
5357
|
-
});
|
|
5358
|
-
} catch (err) {
|
|
5359
|
-
return {
|
|
5360
|
-
ok: false,
|
|
5361
|
-
status: 0,
|
|
5362
|
-
message: `couldn't reach ${input.apiUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
5363
|
-
};
|
|
5364
|
-
}
|
|
5365
|
-
const body = await res.json().catch(() => ({}));
|
|
5366
|
-
if (!res.ok) {
|
|
5367
|
-
const code = typeof body.code === "string" ? body.code : body.error?.code;
|
|
5368
|
-
const message = typeof body.message === "string" ? body.message : typeof body.error?.message === "string" ? body.error.message : `HTTP ${res.status}`;
|
|
5369
|
-
return {
|
|
5370
|
-
ok: false,
|
|
5371
|
-
status: res.status,
|
|
5372
|
-
message,
|
|
5373
|
-
...typeof code === "string" ? { code } : {}
|
|
5374
|
-
};
|
|
5375
|
-
}
|
|
5376
|
-
const reachable = body.reachable === true;
|
|
5377
|
-
return {
|
|
5378
|
-
ok: true,
|
|
5379
|
-
reachable,
|
|
5380
|
-
ready: body.ready === true,
|
|
5381
|
-
...typeof body.reason === "string" ? { reason: body.reason } : {},
|
|
5382
|
-
failures: parseWireFailures(body.failures)
|
|
5383
|
-
};
|
|
5384
|
-
}
|
|
5385
|
-
var DIAL_FAILURE_CODES;
|
|
5386
|
-
var init_remoteValidation = __esm({
|
|
5387
|
-
"src/cli/remoteValidation.ts"() {
|
|
5388
|
-
"use strict";
|
|
5389
|
-
init_cjs_shims();
|
|
5390
|
-
DIAL_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
5391
|
-
"ENOTFOUND",
|
|
5392
|
-
"EAI_AGAIN",
|
|
5393
|
-
"ECONNREFUSED",
|
|
5394
|
-
"ECONNRESET",
|
|
5395
|
-
"ETIMEDOUT",
|
|
5396
|
-
"ENETUNREACH",
|
|
5397
|
-
"EHOSTUNREACH",
|
|
5398
|
-
"CONNECT_TIMEOUT"
|
|
5399
|
-
]);
|
|
5400
|
-
}
|
|
5401
|
-
});
|
|
5402
|
-
|
|
5403
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
|
+
}
|
|
5404
5304
|
function idempotencyLedgerMigrations() {
|
|
5405
5305
|
return [
|
|
5406
5306
|
{
|
|
@@ -5418,13 +5318,19 @@ function idempotencyLedgerMigrations() {
|
|
|
5418
5318
|
// Nullable only for rows created by older adapter versions. New writes
|
|
5419
5319
|
// always populate it; replaying a legacy NULL row fails closed because
|
|
5420
5320
|
// the adapter cannot prove that the intent matches.
|
|
5421
|
-
|
|
5422
|
-
|
|
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")
|
|
5423
5330
|
},
|
|
5424
5331
|
{
|
|
5425
5332
|
name: "ablo_idempotency_permanent_retention",
|
|
5426
|
-
up: `
|
|
5427
|
-
ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ NOT NULL DEFAULT 'infinity';`
|
|
5333
|
+
up: addColumnIfAbsent("expires_at", `TIMESTAMPTZ NOT NULL DEFAULT 'infinity'`)
|
|
5428
5334
|
}
|
|
5429
5335
|
];
|
|
5430
5336
|
}
|
|
@@ -5435,10 +5341,501 @@ var init_migrations = __esm({
|
|
|
5435
5341
|
}
|
|
5436
5342
|
});
|
|
5437
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
|
+
function reconcilePublicationPlan(current, desiredTables) {
|
|
5435
|
+
const pub = quoteIdent(ABLO_PUBLICATION);
|
|
5436
|
+
const desiredAll = desiredTables.length === 0;
|
|
5437
|
+
const target = desiredAll ? "FOR ALL TABLES" : `FOR TABLE ${desiredTables.map(quoteIdent).join(", ")}`;
|
|
5438
|
+
if (!current.exists) {
|
|
5439
|
+
return {
|
|
5440
|
+
sql: [`CREATE PUBLICATION ${pub} ${target};`],
|
|
5441
|
+
added: desiredAll ? [] : [...desiredTables],
|
|
5442
|
+
removed: [],
|
|
5443
|
+
recreated: false
|
|
5444
|
+
};
|
|
5445
|
+
}
|
|
5446
|
+
if (current.allTables !== desiredAll) {
|
|
5447
|
+
return {
|
|
5448
|
+
sql: [`DROP PUBLICATION IF EXISTS ${pub};`, `CREATE PUBLICATION ${pub} ${target};`],
|
|
5449
|
+
added: desiredAll ? [] : desiredTables.filter((t) => !current.tables.includes(t)),
|
|
5450
|
+
removed: current.allTables ? [] : current.tables.filter((t) => !desiredTables.includes(t)),
|
|
5451
|
+
recreated: true
|
|
5452
|
+
};
|
|
5453
|
+
}
|
|
5454
|
+
if (desiredAll) {
|
|
5455
|
+
return { sql: [], added: [], removed: [], recreated: false };
|
|
5456
|
+
}
|
|
5457
|
+
const added = desiredTables.filter((t) => !current.tables.includes(t));
|
|
5458
|
+
const removed = current.tables.filter((t) => !desiredTables.includes(t));
|
|
5459
|
+
if (added.length === 0 && removed.length === 0) {
|
|
5460
|
+
return { sql: [], added: [], removed: [], recreated: false };
|
|
5461
|
+
}
|
|
5462
|
+
return {
|
|
5463
|
+
sql: [`ALTER PUBLICATION ${pub} SET TABLE ${desiredTables.map(quoteIdent).join(", ")};`],
|
|
5464
|
+
added,
|
|
5465
|
+
removed,
|
|
5466
|
+
recreated: false
|
|
5467
|
+
};
|
|
5468
|
+
}
|
|
5469
|
+
async function readPublicationState(sql) {
|
|
5470
|
+
const pubRows = await sql.unsafe(
|
|
5471
|
+
`SELECT puballtables FROM pg_publication WHERE pubname = $1`,
|
|
5472
|
+
[ABLO_PUBLICATION]
|
|
5473
|
+
);
|
|
5474
|
+
const pubRow = pubRows[0];
|
|
5475
|
+
if (!pubRow) return { exists: false, allTables: false, tables: [] };
|
|
5476
|
+
if (pubRow.puballtables) return { exists: true, allTables: true, tables: [] };
|
|
5477
|
+
const tableRows = await sql.unsafe(
|
|
5478
|
+
`SELECT tablename FROM pg_publication_tables WHERE pubname = $1 AND schemaname = 'public' ORDER BY tablename`,
|
|
5479
|
+
[ABLO_PUBLICATION]
|
|
5480
|
+
);
|
|
5481
|
+
return { exists: true, allTables: false, tables: tableRows.map((r2) => r2.tablename) };
|
|
5482
|
+
}
|
|
5483
|
+
async function probeReadiness(sql, opts = {}) {
|
|
5484
|
+
const publication = opts.publication ?? ABLO_PUBLICATION;
|
|
5485
|
+
const items = [];
|
|
5486
|
+
const walRows = await sql.unsafe(
|
|
5487
|
+
`SELECT setting FROM pg_settings WHERE name = 'wal_level'`
|
|
5488
|
+
);
|
|
5489
|
+
const walLevel = walRows[0]?.setting ?? "unknown";
|
|
5490
|
+
items.push(
|
|
5491
|
+
walLevel === "logical" ? { ok: true, label: `wal_level is ${import_picocolors6.default.bold("logical")}` } : {
|
|
5492
|
+
ok: false,
|
|
5493
|
+
label: `wal_level is ${import_picocolors6.default.bold(walLevel)} (need ${import_picocolors6.default.bold("logical")})`,
|
|
5494
|
+
fix: `ALTER SYSTEM SET wal_level = 'logical'; then RESTART Postgres.
|
|
5495
|
+
On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.
|
|
5496
|
+
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.`
|
|
5497
|
+
}
|
|
5498
|
+
);
|
|
5499
|
+
const pubRows = await sql.unsafe(
|
|
5500
|
+
`SELECT puballtables FROM pg_publication WHERE pubname = $1`,
|
|
5501
|
+
[publication]
|
|
5502
|
+
);
|
|
5503
|
+
const pubRow = pubRows[0];
|
|
5504
|
+
items.push(
|
|
5505
|
+
pubRow ? {
|
|
5506
|
+
ok: true,
|
|
5507
|
+
label: `publication ${import_picocolors6.default.bold(publication)} exists ${import_picocolors6.default.dim(pubRow.puballtables ? "(all tables)" : "(table subset)")}`
|
|
5508
|
+
} : {
|
|
5509
|
+
ok: false,
|
|
5510
|
+
label: `publication ${import_picocolors6.default.bold(publication)} not found`,
|
|
5511
|
+
fix: `CREATE PUBLICATION ${quoteIdent(publication)} FOR ALL TABLES;`
|
|
5512
|
+
}
|
|
5513
|
+
);
|
|
5514
|
+
const roleRows = await sql.unsafe(
|
|
5515
|
+
`SELECT rolreplication, rolsuper FROM pg_roles WHERE rolname = current_user`
|
|
5516
|
+
);
|
|
5517
|
+
const role = roleRows[0];
|
|
5518
|
+
const hasReplication = Boolean(role && (role.rolreplication || role.rolsuper));
|
|
5519
|
+
items.push(
|
|
5520
|
+
hasReplication ? {
|
|
5521
|
+
ok: true,
|
|
5522
|
+
label: `the replication role can stream replication ${import_picocolors6.default.dim("(REPLICATION)")}`
|
|
5523
|
+
} : {
|
|
5524
|
+
ok: false,
|
|
5525
|
+
label: `the replication role lacks the ${import_picocolors6.default.bold("REPLICATION")} attribute`,
|
|
5526
|
+
fix: `ALTER ROLE current_user WITH REPLICATION;
|
|
5527
|
+
On RDS: GRANT rds_replication TO <your_role>;`
|
|
5528
|
+
}
|
|
5529
|
+
);
|
|
5530
|
+
if (pubRows.length > 0) {
|
|
5531
|
+
const badRows = await sql.unsafe(
|
|
5532
|
+
`SELECT c.relname AS table_name, c.relreplident
|
|
5533
|
+
FROM pg_publication_tables pt
|
|
5534
|
+
JOIN pg_class c ON c.relname = pt.tablename
|
|
5535
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
|
|
5536
|
+
WHERE pt.pubname = $1
|
|
5537
|
+
AND (
|
|
5538
|
+
c.relreplident = 'n'
|
|
5539
|
+
OR (
|
|
5540
|
+
c.relreplident = 'd'
|
|
5541
|
+
AND NOT EXISTS (
|
|
5542
|
+
SELECT 1 FROM pg_index i
|
|
5543
|
+
WHERE i.indrelid = c.oid AND i.indisprimary
|
|
5544
|
+
)
|
|
5545
|
+
)
|
|
5546
|
+
)`,
|
|
5547
|
+
[publication]
|
|
5548
|
+
);
|
|
5549
|
+
items.push(
|
|
5550
|
+
badRows.length === 0 ? { ok: true, label: `all published tables have a usable REPLICA IDENTITY` } : {
|
|
5551
|
+
ok: false,
|
|
5552
|
+
label: `${badRows.length} published table${badRows.length === 1 ? "" : "s"} cannot replicate UPDATE/DELETE`,
|
|
5553
|
+
fix: badRows.map(
|
|
5554
|
+
(r2) => `${r2.table_name}: add a PRIMARY KEY, or ALTER TABLE ${quoteIdent(r2.table_name)} REPLICA IDENTITY FULL;`
|
|
5555
|
+
).join("\n")
|
|
5556
|
+
}
|
|
5557
|
+
);
|
|
5558
|
+
}
|
|
5559
|
+
return items;
|
|
5560
|
+
}
|
|
5561
|
+
function registerEndpoint(baseUrl2) {
|
|
5562
|
+
return `${baseUrl2.replace(/\/+$/, "")}/api/v1/datasources`;
|
|
5563
|
+
}
|
|
5564
|
+
async function parseJsonBody(res, schema) {
|
|
5565
|
+
const parsed = schema.safeParse(await res.json().catch(() => null));
|
|
5566
|
+
return parsed.success ? parsed.data : {};
|
|
5567
|
+
}
|
|
5568
|
+
async function registerDirectDataSource(opts) {
|
|
5569
|
+
let res;
|
|
5570
|
+
try {
|
|
5571
|
+
res = await fetch(registerEndpoint(opts.apiUrl), {
|
|
5572
|
+
method: "POST",
|
|
5573
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${opts.apiKey}` },
|
|
5574
|
+
body: JSON.stringify({
|
|
5575
|
+
connection: "direct",
|
|
5576
|
+
connectionString: opts.replicationUrl,
|
|
5577
|
+
writeConnectionString: opts.writeUrl,
|
|
5578
|
+
route: opts.route
|
|
5579
|
+
})
|
|
5580
|
+
});
|
|
5581
|
+
} catch (err) {
|
|
5582
|
+
console.error(
|
|
5583
|
+
import_picocolors6.default.red(
|
|
5584
|
+
`
|
|
5585
|
+
Couldn't reach ${opts.apiUrl}: ${err instanceof Error ? err.message : String(err)}
|
|
5586
|
+
`
|
|
5587
|
+
)
|
|
5588
|
+
);
|
|
5589
|
+
return false;
|
|
5590
|
+
}
|
|
5591
|
+
if (res.ok) {
|
|
5592
|
+
const body2 = await parseJsonBody(res, DataSourceRegisterSuccess);
|
|
5593
|
+
const statusNote = body2.status === "active" ? `${opts.route}, active` : opts.route;
|
|
5594
|
+
console.log(
|
|
5595
|
+
`
|
|
5596
|
+
${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}).
|
|
5597
|
+
Customer COMMIT is durable acceptance; correlated WAL promotes queued writes to confirmed.
|
|
5598
|
+
`
|
|
5599
|
+
);
|
|
5600
|
+
return true;
|
|
5601
|
+
}
|
|
5602
|
+
const body = await parseJsonBody(res, DataSourceRegisterError);
|
|
5603
|
+
const code = body.code ?? body.error?.code;
|
|
5604
|
+
const message = body.message ?? body.error?.message ?? `HTTP ${res.status}`;
|
|
5605
|
+
console.error(import_picocolors6.default.red(`
|
|
5606
|
+
Registration failed: ${message}`));
|
|
5607
|
+
if (code === "forbidden") {
|
|
5608
|
+
console.error(
|
|
5609
|
+
import_picocolors6.default.dim(
|
|
5610
|
+
` Registering a database needs a ${import_picocolors6.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors6.default.bold("ablo login")} for one.`
|
|
5611
|
+
)
|
|
5612
|
+
);
|
|
5613
|
+
} else if (code === "datasource_connection_unsupported") {
|
|
5614
|
+
console.error(
|
|
5615
|
+
import_picocolors6.default.dim(
|
|
5616
|
+
` This deployment can\u2019t accept connection strings \u2014 use a self-hosted/hosted engine, or the signed endpoint fallback.`
|
|
5617
|
+
)
|
|
5618
|
+
);
|
|
5619
|
+
} else if (code === "database_not_replication_ready" || code === "data_source_blocked") {
|
|
5620
|
+
for (const f of body.failures ?? body.details?.failures ?? []) {
|
|
5621
|
+
console.error(
|
|
5622
|
+
` ${import_picocolors6.default.red("\u2717")} ${import_picocolors6.default.bold(f.item ?? "item")}${f.actual ? import_picocolors6.default.dim(` (${f.actual})`) : ""}`
|
|
5623
|
+
);
|
|
5624
|
+
if (f.fix)
|
|
5625
|
+
for (const line of f.fix.split("\n")) console.error(` ${import_picocolors6.default.red("\u2022")} ${line}`);
|
|
5626
|
+
}
|
|
5627
|
+
console.error(
|
|
5628
|
+
import_picocolors6.default.dim(`
|
|
5629
|
+
Apply the fixes, verify with ${import_picocolors6.default.bold("ablo connect check")}, then re-run.`)
|
|
5630
|
+
);
|
|
5631
|
+
} else if (code === "database_unreachable" || code === "source_unreachable") {
|
|
5632
|
+
const reason = body.reason ?? body.details?.reason;
|
|
5633
|
+
if (reason) console.error(import_picocolors6.default.dim(` ${reason}`));
|
|
5634
|
+
console.error(
|
|
5635
|
+
import_picocolors6.default.dim(
|
|
5636
|
+
` Ablo's servers must be able to reach this database \u2014 a localhost or private-network
|
|
5637
|
+
Postgres can't use the direct path. Establish an allowlist, PrivateLink, peering, or VPN.
|
|
5638
|
+
Only when no inbound route is possible, register the signed ${import_picocolors6.default.bold("dataSource()")} endpoint fallback.`
|
|
5639
|
+
)
|
|
5640
|
+
);
|
|
5641
|
+
}
|
|
5642
|
+
console.error();
|
|
5643
|
+
return false;
|
|
5644
|
+
}
|
|
5645
|
+
var import_picocolors6, import_zod6, ABLO_PUBLICATION, ABLO_REPLICATION_ROLE, ABLO_WRITE_ROLE, DIRECT_DATA_SOURCE_ROUTES, DataSourceRegisterSuccess, ReadinessFailure, DataSourceRegisterError;
|
|
5646
|
+
var init_connectSetup = __esm({
|
|
5647
|
+
"src/cli/connectSetup.ts"() {
|
|
5648
|
+
"use strict";
|
|
5649
|
+
init_cjs_shims();
|
|
5650
|
+
import_picocolors6 = __toESM(require_picocolors(), 1);
|
|
5651
|
+
import_zod6 = require("zod");
|
|
5652
|
+
init_migrations();
|
|
5653
|
+
ABLO_PUBLICATION = "ablo_publication";
|
|
5654
|
+
ABLO_REPLICATION_ROLE = "ablo_replicator";
|
|
5655
|
+
ABLO_WRITE_ROLE = "ablo_writer";
|
|
5656
|
+
DIRECT_DATA_SOURCE_ROUTES = [
|
|
5657
|
+
"public-allowlist",
|
|
5658
|
+
"privatelink",
|
|
5659
|
+
"peering",
|
|
5660
|
+
"vpn"
|
|
5661
|
+
];
|
|
5662
|
+
DataSourceRegisterSuccess = import_zod6.z.object({
|
|
5663
|
+
id: import_zod6.z.string().optional(),
|
|
5664
|
+
host: import_zod6.z.string().optional(),
|
|
5665
|
+
status: import_zod6.z.string().optional()
|
|
5666
|
+
});
|
|
5667
|
+
ReadinessFailure = import_zod6.z.object({
|
|
5668
|
+
item: import_zod6.z.string().optional(),
|
|
5669
|
+
actual: import_zod6.z.string().optional(),
|
|
5670
|
+
fix: import_zod6.z.string().optional()
|
|
5671
|
+
});
|
|
5672
|
+
DataSourceRegisterError = import_zod6.z.object({
|
|
5673
|
+
code: import_zod6.z.string().optional(),
|
|
5674
|
+
message: import_zod6.z.string().optional(),
|
|
5675
|
+
// Canonical: top-level, spread from the engine's `details`.
|
|
5676
|
+
failures: import_zod6.z.array(ReadinessFailure).optional(),
|
|
5677
|
+
reason: import_zod6.z.string().optional(),
|
|
5678
|
+
// Fallback: a wrapping proxy or older engine that nested the same payload.
|
|
5679
|
+
details: import_zod6.z.object({
|
|
5680
|
+
failures: import_zod6.z.array(ReadinessFailure).optional(),
|
|
5681
|
+
reason: import_zod6.z.string().optional()
|
|
5682
|
+
}).optional(),
|
|
5683
|
+
error: import_zod6.z.object({ code: import_zod6.z.string().optional(), message: import_zod6.z.string().optional() }).optional()
|
|
5684
|
+
});
|
|
5685
|
+
}
|
|
5686
|
+
});
|
|
5687
|
+
|
|
5688
|
+
// src/cli/disconnect.ts
|
|
5689
|
+
var disconnect_exports = {};
|
|
5690
|
+
__export(disconnect_exports, {
|
|
5691
|
+
DISCONNECT_USAGE: () => DISCONNECT_USAGE,
|
|
5692
|
+
disconnect: () => disconnect
|
|
5693
|
+
});
|
|
5694
|
+
function planeLabel(target) {
|
|
5695
|
+
const confirmed = target.confirmed;
|
|
5696
|
+
const project = confirmed?.project?.name ?? confirmed?.project?.slug ?? confirmed?.projectId ?? "the default project";
|
|
5697
|
+
const env = confirmed?.environment ?? target.keyEnv ?? "unknown environment";
|
|
5698
|
+
return { project, env };
|
|
5699
|
+
}
|
|
5700
|
+
async function disconnect(argv) {
|
|
5701
|
+
let skipConfirm = false;
|
|
5702
|
+
for (const arg of argv) {
|
|
5703
|
+
if (arg === "--yes" || arg === "-y") skipConfirm = true;
|
|
5704
|
+
else if (arg === "--help" || arg === "-h") {
|
|
5705
|
+
console.log(DISCONNECT_USAGE);
|
|
5706
|
+
return;
|
|
5707
|
+
} else {
|
|
5708
|
+
console.error(
|
|
5709
|
+
import_picocolors7.default.red(` Unknown flag ${import_picocolors7.default.bold(arg)}.`) + import_picocolors7.default.dim(` See ${import_picocolors7.default.bold("ablo connect deregister --help")}.`)
|
|
5710
|
+
);
|
|
5711
|
+
process.exit(1);
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
console.log(
|
|
5715
|
+
`
|
|
5716
|
+
${brand("ablo")} ${import_picocolors7.default.dim("connect deregister")} ${import_picocolors7.default.dim("remove this project's data source")}
|
|
5717
|
+
`
|
|
5718
|
+
);
|
|
5719
|
+
const resolved = resolveEffectiveApiKey();
|
|
5720
|
+
const apiKey = resolved.key;
|
|
5721
|
+
const keySource = resolved.source ?? "stored";
|
|
5722
|
+
if (!apiKey) {
|
|
5723
|
+
console.error(
|
|
5724
|
+
import_picocolors7.default.red(` Disconnecting needs an API key, and none was found.`) + import_picocolors7.default.dim(
|
|
5725
|
+
` 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")}.`
|
|
5726
|
+
)
|
|
5727
|
+
);
|
|
5728
|
+
process.exit(1);
|
|
5729
|
+
}
|
|
5730
|
+
const apiUrl2 = apiBaseUrl();
|
|
5731
|
+
const target = await resolveTarget({ url: apiUrl2, apiKey, keySource });
|
|
5732
|
+
const { project, env } = planeLabel(target);
|
|
5733
|
+
const envLabel = env === "production" ? import_picocolors7.default.yellow(env) : import_picocolors7.default.dim(env);
|
|
5734
|
+
for (const mismatch of target.mismatches) {
|
|
5735
|
+
console.log(` ${import_picocolors7.default.yellow("\u26A0")} ${describeMismatch(mismatch)}
|
|
5736
|
+
`);
|
|
5737
|
+
}
|
|
5738
|
+
if (!skipConfirm) {
|
|
5739
|
+
if (!process.stdout.isTTY) {
|
|
5740
|
+
console.error(
|
|
5741
|
+
import_picocolors7.default.dim(` Re-run with ${import_picocolors7.default.bold("--yes")} to disconnect in a non-interactive session.
|
|
5742
|
+
`)
|
|
5743
|
+
);
|
|
5744
|
+
process.exit(1);
|
|
5745
|
+
}
|
|
5746
|
+
const proceed = await ye({
|
|
5747
|
+
message: `Disconnect the data source for ${import_picocolors7.default.bold(project)} in ${envLabel}?`,
|
|
5748
|
+
initialValue: true
|
|
5749
|
+
});
|
|
5750
|
+
if (pD(proceed) || !proceed) {
|
|
5751
|
+
console.log(import_picocolors7.default.dim(` Nothing changed. The data source is still connected.
|
|
5752
|
+
`));
|
|
5753
|
+
process.exit(0);
|
|
5754
|
+
}
|
|
5755
|
+
}
|
|
5756
|
+
let res;
|
|
5757
|
+
try {
|
|
5758
|
+
res = await fetch(registerEndpoint(apiUrl2), {
|
|
5759
|
+
method: "DELETE",
|
|
5760
|
+
headers: { authorization: `Bearer ${apiKey}` }
|
|
5761
|
+
});
|
|
5762
|
+
} catch (err) {
|
|
5763
|
+
console.error(
|
|
5764
|
+
import_picocolors7.default.red(`
|
|
5765
|
+
Couldn't reach ${apiUrl2}: ${err instanceof Error ? err.message : String(err)}
|
|
5766
|
+
`)
|
|
5767
|
+
);
|
|
5768
|
+
process.exit(1);
|
|
5769
|
+
}
|
|
5770
|
+
if (res.ok) {
|
|
5771
|
+
const body2 = await res.json().catch(() => ({}));
|
|
5772
|
+
const parts = [];
|
|
5773
|
+
if (body2.cleared?.direct) parts.push("the direct database registration");
|
|
5774
|
+
if (body2.cleared?.endpoints) {
|
|
5775
|
+
parts.push(
|
|
5776
|
+
`${body2.cleared.endpoints} endpoint registration${body2.cleared.endpoints === 1 ? "" : "s"}`
|
|
5777
|
+
);
|
|
5778
|
+
}
|
|
5779
|
+
const what = parts.length > 0 ? parts.join(" and ") : "the data source";
|
|
5780
|
+
console.log(
|
|
5781
|
+
`
|
|
5782
|
+
${import_picocolors7.default.green("\u2713")} Disconnected ${what} for ${import_picocolors7.default.bold(project)} in ${envLabel}. Reconnect with ${import_picocolors7.default.bold("ablo connect")}.
|
|
5783
|
+
`
|
|
5784
|
+
);
|
|
5785
|
+
return;
|
|
5786
|
+
}
|
|
5787
|
+
const body = await res.json().catch(() => ({}));
|
|
5788
|
+
const code = body.code ?? body.error?.code;
|
|
5789
|
+
const message = body.message ?? body.error?.message ?? `HTTP ${res.status}`;
|
|
5790
|
+
if (code === "entity_not_found") {
|
|
5791
|
+
console.log(import_picocolors7.default.dim(` No data source registered for ${project} in ${env}.
|
|
5792
|
+
`));
|
|
5793
|
+
return;
|
|
5794
|
+
}
|
|
5795
|
+
console.error(import_picocolors7.default.red(`
|
|
5796
|
+
Disconnect failed: ${message}`));
|
|
5797
|
+
if (code === "forbidden") {
|
|
5798
|
+
console.error(
|
|
5799
|
+
import_picocolors7.default.dim(
|
|
5800
|
+
` Disconnecting needs a ${import_picocolors7.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors7.default.bold("ablo login")} for one.`
|
|
5801
|
+
)
|
|
5802
|
+
);
|
|
5803
|
+
}
|
|
5804
|
+
console.error();
|
|
5805
|
+
process.exit(1);
|
|
5806
|
+
}
|
|
5807
|
+
var import_picocolors7, DISCONNECT_USAGE;
|
|
5808
|
+
var init_disconnect = __esm({
|
|
5809
|
+
"src/cli/disconnect.ts"() {
|
|
5810
|
+
"use strict";
|
|
5811
|
+
init_cjs_shims();
|
|
5812
|
+
import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
5813
|
+
init_dist2();
|
|
5814
|
+
init_config();
|
|
5815
|
+
init_push();
|
|
5816
|
+
init_theme();
|
|
5817
|
+
init_connectSetup();
|
|
5818
|
+
init_target();
|
|
5819
|
+
DISCONNECT_USAGE = `${brand("ablo")} connect deregister ${import_picocolors7.default.dim("remove this project's data source")}
|
|
5820
|
+
|
|
5821
|
+
Usage
|
|
5822
|
+
npx ablo connect deregister Remove the active project's data source (confirms first)
|
|
5823
|
+
npx ablo connect deregister --yes Skip the confirmation
|
|
5824
|
+
|
|
5825
|
+
Acts on one plane \u2014 the active project (${import_picocolors7.default.bold("ablo projects use")}) in the active
|
|
5826
|
+
environment (${import_picocolors7.default.bold("ablo mode")}), shown before it runs. Removes the registration and
|
|
5827
|
+
Ablo's replication state for that plane, so Ablo stops reading and writing the
|
|
5828
|
+
database. Reconnect with ${import_picocolors7.default.bold("ablo connect")}.`;
|
|
5829
|
+
}
|
|
5830
|
+
});
|
|
5831
|
+
|
|
5438
5832
|
// src/cli/connectApply.ts
|
|
5439
5833
|
var connectApply_exports = {};
|
|
5440
5834
|
__export(connectApply_exports, {
|
|
5441
5835
|
connectApplyPlan: () => connectApplyPlan,
|
|
5836
|
+
detectProvider: () => detectProvider,
|
|
5837
|
+
ledgerBlockedBy: () => ledgerBlockedBy,
|
|
5838
|
+
logicalReplicationGuidance: () => logicalReplicationGuidance,
|
|
5442
5839
|
passwordClause: () => passwordClause,
|
|
5443
5840
|
postRegistrationOutcome: () => postRegistrationOutcome,
|
|
5444
5841
|
runConnectApply: () => runConnectApply
|
|
@@ -5448,7 +5845,7 @@ function postRegistrationOutcome(input) {
|
|
|
5448
5845
|
if (!input.rotating) return { exitCode: 1, notice: null };
|
|
5449
5846
|
return {
|
|
5450
5847
|
exitCode: 1,
|
|
5451
|
-
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
|
|
5848
|
+
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)."
|
|
5452
5849
|
};
|
|
5453
5850
|
}
|
|
5454
5851
|
function passwordClause(password, mode2) {
|
|
@@ -5465,29 +5862,41 @@ function connectApplyPlan(input) {
|
|
|
5465
5862
|
const role = input.role && input.role.length > 0 ? input.role : ABLO_REPLICATION_ROLE;
|
|
5466
5863
|
const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : ABLO_WRITE_ROLE;
|
|
5467
5864
|
const tables = input.tables ?? [];
|
|
5865
|
+
const provider = input.provider ?? "generic";
|
|
5468
5866
|
const recipe = connectSetupSql({ tables, role, writeRole });
|
|
5469
5867
|
const isWal = (s) => s.startsWith("ALTER SYSTEM SET wal_level");
|
|
5470
5868
|
const isPublication = (s) => s.startsWith("CREATE PUBLICATION");
|
|
5471
|
-
const isRoleCreate = (s) =>
|
|
5869
|
+
const isRoleCreate = (s) => s.startsWith("CREATE ROLE ");
|
|
5472
5870
|
const grants = recipe.filter((s) => !isWal(s) && !isPublication(s) && !isRoleCreate(s));
|
|
5473
5871
|
const publicationTarget = tables.length > 0 ? `FOR TABLE ${tables.map(quoteIdent).join(", ")}` : "FOR ALL TABLES";
|
|
5474
|
-
|
|
5475
|
-
{
|
|
5872
|
+
const walStep = input.walAlreadyLogical ? [] : [
|
|
5873
|
+
provider === "generic" ? {
|
|
5476
5874
|
key: "wal",
|
|
5477
5875
|
title: "Turn on logical replication",
|
|
5478
|
-
detail: "lets Ablo read your changes as they happen",
|
|
5876
|
+
detail: "lets Ablo read your changes as they happen (needs a restart to take effect)",
|
|
5479
5877
|
sql: [`ALTER SYSTEM SET wal_level = 'logical';`]
|
|
5480
|
-
}
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5878
|
+
} : {
|
|
5879
|
+
key: "wal",
|
|
5880
|
+
title: "Turn on logical replication",
|
|
5881
|
+
detail: logicalReplicationGuidance(provider),
|
|
5882
|
+
sql: []
|
|
5883
|
+
}
|
|
5884
|
+
];
|
|
5885
|
+
const reconcile2 = input.existingPublication ? reconcilePublicationPlan(input.existingPublication, tables) : null;
|
|
5886
|
+
const publicationSql = reconcile2 ? reconcile2.sql : [
|
|
5887
|
+
`DO $$ BEGIN
|
|
5487
5888
|
CREATE PUBLICATION ${quoteIdent(ABLO_PUBLICATION)} ${publicationTarget};
|
|
5488
5889
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
5489
5890
|
END $$;`
|
|
5490
|
-
|
|
5891
|
+
];
|
|
5892
|
+
const publicationDetail = reconcile2?.sql.length === 0 ? "already publishing exactly these tables \u2014 nothing to change" : tables.length > 0 ? `a read stream of the ${tables.length} table${tables.length === 1 ? "" : "s"} you chose` : "a read stream of your tables";
|
|
5893
|
+
return [
|
|
5894
|
+
...walStep,
|
|
5895
|
+
{
|
|
5896
|
+
key: "publication",
|
|
5897
|
+
title: "Publish your tables to Ablo",
|
|
5898
|
+
detail: publicationDetail,
|
|
5899
|
+
sql: publicationSql
|
|
5491
5900
|
},
|
|
5492
5901
|
{
|
|
5493
5902
|
key: "replication-role",
|
|
@@ -5498,7 +5907,7 @@ END $$;`
|
|
|
5498
5907
|
{
|
|
5499
5908
|
key: "write-role",
|
|
5500
5909
|
title: "Create a scoped writer role",
|
|
5501
|
-
detail: `${writeRole} \u2014 writes rows through Ablo
|
|
5910
|
+
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)`,
|
|
5502
5911
|
sql: [
|
|
5503
5912
|
idempotentRole(
|
|
5504
5913
|
writeRole,
|
|
@@ -5523,17 +5932,19 @@ function printPlan2(steps, showSql) {
|
|
|
5523
5932
|
console.log(` This sets up your database for Ablo:
|
|
5524
5933
|
`);
|
|
5525
5934
|
for (const step of steps) {
|
|
5526
|
-
console.log(` ${
|
|
5935
|
+
console.log(` ${import_picocolors8.default.green("\u2022")} ${step.title}`);
|
|
5527
5936
|
if (showSql) {
|
|
5528
5937
|
for (const statement of step.sql) {
|
|
5529
|
-
for (const line of statement.split("\n")) console.log(` ${
|
|
5938
|
+
for (const line of statement.split("\n")) console.log(` ${import_picocolors8.default.dim(line)}`);
|
|
5530
5939
|
}
|
|
5531
5940
|
}
|
|
5532
5941
|
}
|
|
5533
5942
|
console.log(
|
|
5534
|
-
|
|
5943
|
+
import_picocolors8.default.dim(
|
|
5944
|
+
`
|
|
5535
5945
|
Your admin password stays on this machine.${showSql ? "" : " (--show-sql for the exact statements)"}
|
|
5536
|
-
`
|
|
5946
|
+
`
|
|
5947
|
+
)
|
|
5537
5948
|
);
|
|
5538
5949
|
}
|
|
5539
5950
|
async function adminCanCreateRoles(sql) {
|
|
@@ -5542,8 +5953,54 @@ async function adminCanCreateRoles(sql) {
|
|
|
5542
5953
|
);
|
|
5543
5954
|
return rows[0] ?? null;
|
|
5544
5955
|
}
|
|
5956
|
+
function ledgerBlockedBy(row) {
|
|
5957
|
+
if (!row || row.is_owner || row.is_superuser) return null;
|
|
5958
|
+
return row.owner;
|
|
5959
|
+
}
|
|
5960
|
+
async function unmanageableLedgerOwner(sql) {
|
|
5961
|
+
const rows = await sql.unsafe(
|
|
5962
|
+
`SELECT pg_get_userbyid(c.relowner) AS owner,
|
|
5963
|
+
c.relowner = r.oid AS is_owner,
|
|
5964
|
+
r.rolsuper AS is_superuser
|
|
5965
|
+
FROM pg_class c
|
|
5966
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
5967
|
+
JOIN pg_roles r ON r.rolname = current_user
|
|
5968
|
+
WHERE c.relkind = 'r'
|
|
5969
|
+
AND c.relname = 'ablo_idempotency'
|
|
5970
|
+
AND n.nspname = 'public'`
|
|
5971
|
+
);
|
|
5972
|
+
return ledgerBlockedBy(rows[0]);
|
|
5973
|
+
}
|
|
5974
|
+
function detectProvider(hostOrTarget) {
|
|
5975
|
+
const host = hostOrTarget.toLowerCase();
|
|
5976
|
+
if (host.includes("neon.tech")) return "neon";
|
|
5977
|
+
if (host.includes("supabase.co") || host.includes("supabase.com") || host.includes("pooler.supabase")) {
|
|
5978
|
+
return "supabase";
|
|
5979
|
+
}
|
|
5980
|
+
if (host.includes("rds.amazonaws.com") || host.includes(".rds.")) return "rds";
|
|
5981
|
+
return "generic";
|
|
5982
|
+
}
|
|
5983
|
+
function logicalReplicationGuidance(provider) {
|
|
5984
|
+
switch (provider) {
|
|
5985
|
+
case "neon":
|
|
5986
|
+
return `enable logical replication in your Neon project settings \u2014 it can't be set over SQL`;
|
|
5987
|
+
case "supabase":
|
|
5988
|
+
return `raise wal_level to logical in your Supabase project's database settings`;
|
|
5989
|
+
case "rds":
|
|
5990
|
+
return `set rds.logical_replication = 1 in the instance's parameter group, then reboot`;
|
|
5991
|
+
case "generic":
|
|
5992
|
+
return `run the ALTER SYSTEM above, then restart Postgres \u2014 wal_level is not reloadable`;
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
async function currentWalLevel(sql) {
|
|
5996
|
+
try {
|
|
5997
|
+
const rows = await sql.unsafe(`SHOW wal_level`);
|
|
5998
|
+
return rows[0]?.wal_level ?? "";
|
|
5999
|
+
} catch {
|
|
6000
|
+
return "";
|
|
6001
|
+
}
|
|
6002
|
+
}
|
|
5545
6003
|
async function executePlan(sql, steps, rebuildPlaintext) {
|
|
5546
|
-
let walDeferred = false;
|
|
5547
6004
|
let plaintextSteps = null;
|
|
5548
6005
|
for (const step of steps) {
|
|
5549
6006
|
for (const statement of step.sql) {
|
|
@@ -5551,7 +6008,6 @@ async function executePlan(sql, steps, rebuildPlaintext) {
|
|
|
5551
6008
|
await sql.unsafe(statement);
|
|
5552
6009
|
} catch (err) {
|
|
5553
6010
|
if (step.key === "wal") {
|
|
5554
|
-
walDeferred = true;
|
|
5555
6011
|
continue;
|
|
5556
6012
|
}
|
|
5557
6013
|
if ((step.key === "replication-role" || step.key === "write-role") && isPlaintextRefusal(err)) {
|
|
@@ -5566,15 +6022,16 @@ async function executePlan(sql, steps, rebuildPlaintext) {
|
|
|
5566
6022
|
}
|
|
5567
6023
|
}
|
|
5568
6024
|
}
|
|
5569
|
-
return { walDeferred };
|
|
5570
6025
|
}
|
|
5571
6026
|
async function runConnectApply(args) {
|
|
5572
6027
|
const rotating = args.rotate;
|
|
5573
|
-
const verb = rotating ? "connect
|
|
6028
|
+
const verb = rotating ? "connect rotate" : "connect apply";
|
|
5574
6029
|
const adminUrl = args.url ?? readProjectAdminDatabaseUrl();
|
|
5575
6030
|
if (!adminUrl) {
|
|
5576
6031
|
console.error(
|
|
5577
|
-
|
|
6032
|
+
import_picocolors8.default.red(" No admin connection string.") + import_picocolors8.default.dim(
|
|
6033
|
+
` Pass ${import_picocolors8.default.bold("--url <admin-conn>")} (or set ${import_picocolors8.default.bold("DATABASE_URL")}) and re-run.`
|
|
6034
|
+
)
|
|
5578
6035
|
);
|
|
5579
6036
|
process.exit(1);
|
|
5580
6037
|
}
|
|
@@ -5588,39 +6045,67 @@ async function runConnectApply(args) {
|
|
|
5588
6045
|
const apiKey = resolveApiKey();
|
|
5589
6046
|
if (!apiKey) {
|
|
5590
6047
|
console.error(
|
|
5591
|
-
|
|
5592
|
-
` Run ${
|
|
6048
|
+
import_picocolors8.default.red(" Not logged in.") + import_picocolors8.default.dim(
|
|
6049
|
+
` 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.`
|
|
5593
6050
|
)
|
|
5594
6051
|
);
|
|
5595
6052
|
process.exit(1);
|
|
5596
6053
|
}
|
|
5597
6054
|
console.log(
|
|
5598
6055
|
`
|
|
5599
|
-
${brand("ablo")} ${
|
|
6056
|
+
${brand("ablo")} ${import_picocolors8.default.dim(verb)} ${import_picocolors8.default.dim(rotating ? "re-key the scoped roles" : "set up your database for Ablo")}
|
|
5600
6057
|
`
|
|
5601
6058
|
);
|
|
5602
6059
|
console.log(
|
|
5603
|
-
` ${
|
|
6060
|
+
` ${import_picocolors8.default.dim("\u2192")} ${import_picocolors8.default.bold(target)}${adminSource === "DATABASE_URL" ? import_picocolors8.default.dim(" (admin via DATABASE_URL)") : ""}
|
|
5604
6061
|
`
|
|
5605
6062
|
);
|
|
5606
|
-
const admin = src_default(adminUrl, {
|
|
5607
|
-
|
|
6063
|
+
const admin = src_default(adminUrl, {
|
|
6064
|
+
max: 1,
|
|
6065
|
+
prepare: false,
|
|
6066
|
+
connect_timeout: 10,
|
|
6067
|
+
onnotice: () => {
|
|
6068
|
+
}
|
|
6069
|
+
});
|
|
5608
6070
|
let capability;
|
|
5609
6071
|
try {
|
|
5610
6072
|
capability = await adminCanCreateRoles(admin);
|
|
5611
6073
|
} catch (err) {
|
|
5612
6074
|
await admin.end({ timeout: 2 }).catch(() => void 0);
|
|
5613
6075
|
const pg = err ?? {};
|
|
5614
|
-
console.error(
|
|
6076
|
+
console.error(import_picocolors8.default.red(` Couldn't connect: ${pg.message ?? String(err)}`));
|
|
5615
6077
|
process.exit(1);
|
|
5616
6078
|
}
|
|
5617
6079
|
if (!capability || !(capability.rolsuper || capability.rolcreaterole)) {
|
|
5618
6080
|
await admin.end({ timeout: 2 });
|
|
5619
6081
|
console.error(
|
|
5620
|
-
|
|
6082
|
+
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.`)
|
|
5621
6083
|
);
|
|
5622
6084
|
process.exit(1);
|
|
5623
6085
|
}
|
|
6086
|
+
const blockingOwner = await unmanageableLedgerOwner(admin).catch(() => null);
|
|
6087
|
+
if (blockingOwner) {
|
|
6088
|
+
await admin.end({ timeout: 2 });
|
|
6089
|
+
console.error(
|
|
6090
|
+
import_picocolors8.default.red(
|
|
6091
|
+
`
|
|
6092
|
+
${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)}.`
|
|
6093
|
+
) + `
|
|
6094
|
+
Ablo's setup grants the writer role access to this ledger, and Postgres reserves that
|
|
6095
|
+
for the table's owner. Either re-run pointing ${import_picocolors8.default.bold("--url")} at ${import_picocolors8.default.bold(blockingOwner)}'s connection,
|
|
6096
|
+
or drop the existing ledger so Ablo recreates it under this admin \u2014 it holds only
|
|
6097
|
+
idempotency replay records, safe to drop when no commit is in flight:
|
|
6098
|
+
${import_picocolors8.default.cyan("DROP TABLE ablo_idempotency;")}
|
|
6099
|
+
`
|
|
6100
|
+
);
|
|
6101
|
+
process.exit(1);
|
|
6102
|
+
}
|
|
6103
|
+
const provider = detectProvider(target);
|
|
6104
|
+
const walReady = await currentWalLevel(admin) === "logical";
|
|
6105
|
+
const existingPublication = await readPublicationState(admin).catch(
|
|
6106
|
+
() => ({ exists: false, allTables: false, tables: [] })
|
|
6107
|
+
);
|
|
6108
|
+
const pubReconcile = reconcilePublicationPlan(existingPublication, args.tables);
|
|
5624
6109
|
const replicationPassword = generateRolePassword();
|
|
5625
6110
|
const writePassword = generateRolePassword();
|
|
5626
6111
|
const buildPlan = (mode2) => connectApplyPlan({
|
|
@@ -5630,15 +6115,29 @@ async function runConnectApply(args) {
|
|
|
5630
6115
|
credentials: {
|
|
5631
6116
|
replicationClause: passwordClause(replicationPassword, mode2),
|
|
5632
6117
|
writeClause: passwordClause(writePassword, mode2)
|
|
5633
|
-
}
|
|
6118
|
+
},
|
|
6119
|
+
walAlreadyLogical: walReady,
|
|
6120
|
+
provider,
|
|
6121
|
+
existingPublication
|
|
5634
6122
|
});
|
|
5635
6123
|
const steps = buildPlan("scram-verifier");
|
|
6124
|
+
if (pubReconcile.removed.length > 0 || pubReconcile.recreated) {
|
|
6125
|
+
console.log(
|
|
6126
|
+
` ${import_picocolors8.default.yellow("!")} ${import_picocolors8.default.bold(ABLO_PUBLICATION)} already publishes a different set; reconciling to your ${import_picocolors8.default.bold("--tables")}:`
|
|
6127
|
+
);
|
|
6128
|
+
for (const t of pubReconcile.added) console.log(` ${import_picocolors8.default.green("+")} ${t}`);
|
|
6129
|
+
for (const t of pubReconcile.removed)
|
|
6130
|
+
console.log(` ${import_picocolors8.default.red("-")} ${t} ${import_picocolors8.default.dim("(stops replicating to Ablo)")}`);
|
|
6131
|
+
if (pubReconcile.recreated && existingPublication.allTables)
|
|
6132
|
+
console.log(` ${import_picocolors8.default.red("-")} ${import_picocolors8.default.dim("every other table (was FOR ALL TABLES)")}`);
|
|
6133
|
+
console.log();
|
|
6134
|
+
}
|
|
5636
6135
|
printPlan2(steps, args.showSql);
|
|
5637
6136
|
if (!args.yes) {
|
|
5638
6137
|
if (!process.stdout.isTTY) {
|
|
5639
6138
|
await admin.end({ timeout: 2 });
|
|
5640
6139
|
console.error(
|
|
5641
|
-
|
|
6140
|
+
import_picocolors8.default.dim(` Re-run with ${import_picocolors8.default.bold("--yes")} to apply this in a non-interactive session.
|
|
5642
6141
|
`)
|
|
5643
6142
|
);
|
|
5644
6143
|
process.exit(1);
|
|
@@ -5649,20 +6148,21 @@ async function runConnectApply(args) {
|
|
|
5649
6148
|
});
|
|
5650
6149
|
if (pD(proceed) || !proceed) {
|
|
5651
6150
|
await admin.end({ timeout: 2 });
|
|
5652
|
-
console.log(
|
|
5653
|
-
`)
|
|
6151
|
+
console.log(
|
|
6152
|
+
import_picocolors8.default.dim(` Nothing applied. Run ${import_picocolors8.default.bold("ablo connect")} to see the manual recipe.
|
|
6153
|
+
`)
|
|
6154
|
+
);
|
|
5654
6155
|
process.exit(0);
|
|
5655
6156
|
}
|
|
5656
6157
|
}
|
|
5657
|
-
let walDeferred = false;
|
|
5658
6158
|
try {
|
|
5659
|
-
|
|
6159
|
+
await executePlan(admin, steps, () => buildPlan("plaintext"));
|
|
5660
6160
|
} catch (err) {
|
|
5661
6161
|
await admin.end({ timeout: 2 }).catch(() => void 0);
|
|
5662
6162
|
const pg = err ?? {};
|
|
5663
6163
|
console.error(
|
|
5664
|
-
|
|
5665
|
-
Setup stopped: ${pg.message ?? String(err)}`) +
|
|
6164
|
+
import_picocolors8.default.red(`
|
|
6165
|
+
Setup stopped: ${pg.message ?? String(err)}`) + import_picocolors8.default.dim(` Every step is safe to re-run.
|
|
5666
6166
|
`)
|
|
5667
6167
|
);
|
|
5668
6168
|
process.exit(1);
|
|
@@ -5673,38 +6173,43 @@ async function runConnectApply(args) {
|
|
|
5673
6173
|
const replicationUrl = rewriteDatabaseUrl(adminUrl, role, replicationPassword);
|
|
5674
6174
|
const writeUrl = rewriteDatabaseUrl(adminUrl, writeRole, writePassword);
|
|
5675
6175
|
console.log(`
|
|
5676
|
-
${
|
|
6176
|
+
${import_picocolors8.default.green("\u2713")} Roles ${rotating ? "re-keyed" : "created"}.
|
|
5677
6177
|
`);
|
|
5678
|
-
if (
|
|
5679
|
-
console.
|
|
5680
|
-
` ${
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
Then re-run: ${
|
|
6178
|
+
if (!walReady) {
|
|
6179
|
+
console.error(
|
|
6180
|
+
` ${import_picocolors8.default.yellow("!")} One step left \u2014 logical replication isn't on yet.
|
|
6181
|
+
${logicalReplicationGuidance(provider)}.
|
|
6182
|
+
|
|
6183
|
+
Then re-run: ${import_picocolors8.default.cyan(`npx ablo ${verb}`)}
|
|
5684
6184
|
`
|
|
5685
6185
|
);
|
|
5686
|
-
process.exit(
|
|
6186
|
+
process.exit(1);
|
|
5687
6187
|
}
|
|
5688
|
-
const verifier = src_default(replicationUrl, {
|
|
5689
|
-
|
|
6188
|
+
const verifier = src_default(replicationUrl, {
|
|
6189
|
+
max: 1,
|
|
6190
|
+
prepare: false,
|
|
6191
|
+
connect_timeout: 10,
|
|
6192
|
+
onnotice: () => {
|
|
6193
|
+
}
|
|
6194
|
+
});
|
|
5690
6195
|
let items;
|
|
5691
6196
|
try {
|
|
5692
6197
|
items = await probeReadiness(verifier);
|
|
5693
6198
|
} catch {
|
|
5694
6199
|
items = null;
|
|
5695
|
-
console.log(
|
|
6200
|
+
console.log(import_picocolors8.default.dim(` Couldn't verify from here; Ablo will validate from its own network.
|
|
5696
6201
|
`));
|
|
5697
6202
|
}
|
|
5698
6203
|
await verifier.end({ timeout: 2 }).catch(() => void 0);
|
|
5699
6204
|
const failed = items?.filter((i) => !i.ok) ?? [];
|
|
5700
6205
|
if (failed.length > 0) {
|
|
5701
|
-
for (const item of failed) console.log(` ${
|
|
6206
|
+
for (const item of failed) console.log(` ${import_picocolors8.default.yellow("!")} ${item.label}`);
|
|
5702
6207
|
console.log(`
|
|
5703
|
-
${
|
|
6208
|
+
${import_picocolors8.default.dim("Resolve, then re-run")} ${import_picocolors8.default.cyan(`npx ablo ${verb}`)}
|
|
5704
6209
|
`);
|
|
5705
6210
|
process.exit(1);
|
|
5706
6211
|
}
|
|
5707
|
-
const apiUrl2 = (
|
|
6212
|
+
const apiUrl2 = apiBaseUrl();
|
|
5708
6213
|
const registered = await registerDirectDataSource({
|
|
5709
6214
|
apiUrl: apiUrl2,
|
|
5710
6215
|
apiKey,
|
|
@@ -5715,20 +6220,20 @@ async function runConnectApply(args) {
|
|
|
5715
6220
|
const outcome = postRegistrationOutcome({ rotating, registered });
|
|
5716
6221
|
if (outcome.notice) {
|
|
5717
6222
|
console.error(`
|
|
5718
|
-
${
|
|
6223
|
+
${import_picocolors8.default.red(outcome.notice.split("\n").join("\n "))}
|
|
5719
6224
|
`);
|
|
5720
6225
|
}
|
|
5721
6226
|
process.exit(outcome.exitCode);
|
|
5722
6227
|
}
|
|
5723
|
-
var
|
|
6228
|
+
var import_picocolors8;
|
|
5724
6229
|
var init_connectApply = __esm({
|
|
5725
6230
|
"src/cli/connectApply.ts"() {
|
|
5726
6231
|
"use strict";
|
|
5727
6232
|
init_cjs_shims();
|
|
5728
|
-
|
|
6233
|
+
import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
5729
6234
|
init_src();
|
|
5730
6235
|
init_dist2();
|
|
5731
|
-
|
|
6236
|
+
init_connectSetup();
|
|
5732
6237
|
init_dbRole();
|
|
5733
6238
|
init_config();
|
|
5734
6239
|
init_push();
|
|
@@ -5736,776 +6241,6 @@ var init_connectApply = __esm({
|
|
|
5736
6241
|
}
|
|
5737
6242
|
});
|
|
5738
6243
|
|
|
5739
|
-
// src/cli/connect.ts
|
|
5740
|
-
function parseConnectArgs(argv) {
|
|
5741
|
-
let check2 = false;
|
|
5742
|
-
let register = false;
|
|
5743
|
-
let apply2 = false;
|
|
5744
|
-
let rotate = false;
|
|
5745
|
-
let url;
|
|
5746
|
-
let yes = false;
|
|
5747
|
-
let showSql = false;
|
|
5748
|
-
let auditInfra = false;
|
|
5749
|
-
let tables = [];
|
|
5750
|
-
let role = ABLO_REPLICATION_ROLE;
|
|
5751
|
-
let writeRole = ABLO_WRITE_ROLE;
|
|
5752
|
-
let route = "public-allowlist";
|
|
5753
|
-
for (let i = 0; i < argv.length; i++) {
|
|
5754
|
-
const arg = argv[i];
|
|
5755
|
-
switch (arg) {
|
|
5756
|
-
case "--check":
|
|
5757
|
-
check2 = true;
|
|
5758
|
-
break;
|
|
5759
|
-
case "--register":
|
|
5760
|
-
register = true;
|
|
5761
|
-
break;
|
|
5762
|
-
case "--apply":
|
|
5763
|
-
apply2 = true;
|
|
5764
|
-
break;
|
|
5765
|
-
case "--rotate":
|
|
5766
|
-
rotate = true;
|
|
5767
|
-
break;
|
|
5768
|
-
case "--url":
|
|
5769
|
-
url = argv[++i] ?? url;
|
|
5770
|
-
break;
|
|
5771
|
-
case "--yes":
|
|
5772
|
-
case "-y":
|
|
5773
|
-
yes = true;
|
|
5774
|
-
break;
|
|
5775
|
-
case "--show-sql":
|
|
5776
|
-
showSql = true;
|
|
5777
|
-
break;
|
|
5778
|
-
case "--audit-infra":
|
|
5779
|
-
auditInfra = true;
|
|
5780
|
-
break;
|
|
5781
|
-
case "--tables": {
|
|
5782
|
-
const value = argv[++i] ?? "";
|
|
5783
|
-
tables = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
5784
|
-
break;
|
|
5785
|
-
}
|
|
5786
|
-
case "--role":
|
|
5787
|
-
role = argv[++i] ?? role;
|
|
5788
|
-
break;
|
|
5789
|
-
case "--write-role":
|
|
5790
|
-
writeRole = argv[++i] ?? writeRole;
|
|
5791
|
-
break;
|
|
5792
|
-
case "--route": {
|
|
5793
|
-
const value = argv[++i] ?? "";
|
|
5794
|
-
if (!DIRECT_DATA_SOURCE_ROUTES.includes(value)) {
|
|
5795
|
-
throw new AbloValidationError(
|
|
5796
|
-
`invalid direct route: ${value || "(missing)"} (expected ${DIRECT_DATA_SOURCE_ROUTES.join(", ")})`,
|
|
5797
|
-
{ code: "cli_invalid_arguments" }
|
|
5798
|
-
);
|
|
5799
|
-
}
|
|
5800
|
-
route = value;
|
|
5801
|
-
break;
|
|
5802
|
-
}
|
|
5803
|
-
default:
|
|
5804
|
-
throw new AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
|
|
5805
|
-
}
|
|
5806
|
-
}
|
|
5807
|
-
if (role === writeRole) {
|
|
5808
|
-
throw new AbloValidationError("replication and write roles must be different", {
|
|
5809
|
-
code: "cli_invalid_arguments"
|
|
5810
|
-
});
|
|
5811
|
-
}
|
|
5812
|
-
return { check: check2, register, apply: apply2, rotate, url, yes, showSql, auditInfra, tables, role, writeRole, route };
|
|
5813
|
-
}
|
|
5814
|
-
function quoteIdent(id) {
|
|
5815
|
-
return `"${id.replace(/"/g, '""')}"`;
|
|
5816
|
-
}
|
|
5817
|
-
function connectSetupSql(input) {
|
|
5818
|
-
const role = input.role && input.role.length > 0 ? input.role : ABLO_REPLICATION_ROLE;
|
|
5819
|
-
const writeRole = input.writeRole && input.writeRole.length > 0 ? input.writeRole : ABLO_WRITE_ROLE;
|
|
5820
|
-
const tables = input.tables ?? [];
|
|
5821
|
-
const publicationTarget = tables.length > 0 ? `FOR TABLE ${tables.map(quoteIdent).join(", ")}` : "FOR ALL TABLES";
|
|
5822
|
-
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)};`;
|
|
5823
|
-
const ledger = idempotencyLedgerMigrations().map((migration) => migration.up);
|
|
5824
|
-
return [
|
|
5825
|
-
// 1. Turn on logical decoding. Requires a restart (it's not reloadable).
|
|
5826
|
-
`ALTER SYSTEM SET wal_level = 'logical';`,
|
|
5827
|
-
// 2. Publish the tables Ablo should read.
|
|
5828
|
-
`CREATE PUBLICATION ${quoteIdent(ABLO_PUBLICATION)} ${publicationTarget};`,
|
|
5829
|
-
// 3. A least-privilege role: it can stream replication and SELECT, nothing more.
|
|
5830
|
-
`CREATE ROLE ${quoteIdent(role)} WITH REPLICATION LOGIN PASSWORD '<password>';`,
|
|
5831
|
-
`GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${quoteIdent(role)};`,
|
|
5832
|
-
// Future tables get SELECT automatically, so the publication doesn't outgrow the grant.
|
|
5833
|
-
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${quoteIdent(role)};`,
|
|
5834
|
-
// 4. A distinct DML role: no replication, role administration, ownership,
|
|
5835
|
-
// schema creation, or DDL. It is subject to RLS on every transaction.
|
|
5836
|
-
`CREATE ROLE ${quoteIdent(writeRole)} WITH LOGIN PASSWORD '<write-password>' NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE NOREPLICATION NOINHERIT;`,
|
|
5837
|
-
`ALTER ROLE ${quoteIdent(writeRole)} SET row_security = on;`,
|
|
5838
|
-
// PUBLIC can carry CREATE on older Postgres clusters. A role-level REVOKE
|
|
5839
|
-
// cannot override that inherited grant, so harden the schema explicitly.
|
|
5840
|
-
`REVOKE CREATE ON SCHEMA public FROM PUBLIC;`,
|
|
5841
|
-
`GRANT USAGE ON SCHEMA public TO ${quoteIdent(writeRole)};`,
|
|
5842
|
-
applicationGrant,
|
|
5843
|
-
`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${quoteIdent(writeRole)};`,
|
|
5844
|
-
...tables.length === 0 ? [
|
|
5845
|
-
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${quoteIdent(writeRole)};`,
|
|
5846
|
-
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${quoteIdent(writeRole)};`
|
|
5847
|
-
] : [],
|
|
5848
|
-
// 5. Direct uses the durable replay ledger but deliberately no outbox.
|
|
5849
|
-
...ledger,
|
|
5850
|
-
`REVOKE ALL ON TABLE public.ablo_idempotency FROM PUBLIC;`,
|
|
5851
|
-
`GRANT SELECT, INSERT, UPDATE ON TABLE public.ablo_idempotency TO ${quoteIdent(writeRole)};`,
|
|
5852
|
-
`REVOKE DELETE ON TABLE public.ablo_idempotency FROM ${quoteIdent(writeRole)};`,
|
|
5853
|
-
// Grant every pg_logical_emit_message variant by lookup instead of one
|
|
5854
|
-
// literal signature: PostgreSQL 17 adds an optional fourth `flush`
|
|
5855
|
-
// parameter, so the historical three-argument form no longer exists there
|
|
5856
|
-
// and a signature-pinned GRANT fails on an otherwise healthy database.
|
|
5857
|
-
`DO $$
|
|
5858
|
-
DECLARE fn regprocedure;
|
|
5859
|
-
BEGIN
|
|
5860
|
-
FOR fn IN
|
|
5861
|
-
SELECT p.oid::regprocedure
|
|
5862
|
-
FROM pg_proc p
|
|
5863
|
-
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
5864
|
-
WHERE n.nspname = 'pg_catalog' AND p.proname = 'pg_logical_emit_message'
|
|
5865
|
-
LOOP
|
|
5866
|
-
EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO ${quoteIdent(writeRole)}', fn);
|
|
5867
|
-
END LOOP;
|
|
5868
|
-
END $$;`
|
|
5869
|
-
];
|
|
5870
|
-
}
|
|
5871
|
-
function printConnectRecipe(args) {
|
|
5872
|
-
const sql = connectSetupSql({
|
|
5873
|
-
tables: args.tables,
|
|
5874
|
-
role: args.role,
|
|
5875
|
-
writeRole: args.writeRole
|
|
5876
|
-
});
|
|
5877
|
-
console.log(`
|
|
5878
|
-
${brand("ablo")} ${import_picocolors7.default.dim("connect")} ${import_picocolors7.default.dim("direct writes + WAL-settled sync")}
|
|
5879
|
-
`);
|
|
5880
|
-
console.log(
|
|
5881
|
-
` Ablo applies coordinated writes directly to your Postgres with a scoped DML role. WAL
|
|
5882
|
-
observes what committed, orders it with external writes, and confirms it in the sync log.
|
|
5883
|
-
Your database stays authoritative; Ablo never owns or migrates your application tables.
|
|
5884
|
-
Run this once against your Postgres ${import_picocolors7.default.dim("(as a superuser / DB owner)")}:
|
|
5885
|
-
`
|
|
5886
|
-
);
|
|
5887
|
-
console.log(` ${import_picocolors7.default.bold("1.")} Enable logical decoding ${import_picocolors7.default.dim("(then RESTART Postgres \u2014 wal_level is not reloadable)")}`);
|
|
5888
|
-
console.log(` ${import_picocolors7.default.cyan(sql[0])}`);
|
|
5889
|
-
console.log(
|
|
5890
|
-
import_picocolors7.default.dim(
|
|
5891
|
-
` On Amazon RDS / Aurora you can't ALTER SYSTEM: set ${import_picocolors7.default.bold("rds.logical_replication = 1")} in the
|
|
5892
|
-
instance's parameter group instead, then reboot.`
|
|
5893
|
-
)
|
|
5894
|
-
);
|
|
5895
|
-
console.log(`
|
|
5896
|
-
${import_picocolors7.default.bold("2.")} Publish the tables Ablo should read`);
|
|
5897
|
-
console.log(` ${import_picocolors7.default.cyan(sql[1])}`);
|
|
5898
|
-
if (args.tables.length === 0) {
|
|
5899
|
-
console.log(import_picocolors7.default.dim(` (Scope it with ${import_picocolors7.default.bold("ablo connect --tables a,b,c")} to publish a subset.)`));
|
|
5900
|
-
}
|
|
5901
|
-
console.log(`
|
|
5902
|
-
${import_picocolors7.default.bold("3.")} Create the scoped replication role ${import_picocolors7.default.dim("(pick your own replication password)")}`);
|
|
5903
|
-
console.log(` ${import_picocolors7.default.cyan(sql[2])}`);
|
|
5904
|
-
console.log(` ${import_picocolors7.default.cyan(sql[3])}`);
|
|
5905
|
-
console.log(` ${import_picocolors7.default.cyan(sql[4])}`);
|
|
5906
|
-
console.log(
|
|
5907
|
-
import_picocolors7.default.dim(
|
|
5908
|
-
` On Amazon RDS, the REPLICATION attribute is granted, not set directly:
|
|
5909
|
-
${import_picocolors7.default.bold(`GRANT rds_replication TO ${quoteIdent(args.role)};`)}`
|
|
5910
|
-
)
|
|
5911
|
-
);
|
|
5912
|
-
console.log(
|
|
5913
|
-
`
|
|
5914
|
-
${import_picocolors7.default.bold("4.")} Create the separate DML role and permanent idempotency ledger ` + import_picocolors7.default.dim("(pick your own write password)")
|
|
5915
|
-
);
|
|
5916
|
-
for (const statement of sql.slice(5)) {
|
|
5917
|
-
for (const line of statement.split("\n")) console.log(` ${import_picocolors7.default.cyan(line)}`);
|
|
5918
|
-
}
|
|
5919
|
-
console.log(
|
|
5920
|
-
import_picocolors7.default.dim(
|
|
5921
|
-
` The writer gets row DML + ledger access only. It has no REPLICATION, schema CREATE,
|
|
5922
|
-
role administration, database creation, ownership, or customer-table DDL. Direct uses
|
|
5923
|
-
${import_picocolors7.default.bold("ablo_idempotency")} but no outbox; WAL carries the committed row changes.`
|
|
5924
|
-
)
|
|
5925
|
-
);
|
|
5926
|
-
console.log(
|
|
5927
|
-
`
|
|
5928
|
-
${import_picocolors7.default.bold("5.")} Configure both scoped connections, then verify:
|
|
5929
|
-
${import_picocolors7.default.bold("DATABASE_URL")} ${import_picocolors7.default.dim(`\u2192 ${args.role} (replication only)`)}
|
|
5930
|
-
${import_picocolors7.default.bold("ABLO_WRITE_DATABASE_URL")} ${import_picocolors7.default.dim(`\u2192 ${args.writeRole} (DML only)`)}
|
|
5931
|
-
${import_picocolors7.default.cyan("npx ablo connect --check")}
|
|
5932
|
-
`
|
|
5933
|
-
);
|
|
5934
|
-
console.log(
|
|
5935
|
-
import_picocolors7.default.dim(
|
|
5936
|
-
` Reachable databases use this direct path. If no inbound route can be established, use the
|
|
5937
|
-
signed ${import_picocolors7.default.bold("dataSource()")} endpoint fallback; its correlated events confirm writes
|
|
5938
|
-
without an Ablo-side customer database socket.`
|
|
5939
|
-
)
|
|
5940
|
-
);
|
|
5941
|
-
console.log();
|
|
5942
|
-
}
|
|
5943
|
-
function printCheckItem(item) {
|
|
5944
|
-
if (item.ok) {
|
|
5945
|
-
console.log(` ${import_picocolors7.default.green("\u2713")} ${item.label}`);
|
|
5946
|
-
} else {
|
|
5947
|
-
console.log(` ${import_picocolors7.default.red("\u2717")} ${item.label}`);
|
|
5948
|
-
if (item.fix) {
|
|
5949
|
-
for (const line of item.fix.split("\n")) console.log(` ${import_picocolors7.default.red("\u2022")} ${line}`);
|
|
5950
|
-
}
|
|
5951
|
-
}
|
|
5952
|
-
}
|
|
5953
|
-
async function probeReadiness(sql, opts = {}) {
|
|
5954
|
-
const publication = opts.publication ?? ABLO_PUBLICATION;
|
|
5955
|
-
const items = [];
|
|
5956
|
-
const walRows = await sql.unsafe(
|
|
5957
|
-
`SELECT setting FROM pg_settings WHERE name = 'wal_level'`
|
|
5958
|
-
);
|
|
5959
|
-
const walLevel = walRows[0]?.setting ?? "unknown";
|
|
5960
|
-
items.push(
|
|
5961
|
-
walLevel === "logical" ? { ok: true, label: `wal_level is ${import_picocolors7.default.bold("logical")}` } : {
|
|
5962
|
-
ok: false,
|
|
5963
|
-
label: `wal_level is ${import_picocolors7.default.bold(walLevel)} (need ${import_picocolors7.default.bold("logical")})`,
|
|
5964
|
-
fix: `ALTER SYSTEM SET wal_level = 'logical'; then RESTART Postgres.
|
|
5965
|
-
On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.
|
|
5966
|
-
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.`
|
|
5967
|
-
}
|
|
5968
|
-
);
|
|
5969
|
-
const pubRows = await sql.unsafe(
|
|
5970
|
-
`SELECT puballtables FROM pg_publication WHERE pubname = $1`,
|
|
5971
|
-
[publication]
|
|
5972
|
-
);
|
|
5973
|
-
const pubRow = pubRows[0];
|
|
5974
|
-
items.push(
|
|
5975
|
-
pubRow ? {
|
|
5976
|
-
ok: true,
|
|
5977
|
-
label: `publication ${import_picocolors7.default.bold(publication)} exists ${import_picocolors7.default.dim(pubRow.puballtables ? "(all tables)" : "(table subset)")}`
|
|
5978
|
-
} : {
|
|
5979
|
-
ok: false,
|
|
5980
|
-
label: `publication ${import_picocolors7.default.bold(publication)} not found`,
|
|
5981
|
-
fix: `CREATE PUBLICATION ${quoteIdent(publication)} FOR ALL TABLES;`
|
|
5982
|
-
}
|
|
5983
|
-
);
|
|
5984
|
-
const roleRows = await sql.unsafe(
|
|
5985
|
-
`SELECT rolreplication, rolsuper FROM pg_roles WHERE rolname = current_user`
|
|
5986
|
-
);
|
|
5987
|
-
const role = roleRows[0];
|
|
5988
|
-
const hasReplication = Boolean(role && (role.rolreplication || role.rolsuper));
|
|
5989
|
-
items.push(
|
|
5990
|
-
hasReplication ? { ok: true, label: `DATABASE_URL role can stream replication ${import_picocolors7.default.dim("(REPLICATION)")}` } : {
|
|
5991
|
-
ok: false,
|
|
5992
|
-
label: `DATABASE_URL role lacks the ${import_picocolors7.default.bold("REPLICATION")} attribute`,
|
|
5993
|
-
fix: `ALTER ROLE current_user WITH REPLICATION;
|
|
5994
|
-
On RDS: GRANT rds_replication TO <your_role>;`
|
|
5995
|
-
}
|
|
5996
|
-
);
|
|
5997
|
-
if (pubRows.length > 0) {
|
|
5998
|
-
const badRows = await sql.unsafe(
|
|
5999
|
-
`SELECT c.relname AS table_name, c.relreplident
|
|
6000
|
-
FROM pg_publication_tables pt
|
|
6001
|
-
JOIN pg_class c ON c.relname = pt.tablename
|
|
6002
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
|
|
6003
|
-
WHERE pt.pubname = $1
|
|
6004
|
-
AND (
|
|
6005
|
-
c.relreplident = 'n'
|
|
6006
|
-
OR (
|
|
6007
|
-
c.relreplident = 'd'
|
|
6008
|
-
AND NOT EXISTS (
|
|
6009
|
-
SELECT 1 FROM pg_index i
|
|
6010
|
-
WHERE i.indrelid = c.oid AND i.indisprimary
|
|
6011
|
-
)
|
|
6012
|
-
)
|
|
6013
|
-
)`,
|
|
6014
|
-
[publication]
|
|
6015
|
-
);
|
|
6016
|
-
items.push(
|
|
6017
|
-
badRows.length === 0 ? { ok: true, label: `all published tables have a usable REPLICA IDENTITY` } : {
|
|
6018
|
-
ok: false,
|
|
6019
|
-
label: `${badRows.length} published table${badRows.length === 1 ? "" : "s"} cannot replicate UPDATE/DELETE`,
|
|
6020
|
-
fix: badRows.map(
|
|
6021
|
-
(r2) => `${r2.table_name}: add a PRIMARY KEY, or ALTER TABLE ${quoteIdent(r2.table_name)} REPLICA IDENTITY FULL;`
|
|
6022
|
-
).join("\n")
|
|
6023
|
-
}
|
|
6024
|
-
);
|
|
6025
|
-
}
|
|
6026
|
-
return items;
|
|
6027
|
-
}
|
|
6028
|
-
async function probeDirectWriteReadiness(sql, opts = {}) {
|
|
6029
|
-
const schema = opts.schema ?? "public";
|
|
6030
|
-
const publication = opts.publication ?? ABLO_PUBLICATION;
|
|
6031
|
-
const items = [];
|
|
6032
|
-
const roleRows = await sql.unsafe(
|
|
6033
|
-
`SELECT rolname, rolsuper, rolbypassrls, rolcreatedb, rolcreaterole, rolreplication
|
|
6034
|
-
FROM pg_roles WHERE rolname = current_user`
|
|
6035
|
-
);
|
|
6036
|
-
const role = roleRows[0];
|
|
6037
|
-
const dangerous = Boolean(
|
|
6038
|
-
!role || role.rolsuper || role.rolbypassrls || role.rolcreatedb || role.rolcreaterole || role.rolreplication
|
|
6039
|
-
);
|
|
6040
|
-
items.push(
|
|
6041
|
-
dangerous ? {
|
|
6042
|
-
ok: false,
|
|
6043
|
-
label: `ABLO_WRITE_DATABASE_URL is not a scoped DML role`,
|
|
6044
|
-
fix: `Use ${ABLO_WRITE_ROLE} from the setup recipe: NOSUPERUSER, NOBYPASSRLS, NOCREATEDB, NOCREATEROLE, NOREPLICATION.`
|
|
6045
|
-
} : { ok: true, label: `write role ${import_picocolors7.default.bold(role?.rolname ?? ABLO_WRITE_ROLE)} is DML-only` }
|
|
6046
|
-
);
|
|
6047
|
-
const schemaRows = await sql.unsafe(
|
|
6048
|
-
`SELECT
|
|
6049
|
-
has_schema_privilege(current_user, $1, 'USAGE') AS usage,
|
|
6050
|
-
has_schema_privilege(current_user, $1, 'CREATE') AS create,
|
|
6051
|
-
current_setting('row_security') AS row_security`,
|
|
6052
|
-
[schema]
|
|
6053
|
-
);
|
|
6054
|
-
const schemaRow = schemaRows[0];
|
|
6055
|
-
items.push(
|
|
6056
|
-
schemaRow?.usage && !schemaRow.create && schemaRow.row_security === "on" ? { ok: true, label: `writer uses ${schema} with RLS on and no schema CREATE` } : {
|
|
6057
|
-
ok: false,
|
|
6058
|
-
label: `writer schema/RLS privileges are not least-privilege`,
|
|
6059
|
-
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;`
|
|
6060
|
-
}
|
|
6061
|
-
);
|
|
6062
|
-
const ledgerName = `${quoteIdent(schema)}.${quoteIdent("ablo_idempotency")}`;
|
|
6063
|
-
const ledgerRows = await sql.unsafe(
|
|
6064
|
-
`SELECT
|
|
6065
|
-
to_regclass($1) IS NOT NULL AS present,
|
|
6066
|
-
CASE WHEN to_regclass($1) IS NULL THEN false ELSE has_table_privilege(current_user, $1, 'SELECT,INSERT,UPDATE') END AS writes,
|
|
6067
|
-
CASE WHEN to_regclass($1) IS NULL THEN false ELSE has_table_privilege(current_user, $1, 'DELETE') END AS deletes`,
|
|
6068
|
-
[ledgerName]
|
|
6069
|
-
);
|
|
6070
|
-
const ledger = ledgerRows[0];
|
|
6071
|
-
items.push(
|
|
6072
|
-
ledger?.present && ledger.writes && !ledger.deletes ? { ok: true, label: `${import_picocolors7.default.bold("ablo_idempotency")} is durable and protected from DELETE` } : {
|
|
6073
|
-
ok: false,
|
|
6074
|
-
label: `${import_picocolors7.default.bold("ablo_idempotency")} is missing or has unsafe grants`,
|
|
6075
|
-
fix: `Apply the DML-role and idempotency-ledger statements printed by ${import_picocolors7.default.bold("ablo connect")}.`
|
|
6076
|
-
}
|
|
6077
|
-
);
|
|
6078
|
-
const tableRows = await sql.unsafe(
|
|
6079
|
-
`SELECT format('%I.%I', schemaname, tablename) AS relation
|
|
6080
|
-
FROM pg_publication_tables
|
|
6081
|
-
WHERE pubname = $1 AND schemaname = $2 AND tablename <> 'ablo_idempotency'
|
|
6082
|
-
AND NOT has_table_privilege(
|
|
6083
|
-
current_user,
|
|
6084
|
-
format('%I.%I', schemaname, tablename),
|
|
6085
|
-
'SELECT,INSERT,UPDATE,DELETE'
|
|
6086
|
-
)`,
|
|
6087
|
-
[publication, schema]
|
|
6088
|
-
);
|
|
6089
|
-
items.push(
|
|
6090
|
-
tableRows.length === 0 ? { ok: true, label: `writer can apply DML to every published application table` } : {
|
|
6091
|
-
ok: false,
|
|
6092
|
-
label: `writer lacks DML on ${tableRows.length} published table${tableRows.length === 1 ? "" : "s"}`,
|
|
6093
|
-
fix: `Grant SELECT, INSERT, UPDATE, DELETE on: ${tableRows.map((row) => row.relation).join(", ")}`
|
|
6094
|
-
}
|
|
6095
|
-
);
|
|
6096
|
-
return items;
|
|
6097
|
-
}
|
|
6098
|
-
async function auditTenantSyncInfra(sql) {
|
|
6099
|
-
const artifacts = [];
|
|
6100
|
-
for (const name of SYNC_INFRA_RELATIONS) {
|
|
6101
|
-
const rows = await sql.unsafe(
|
|
6102
|
-
`SELECT to_regclass($1)::text AS reg`,
|
|
6103
|
-
[`public.${name}`]
|
|
6104
|
-
);
|
|
6105
|
-
artifacts.push({ kind: "relation", name, present: rows[0]?.reg != null });
|
|
6106
|
-
}
|
|
6107
|
-
for (const name of SYNC_INFRA_TYPES) {
|
|
6108
|
-
const rows = await sql.unsafe(
|
|
6109
|
-
`SELECT to_regtype($1)::text AS reg`,
|
|
6110
|
-
[`public.${name}`]
|
|
6111
|
-
);
|
|
6112
|
-
artifacts.push({ kind: "type", name, present: rows[0]?.reg != null });
|
|
6113
|
-
}
|
|
6114
|
-
return artifacts;
|
|
6115
|
-
}
|
|
6116
|
-
function requireDatabaseUrl(verb, opts) {
|
|
6117
|
-
const resolved = readProjectReplicationUrlWithSource();
|
|
6118
|
-
if (!resolved) {
|
|
6119
|
-
console.error(
|
|
6120
|
-
import_picocolors7.default.red(" No replication connection found (checked process env, .env.local, .env).") + import_picocolors7.default.dim(
|
|
6121
|
-
` Set ${import_picocolors7.default.bold("ABLO_REPLICATION_DATABASE_URL")} to the ${ABLO_REPLICATION_ROLE} connection printed by ${import_picocolors7.default.bold("ablo connect")}, then re-run ${import_picocolors7.default.bold(`ablo connect ${verb}`)}.`
|
|
6122
|
-
)
|
|
6123
|
-
);
|
|
6124
|
-
process.exit(1);
|
|
6125
|
-
}
|
|
6126
|
-
if (resolved.variable === "DATABASE_URL") {
|
|
6127
|
-
console.warn(
|
|
6128
|
-
import_picocolors7.default.yellow(` ! Falling back to ${import_picocolors7.default.bold("DATABASE_URL")} for the replication role \u2014 deprecated,`) + import_picocolors7.default.dim(
|
|
6129
|
-
` removed in ${DATABASE_URL_REMOVAL_VERSION}.
|
|
6130
|
-
If this is your app's own connection string, this is NOT validating the ${import_picocolors7.default.bold(ABLO_REPLICATION_ROLE)} role.
|
|
6131
|
-
Set ${import_picocolors7.default.bold("ABLO_REPLICATION_DATABASE_URL")} to the connection ${import_picocolors7.default.bold("ablo connect")} printed.
|
|
6132
|
-
`
|
|
6133
|
-
)
|
|
6134
|
-
);
|
|
6135
|
-
if (!(opts?.allowDeprecatedFallback ?? false)) {
|
|
6136
|
-
console.error(
|
|
6137
|
-
import_picocolors7.default.red(` Refusing to ${verb.replace(/^--/, "")} against a ${import_picocolors7.default.bold("DATABASE_URL")} fallback.`) + import_picocolors7.default.dim(
|
|
6138
|
-
` Set ${import_picocolors7.default.bold("ABLO_REPLICATION_DATABASE_URL")} explicitly and re-run. (${import_picocolors7.default.bold("ablo connect --check")} still accepts the fallback so you can inspect it.)
|
|
6139
|
-
`
|
|
6140
|
-
)
|
|
6141
|
-
);
|
|
6142
|
-
process.exit(1);
|
|
6143
|
-
}
|
|
6144
|
-
}
|
|
6145
|
-
return resolved.url;
|
|
6146
|
-
}
|
|
6147
|
-
function requireWriteDatabaseUrl(verb) {
|
|
6148
|
-
const dbUrl = readProjectWriteDatabaseUrl();
|
|
6149
|
-
if (!dbUrl) {
|
|
6150
|
-
console.error(
|
|
6151
|
-
import_picocolors7.default.red(" No ABLO_WRITE_DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors7.default.dim(
|
|
6152
|
-
` Set it to the ${ABLO_WRITE_ROLE} connection printed by ${import_picocolors7.default.bold("ablo connect")}, then re-run ${import_picocolors7.default.bold(`ablo connect ${verb}`)}. The replication credential is never reused for DML.`
|
|
6153
|
-
)
|
|
6154
|
-
);
|
|
6155
|
-
process.exit(1);
|
|
6156
|
-
}
|
|
6157
|
-
return dbUrl;
|
|
6158
|
-
}
|
|
6159
|
-
async function probeAndReport(dbUrl, kind = "replication") {
|
|
6160
|
-
const sql = src_default(dbUrl, { max: 1, prepare: false, connect_timeout: 10, onnotice: () => {
|
|
6161
|
-
} });
|
|
6162
|
-
let items;
|
|
6163
|
-
try {
|
|
6164
|
-
items = kind === "replication" ? await probeReadiness(sql) : await probeDirectWriteReadiness(sql);
|
|
6165
|
-
} catch (err) {
|
|
6166
|
-
await sql.end({ timeout: 2 }).catch(() => void 0);
|
|
6167
|
-
const dial = dialFailureReason(err);
|
|
6168
|
-
if (dial) return { kind: "no-dial", reason: dial };
|
|
6169
|
-
const pg = err ?? {};
|
|
6170
|
-
console.error(import_picocolors7.default.red(` Couldn't read the database: ${pg.message ?? String(err)}`));
|
|
6171
|
-
process.exit(1);
|
|
6172
|
-
}
|
|
6173
|
-
await sql.end({ timeout: 2 });
|
|
6174
|
-
for (const item of items) printCheckItem(item);
|
|
6175
|
-
return { kind: "probed", failures: items.filter((i) => !i.ok).length };
|
|
6176
|
-
}
|
|
6177
|
-
async function runRemoteCheck(dbUrl, writeDbUrl, localReason) {
|
|
6178
|
-
console.log(
|
|
6179
|
-
` This machine can't reach one or both scoped connections (${import_picocolors7.default.dim(localReason)}).
|
|
6180
|
-
That is not the verdict: direct writes and replication run from Ablo's infrastructure.
|
|
6181
|
-
Asking Ablo to check both roles from its side\u2026
|
|
6182
|
-
`
|
|
6183
|
-
);
|
|
6184
|
-
const apiKey = resolveApiKey();
|
|
6185
|
-
if (!apiKey) {
|
|
6186
|
-
console.error(
|
|
6187
|
-
import_picocolors7.default.red(` The engine-side check needs an API key, and none was found.`) + import_picocolors7.default.dim(
|
|
6188
|
-
` 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")}.`
|
|
6189
|
-
)
|
|
6190
|
-
);
|
|
6191
|
-
process.exit(1);
|
|
6192
|
-
}
|
|
6193
|
-
const apiUrl2 = (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
6194
|
-
const result = await requestRemoteValidation({
|
|
6195
|
-
apiUrl: apiUrl2,
|
|
6196
|
-
apiKey,
|
|
6197
|
-
connectionString: dbUrl,
|
|
6198
|
-
writeConnectionString: writeDbUrl
|
|
6199
|
-
});
|
|
6200
|
-
if (!result.ok) {
|
|
6201
|
-
console.error(import_picocolors7.default.red(` The engine-side check failed: ${result.message}`));
|
|
6202
|
-
if (result.code === "forbidden") {
|
|
6203
|
-
console.error(
|
|
6204
|
-
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.`)
|
|
6205
|
-
);
|
|
6206
|
-
}
|
|
6207
|
-
console.error();
|
|
6208
|
-
process.exit(1);
|
|
6209
|
-
}
|
|
6210
|
-
if (!result.reachable) {
|
|
6211
|
-
console.error(
|
|
6212
|
-
` ${import_picocolors7.default.red("\u2717")} Ablo's infrastructure can't reach both direct connections${result.reason ? ` ${import_picocolors7.default.dim(`(${result.reason})`)}` : ""}.`
|
|
6213
|
-
);
|
|
6214
|
-
console.error(
|
|
6215
|
-
import_picocolors7.default.dim(
|
|
6216
|
-
` Direct needs a route Ablo's servers can dial \u2014 public allowlist, PrivateLink, peering,
|
|
6217
|
-
or VPN. Only when no inbound route can exist, use the signed ${import_picocolors7.default.bold("dataSource()")} endpoint fallback.
|
|
6218
|
-
`
|
|
6219
|
-
)
|
|
6220
|
-
);
|
|
6221
|
-
process.exit(1);
|
|
6222
|
-
}
|
|
6223
|
-
for (const failure of result.failures) {
|
|
6224
|
-
const { label, fix } = describeRemoteFailure(failure);
|
|
6225
|
-
printCheckItem({ ok: false, label, fix });
|
|
6226
|
-
}
|
|
6227
|
-
console.log();
|
|
6228
|
-
if (result.ready) {
|
|
6229
|
-
console.log(
|
|
6230
|
-
` ${import_picocolors7.default.green("\u2713")} Ready \u2014 checked from Ablo's infrastructure. Both direct DML and WAL settlement are available.
|
|
6231
|
-
`
|
|
6232
|
-
);
|
|
6233
|
-
process.exit(0);
|
|
6234
|
-
}
|
|
6235
|
-
const count = result.failures.length;
|
|
6236
|
-
console.log(
|
|
6237
|
-
` ${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")}.`)}
|
|
6238
|
-
`
|
|
6239
|
-
);
|
|
6240
|
-
process.exit(1);
|
|
6241
|
-
}
|
|
6242
|
-
async function runCheck() {
|
|
6243
|
-
const dbUrl = requireDatabaseUrl("--check", { allowDeprecatedFallback: true });
|
|
6244
|
-
const writeDbUrl = requireWriteDatabaseUrl("--check");
|
|
6245
|
-
console.log(`
|
|
6246
|
-
${brand("ablo")} ${import_picocolors7.default.dim("connect --check")} ${import_picocolors7.default.dim("direct-write + WAL readiness")}
|
|
6247
|
-
`);
|
|
6248
|
-
console.log(` ${import_picocolors7.default.bold("Replication role")}
|
|
6249
|
-
`);
|
|
6250
|
-
const replication = await probeAndReport(dbUrl, "replication");
|
|
6251
|
-
console.log(`
|
|
6252
|
-
${import_picocolors7.default.bold("Direct-write role")}
|
|
6253
|
-
`);
|
|
6254
|
-
const write = await probeAndReport(writeDbUrl, "write");
|
|
6255
|
-
if (replication.kind === "no-dial" || write.kind === "no-dial") {
|
|
6256
|
-
const reasons = [
|
|
6257
|
-
replication.kind === "no-dial" ? `replication: ${replication.reason}` : null,
|
|
6258
|
-
write.kind === "no-dial" ? `write: ${write.reason}` : null
|
|
6259
|
-
].filter((reason) => reason !== null);
|
|
6260
|
-
return runRemoteCheck(dbUrl, writeDbUrl, reasons.join("; "));
|
|
6261
|
-
}
|
|
6262
|
-
const failures = replication.failures + write.failures;
|
|
6263
|
-
console.log();
|
|
6264
|
-
if (failures === 0) {
|
|
6265
|
-
console.log(` ${import_picocolors7.default.green("\u2713")} Ready \u2014 Ablo can apply scoped DML and settle it from WAL.
|
|
6266
|
-
`);
|
|
6267
|
-
process.exit(0);
|
|
6268
|
-
}
|
|
6269
|
-
console.log(
|
|
6270
|
-
` ${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")}.`)}
|
|
6271
|
-
`
|
|
6272
|
-
);
|
|
6273
|
-
process.exit(1);
|
|
6274
|
-
}
|
|
6275
|
-
function registerEndpoint(baseUrl2) {
|
|
6276
|
-
return `${baseUrl2.replace(/\/+$/, "")}/api/v1/datasources`;
|
|
6277
|
-
}
|
|
6278
|
-
async function runRegister(args) {
|
|
6279
|
-
const dbUrl = requireDatabaseUrl("--register");
|
|
6280
|
-
const writeDbUrl = requireWriteDatabaseUrl("--register");
|
|
6281
|
-
const apiKey = resolveApiKey();
|
|
6282
|
-
if (!apiKey) {
|
|
6283
|
-
console.error(
|
|
6284
|
-
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.`)
|
|
6285
|
-
);
|
|
6286
|
-
process.exit(1);
|
|
6287
|
-
}
|
|
6288
|
-
console.log(`
|
|
6289
|
-
${brand("ablo")} ${import_picocolors7.default.dim("connect --register")} ${import_picocolors7.default.dim("register a direct DataSource")}
|
|
6290
|
-
`);
|
|
6291
|
-
console.log(` ${import_picocolors7.default.bold("Replication role")}
|
|
6292
|
-
`);
|
|
6293
|
-
const replication = await probeAndReport(dbUrl, "replication");
|
|
6294
|
-
console.log(`
|
|
6295
|
-
${import_picocolors7.default.bold("Direct-write role")}
|
|
6296
|
-
`);
|
|
6297
|
-
const write = await probeAndReport(writeDbUrl, "write");
|
|
6298
|
-
const noDial = [
|
|
6299
|
-
replication.kind === "no-dial" ? `replication: ${replication.reason}` : null,
|
|
6300
|
-
write.kind === "no-dial" ? `write: ${write.reason}` : null
|
|
6301
|
-
].filter((reason) => reason !== null);
|
|
6302
|
-
const failures = (replication.kind === "probed" ? replication.failures : 0) + (write.kind === "probed" ? write.failures : 0);
|
|
6303
|
-
if (noDial.length > 0) {
|
|
6304
|
-
console.log(
|
|
6305
|
-
` This machine can't reach one or both scoped connections (${import_picocolors7.default.dim(noDial.join("; "))}) \u2014 continuing anyway.
|
|
6306
|
-
Ablo validates both credentials from the infrastructure that will use them and refuses
|
|
6307
|
-
registration unless replication and direct DML are both ready.
|
|
6308
|
-
`
|
|
6309
|
-
);
|
|
6310
|
-
}
|
|
6311
|
-
if (failures > 0) {
|
|
6312
|
-
console.log(
|
|
6313
|
-
`
|
|
6314
|
-
${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.")}
|
|
6315
|
-
`
|
|
6316
|
-
);
|
|
6317
|
-
process.exit(1);
|
|
6318
|
-
}
|
|
6319
|
-
const apiUrl2 = (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
6320
|
-
const registered = await registerDirectDataSource({
|
|
6321
|
-
apiUrl: apiUrl2,
|
|
6322
|
-
apiKey,
|
|
6323
|
-
replicationUrl: dbUrl,
|
|
6324
|
-
writeUrl: writeDbUrl,
|
|
6325
|
-
route: args.route
|
|
6326
|
-
});
|
|
6327
|
-
process.exit(registered ? 0 : 1);
|
|
6328
|
-
}
|
|
6329
|
-
async function registerDirectDataSource(opts) {
|
|
6330
|
-
let res;
|
|
6331
|
-
try {
|
|
6332
|
-
res = await fetch(registerEndpoint(opts.apiUrl), {
|
|
6333
|
-
method: "POST",
|
|
6334
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${opts.apiKey}` },
|
|
6335
|
-
body: JSON.stringify({
|
|
6336
|
-
connection: "direct",
|
|
6337
|
-
connectionString: opts.replicationUrl,
|
|
6338
|
-
writeConnectionString: opts.writeUrl,
|
|
6339
|
-
route: opts.route
|
|
6340
|
-
})
|
|
6341
|
-
});
|
|
6342
|
-
} catch (err) {
|
|
6343
|
-
console.error(import_picocolors7.default.red(`
|
|
6344
|
-
Couldn't reach ${opts.apiUrl}: ${err instanceof Error ? err.message : String(err)}
|
|
6345
|
-
`));
|
|
6346
|
-
return false;
|
|
6347
|
-
}
|
|
6348
|
-
if (res.ok) {
|
|
6349
|
-
const body2 = await res.json().catch(() => ({}));
|
|
6350
|
-
const statusNote = body2.status === "active" ? `${opts.route}, active` : opts.route;
|
|
6351
|
-
console.log(
|
|
6352
|
-
`
|
|
6353
|
-
${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}).
|
|
6354
|
-
Customer COMMIT is durable acceptance; correlated WAL promotes queued writes to confirmed.
|
|
6355
|
-
`
|
|
6356
|
-
);
|
|
6357
|
-
return true;
|
|
6358
|
-
}
|
|
6359
|
-
const body = await res.json().catch(() => ({}));
|
|
6360
|
-
const code = body.code ?? body.error?.code;
|
|
6361
|
-
const message = body.message ?? body.error?.message ?? `HTTP ${res.status}`;
|
|
6362
|
-
console.error(import_picocolors7.default.red(`
|
|
6363
|
-
Registration failed: ${message}`));
|
|
6364
|
-
if (code === "forbidden") {
|
|
6365
|
-
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.`));
|
|
6366
|
-
} else if (code === "datasource_connection_unsupported") {
|
|
6367
|
-
console.error(
|
|
6368
|
-
import_picocolors7.default.dim(` This deployment can\u2019t accept connection strings \u2014 use a self-hosted/hosted engine, or the signed endpoint fallback.`)
|
|
6369
|
-
);
|
|
6370
|
-
} else if (code === "database_not_replication_ready" || code === "data_source_blocked") {
|
|
6371
|
-
for (const f of body.details?.failures ?? []) {
|
|
6372
|
-
console.error(` ${import_picocolors7.default.red("\u2717")} ${import_picocolors7.default.bold(f.item ?? "item")}${f.actual ? import_picocolors7.default.dim(` (${f.actual})`) : ""}`);
|
|
6373
|
-
if (f.fix) for (const line of f.fix.split("\n")) console.error(` ${import_picocolors7.default.red("\u2022")} ${line}`);
|
|
6374
|
-
}
|
|
6375
|
-
console.error(import_picocolors7.default.dim(`
|
|
6376
|
-
Apply the fixes, verify with ${import_picocolors7.default.bold("ablo connect --check")}, then re-run.`));
|
|
6377
|
-
} else if (code === "database_unreachable" || code === "source_unreachable") {
|
|
6378
|
-
if (body.details?.reason) console.error(import_picocolors7.default.dim(` ${body.details.reason}`));
|
|
6379
|
-
console.error(
|
|
6380
|
-
import_picocolors7.default.dim(
|
|
6381
|
-
` Ablo's servers must be able to reach this database \u2014 a localhost or private-network
|
|
6382
|
-
Postgres can't use the direct path. Establish an allowlist, PrivateLink, peering, or VPN.
|
|
6383
|
-
Only when no inbound route is possible, register the signed ${import_picocolors7.default.bold("dataSource()")} endpoint fallback.`
|
|
6384
|
-
)
|
|
6385
|
-
);
|
|
6386
|
-
}
|
|
6387
|
-
console.error();
|
|
6388
|
-
return false;
|
|
6389
|
-
}
|
|
6390
|
-
async function runAuditInfra() {
|
|
6391
|
-
const dbUrl = requireDatabaseUrl("--audit-infra", { allowDeprecatedFallback: true });
|
|
6392
|
-
const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
|
|
6393
|
-
} });
|
|
6394
|
-
let artifacts;
|
|
6395
|
-
try {
|
|
6396
|
-
artifacts = await auditTenantSyncInfra(sql);
|
|
6397
|
-
} catch (err) {
|
|
6398
|
-
const pg = err ?? {};
|
|
6399
|
-
console.error(import_picocolors7.default.red(` Couldn't audit the database: ${pg.message ?? String(err)}`));
|
|
6400
|
-
await sql.end({ timeout: 2 });
|
|
6401
|
-
process.exit(1);
|
|
6402
|
-
}
|
|
6403
|
-
await sql.end({ timeout: 2 });
|
|
6404
|
-
console.log(`
|
|
6405
|
-
${brand("ablo")} ${import_picocolors7.default.dim("connect --audit-infra")} ${import_picocolors7.default.dim("Stage 5 tenant DB sync-infra audit")}
|
|
6406
|
-
`);
|
|
6407
|
-
const present = artifacts.filter((a) => a.present);
|
|
6408
|
-
if (present.length === 0) {
|
|
6409
|
-
console.log(` ${import_picocolors7.default.green("\u2713")} No deprecated Ablo sync infrastructure found in public.
|
|
6410
|
-
`);
|
|
6411
|
-
process.exit(0);
|
|
6412
|
-
}
|
|
6413
|
-
for (const artifact of present) {
|
|
6414
|
-
const label = artifact.kind === "type" ? "type" : "relation";
|
|
6415
|
-
console.log(` ${import_picocolors7.default.yellow("!")} ${label} ${import_picocolors7.default.bold(`public.${artifact.name}`)} exists`);
|
|
6416
|
-
}
|
|
6417
|
-
console.log(
|
|
6418
|
-
`
|
|
6419
|
-
${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")
|
|
6420
|
-
);
|
|
6421
|
-
process.exit(1);
|
|
6422
|
-
}
|
|
6423
|
-
async function connect(argv) {
|
|
6424
|
-
let args;
|
|
6425
|
-
try {
|
|
6426
|
-
args = parseConnectArgs(argv);
|
|
6427
|
-
} catch (err) {
|
|
6428
|
-
console.error(import_picocolors7.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
6429
|
-
process.exit(1);
|
|
6430
|
-
}
|
|
6431
|
-
if (args.apply || args.rotate) {
|
|
6432
|
-
const { runConnectApply: runConnectApply2 } = await Promise.resolve().then(() => (init_connectApply(), connectApply_exports));
|
|
6433
|
-
await runConnectApply2(args);
|
|
6434
|
-
return;
|
|
6435
|
-
}
|
|
6436
|
-
if (args.check) {
|
|
6437
|
-
await runCheck();
|
|
6438
|
-
return;
|
|
6439
|
-
}
|
|
6440
|
-
if (args.register) {
|
|
6441
|
-
await runRegister(args);
|
|
6442
|
-
return;
|
|
6443
|
-
}
|
|
6444
|
-
if (args.auditInfra) {
|
|
6445
|
-
await runAuditInfra();
|
|
6446
|
-
return;
|
|
6447
|
-
}
|
|
6448
|
-
printConnectRecipe(args);
|
|
6449
|
-
}
|
|
6450
|
-
var import_picocolors7, ABLO_PUBLICATION, ABLO_REPLICATION_ROLE, ABLO_WRITE_ROLE, DIRECT_DATA_SOURCE_ROUTES, SYNC_INFRA_RELATIONS, SYNC_INFRA_TYPES, CONNECT_USAGE;
|
|
6451
|
-
var init_connect = __esm({
|
|
6452
|
-
"src/cli/connect.ts"() {
|
|
6453
|
-
"use strict";
|
|
6454
|
-
init_cjs_shims();
|
|
6455
|
-
init_errors();
|
|
6456
|
-
import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
6457
|
-
init_src();
|
|
6458
|
-
init_dbRole();
|
|
6459
|
-
init_config();
|
|
6460
|
-
init_push();
|
|
6461
|
-
init_theme();
|
|
6462
|
-
init_remoteValidation();
|
|
6463
|
-
init_migrations();
|
|
6464
|
-
ABLO_PUBLICATION = "ablo_publication";
|
|
6465
|
-
ABLO_REPLICATION_ROLE = "ablo_replicator";
|
|
6466
|
-
ABLO_WRITE_ROLE = "ablo_writer";
|
|
6467
|
-
DIRECT_DATA_SOURCE_ROUTES = [
|
|
6468
|
-
"public-allowlist",
|
|
6469
|
-
"privatelink",
|
|
6470
|
-
"peering",
|
|
6471
|
-
"vpn"
|
|
6472
|
-
];
|
|
6473
|
-
SYNC_INFRA_RELATIONS = [
|
|
6474
|
-
"sync_deltas",
|
|
6475
|
-
"sync_id_seq",
|
|
6476
|
-
"sync_metadata",
|
|
6477
|
-
"mutation_log"
|
|
6478
|
-
];
|
|
6479
|
-
SYNC_INFRA_TYPES = [
|
|
6480
|
-
"participant_kind",
|
|
6481
|
-
"backfill_provenance",
|
|
6482
|
-
"confirmation_state"
|
|
6483
|
-
];
|
|
6484
|
-
CONNECT_USAGE = ` ablo connect \u2014 direct writes, settled by logical replication
|
|
6485
|
-
|
|
6486
|
-
Ablo applies coordinated DML with a scoped writer role. WAL observes what committed,
|
|
6487
|
-
orders it with external changes, and confirms it. Your Postgres remains authoritative.
|
|
6488
|
-
|
|
6489
|
-
Usage:
|
|
6490
|
-
npx ablo connect --apply Set it up end to end: create the roles, publish, register
|
|
6491
|
-
npx ablo connect --url <admin-conn> Admin connection for --apply/--rotate (else DATABASE_URL)
|
|
6492
|
-
npx ablo connect --rotate New passwords for both scoped roles, then re-register
|
|
6493
|
-
npx ablo connect Print the exact setup SQL instead of running it
|
|
6494
|
-
npx ablo connect --tables a,b,c Publish only these tables (default: all tables)
|
|
6495
|
-
npx ablo connect --role <name> Name the replication role (default: ablo_replicator)
|
|
6496
|
-
npx ablo connect --write-role <name> Name the DML role (default: ablo_writer)
|
|
6497
|
-
npx ablo connect --route <route> public-allowlist | privatelink | peering | vpn
|
|
6498
|
-
npx ablo connect --check Validate the scoped roles (ABLO_REPLICATION_DATABASE_URL + ABLO_WRITE_DATABASE_URL)
|
|
6499
|
-
npx ablo connect --register Register both scoped credentials as one direct DataSource
|
|
6500
|
-
npx ablo connect --audit-infra Read-only Stage 5 audit for deprecated Ablo sync tables/types
|
|
6501
|
-
|
|
6502
|
-
--apply registers with Ablo directly; the admin credential is used only on this
|
|
6503
|
-
machine and never persisted. Your app holds only ABLO_API_KEY.
|
|
6504
|
-
|
|
6505
|
-
--apply also takes --yes (skip the confirm) and --show-sql (show the exact statements).`;
|
|
6506
|
-
}
|
|
6507
|
-
});
|
|
6508
|
-
|
|
6509
6244
|
// node_modules/source-map/lib/base64.js
|
|
6510
6245
|
var require_base64 = __commonJS({
|
|
6511
6246
|
"node_modules/source-map/lib/base64.js"(exports2) {
|
|
@@ -218523,11 +218258,11 @@ var require_commonjs2 = __commonJS({
|
|
|
218523
218258
|
if (pad) {
|
|
218524
218259
|
const need = width - c.length;
|
|
218525
218260
|
if (need > 0) {
|
|
218526
|
-
const
|
|
218261
|
+
const z8 = new Array(need + 1).join("0");
|
|
218527
218262
|
if (i < 0) {
|
|
218528
|
-
c = "-" +
|
|
218263
|
+
c = "-" + z8 + c.slice(1);
|
|
218529
218264
|
} else {
|
|
218530
|
-
c =
|
|
218265
|
+
c = z8 + c;
|
|
218531
218266
|
}
|
|
218532
218267
|
}
|
|
218533
218268
|
}
|
|
@@ -282456,7 +282191,7 @@ Node text: ${this.#forgottenText}`;
|
|
|
282456
282191
|
// src/cli/index.ts
|
|
282457
282192
|
init_cjs_shims();
|
|
282458
282193
|
init_dist2();
|
|
282459
|
-
var
|
|
282194
|
+
var import_picocolors23 = __toESM(require_picocolors(), 1);
|
|
282460
282195
|
var import_fs12 = require("fs");
|
|
282461
282196
|
var import_path7 = require("path");
|
|
282462
282197
|
var import_child_process3 = require("child_process");
|
|
@@ -282644,8 +282379,739 @@ async function migrate(argv) {
|
|
|
282644
282379
|
}
|
|
282645
282380
|
}
|
|
282646
282381
|
|
|
282382
|
+
// src/cli/connect.ts
|
|
282383
|
+
init_cjs_shims();
|
|
282384
|
+
init_errors();
|
|
282385
|
+
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
282386
|
+
init_src();
|
|
282387
|
+
init_dbRole();
|
|
282388
|
+
init_config();
|
|
282389
|
+
init_push();
|
|
282390
|
+
init_theme();
|
|
282391
|
+
|
|
282392
|
+
// src/cli/remoteValidation.ts
|
|
282393
|
+
init_cjs_shims();
|
|
282394
|
+
var DIAL_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
282395
|
+
"ENOTFOUND",
|
|
282396
|
+
"EAI_AGAIN",
|
|
282397
|
+
"ECONNREFUSED",
|
|
282398
|
+
"ECONNRESET",
|
|
282399
|
+
"ETIMEDOUT",
|
|
282400
|
+
"ENETUNREACH",
|
|
282401
|
+
"EHOSTUNREACH",
|
|
282402
|
+
"CONNECT_TIMEOUT"
|
|
282403
|
+
]);
|
|
282404
|
+
function dialFailureReason(err) {
|
|
282405
|
+
if (err === null || typeof err !== "object") return null;
|
|
282406
|
+
const coded = err;
|
|
282407
|
+
const code = typeof coded.code === "string" ? coded.code : null;
|
|
282408
|
+
if (code && DIAL_FAILURE_CODES.has(code)) {
|
|
282409
|
+
return typeof coded.message === "string" && coded.message.length > 0 ? coded.message : code;
|
|
282410
|
+
}
|
|
282411
|
+
if (Array.isArray(coded.errors)) {
|
|
282412
|
+
for (const member of coded.errors) {
|
|
282413
|
+
const reason = dialFailureReason(member);
|
|
282414
|
+
if (reason) return reason;
|
|
282415
|
+
}
|
|
282416
|
+
}
|
|
282417
|
+
return null;
|
|
282418
|
+
}
|
|
282419
|
+
function validateEndpoint(baseUrl2) {
|
|
282420
|
+
return `${baseUrl2.replace(/\/+$/, "")}/api/v1/datasources/validate`;
|
|
282421
|
+
}
|
|
282422
|
+
function describeRemoteFailure(failure) {
|
|
282423
|
+
switch (failure.item) {
|
|
282424
|
+
case "wal_level":
|
|
282425
|
+
return {
|
|
282426
|
+
label: failure.actual ? `wal_level is ${failure.actual} (need logical)` : `wal_level must be logical`,
|
|
282427
|
+
fix: failure.fix
|
|
282428
|
+
};
|
|
282429
|
+
case "publication":
|
|
282430
|
+
return { label: "the Ablo publication does not exist", fix: failure.fix };
|
|
282431
|
+
case "replication_role":
|
|
282432
|
+
return { label: "the DATABASE_URL role lacks the REPLICATION attribute", fix: failure.fix };
|
|
282433
|
+
case "replica_identity":
|
|
282434
|
+
return {
|
|
282435
|
+
label: `published tables cannot replicate UPDATE/DELETE${failure.actual ? ` (${failure.actual})` : ""}`,
|
|
282436
|
+
fix: failure.fix
|
|
282437
|
+
};
|
|
282438
|
+
case "write_role":
|
|
282439
|
+
return { label: "ABLO_WRITE_DATABASE_URL is not a scoped DML role", fix: failure.fix };
|
|
282440
|
+
case "row_security":
|
|
282441
|
+
return { label: "the direct-write role does not enforce row_security", fix: failure.fix };
|
|
282442
|
+
case "schema_privileges":
|
|
282443
|
+
return { label: "the direct-write role has unsafe schema privileges", fix: failure.fix };
|
|
282444
|
+
case "idempotency_ledger":
|
|
282445
|
+
return { label: "ablo_idempotency is missing or has unsafe grants", fix: failure.fix };
|
|
282446
|
+
case "table_privileges":
|
|
282447
|
+
return {
|
|
282448
|
+
label: `the direct-write role lacks application DML${failure.actual ? ` (${failure.actual})` : ""}`,
|
|
282449
|
+
fix: failure.fix
|
|
282450
|
+
};
|
|
282451
|
+
case "logical_marker":
|
|
282452
|
+
return { label: "the direct-write role cannot emit the correlation marker", fix: failure.fix };
|
|
282453
|
+
default:
|
|
282454
|
+
return { label: failure.item, fix: failure.fix };
|
|
282455
|
+
}
|
|
282456
|
+
}
|
|
282457
|
+
function parseWireFailures(value) {
|
|
282458
|
+
if (!Array.isArray(value)) return [];
|
|
282459
|
+
const failures = [];
|
|
282460
|
+
for (const entry of value) {
|
|
282461
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
282462
|
+
const { item, actual, fix } = entry;
|
|
282463
|
+
if (typeof item !== "string" || typeof fix !== "string") continue;
|
|
282464
|
+
failures.push({ item, fix, ...typeof actual === "string" ? { actual } : {} });
|
|
282465
|
+
}
|
|
282466
|
+
return failures;
|
|
282467
|
+
}
|
|
282468
|
+
async function requestRemoteValidation(input) {
|
|
282469
|
+
const doFetch = input.fetchImpl ?? fetch;
|
|
282470
|
+
let res;
|
|
282471
|
+
try {
|
|
282472
|
+
res = await doFetch(validateEndpoint(input.apiUrl), {
|
|
282473
|
+
method: "POST",
|
|
282474
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${input.apiKey}` },
|
|
282475
|
+
body: JSON.stringify({
|
|
282476
|
+
...input.connectionString ? { connectionString: input.connectionString } : {},
|
|
282477
|
+
...input.writeConnectionString ? { writeConnectionString: input.writeConnectionString } : {}
|
|
282478
|
+
})
|
|
282479
|
+
});
|
|
282480
|
+
} catch (err) {
|
|
282481
|
+
return {
|
|
282482
|
+
ok: false,
|
|
282483
|
+
status: 0,
|
|
282484
|
+
message: `couldn't reach ${input.apiUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
282485
|
+
};
|
|
282486
|
+
}
|
|
282487
|
+
const body = await res.json().catch(() => ({}));
|
|
282488
|
+
if (!res.ok) {
|
|
282489
|
+
const code = typeof body.code === "string" ? body.code : body.error?.code;
|
|
282490
|
+
const message = typeof body.message === "string" ? body.message : typeof body.error?.message === "string" ? body.error.message : `HTTP ${res.status}`;
|
|
282491
|
+
return {
|
|
282492
|
+
ok: false,
|
|
282493
|
+
status: res.status,
|
|
282494
|
+
message,
|
|
282495
|
+
...typeof code === "string" ? { code } : {}
|
|
282496
|
+
};
|
|
282497
|
+
}
|
|
282498
|
+
const reachable = body.reachable === true;
|
|
282499
|
+
return {
|
|
282500
|
+
ok: true,
|
|
282501
|
+
reachable,
|
|
282502
|
+
ready: body.ready === true,
|
|
282503
|
+
...typeof body.reason === "string" ? { reason: body.reason } : {},
|
|
282504
|
+
failures: parseWireFailures(body.failures)
|
|
282505
|
+
};
|
|
282506
|
+
}
|
|
282507
|
+
|
|
282508
|
+
// src/cli/connect.ts
|
|
282509
|
+
init_connectSetup();
|
|
282510
|
+
function parseConnectArgs(argv) {
|
|
282511
|
+
let check2 = false;
|
|
282512
|
+
let register = false;
|
|
282513
|
+
let apply2 = false;
|
|
282514
|
+
let rotate = false;
|
|
282515
|
+
let url;
|
|
282516
|
+
let yes = false;
|
|
282517
|
+
let showSql = false;
|
|
282518
|
+
let scan = false;
|
|
282519
|
+
let tables = [];
|
|
282520
|
+
let role = ABLO_REPLICATION_ROLE;
|
|
282521
|
+
let writeRole = ABLO_WRITE_ROLE;
|
|
282522
|
+
let route = "public-allowlist";
|
|
282523
|
+
let start = 0;
|
|
282524
|
+
const lead = argv[0];
|
|
282525
|
+
if (lead !== void 0 && !lead.startsWith("-")) {
|
|
282526
|
+
switch (lead) {
|
|
282527
|
+
case "register":
|
|
282528
|
+
register = true;
|
|
282529
|
+
break;
|
|
282530
|
+
case "check":
|
|
282531
|
+
check2 = true;
|
|
282532
|
+
break;
|
|
282533
|
+
case "apply":
|
|
282534
|
+
apply2 = true;
|
|
282535
|
+
break;
|
|
282536
|
+
case "rotate":
|
|
282537
|
+
rotate = true;
|
|
282538
|
+
break;
|
|
282539
|
+
case "scan":
|
|
282540
|
+
scan = true;
|
|
282541
|
+
break;
|
|
282542
|
+
default:
|
|
282543
|
+
throw new AbloValidationError(
|
|
282544
|
+
`unknown connect subcommand: ${lead} (expected register, deregister, check, apply, rotate, scan)`,
|
|
282545
|
+
{ code: "cli_invalid_arguments" }
|
|
282546
|
+
);
|
|
282547
|
+
}
|
|
282548
|
+
start = 1;
|
|
282549
|
+
}
|
|
282550
|
+
for (let i = start; i < argv.length; i++) {
|
|
282551
|
+
const arg = argv[i];
|
|
282552
|
+
switch (arg) {
|
|
282553
|
+
case "--url":
|
|
282554
|
+
url = argv[++i] ?? url;
|
|
282555
|
+
break;
|
|
282556
|
+
case "--yes":
|
|
282557
|
+
case "-y":
|
|
282558
|
+
yes = true;
|
|
282559
|
+
break;
|
|
282560
|
+
case "--show-sql":
|
|
282561
|
+
showSql = true;
|
|
282562
|
+
break;
|
|
282563
|
+
case "--tables": {
|
|
282564
|
+
const value = argv[++i] ?? "";
|
|
282565
|
+
tables = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
282566
|
+
break;
|
|
282567
|
+
}
|
|
282568
|
+
case "--role":
|
|
282569
|
+
role = argv[++i] ?? role;
|
|
282570
|
+
break;
|
|
282571
|
+
case "--write-role":
|
|
282572
|
+
writeRole = argv[++i] ?? writeRole;
|
|
282573
|
+
break;
|
|
282574
|
+
case "--route": {
|
|
282575
|
+
const value = argv[++i] ?? "";
|
|
282576
|
+
if (!DIRECT_DATA_SOURCE_ROUTES.includes(value)) {
|
|
282577
|
+
throw new AbloValidationError(
|
|
282578
|
+
`invalid direct route: ${value || "(missing)"} (expected ${DIRECT_DATA_SOURCE_ROUTES.join(", ")})`,
|
|
282579
|
+
{ code: "cli_invalid_arguments" }
|
|
282580
|
+
);
|
|
282581
|
+
}
|
|
282582
|
+
route = value;
|
|
282583
|
+
break;
|
|
282584
|
+
}
|
|
282585
|
+
default:
|
|
282586
|
+
throw new AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
|
|
282587
|
+
}
|
|
282588
|
+
}
|
|
282589
|
+
if (role === writeRole) {
|
|
282590
|
+
throw new AbloValidationError("replication and write roles must be different", {
|
|
282591
|
+
code: "cli_invalid_arguments"
|
|
282592
|
+
});
|
|
282593
|
+
}
|
|
282594
|
+
return {
|
|
282595
|
+
check: check2,
|
|
282596
|
+
register,
|
|
282597
|
+
apply: apply2,
|
|
282598
|
+
rotate,
|
|
282599
|
+
url,
|
|
282600
|
+
yes,
|
|
282601
|
+
showSql,
|
|
282602
|
+
scan,
|
|
282603
|
+
tables,
|
|
282604
|
+
role,
|
|
282605
|
+
writeRole,
|
|
282606
|
+
route
|
|
282607
|
+
};
|
|
282608
|
+
}
|
|
282609
|
+
function printConnectRecipe(args) {
|
|
282610
|
+
const sql = connectSetupSql({
|
|
282611
|
+
tables: args.tables,
|
|
282612
|
+
role: args.role,
|
|
282613
|
+
writeRole: args.writeRole
|
|
282614
|
+
});
|
|
282615
|
+
console.log(
|
|
282616
|
+
`
|
|
282617
|
+
${brand("ablo")} ${import_picocolors9.default.dim("connect")} ${import_picocolors9.default.dim("direct writes + WAL-settled sync")}
|
|
282618
|
+
`
|
|
282619
|
+
);
|
|
282620
|
+
console.log(
|
|
282621
|
+
` Ablo applies coordinated writes directly to your Postgres with a scoped DML role. WAL
|
|
282622
|
+
observes what committed, orders it with external writes, and confirms it in the sync log.
|
|
282623
|
+
Your database stays authoritative; Ablo never owns or migrates your application tables.
|
|
282624
|
+
|
|
282625
|
+
${import_picocolors9.default.bold("ablo connect apply")} runs every step below for you from a one-time admin
|
|
282626
|
+
connection and leaves your app holding only ${import_picocolors9.default.bold("ABLO_API_KEY")}. To do it by hand,
|
|
282627
|
+
run this once against your Postgres ${import_picocolors9.default.dim("(as a superuser / DB owner)")}:
|
|
282628
|
+
`
|
|
282629
|
+
);
|
|
282630
|
+
console.log(
|
|
282631
|
+
` ${import_picocolors9.default.bold("1.")} Enable logical decoding ${import_picocolors9.default.dim("(then RESTART Postgres \u2014 wal_level is not reloadable)")}`
|
|
282632
|
+
);
|
|
282633
|
+
console.log(` ${import_picocolors9.default.cyan(sql[0])}`);
|
|
282634
|
+
console.log(
|
|
282635
|
+
import_picocolors9.default.dim(
|
|
282636
|
+
` Managed hosts don't take ALTER SYSTEM: on Amazon RDS / Aurora set ${import_picocolors9.default.bold("rds.logical_replication = 1")}
|
|
282637
|
+
in the parameter group and reboot; on Neon or Supabase enable logical replication in the
|
|
282638
|
+
project settings. ${import_picocolors9.default.bold("ablo connect apply")} detects the host and does the right thing.`
|
|
282639
|
+
)
|
|
282640
|
+
);
|
|
282641
|
+
console.log(`
|
|
282642
|
+
${import_picocolors9.default.bold("2.")} Publish the tables Ablo should read`);
|
|
282643
|
+
console.log(` ${import_picocolors9.default.cyan(sql[1])}`);
|
|
282644
|
+
if (args.tables.length === 0) {
|
|
282645
|
+
console.log(
|
|
282646
|
+
import_picocolors9.default.dim(
|
|
282647
|
+
` (Scope it with ${import_picocolors9.default.bold("ablo connect --tables a,b,c")} to publish a subset.)`
|
|
282648
|
+
)
|
|
282649
|
+
);
|
|
282650
|
+
}
|
|
282651
|
+
const writerStart = sql.findIndex((s) => s.includes("NOSUPERUSER NOBYPASSRLS"));
|
|
282652
|
+
const replicationStatements = sql.slice(2, writerStart);
|
|
282653
|
+
const writerStatements = sql.slice(writerStart);
|
|
282654
|
+
console.log(
|
|
282655
|
+
`
|
|
282656
|
+
${import_picocolors9.default.bold("3.")} Create the scoped replication role ${import_picocolors9.default.dim("(pick your own replication password)")}`
|
|
282657
|
+
);
|
|
282658
|
+
for (const statement of replicationStatements) {
|
|
282659
|
+
for (const line of statement.split("\n")) console.log(` ${import_picocolors9.default.cyan(line)}`);
|
|
282660
|
+
}
|
|
282661
|
+
console.log(
|
|
282662
|
+
import_picocolors9.default.dim(
|
|
282663
|
+
` On Amazon RDS, the REPLICATION attribute is granted, not set directly:
|
|
282664
|
+
${import_picocolors9.default.bold(`GRANT rds_replication TO ${quoteIdent(args.role)};`)}`
|
|
282665
|
+
)
|
|
282666
|
+
);
|
|
282667
|
+
console.log(
|
|
282668
|
+
`
|
|
282669
|
+
${import_picocolors9.default.bold("4.")} Create the separate DML role and the idempotency ledger ` + import_picocolors9.default.dim("(pick your own write password)")
|
|
282670
|
+
);
|
|
282671
|
+
for (const statement of writerStatements) {
|
|
282672
|
+
for (const line of statement.split("\n")) console.log(` ${import_picocolors9.default.cyan(line)}`);
|
|
282673
|
+
}
|
|
282674
|
+
console.log(
|
|
282675
|
+
import_picocolors9.default.dim(
|
|
282676
|
+
` The writer gets row DML + ledger access only. It has no REPLICATION, schema CREATE,
|
|
282677
|
+
role administration, database creation, ownership, or customer-table DDL. Direct uses
|
|
282678
|
+
${import_picocolors9.default.bold("ablo_idempotency")} but no outbox; WAL carries the committed row changes.
|
|
282679
|
+
Each ledger row carries an ${import_picocolors9.default.bold("expires_at")}; the writer can't DELETE (tamper-
|
|
282680
|
+
resistance), so prune it from your own admin/cron when convenient:
|
|
282681
|
+
${import_picocolors9.default.bold("DELETE FROM ablo_idempotency WHERE expires_at < now();")}`
|
|
282682
|
+
)
|
|
282683
|
+
);
|
|
282684
|
+
console.log(
|
|
282685
|
+
`
|
|
282686
|
+
${import_picocolors9.default.bold("5.")} Register the two roles with Ablo. Set them just long enough to register \u2014
|
|
282687
|
+
Ablo holds them from here, so your app keeps only ${import_picocolors9.default.bold("ABLO_API_KEY")}:
|
|
282688
|
+
${import_picocolors9.default.bold("ABLO_REPLICATION_DATABASE_URL")} ${import_picocolors9.default.dim(`\u2192 ${args.role} (replication only)`)}
|
|
282689
|
+
${import_picocolors9.default.bold("ABLO_WRITE_DATABASE_URL")} ${import_picocolors9.default.dim(`\u2192 ${args.writeRole} (DML only)`)}
|
|
282690
|
+
${import_picocolors9.default.cyan("npx ablo connect register")}
|
|
282691
|
+
`
|
|
282692
|
+
);
|
|
282693
|
+
console.log(
|
|
282694
|
+
` ${import_picocolors9.default.bold("6.")} Verify readiness ${import_picocolors9.default.dim("(checked from Ablo\u2019s side \u2014 needs only ABLO_API_KEY):")}
|
|
282695
|
+
${import_picocolors9.default.cyan("npx ablo connect check")}
|
|
282696
|
+
`
|
|
282697
|
+
);
|
|
282698
|
+
console.log(
|
|
282699
|
+
import_picocolors9.default.dim(
|
|
282700
|
+
` Reachable databases use this direct path. If no inbound route can be established, use the
|
|
282701
|
+
signed ${import_picocolors9.default.bold("dataSource()")} endpoint fallback; its correlated events confirm writes
|
|
282702
|
+
without an Ablo-side customer database socket.`
|
|
282703
|
+
)
|
|
282704
|
+
);
|
|
282705
|
+
console.log();
|
|
282706
|
+
}
|
|
282707
|
+
var SYNC_INFRA_RELATIONS = [
|
|
282708
|
+
"sync_deltas",
|
|
282709
|
+
"sync_id_seq",
|
|
282710
|
+
"sync_metadata",
|
|
282711
|
+
"mutation_log"
|
|
282712
|
+
];
|
|
282713
|
+
var SYNC_INFRA_TYPES = ["participant_kind", "backfill_provenance", "confirmation_state"];
|
|
282714
|
+
function printCheckItem(item) {
|
|
282715
|
+
if (item.ok) {
|
|
282716
|
+
console.log(` ${import_picocolors9.default.green("\u2713")} ${item.label}`);
|
|
282717
|
+
} else {
|
|
282718
|
+
console.log(` ${import_picocolors9.default.red("\u2717")} ${item.label}`);
|
|
282719
|
+
if (item.fix) {
|
|
282720
|
+
for (const line of item.fix.split("\n")) console.log(` ${import_picocolors9.default.red("\u2022")} ${line}`);
|
|
282721
|
+
}
|
|
282722
|
+
}
|
|
282723
|
+
}
|
|
282724
|
+
async function probeDirectWriteReadiness(sql, opts = {}) {
|
|
282725
|
+
const schema = opts.schema ?? "public";
|
|
282726
|
+
const publication = opts.publication ?? ABLO_PUBLICATION;
|
|
282727
|
+
const items = [];
|
|
282728
|
+
const roleRows = await sql.unsafe(
|
|
282729
|
+
`SELECT rolname, rolsuper, rolbypassrls, rolcreatedb, rolcreaterole, rolreplication
|
|
282730
|
+
FROM pg_roles WHERE rolname = current_user`
|
|
282731
|
+
);
|
|
282732
|
+
const role = roleRows[0];
|
|
282733
|
+
const dangerous = Boolean(
|
|
282734
|
+
!role || role.rolsuper || role.rolbypassrls || role.rolcreatedb || role.rolcreaterole || role.rolreplication
|
|
282735
|
+
);
|
|
282736
|
+
items.push(
|
|
282737
|
+
dangerous ? {
|
|
282738
|
+
ok: false,
|
|
282739
|
+
label: `ABLO_WRITE_DATABASE_URL is not a scoped DML role`,
|
|
282740
|
+
fix: `Use ${ABLO_WRITE_ROLE} from the setup recipe: NOSUPERUSER, NOBYPASSRLS, NOCREATEDB, NOCREATEROLE, NOREPLICATION.`
|
|
282741
|
+
} : { ok: true, label: `write role ${import_picocolors9.default.bold(role?.rolname ?? ABLO_WRITE_ROLE)} is DML-only` }
|
|
282742
|
+
);
|
|
282743
|
+
const schemaRows = await sql.unsafe(
|
|
282744
|
+
`SELECT
|
|
282745
|
+
has_schema_privilege(current_user, $1, 'USAGE') AS usage,
|
|
282746
|
+
has_schema_privilege(current_user, $1, 'CREATE') AS create,
|
|
282747
|
+
current_setting('row_security') AS row_security`,
|
|
282748
|
+
[schema]
|
|
282749
|
+
);
|
|
282750
|
+
const schemaRow = schemaRows[0];
|
|
282751
|
+
items.push(
|
|
282752
|
+
schemaRow?.usage && !schemaRow.create && schemaRow.row_security === "on" ? { ok: true, label: `writer uses ${schema} with row_security on and no schema CREATE` } : {
|
|
282753
|
+
ok: false,
|
|
282754
|
+
label: `writer schema/RLS privileges are not least-privilege`,
|
|
282755
|
+
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;`
|
|
282756
|
+
}
|
|
282757
|
+
);
|
|
282758
|
+
const ledgerName = `${quoteIdent(schema)}.${quoteIdent("ablo_idempotency")}`;
|
|
282759
|
+
const ledgerRows = await sql.unsafe(
|
|
282760
|
+
`SELECT
|
|
282761
|
+
to_regclass($1) IS NOT NULL AS present,
|
|
282762
|
+
CASE WHEN to_regclass($1) IS NULL THEN false ELSE has_table_privilege(current_user, $1, 'SELECT,INSERT,UPDATE') END AS writes,
|
|
282763
|
+
CASE WHEN to_regclass($1) IS NULL THEN false ELSE has_table_privilege(current_user, $1, 'DELETE') END AS deletes`,
|
|
282764
|
+
[ledgerName]
|
|
282765
|
+
);
|
|
282766
|
+
const ledger = ledgerRows[0];
|
|
282767
|
+
items.push(
|
|
282768
|
+
ledger?.present && ledger.writes && !ledger.deletes ? { ok: true, label: `${import_picocolors9.default.bold("ablo_idempotency")} is durable and protected from DELETE` } : {
|
|
282769
|
+
ok: false,
|
|
282770
|
+
label: `${import_picocolors9.default.bold("ablo_idempotency")} is missing or has unsafe grants`,
|
|
282771
|
+
fix: `Apply the DML-role and idempotency-ledger statements printed by ${import_picocolors9.default.bold("ablo connect")}.`
|
|
282772
|
+
}
|
|
282773
|
+
);
|
|
282774
|
+
const tableRows = await sql.unsafe(
|
|
282775
|
+
`SELECT format('%I.%I', schemaname, tablename) AS relation
|
|
282776
|
+
FROM pg_publication_tables
|
|
282777
|
+
WHERE pubname = $1 AND schemaname = $2 AND tablename <> 'ablo_idempotency'
|
|
282778
|
+
AND NOT has_table_privilege(
|
|
282779
|
+
current_user,
|
|
282780
|
+
format('%I.%I', schemaname, tablename),
|
|
282781
|
+
'SELECT,INSERT,UPDATE,DELETE'
|
|
282782
|
+
)`,
|
|
282783
|
+
[publication, schema]
|
|
282784
|
+
);
|
|
282785
|
+
items.push(
|
|
282786
|
+
tableRows.length === 0 ? { ok: true, label: `writer can apply DML to every published application table` } : {
|
|
282787
|
+
ok: false,
|
|
282788
|
+
label: `writer lacks DML on ${tableRows.length} published table${tableRows.length === 1 ? "" : "s"}`,
|
|
282789
|
+
fix: `Grant SELECT, INSERT, UPDATE, DELETE on: ${tableRows.map((row) => row.relation).join(", ")}`
|
|
282790
|
+
}
|
|
282791
|
+
);
|
|
282792
|
+
const rlsCoverageRows = await sql.unsafe(
|
|
282793
|
+
`SELECT
|
|
282794
|
+
count(*)::int AS total,
|
|
282795
|
+
(count(*) FILTER (WHERE c.relrowsecurity))::int AS with_rls
|
|
282796
|
+
FROM pg_publication_tables pt
|
|
282797
|
+
JOIN pg_class c ON c.relname = pt.tablename
|
|
282798
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
|
|
282799
|
+
WHERE pt.pubname = $1 AND pt.schemaname = $2 AND pt.tablename <> 'ablo_idempotency'`,
|
|
282800
|
+
[publication, schema]
|
|
282801
|
+
);
|
|
282802
|
+
const coverage = rlsCoverageRows[0];
|
|
282803
|
+
if (coverage && coverage.total > 0) {
|
|
282804
|
+
const { total, with_rls: withRls } = coverage;
|
|
282805
|
+
const plural = total === 1 ? "" : "s";
|
|
282806
|
+
items.push(
|
|
282807
|
+
withRls === total ? {
|
|
282808
|
+
ok: true,
|
|
282809
|
+
label: `row-level security governs the writer on all ${total} published table${plural}`
|
|
282810
|
+
} : {
|
|
282811
|
+
ok: true,
|
|
282812
|
+
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`
|
|
282813
|
+
}
|
|
282814
|
+
);
|
|
282815
|
+
}
|
|
282816
|
+
const ownerBypassRows = await sql.unsafe(
|
|
282817
|
+
`SELECT format('%I.%I', n.nspname, c.relname) AS relation
|
|
282818
|
+
FROM pg_publication_tables pt
|
|
282819
|
+
JOIN pg_class c ON c.relname = pt.tablename
|
|
282820
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = pt.schemaname
|
|
282821
|
+
WHERE pt.pubname = $1 AND pt.schemaname = $2 AND pt.tablename <> 'ablo_idempotency'
|
|
282822
|
+
AND c.relowner = (SELECT oid FROM pg_roles WHERE rolname = current_user)
|
|
282823
|
+
AND c.relrowsecurity = true
|
|
282824
|
+
AND c.relforcerowsecurity = false`,
|
|
282825
|
+
[publication, schema]
|
|
282826
|
+
);
|
|
282827
|
+
items.push(
|
|
282828
|
+
ownerBypassRows.length === 0 ? {
|
|
282829
|
+
ok: true,
|
|
282830
|
+
label: `row-level security is enforced on the writer \u2014 it owns no published table that could bypass its own policies`
|
|
282831
|
+
} : {
|
|
282832
|
+
ok: false,
|
|
282833
|
+
label: `writer owns ${ownerBypassRows.length} published table${ownerBypassRows.length === 1 ? "" : "s"} whose RLS is enabled but not forced \u2014 its policies are silently skipped`,
|
|
282834
|
+
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(" ")}`
|
|
282835
|
+
}
|
|
282836
|
+
);
|
|
282837
|
+
return items;
|
|
282838
|
+
}
|
|
282839
|
+
async function auditTenantSyncInfra(sql) {
|
|
282840
|
+
const artifacts = [];
|
|
282841
|
+
for (const name of SYNC_INFRA_RELATIONS) {
|
|
282842
|
+
const rows = await sql.unsafe(`SELECT to_regclass($1)::text AS reg`, [
|
|
282843
|
+
`public.${name}`
|
|
282844
|
+
]);
|
|
282845
|
+
artifacts.push({ kind: "relation", name, present: rows[0]?.reg != null });
|
|
282846
|
+
}
|
|
282847
|
+
for (const name of SYNC_INFRA_TYPES) {
|
|
282848
|
+
const rows = await sql.unsafe(`SELECT to_regtype($1)::text AS reg`, [
|
|
282849
|
+
`public.${name}`
|
|
282850
|
+
]);
|
|
282851
|
+
artifacts.push({ kind: "type", name, present: rows[0]?.reg != null });
|
|
282852
|
+
}
|
|
282853
|
+
return artifacts;
|
|
282854
|
+
}
|
|
282855
|
+
function requireScopedUrl(kind, verb) {
|
|
282856
|
+
if (kind === "replication") {
|
|
282857
|
+
const resolved = readProjectReplicationUrlWithSource();
|
|
282858
|
+
if (resolved) return resolved.url;
|
|
282859
|
+
console.error(
|
|
282860
|
+
import_picocolors9.default.red(" No replication connection found (checked process env, .env.local, .env).") + import_picocolors9.default.dim(
|
|
282861
|
+
` 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}`)}.`
|
|
282862
|
+
)
|
|
282863
|
+
);
|
|
282864
|
+
process.exit(1);
|
|
282865
|
+
}
|
|
282866
|
+
const dbUrl = readProjectWriteDatabaseUrl();
|
|
282867
|
+
if (dbUrl) return dbUrl;
|
|
282868
|
+
console.error(
|
|
282869
|
+
import_picocolors9.default.red(" No ABLO_WRITE_DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors9.default.dim(
|
|
282870
|
+
` 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.`
|
|
282871
|
+
)
|
|
282872
|
+
);
|
|
282873
|
+
process.exit(1);
|
|
282874
|
+
}
|
|
282875
|
+
async function probeAndReport(dbUrl, kind = "replication") {
|
|
282876
|
+
const sql = src_default(dbUrl, { max: 1, prepare: false, connect_timeout: 10, onnotice: () => {
|
|
282877
|
+
} });
|
|
282878
|
+
let items;
|
|
282879
|
+
try {
|
|
282880
|
+
items = kind === "replication" ? await probeReadiness(sql) : await probeDirectWriteReadiness(sql);
|
|
282881
|
+
} catch (err) {
|
|
282882
|
+
await sql.end({ timeout: 2 }).catch(() => void 0);
|
|
282883
|
+
const dial = dialFailureReason(err);
|
|
282884
|
+
if (dial) return { kind: "no-dial", reason: dial };
|
|
282885
|
+
const pg = err ?? {};
|
|
282886
|
+
console.error(import_picocolors9.default.red(` Couldn't read the database: ${pg.message ?? String(err)}`));
|
|
282887
|
+
process.exit(1);
|
|
282888
|
+
}
|
|
282889
|
+
await sql.end({ timeout: 2 });
|
|
282890
|
+
for (const item of items) printCheckItem(item);
|
|
282891
|
+
return { kind: "probed", failures: items.filter((i) => !i.ok).length };
|
|
282892
|
+
}
|
|
282893
|
+
async function runCheck() {
|
|
282894
|
+
console.log(
|
|
282895
|
+
`
|
|
282896
|
+
${brand("ablo")} ${import_picocolors9.default.dim("connect check")} ${import_picocolors9.default.dim("direct-write + WAL readiness")}
|
|
282897
|
+
`
|
|
282898
|
+
);
|
|
282899
|
+
const apiKey = resolveApiKey();
|
|
282900
|
+
if (!apiKey) {
|
|
282901
|
+
console.error(
|
|
282902
|
+
import_picocolors9.default.red(" No API key found.") + import_picocolors9.default.dim(
|
|
282903
|
+
` 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")}.`
|
|
282904
|
+
)
|
|
282905
|
+
);
|
|
282906
|
+
process.exit(1);
|
|
282907
|
+
}
|
|
282908
|
+
const apiUrl2 = apiBaseUrl();
|
|
282909
|
+
const result = await requestRemoteValidation({ apiUrl: apiUrl2, apiKey });
|
|
282910
|
+
if (!result.ok) {
|
|
282911
|
+
if (result.code === "no_data_source_registered") {
|
|
282912
|
+
console.error(
|
|
282913
|
+
` ${import_picocolors9.default.yellow("\u2014")} No database is connected to this plane yet, so there's nothing to check.
|
|
282914
|
+
` + import_picocolors9.default.dim(
|
|
282915
|
+
` Connect one with ${import_picocolors9.default.bold("ablo connect apply")}, then re-run ${import_picocolors9.default.bold("ablo connect check")}.
|
|
282916
|
+
`
|
|
282917
|
+
)
|
|
282918
|
+
);
|
|
282919
|
+
process.exit(1);
|
|
282920
|
+
}
|
|
282921
|
+
console.error(import_picocolors9.default.red(` The check failed: ${result.message}`));
|
|
282922
|
+
if (result.code === "forbidden") {
|
|
282923
|
+
console.error(
|
|
282924
|
+
import_picocolors9.default.dim(
|
|
282925
|
+
` Checking a connected database needs a ${import_picocolors9.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors9.default.bold("ablo login")} for one.`
|
|
282926
|
+
)
|
|
282927
|
+
);
|
|
282928
|
+
}
|
|
282929
|
+
console.error();
|
|
282930
|
+
process.exit(1);
|
|
282931
|
+
}
|
|
282932
|
+
if (!result.reachable) {
|
|
282933
|
+
console.error(
|
|
282934
|
+
` ${import_picocolors9.default.red("\u2717")} Ablo's infrastructure can't reach your database${result.reason ? ` ${import_picocolors9.default.dim(`(${result.reason})`)}` : ""}.`
|
|
282935
|
+
);
|
|
282936
|
+
console.error(
|
|
282937
|
+
import_picocolors9.default.dim(
|
|
282938
|
+
` Direct needs a route Ablo's servers can dial \u2014 public allowlist, PrivateLink, peering,
|
|
282939
|
+
or VPN. Only when no inbound route can exist, use the signed ${import_picocolors9.default.bold("dataSource()")} endpoint fallback.
|
|
282940
|
+
`
|
|
282941
|
+
)
|
|
282942
|
+
);
|
|
282943
|
+
process.exit(1);
|
|
282944
|
+
}
|
|
282945
|
+
for (const failure of result.failures) {
|
|
282946
|
+
const { label, fix } = describeRemoteFailure(failure);
|
|
282947
|
+
printCheckItem({ ok: false, label, fix });
|
|
282948
|
+
}
|
|
282949
|
+
console.log();
|
|
282950
|
+
if (result.ready) {
|
|
282951
|
+
console.log(
|
|
282952
|
+
` ${import_picocolors9.default.green("\u2713")} Ready \u2014 checked from Ablo's infrastructure. Ablo can apply scoped DML and settle it from WAL.
|
|
282953
|
+
`
|
|
282954
|
+
);
|
|
282955
|
+
process.exit(0);
|
|
282956
|
+
}
|
|
282957
|
+
const count = result.failures.length;
|
|
282958
|
+
console.log(
|
|
282959
|
+
` ${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")}.`)}
|
|
282960
|
+
`
|
|
282961
|
+
);
|
|
282962
|
+
process.exit(1);
|
|
282963
|
+
}
|
|
282964
|
+
async function runRegister(args) {
|
|
282965
|
+
const dbUrl = requireScopedUrl("replication", "register");
|
|
282966
|
+
const writeDbUrl = requireScopedUrl("write", "register");
|
|
282967
|
+
const apiKey = resolveApiKey();
|
|
282968
|
+
if (!apiKey) {
|
|
282969
|
+
console.error(
|
|
282970
|
+
import_picocolors9.default.red(" Not logged in.") + import_picocolors9.default.dim(
|
|
282971
|
+
` 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.`
|
|
282972
|
+
)
|
|
282973
|
+
);
|
|
282974
|
+
process.exit(1);
|
|
282975
|
+
}
|
|
282976
|
+
console.log(
|
|
282977
|
+
`
|
|
282978
|
+
${brand("ablo")} ${import_picocolors9.default.dim("connect register")} ${import_picocolors9.default.dim("register a direct DataSource")}
|
|
282979
|
+
`
|
|
282980
|
+
);
|
|
282981
|
+
console.log(` ${import_picocolors9.default.bold("Replication role")}
|
|
282982
|
+
`);
|
|
282983
|
+
const replication = await probeAndReport(dbUrl, "replication");
|
|
282984
|
+
console.log(`
|
|
282985
|
+
${import_picocolors9.default.bold("Direct-write role")}
|
|
282986
|
+
`);
|
|
282987
|
+
const write = await probeAndReport(writeDbUrl, "write");
|
|
282988
|
+
const noDial = [
|
|
282989
|
+
replication.kind === "no-dial" ? `replication: ${replication.reason}` : null,
|
|
282990
|
+
write.kind === "no-dial" ? `write: ${write.reason}` : null
|
|
282991
|
+
].filter((reason) => reason !== null);
|
|
282992
|
+
const failures = (replication.kind === "probed" ? replication.failures : 0) + (write.kind === "probed" ? write.failures : 0);
|
|
282993
|
+
if (noDial.length > 0) {
|
|
282994
|
+
console.log(
|
|
282995
|
+
` This machine can't reach one or both scoped connections (${import_picocolors9.default.dim(noDial.join("; "))}) \u2014 continuing anyway.
|
|
282996
|
+
Ablo validates both credentials from the infrastructure that will use them and refuses
|
|
282997
|
+
registration unless replication and direct DML are both ready.
|
|
282998
|
+
`
|
|
282999
|
+
);
|
|
283000
|
+
}
|
|
283001
|
+
if (failures > 0) {
|
|
283002
|
+
console.log(
|
|
283003
|
+
`
|
|
283004
|
+
${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.")}
|
|
283005
|
+
`
|
|
283006
|
+
);
|
|
283007
|
+
process.exit(1);
|
|
283008
|
+
}
|
|
283009
|
+
const apiUrl2 = apiBaseUrl();
|
|
283010
|
+
const registered = await registerDirectDataSource({
|
|
283011
|
+
apiUrl: apiUrl2,
|
|
283012
|
+
apiKey,
|
|
283013
|
+
replicationUrl: dbUrl,
|
|
283014
|
+
writeUrl: writeDbUrl,
|
|
283015
|
+
route: args.route
|
|
283016
|
+
});
|
|
283017
|
+
process.exit(registered ? 0 : 1);
|
|
283018
|
+
}
|
|
283019
|
+
async function runScan() {
|
|
283020
|
+
const dbUrl = requireScopedUrl("replication", "scan");
|
|
283021
|
+
const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
|
|
283022
|
+
} });
|
|
283023
|
+
let artifacts;
|
|
283024
|
+
try {
|
|
283025
|
+
artifacts = await auditTenantSyncInfra(sql);
|
|
283026
|
+
} catch (err) {
|
|
283027
|
+
const pg = err ?? {};
|
|
283028
|
+
console.error(import_picocolors9.default.red(` Couldn't audit the database: ${pg.message ?? String(err)}`));
|
|
283029
|
+
await sql.end({ timeout: 2 });
|
|
283030
|
+
process.exit(1);
|
|
283031
|
+
}
|
|
283032
|
+
await sql.end({ timeout: 2 });
|
|
283033
|
+
console.log(
|
|
283034
|
+
`
|
|
283035
|
+
${brand("ablo")} ${import_picocolors9.default.dim("connect scan")} ${import_picocolors9.default.dim("audit for leftover Ablo sync infrastructure")}
|
|
283036
|
+
`
|
|
283037
|
+
);
|
|
283038
|
+
const present = artifacts.filter((a) => a.present);
|
|
283039
|
+
if (present.length === 0) {
|
|
283040
|
+
console.log(` ${import_picocolors9.default.green("\u2713")} No deprecated Ablo sync infrastructure found in public.
|
|
283041
|
+
`);
|
|
283042
|
+
process.exit(0);
|
|
283043
|
+
}
|
|
283044
|
+
for (const artifact of present) {
|
|
283045
|
+
const label = artifact.kind === "type" ? "type" : "relation";
|
|
283046
|
+
console.log(` ${import_picocolors9.default.yellow("!")} ${label} ${import_picocolors9.default.bold(`public.${artifact.name}`)} exists`);
|
|
283047
|
+
}
|
|
283048
|
+
console.log(
|
|
283049
|
+
`
|
|
283050
|
+
${import_picocolors9.default.yellow(`${present.length} artifact${present.length === 1 ? "" : "s"} found`)} ` + import_picocolors9.default.dim(
|
|
283051
|
+
"\u2014 do not drop automatically. Confirm the org/environment is log-authoritative, then follow "
|
|
283052
|
+
) + import_picocolors9.default.bold("docs/runbooks/wal-stage5-customer-db-infra-cleanup.md") + import_picocolors9.default.dim(".\n")
|
|
283053
|
+
);
|
|
283054
|
+
process.exit(1);
|
|
283055
|
+
}
|
|
283056
|
+
async function connect(argv) {
|
|
283057
|
+
if (argv[0] === "deregister") {
|
|
283058
|
+
const { disconnect: disconnect2 } = await Promise.resolve().then(() => (init_disconnect(), disconnect_exports));
|
|
283059
|
+
await disconnect2(argv.slice(1));
|
|
283060
|
+
return;
|
|
283061
|
+
}
|
|
283062
|
+
let args;
|
|
283063
|
+
try {
|
|
283064
|
+
args = parseConnectArgs(argv);
|
|
283065
|
+
} catch (err) {
|
|
283066
|
+
console.error(import_picocolors9.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
283067
|
+
process.exit(1);
|
|
283068
|
+
}
|
|
283069
|
+
if (args.apply || args.rotate) {
|
|
283070
|
+
const { runConnectApply: runConnectApply2 } = await Promise.resolve().then(() => (init_connectApply(), connectApply_exports));
|
|
283071
|
+
await runConnectApply2(args);
|
|
283072
|
+
return;
|
|
283073
|
+
}
|
|
283074
|
+
if (args.check) {
|
|
283075
|
+
await runCheck();
|
|
283076
|
+
return;
|
|
283077
|
+
}
|
|
283078
|
+
if (args.register) {
|
|
283079
|
+
await runRegister(args);
|
|
283080
|
+
return;
|
|
283081
|
+
}
|
|
283082
|
+
if (args.scan) {
|
|
283083
|
+
await runScan();
|
|
283084
|
+
return;
|
|
283085
|
+
}
|
|
283086
|
+
printConnectRecipe(args);
|
|
283087
|
+
}
|
|
283088
|
+
var CONNECT_USAGE = ` ablo connect \u2014 direct writes, settled by logical replication
|
|
283089
|
+
|
|
283090
|
+
Ablo applies coordinated DML with a scoped writer role. WAL observes what committed,
|
|
283091
|
+
orders it with external changes, and confirms it. Your Postgres remains authoritative.
|
|
283092
|
+
|
|
283093
|
+
Usage:
|
|
283094
|
+
npx ablo connect Print the exact setup SQL instead of running it
|
|
283095
|
+
npx ablo connect apply Set it up end to end: create the roles, publish, register
|
|
283096
|
+
npx ablo connect register Register both scoped credentials as one direct DataSource (hand-run-SQL path)
|
|
283097
|
+
npx ablo connect deregister Remove this project's data source \u2014 Ablo stops reading/writing it
|
|
283098
|
+
npx ablo connect check Verify the connected database from Ablo's side (needs only ABLO_API_KEY)
|
|
283099
|
+
npx ablo connect rotate New passwords for both scoped roles, then re-register
|
|
283100
|
+
npx ablo connect scan Read-only audit for leftover Ablo sync tables/types (never drops)
|
|
283101
|
+
|
|
283102
|
+
Modifiers:
|
|
283103
|
+
--url <admin-conn> Admin connection for apply/rotate (else DATABASE_URL)
|
|
283104
|
+
--tables a,b,c Publish only these tables (default: all tables)
|
|
283105
|
+
--role <name> Name the replication role (default: ablo_replicator)
|
|
283106
|
+
--write-role <name> Name the DML role (default: ablo_writer)
|
|
283107
|
+
--route <route> public-allowlist | privatelink | peering | vpn
|
|
283108
|
+
--yes Skip the apply confirmation (non-interactive)
|
|
283109
|
+
--show-sql Show the exact statements in the apply plan
|
|
283110
|
+
|
|
283111
|
+
apply registers with Ablo directly; the admin credential is used only on this
|
|
283112
|
+
machine and never persisted. Your app holds only ABLO_API_KEY.`;
|
|
283113
|
+
|
|
282647
283114
|
// src/cli/index.ts
|
|
282648
|
-
init_connect();
|
|
282649
283115
|
init_push();
|
|
282650
283116
|
|
|
282651
283117
|
// src/cli/generate.ts
|
|
@@ -282653,7 +283119,7 @@ init_cjs_shims();
|
|
|
282653
283119
|
init_errors();
|
|
282654
283120
|
var import_fs6 = require("fs");
|
|
282655
283121
|
var import_path4 = require("path");
|
|
282656
|
-
var
|
|
283122
|
+
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
282657
283123
|
var import_schema4 = require("@abloatai/ablo/schema");
|
|
282658
283124
|
init_push();
|
|
282659
283125
|
var DEFAULT_SCHEMA_PATH3 = "ablo/schema.ts";
|
|
@@ -282686,7 +283152,7 @@ async function generate(argv) {
|
|
|
282686
283152
|
try {
|
|
282687
283153
|
args = parseGenerateArgs(argv);
|
|
282688
283154
|
} catch (err) {
|
|
282689
|
-
console.error(
|
|
283155
|
+
console.error(import_picocolors10.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
282690
283156
|
process.exit(1);
|
|
282691
283157
|
}
|
|
282692
283158
|
let source;
|
|
@@ -282695,20 +283161,20 @@ async function generate(argv) {
|
|
|
282695
283161
|
const schemaJson = JSON.parse((0, import_schema4.serializeSchema)(schema));
|
|
282696
283162
|
source = (0, import_schema4.generateTypes)(schemaJson);
|
|
282697
283163
|
} catch (err) {
|
|
282698
|
-
console.error(
|
|
283164
|
+
console.error(import_picocolors10.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
282699
283165
|
process.exit(1);
|
|
282700
283166
|
}
|
|
282701
283167
|
const abs = (0, import_path4.resolve)(process.cwd(), args.out);
|
|
282702
283168
|
(0, import_fs6.mkdirSync)((0, import_path4.dirname)(abs), { recursive: true });
|
|
282703
283169
|
(0, import_fs6.writeFileSync)(abs, source);
|
|
282704
|
-
console.log(` ${
|
|
283170
|
+
console.log(` ${import_picocolors10.default.green("\u2713")} Generated types \u2192 ${import_picocolors10.default.bold(args.out)}`);
|
|
282705
283171
|
}
|
|
282706
283172
|
|
|
282707
283173
|
// src/cli/dev.ts
|
|
282708
283174
|
init_cjs_shims();
|
|
282709
283175
|
init_errors();
|
|
282710
283176
|
init_credentialPolicy();
|
|
282711
|
-
var
|
|
283177
|
+
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
282712
283178
|
init_dist2();
|
|
282713
283179
|
var import_fs7 = require("fs");
|
|
282714
283180
|
var import_path5 = require("path");
|
|
@@ -282750,25 +283216,25 @@ function classifyKey(apiKey, activeMode) {
|
|
|
282750
283216
|
if (!apiKey) {
|
|
282751
283217
|
return {
|
|
282752
283218
|
ok: false,
|
|
282753
|
-
reason: `No API key. Run ${
|
|
283219
|
+
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}'.`)
|
|
282754
283220
|
};
|
|
282755
283221
|
}
|
|
282756
283222
|
if (apiKey.startsWith("sk_test_")) return { ok: true };
|
|
282757
283223
|
if (apiKey.startsWith("sk_live_")) {
|
|
282758
283224
|
return {
|
|
282759
283225
|
ok: false,
|
|
282760
|
-
reason: `Production schema deploys run one-shot: ${
|
|
283226
|
+
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.`
|
|
282761
283227
|
};
|
|
282762
283228
|
}
|
|
282763
283229
|
if (classifyCredentialKind(apiKey) === "restricted") {
|
|
282764
283230
|
return {
|
|
282765
283231
|
ok: false,
|
|
282766
|
-
reason: `Restricted (${
|
|
283232
|
+
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.`
|
|
282767
283233
|
};
|
|
282768
283234
|
}
|
|
282769
283235
|
return {
|
|
282770
283236
|
ok: false,
|
|
282771
|
-
reason: `${
|
|
283237
|
+
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")}).`
|
|
282772
283238
|
};
|
|
282773
283239
|
}
|
|
282774
283240
|
function wireEnvLocal(apiKey, cwd = process.cwd()) {
|
|
@@ -282778,7 +283244,7 @@ function wireEnvLocal(apiKey, cwd = process.cwd()) {
|
|
|
282778
283244
|
if (!(0, import_fs7.existsSync)(envPath)) {
|
|
282779
283245
|
(0, import_fs7.writeFileSync)(envPath, `${line}
|
|
282780
283246
|
`, { mode: 384 });
|
|
282781
|
-
action = `Created ${
|
|
283247
|
+
action = `Created ${import_picocolors11.default.bold(".env.local")} with ${import_picocolors11.default.bold("ABLO_API_KEY")}`;
|
|
282782
283248
|
} else {
|
|
282783
283249
|
const content = (0, import_fs7.readFileSync)(envPath, "utf8");
|
|
282784
283250
|
const match = /^ABLO_API_KEY=(.*)$/m.exec(content);
|
|
@@ -282786,12 +283252,12 @@ function wireEnvLocal(apiKey, cwd = process.cwd()) {
|
|
|
282786
283252
|
if (!match) {
|
|
282787
283253
|
(0, import_fs7.appendFileSync)(envPath, `${content.endsWith("\n") || content.length === 0 ? "" : "\n"}${line}
|
|
282788
283254
|
`);
|
|
282789
|
-
action = `Added ${
|
|
283255
|
+
action = `Added ${import_picocolors11.default.bold("ABLO_API_KEY")} to ${import_picocolors11.default.bold(".env.local")}`;
|
|
282790
283256
|
} else if (existing === apiKey) {
|
|
282791
|
-
action = `${
|
|
283257
|
+
action = `${import_picocolors11.default.bold(".env.local")} already has this key`;
|
|
282792
283258
|
} else {
|
|
282793
283259
|
(0, import_fs7.writeFileSync)(envPath, content.replace(/^ABLO_API_KEY=.*$/m, line));
|
|
282794
|
-
action = `Updated ${
|
|
283260
|
+
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)`)}`;
|
|
282795
283261
|
}
|
|
282796
283262
|
}
|
|
282797
283263
|
const gitignorePath = (0, import_path5.resolve)(cwd, ".gitignore");
|
|
@@ -282805,7 +283271,7 @@ function wireEnvLocal(apiKey, cwd = process.cwd()) {
|
|
|
282805
283271
|
`}.env.local
|
|
282806
283272
|
`
|
|
282807
283273
|
);
|
|
282808
|
-
gitignoreNote = ` Added ${
|
|
283274
|
+
gitignoreNote = ` Added ${import_picocolors11.default.bold(".env.local")} to ${import_picocolors11.default.bold(".gitignore")} so the key can't be committed.`;
|
|
282809
283275
|
}
|
|
282810
283276
|
return `${action}.${gitignoreNote}`;
|
|
282811
283277
|
}
|
|
@@ -282820,7 +283286,7 @@ async function runPush(schema, args) {
|
|
|
282820
283286
|
if (ok) {
|
|
282821
283287
|
return {
|
|
282822
283288
|
ok: true,
|
|
282823
|
-
message: body.unchanged ? `schema unchanged ${
|
|
283289
|
+
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})`)}`
|
|
282824
283290
|
};
|
|
282825
283291
|
}
|
|
282826
283292
|
if (status2 === 409) {
|
|
@@ -282830,33 +283296,33 @@ async function runPush(schema, args) {
|
|
|
282830
283296
|
(s) => s.shadowed != null
|
|
282831
283297
|
);
|
|
282832
283298
|
const lines = [
|
|
282833
|
-
|
|
283299
|
+
import_picocolors11.default.bold("Incompatible schema change \u2014 not safe to apply as-is."),
|
|
282834
283300
|
"",
|
|
282835
|
-
...unexecutable.map((u2) =>
|
|
282836
|
-
...warnings.map((w2) =>
|
|
283301
|
+
...unexecutable.map((u2) => import_picocolors11.default.red(fmtSignal(u2))),
|
|
283302
|
+
...warnings.map((w2) => import_picocolors11.default.yellow(fmtSignal(w2))),
|
|
282837
283303
|
"",
|
|
282838
283304
|
...hasShadowed ? [
|
|
282839
|
-
|
|
283305
|
+
import_picocolors11.default.dim(
|
|
282840
283306
|
" These models exist in the baseline above but not in your push. Sandbox readers"
|
|
282841
283307
|
),
|
|
282842
|
-
|
|
283308
|
+
import_picocolors11.default.dim(
|
|
282843
283309
|
" fall back to the production schema until you push your own, so applying this drops them."
|
|
282844
283310
|
),
|
|
282845
283311
|
""
|
|
282846
283312
|
] : [],
|
|
282847
|
-
|
|
282848
|
-
` Fix: ${
|
|
283313
|
+
import_picocolors11.default.dim(
|
|
283314
|
+
` Fix: ${import_picocolors11.default.bold("ablo push --force")} to apply anyway, or ${import_picocolors11.default.bold("--rename old:new")} if you renamed a model.`
|
|
282849
283315
|
)
|
|
282850
283316
|
];
|
|
282851
283317
|
return { ok: false, message: lines.join("\n") };
|
|
282852
283318
|
}
|
|
282853
283319
|
if (status2 === 403) {
|
|
282854
283320
|
const serverSays = body.message ?? body.reason;
|
|
282855
|
-
const hint = body.code === "database_role_cannot_enforce_rls" ? `Run ${
|
|
283321
|
+
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")}.`;
|
|
282856
283322
|
return {
|
|
282857
283323
|
ok: false,
|
|
282858
283324
|
message: `${serverSays ?? "This key can't author schema (missing schema:push scope)."}
|
|
282859
|
-
` +
|
|
283325
|
+
` + import_picocolors11.default.dim(hint)
|
|
282860
283326
|
};
|
|
282861
283327
|
}
|
|
282862
283328
|
return { ok: false, message: `Push failed (${status2}): ${body.message ?? body.reason ?? bodyText}` };
|
|
@@ -282866,25 +283332,25 @@ async function dev(argv) {
|
|
|
282866
283332
|
try {
|
|
282867
283333
|
args = parseDevArgs(argv);
|
|
282868
283334
|
} catch (err) {
|
|
282869
|
-
console.error(
|
|
283335
|
+
console.error(import_picocolors11.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
282870
283336
|
process.exit(1);
|
|
282871
283337
|
}
|
|
282872
283338
|
if (!args.apiKey) args.apiKey = resolveEffectiveApiKey("sandbox").key;
|
|
282873
283339
|
const key = classifyKey(args.apiKey, getMode());
|
|
282874
283340
|
if (!key.ok) {
|
|
282875
|
-
console.error(
|
|
283341
|
+
console.error(import_picocolors11.default.red(` ${key.reason}`));
|
|
282876
283342
|
process.exit(1);
|
|
282877
283343
|
}
|
|
282878
283344
|
console.log(`
|
|
282879
|
-
${brand("ablo")} ${
|
|
283345
|
+
${brand("ablo")} ${import_picocolors11.default.dim("push")} ${import_picocolors11.default.dim("(sandbox)")}
|
|
282880
283346
|
`);
|
|
282881
283347
|
const schema = await loadSchema(args.schemaPath, args.exportName);
|
|
282882
283348
|
const modelCount = Object.keys(schema.models).length;
|
|
282883
283349
|
console.log(
|
|
282884
|
-
` ${
|
|
283350
|
+
` ${import_picocolors11.default.dim("schema")} ${import_picocolors11.default.bold(args.schemaPath)} ${import_picocolors11.default.dim(`(${modelCount} models, hash ${(0, import_schema5.schemaHash)(schema)})`)}`
|
|
282885
283351
|
);
|
|
282886
|
-
console.log(` ${
|
|
282887
|
-
console.log(` ${
|
|
283352
|
+
console.log(` ${import_picocolors11.default.dim("key")} ${args.apiKey.slice(0, 12)}\u2026`);
|
|
283353
|
+
console.log(` ${import_picocolors11.default.dim("api")} ${args.url}
|
|
282888
283354
|
`);
|
|
282889
283355
|
const s = Y2();
|
|
282890
283356
|
s.start("Pushing schema definition (sandbox)");
|
|
@@ -282893,16 +283359,16 @@ async function dev(argv) {
|
|
|
282893
283359
|
if (!first.ok) process.exit(1);
|
|
282894
283360
|
if (process.env.ABLO_API_KEY) {
|
|
282895
283361
|
console.log(`
|
|
282896
|
-
${
|
|
283362
|
+
${import_picocolors11.default.green("\u2713")} ${import_picocolors11.default.bold("ABLO_API_KEY")} is set in this shell \u2014 the SDK reads it directly.`);
|
|
282897
283363
|
} else {
|
|
282898
283364
|
console.log(`
|
|
282899
|
-
${
|
|
282900
|
-
console.log(` ${
|
|
283365
|
+
${import_picocolors11.default.green("\u2713")} ${wireEnvLocal(args.apiKey)}`);
|
|
283366
|
+
console.log(` ${import_picocolors11.default.dim("Frameworks load it automatically; plain Node: node --env-file=.env.local app.ts")}`);
|
|
282901
283367
|
}
|
|
282902
283368
|
console.log(` Your app is wired for the sandbox.`);
|
|
282903
283369
|
if (!args.watch) return;
|
|
282904
283370
|
const abs = (0, import_path5.resolve)(process.cwd(), args.schemaPath);
|
|
282905
|
-
console.log(` ${
|
|
283371
|
+
console.log(` ${import_picocolors11.default.dim(`watching ${args.schemaPath} \u2026 (Ctrl-C to stop)`)}
|
|
282906
283372
|
`);
|
|
282907
283373
|
let timer2 = null;
|
|
282908
283374
|
let pushing = false;
|
|
@@ -282922,7 +283388,7 @@ async function dev(argv) {
|
|
|
282922
283388
|
const r2 = await runPush(next, args);
|
|
282923
283389
|
s2.stop(r2.message, r2.ok ? 0 : 1);
|
|
282924
283390
|
} catch (err) {
|
|
282925
|
-
s2.stop(
|
|
283391
|
+
s2.stop(import_picocolors11.default.red(`schema reload failed: ${err instanceof Error ? err.message : String(err)}`), 1);
|
|
282926
283392
|
} finally {
|
|
282927
283393
|
pushing = false;
|
|
282928
283394
|
}
|
|
@@ -282930,7 +283396,7 @@ async function dev(argv) {
|
|
|
282930
283396
|
const stop = () => {
|
|
282931
283397
|
watcher.close();
|
|
282932
283398
|
console.log(`
|
|
282933
|
-
${
|
|
283399
|
+
${import_picocolors11.default.dim("stopped.")}`);
|
|
282934
283400
|
process.exit(0);
|
|
282935
283401
|
};
|
|
282936
283402
|
process.on("SIGINT", stop);
|
|
@@ -282942,7 +283408,7 @@ async function dev(argv) {
|
|
|
282942
283408
|
// src/cli/login.ts
|
|
282943
283409
|
init_cjs_shims();
|
|
282944
283410
|
var import_child_process2 = require("child_process");
|
|
282945
|
-
var
|
|
283411
|
+
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
282946
283412
|
init_dist2();
|
|
282947
283413
|
init_config();
|
|
282948
283414
|
init_theme();
|
|
@@ -283003,9 +283469,9 @@ async function deviceLogin(argv, deps = {}) {
|
|
|
283003
283469
|
const code = await codeRes.json();
|
|
283004
283470
|
const approvePath = `/cli?user_code=${code.user_code}`;
|
|
283005
283471
|
const url = account === "signup" ? `${DASHBOARD_URL}/signup?next=${encodeURIComponent(approvePath)}` : `${DASHBOARD_URL}${approvePath}`;
|
|
283006
|
-
Me(`${
|
|
283472
|
+
Me(`${import_picocolors12.default.bold(code.user_code)}
|
|
283007
283473
|
|
|
283008
|
-
${
|
|
283474
|
+
${import_picocolors12.default.dim(url)}`, "Approve in your browser");
|
|
283009
283475
|
openUrl(url);
|
|
283010
283476
|
const s = Y2();
|
|
283011
283477
|
s.start("Waiting for approval\u2026");
|
|
@@ -283072,7 +283538,7 @@ ${import_picocolors10.default.dim(url)}`, "Approve in your browser");
|
|
|
283072
283538
|
if (reason) M2.error(reason);
|
|
283073
283539
|
else if (provRes) M2.error(`Key provisioning returned ${provRes.status} from ${DASHBOARD_URL}/api/cli/provision-key.`);
|
|
283074
283540
|
M2.error(
|
|
283075
|
-
`The browser approval succeeded but the key handoff failed. Try again, or grab a ${
|
|
283541
|
+
`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")}.`
|
|
283076
283542
|
);
|
|
283077
283543
|
process.exit(1);
|
|
283078
283544
|
}
|
|
@@ -283092,9 +283558,9 @@ ${import_picocolors10.default.dim(url)}`, "Approve in your browser");
|
|
|
283092
283558
|
{ mode: "sandbox", activeProject: prov.project ?? void 0 }
|
|
283093
283559
|
);
|
|
283094
283560
|
s.stop(`Saved keys to ${path}`);
|
|
283095
|
-
const where = prov.project ? ` ${
|
|
283561
|
+
const where = prov.project ? ` ${import_picocolors12.default.dim(`(project ${prov.project.slug})`)}` : "";
|
|
283096
283562
|
Se(
|
|
283097
|
-
`${
|
|
283563
|
+
`${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.`
|
|
283098
283564
|
);
|
|
283099
283565
|
}
|
|
283100
283566
|
async function login(argv = [], deps = {}) {
|
|
@@ -283103,13 +283569,13 @@ async function login(argv = [], deps = {}) {
|
|
|
283103
283569
|
function logout() {
|
|
283104
283570
|
const removed = clearCredential();
|
|
283105
283571
|
if (removed) {
|
|
283106
|
-
console.log(` ${
|
|
283572
|
+
console.log(` ${import_picocolors12.default.green("\u2713")} Logged out ${import_picocolors12.default.dim(`(credentials removed from ${configDir()})`)}`);
|
|
283107
283573
|
} else {
|
|
283108
|
-
console.log(` ${
|
|
283574
|
+
console.log(` ${import_picocolors12.default.dim("\u25CB")} Not logged in \u2014 nothing to remove.`);
|
|
283109
283575
|
}
|
|
283110
283576
|
if (process.env.ABLO_API_KEY) {
|
|
283111
283577
|
console.log(
|
|
283112
|
-
|
|
283578
|
+
import_picocolors12.default.dim(` Note: ${import_picocolors12.default.bold("ABLO_API_KEY")} is still set in this shell and takes precedence.`)
|
|
283113
283579
|
);
|
|
283114
283580
|
}
|
|
283115
283581
|
}
|
|
@@ -283119,7 +283585,7 @@ init_config();
|
|
|
283119
283585
|
|
|
283120
283586
|
// src/cli/mode.ts
|
|
283121
283587
|
init_cjs_shims();
|
|
283122
|
-
var
|
|
283588
|
+
var import_picocolors13 = __toESM(require_picocolors(), 1);
|
|
283123
283589
|
init_dist2();
|
|
283124
283590
|
init_config();
|
|
283125
283591
|
var PREFIX = { sandbox: "sk_test_", production: "rk_live_" };
|
|
@@ -283131,10 +283597,10 @@ function hintFor(m2, current) {
|
|
|
283131
283597
|
}
|
|
283132
283598
|
function apply(m2) {
|
|
283133
283599
|
setMode(m2);
|
|
283134
|
-
console.log(` ${
|
|
283600
|
+
console.log(` ${import_picocolors13.default.green("\u2713")} now in ${import_picocolors13.default.bold(m2)}`);
|
|
283135
283601
|
if (!getKeyEntry(m2)) {
|
|
283136
283602
|
console.log(
|
|
283137
|
-
|
|
283603
|
+
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`)}.`)
|
|
283138
283604
|
);
|
|
283139
283605
|
}
|
|
283140
283606
|
}
|
|
@@ -283147,14 +283613,14 @@ async function mode(argv) {
|
|
|
283147
283613
|
}
|
|
283148
283614
|
if (arg) {
|
|
283149
283615
|
console.error(
|
|
283150
|
-
|
|
283616
|
+
import_picocolors13.default.red(` unknown mode: ${arg}`) + import_picocolors13.default.dim(` (expected ${import_picocolors13.default.bold("sandbox")} or ${import_picocolors13.default.bold("production")})`)
|
|
283151
283617
|
);
|
|
283152
283618
|
process.exit(1);
|
|
283153
283619
|
}
|
|
283154
283620
|
const current = getMode();
|
|
283155
283621
|
if (!process.stdin.isTTY || process.env.CI) {
|
|
283156
283622
|
console.error(
|
|
283157
|
-
|
|
283623
|
+
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})`)
|
|
283158
283624
|
);
|
|
283159
283625
|
process.exit(1);
|
|
283160
283626
|
}
|
|
@@ -283178,7 +283644,7 @@ init_projects();
|
|
|
283178
283644
|
|
|
283179
283645
|
// src/cli/status.ts
|
|
283180
283646
|
init_cjs_shims();
|
|
283181
|
-
var
|
|
283647
|
+
var import_picocolors14 = __toESM(require_picocolors(), 1);
|
|
283182
283648
|
init_config();
|
|
283183
283649
|
init_target();
|
|
283184
283650
|
init_theme();
|
|
@@ -283186,9 +283652,9 @@ init_push();
|
|
|
283186
283652
|
function expiryLabel(iso) {
|
|
283187
283653
|
const ms = Date.parse(iso) - Date.now();
|
|
283188
283654
|
if (Number.isNaN(ms)) return "";
|
|
283189
|
-
if (ms <= 0) return
|
|
283655
|
+
if (ms <= 0) return import_picocolors14.default.red("expired");
|
|
283190
283656
|
const days = Math.floor(ms / (24 * 60 * 60 * 1e3));
|
|
283191
|
-
return
|
|
283657
|
+
return import_picocolors14.default.dim(days > 0 ? `expires in ${days}d` : "expires <1d");
|
|
283192
283658
|
}
|
|
283193
283659
|
async function ping(apiUrl2) {
|
|
283194
283660
|
const ctrl = new AbortController();
|
|
@@ -283272,31 +283738,31 @@ function formatConflict(conflict) {
|
|
|
283272
283738
|
function printTargetLines(target, localProject) {
|
|
283273
283739
|
const confirmed = target?.confirmed ?? null;
|
|
283274
283740
|
if (confirmed?.organizationId) {
|
|
283275
|
-
console.log(` ${
|
|
283741
|
+
console.log(` ${import_picocolors14.default.dim("org")} ${import_picocolors14.default.dim(confirmed.organizationId)}`);
|
|
283276
283742
|
}
|
|
283277
283743
|
let projectLine;
|
|
283278
283744
|
if (confirmed?.project) {
|
|
283279
283745
|
const p2 = confirmed.project;
|
|
283280
|
-
projectLine = p2.isDefault ? `${
|
|
283746
|
+
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})`)}`;
|
|
283281
283747
|
} else if (confirmed) {
|
|
283282
|
-
projectLine = `${
|
|
283748
|
+
projectLine = `${import_picocolors14.default.bold("default")} ${import_picocolors14.default.dim("(org-default)")}`;
|
|
283283
283749
|
} else if (localProject) {
|
|
283284
|
-
projectLine = `${
|
|
283750
|
+
projectLine = `${import_picocolors14.default.bold(localProject.slug)} ${import_picocolors14.default.dim(`(${localProject.id})`)} ${import_picocolors14.default.yellow("(unconfirmed)")}`;
|
|
283285
283751
|
} else {
|
|
283286
|
-
projectLine = `${
|
|
283752
|
+
projectLine = `${import_picocolors14.default.bold("default")} ${target ? import_picocolors14.default.yellow("(unconfirmed)") : import_picocolors14.default.dim("(org-default)")}`;
|
|
283287
283753
|
}
|
|
283288
|
-
console.log(` ${
|
|
283754
|
+
console.log(` ${import_picocolors14.default.dim("project")} ${projectLine}`);
|
|
283289
283755
|
const env = confirmed?.environment ?? target?.keyEnv ?? null;
|
|
283290
283756
|
if (env) {
|
|
283291
|
-
const suffix = confirmed ? "" : ` ${
|
|
283292
|
-
console.log(` ${
|
|
283757
|
+
const suffix = confirmed ? "" : ` ${import_picocolors14.default.yellow("(unconfirmed)")}`;
|
|
283758
|
+
console.log(` ${import_picocolors14.default.dim("env")} ${import_picocolors14.default.bold(env)}${suffix}`);
|
|
283293
283759
|
}
|
|
283294
283760
|
for (const m2 of target?.mismatches ?? []) {
|
|
283295
|
-
console.log(` ${
|
|
283761
|
+
console.log(` ${import_picocolors14.default.yellow("\u26A0")} ${import_picocolors14.default.yellow(describeMismatch(m2))}`);
|
|
283296
283762
|
}
|
|
283297
283763
|
}
|
|
283298
283764
|
async function status(args = []) {
|
|
283299
|
-
const apiUrl2 = (
|
|
283765
|
+
const apiUrl2 = apiBaseUrl();
|
|
283300
283766
|
const cfg = readConfig();
|
|
283301
283767
|
const mode2 = getMode();
|
|
283302
283768
|
const effective = resolveEffectiveApiKey();
|
|
@@ -283363,80 +283829,80 @@ async function status(args = []) {
|
|
|
283363
283829
|
return;
|
|
283364
283830
|
}
|
|
283365
283831
|
console.log(`
|
|
283366
|
-
${brand("ablo")} ${
|
|
283832
|
+
${brand("ablo")} ${import_picocolors14.default.dim("status")}
|
|
283367
283833
|
`);
|
|
283368
283834
|
if (effective.key && effective.source && effective.source !== "stored") {
|
|
283369
283835
|
const label = effective.source === "env" ? "ABLO_API_KEY env" : effective.source;
|
|
283370
283836
|
console.log(
|
|
283371
|
-
` ${
|
|
283837
|
+
` ${import_picocolors14.default.dim("key")} ${effective.key.slice(0, 12)}\u2026 ${import_picocolors14.default.dim(`(${label} \u2014 overrides stored)`)}`
|
|
283372
283838
|
);
|
|
283373
283839
|
} else if (!cfg) {
|
|
283374
|
-
console.log(` ${
|
|
283840
|
+
console.log(` ${import_picocolors14.default.yellow("!")} Not logged in \u2014 run ${import_picocolors14.default.bold("ablo login")}.`);
|
|
283375
283841
|
}
|
|
283376
|
-
console.log(` ${
|
|
283842
|
+
console.log(` ${import_picocolors14.default.dim("mode")} ${import_picocolors14.default.bold(mode2)}`);
|
|
283377
283843
|
const activeEntry = getKeyEntry(mode2);
|
|
283378
283844
|
const key = describeEffectiveKey(mode2, process.env.ABLO_API_KEY, activeEntry);
|
|
283379
283845
|
if (key.keyMismatch) {
|
|
283380
|
-
console.log(` ${
|
|
283846
|
+
console.log(` ${import_picocolors14.default.yellow("!")} ${import_picocolors14.default.yellow(key.keyMismatch.message)}`);
|
|
283381
283847
|
}
|
|
283382
283848
|
const activeProject = getActiveProject();
|
|
283383
283849
|
printTargetLines(target, activeProject);
|
|
283384
283850
|
for (const m2 of ["sandbox", "production"]) {
|
|
283385
283851
|
const entry = getKeyEntry(m2);
|
|
283386
|
-
const marker = m2 === mode2 ?
|
|
283852
|
+
const marker = m2 === mode2 ? import_picocolors14.default.green("\u25CF") : import_picocolors14.default.dim("\u25CB");
|
|
283387
283853
|
if (entry) {
|
|
283388
283854
|
const exp = entry.expiresAt ? ` ${expiryLabel(entry.expiresAt)}` : "";
|
|
283389
|
-
console.log(` ${marker} ${m2.padEnd(10)} ${
|
|
283855
|
+
console.log(` ${marker} ${m2.padEnd(10)} ${import_picocolors14.default.dim(`${entry.apiKey.slice(0, 12)}\u2026`)}${exp}`);
|
|
283390
283856
|
} else {
|
|
283391
|
-
console.log(` ${marker} ${m2.padEnd(10)} ${
|
|
283857
|
+
console.log(` ${marker} ${m2.padEnd(10)} ${import_picocolors14.default.dim("\u2014 no key")}`);
|
|
283392
283858
|
}
|
|
283393
283859
|
}
|
|
283394
283860
|
const org = target?.confirmed?.organizationId ?? activeEntry?.organizationId;
|
|
283395
283861
|
const plan = resolvePushPlan();
|
|
283396
283862
|
console.log(
|
|
283397
|
-
` ${
|
|
283863
|
+
` ${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")})`)}`}`
|
|
283398
283864
|
);
|
|
283399
|
-
process.stdout.write(` ${
|
|
283865
|
+
process.stdout.write(` ${import_picocolors14.default.dim("api")} ${apiUrl2} `);
|
|
283400
283866
|
const reachable = await ping(apiUrl2);
|
|
283401
|
-
console.log(reachable ?
|
|
283867
|
+
console.log(reachable ? import_picocolors14.default.green("reachable") : import_picocolors14.default.red("unreachable"));
|
|
283402
283868
|
if (reachable) {
|
|
283403
283869
|
const introspectKey = effective.key;
|
|
283404
283870
|
const pushed = await fetchPushedSchema(apiUrl2, introspectKey);
|
|
283405
283871
|
if (pushed?.active) {
|
|
283406
|
-
const when = pushed.pushedAt ? ` ${
|
|
283407
|
-
const ver = pushed.version != null ? ` ${
|
|
283408
|
-
const hashLabel = pushed.hash ? ` ${
|
|
283409
|
-
console.log(` ${
|
|
283872
|
+
const when = pushed.pushedAt ? ` ${import_picocolors14.default.dim(`@ ${pushed.pushedAt.slice(0, 10)}`)}` : "";
|
|
283873
|
+
const ver = pushed.version != null ? ` ${import_picocolors14.default.dim(`(rev ${pushed.version})`)}` : "";
|
|
283874
|
+
const hashLabel = pushed.hash ? ` ${import_picocolors14.default.dim(`hash ${pushed.hash}`)}` : "";
|
|
283875
|
+
console.log(` ${import_picocolors14.default.dim("schema")} ${import_picocolors14.default.bold(`${pushed.models.length} models pushed`)}${ver}${hashLabel}${when}`);
|
|
283410
283876
|
for (const m2 of pushed.models) {
|
|
283411
|
-
const tn = m2.typename === m2.key ?
|
|
283877
|
+
const tn = m2.typename === m2.key ? import_picocolors14.default.dim(`typename=${m2.typename}`) : import_picocolors14.default.yellow(`typename=${m2.typename}`);
|
|
283412
283878
|
const conflict = formatConflict(m2.conflict);
|
|
283413
|
-
const conflictStr2 = conflict ? ` ${
|
|
283414
|
-
console.log(` ${
|
|
283879
|
+
const conflictStr2 = conflict ? ` ${import_picocolors14.default.dim(`conflict=${conflict}`)}` : "";
|
|
283880
|
+
console.log(` ${import_picocolors14.default.dim("\u2022")} ${m2.key.padEnd(14)} ${tn}${conflictStr2}`);
|
|
283415
283881
|
}
|
|
283416
283882
|
} else if (pushed && !pushed.active) {
|
|
283417
|
-
console.log(` ${
|
|
283883
|
+
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")})`)}`);
|
|
283418
283884
|
}
|
|
283419
283885
|
const firstPushedModel = pushed?.active ? pushed.models[0] : void 0;
|
|
283420
283886
|
if (firstPushedModel !== void 0) {
|
|
283421
283887
|
const probe = await probeDataPlane(apiUrl2, introspectKey, firstPushedModel.typename);
|
|
283422
283888
|
if (probe.status === "no_database") {
|
|
283423
|
-
console.log(` ${
|
|
283889
|
+
console.log(` ${import_picocolors14.default.dim("data")} ${import_picocolors14.default.red("\u2717 no database registered")}${org ? import_picocolors14.default.dim(` for org ${org}`) : ""}`);
|
|
283424
283890
|
console.log(
|
|
283425
|
-
` ${
|
|
283426
|
-
`reads/writes will fail with ${
|
|
283891
|
+
` ${import_picocolors14.default.dim(
|
|
283892
|
+
`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.`
|
|
283427
283893
|
)}`
|
|
283428
283894
|
);
|
|
283429
283895
|
} else if (probe.status === "intermittent") {
|
|
283430
|
-
console.log(` ${
|
|
283896
|
+
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}`) : ""}`);
|
|
283431
283897
|
console.log(
|
|
283432
|
-
` ${
|
|
283433
|
-
`some reads/writes fail with ${
|
|
283898
|
+
` ${import_picocolors14.default.dim(
|
|
283899
|
+
`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).`
|
|
283434
283900
|
)}`
|
|
283435
283901
|
);
|
|
283436
283902
|
} else if (probe.status === "forbidden") {
|
|
283437
|
-
console.log(` ${
|
|
283903
|
+
console.log(` ${import_picocolors14.default.dim("data")} ${import_picocolors14.default.yellow("? key not authorized to read")}${probe.detail ? import_picocolors14.default.dim(` (${probe.detail})`) : ""}`);
|
|
283438
283904
|
} else if (probe.status === "unknown") {
|
|
283439
|
-
console.log(` ${
|
|
283905
|
+
console.log(` ${import_picocolors14.default.dim("data")} ${import_picocolors14.default.yellow(`? data-plane check inconclusive (${probe.detail})`)}`);
|
|
283440
283906
|
}
|
|
283441
283907
|
}
|
|
283442
283908
|
}
|
|
@@ -283446,7 +283912,7 @@ async function status(args = []) {
|
|
|
283446
283912
|
// src/cli/logs.ts
|
|
283447
283913
|
init_cjs_shims();
|
|
283448
283914
|
init_errors();
|
|
283449
|
-
var
|
|
283915
|
+
var import_picocolors15 = __toESM(require_picocolors(), 1);
|
|
283450
283916
|
init_config();
|
|
283451
283917
|
init_theme();
|
|
283452
283918
|
init_push();
|
|
@@ -283513,10 +283979,10 @@ function resolveSince(since) {
|
|
|
283513
283979
|
var sleep2 = (ms) => new Promise((r2) => setTimeout(r2, ms));
|
|
283514
283980
|
function colorOp(op) {
|
|
283515
283981
|
const label = op.padEnd(6);
|
|
283516
|
-
if (op === "create") return
|
|
283517
|
-
if (op === "update") return
|
|
283518
|
-
if (op === "delete") return
|
|
283519
|
-
return
|
|
283982
|
+
if (op === "create") return import_picocolors15.default.green(label);
|
|
283983
|
+
if (op === "update") return import_picocolors15.default.yellow(label);
|
|
283984
|
+
if (op === "delete") return import_picocolors15.default.red(label);
|
|
283985
|
+
return import_picocolors15.default.dim(label);
|
|
283520
283986
|
}
|
|
283521
283987
|
function render(e2, json) {
|
|
283522
283988
|
if (json) {
|
|
@@ -283525,25 +283991,25 @@ function render(e2, json) {
|
|
|
283525
283991
|
return;
|
|
283526
283992
|
}
|
|
283527
283993
|
const t = new Date(e2.at).toLocaleTimeString();
|
|
283528
|
-
const actor = e2.actor ?
|
|
283529
|
-
console.log(` ${
|
|
283994
|
+
const actor = e2.actor ? import_picocolors15.default.dim(` ${e2.actor}`) : "";
|
|
283995
|
+
console.log(` ${import_picocolors15.default.dim(t)} ${colorOp(e2.op)} ${import_picocolors15.default.bold(e2.model)} ${import_picocolors15.default.dim(e2.recordId)}${actor}`);
|
|
283530
283996
|
}
|
|
283531
283997
|
async function logs(argv) {
|
|
283532
283998
|
let args;
|
|
283533
283999
|
try {
|
|
283534
284000
|
args = parseLogsArgs(argv);
|
|
283535
284001
|
} catch (err) {
|
|
283536
|
-
console.error(
|
|
284002
|
+
console.error(import_picocolors15.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
283537
284003
|
process.exit(1);
|
|
283538
284004
|
}
|
|
283539
284005
|
const apiKey = resolveApiKey(args.mode);
|
|
283540
284006
|
if (!apiKey) {
|
|
283541
284007
|
console.error(
|
|
283542
|
-
|
|
284008
|
+
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")}.`)
|
|
283543
284009
|
);
|
|
283544
284010
|
process.exit(1);
|
|
283545
284011
|
}
|
|
283546
|
-
const baseUrl2 = (
|
|
284012
|
+
const baseUrl2 = apiBaseUrl();
|
|
283547
284013
|
const since = resolveSince(args.since);
|
|
283548
284014
|
async function fetchPage(params) {
|
|
283549
284015
|
const qs = new URLSearchParams(params).toString();
|
|
@@ -283553,7 +284019,7 @@ async function logs(argv) {
|
|
|
283553
284019
|
if (!res) return null;
|
|
283554
284020
|
if (!res.ok) {
|
|
283555
284021
|
const body = await res.json().catch(() => ({}));
|
|
283556
|
-
console.error(
|
|
284022
|
+
console.error(import_picocolors15.default.red(` logs failed (${res.status}): ${body.reason ?? body.message ?? ""}`));
|
|
283557
284023
|
process.exit(1);
|
|
283558
284024
|
}
|
|
283559
284025
|
const json = await res.json();
|
|
@@ -283564,7 +284030,7 @@ async function logs(argv) {
|
|
|
283564
284030
|
}
|
|
283565
284031
|
if (!args.json) {
|
|
283566
284032
|
console.log(`
|
|
283567
|
-
${brand("ablo")} ${
|
|
284033
|
+
${brand("ablo")} ${import_picocolors15.default.dim("logs")} ${import_picocolors15.default.dim(`(${args.mode ?? "active"} mode)`)}
|
|
283568
284034
|
`);
|
|
283569
284035
|
}
|
|
283570
284036
|
const initial = await fetchPage({
|
|
@@ -283574,13 +284040,13 @@ async function logs(argv) {
|
|
|
283574
284040
|
...args.op ? { op: args.op } : {}
|
|
283575
284041
|
});
|
|
283576
284042
|
if (!initial) {
|
|
283577
|
-
console.error(
|
|
284043
|
+
console.error(import_picocolors15.default.red(` Couldn't reach ${baseUrl2}.`));
|
|
283578
284044
|
process.exit(1);
|
|
283579
284045
|
}
|
|
283580
284046
|
for (const e2 of initial.events) render(e2, args.json);
|
|
283581
284047
|
let cursor = initial.cursor;
|
|
283582
284048
|
if (!args.follow) return;
|
|
283583
|
-
if (!args.json) console.log(` ${
|
|
284049
|
+
if (!args.json) console.log(` ${import_picocolors15.default.dim("watching for new activity \u2026 (Ctrl-C to stop)")}
|
|
283584
284050
|
`);
|
|
283585
284051
|
for (; ; ) {
|
|
283586
284052
|
await sleep2(1500);
|
|
@@ -283598,7 +284064,7 @@ async function logs(argv) {
|
|
|
283598
284064
|
// src/cli/webhooks.ts
|
|
283599
284065
|
init_cjs_shims();
|
|
283600
284066
|
var import_fs8 = require("fs");
|
|
283601
|
-
var
|
|
284067
|
+
var import_picocolors16 = __toESM(require_picocolors(), 1);
|
|
283602
284068
|
init_credentialPolicy();
|
|
283603
284069
|
init_config();
|
|
283604
284070
|
init_theme();
|
|
@@ -283631,17 +284097,17 @@ function requireKey2(mode2) {
|
|
|
283631
284097
|
const apiKey = resolveApiKey(mode2);
|
|
283632
284098
|
if (!apiKey) {
|
|
283633
284099
|
console.error(
|
|
283634
|
-
|
|
284100
|
+
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")}.`)
|
|
283635
284101
|
);
|
|
283636
284102
|
process.exit(1);
|
|
283637
284103
|
}
|
|
283638
284104
|
if (classifyCredentialKind(apiKey) !== "secret") {
|
|
283639
|
-
console.error(
|
|
284105
|
+
console.error(import_picocolors16.default.red(" Managing webhooks requires a secret key ") + import_picocolors16.default.dim("(sk_test_ / sk_live_)."));
|
|
283640
284106
|
process.exit(1);
|
|
283641
284107
|
}
|
|
283642
284108
|
return apiKey;
|
|
283643
284109
|
}
|
|
283644
|
-
var baseUrl = () => (
|
|
284110
|
+
var baseUrl = () => apiBaseUrl();
|
|
283645
284111
|
async function api(apiKey, method, path, body) {
|
|
283646
284112
|
const res = await fetch(`${baseUrl()}/api/v1/webhook_endpoints${path}`, {
|
|
283647
284113
|
method,
|
|
@@ -283652,12 +284118,12 @@ async function api(apiKey, method, path, body) {
|
|
|
283652
284118
|
...body ? { body: JSON.stringify(body) } : {}
|
|
283653
284119
|
}).catch(() => null);
|
|
283654
284120
|
if (!res) {
|
|
283655
|
-
console.error(
|
|
284121
|
+
console.error(import_picocolors16.default.red(` Couldn't reach ${baseUrl()}.`));
|
|
283656
284122
|
process.exit(1);
|
|
283657
284123
|
}
|
|
283658
284124
|
if (!res.ok) {
|
|
283659
284125
|
const err = await res.json().catch(() => ({}));
|
|
283660
|
-
console.error(
|
|
284126
|
+
console.error(import_picocolors16.default.red(` Request failed (${res.status}): ${err.message ?? err.reason ?? ""}`));
|
|
283661
284127
|
process.exit(1);
|
|
283662
284128
|
}
|
|
283663
284129
|
return await res.json();
|
|
@@ -283679,11 +284145,11 @@ ${line}
|
|
|
283679
284145
|
return file;
|
|
283680
284146
|
}
|
|
283681
284147
|
function printEndpoint(e2) {
|
|
283682
|
-
const dot = e2.status === "enabled" ?
|
|
283683
|
-
const health = e2.last_error ?
|
|
283684
|
-
console.log(` ${dot} ${
|
|
284148
|
+
const dot = e2.status === "enabled" ? import_picocolors16.default.green("\u25CF") : import_picocolors16.default.red("\u25CF");
|
|
284149
|
+
const health = e2.last_error ? import_picocolors16.default.red(` last error: ${e2.last_error}`) : "";
|
|
284150
|
+
console.log(` ${dot} ${import_picocolors16.default.bold(e2.id)} ${e2.url}`);
|
|
283685
284151
|
console.log(
|
|
283686
|
-
|
|
284152
|
+
import_picocolors16.default.dim(
|
|
283687
284153
|
` ${e2.status} \xB7 ${e2.environment} \xB7 events ${e2.enabled_events.join(",")} \xB7 cursor ${e2.cursor ?? "\u2014"}${health}`
|
|
283688
284154
|
)
|
|
283689
284155
|
);
|
|
@@ -283695,7 +284161,7 @@ async function webhooks(argv) {
|
|
|
283695
284161
|
if (sub === "create") {
|
|
283696
284162
|
const url = positional(rest);
|
|
283697
284163
|
if (!url) {
|
|
283698
|
-
console.error(
|
|
284164
|
+
console.error(import_picocolors16.default.red(" Usage: ") + brand("ablo webhooks create <url>"));
|
|
283699
284165
|
process.exit(1);
|
|
283700
284166
|
}
|
|
283701
284167
|
const apiKey = requireKey2(mode2);
|
|
@@ -283707,8 +284173,8 @@ async function webhooks(argv) {
|
|
|
283707
284173
|
});
|
|
283708
284174
|
const file = writeSecretToEnv(created.secret);
|
|
283709
284175
|
console.log(`
|
|
283710
|
-
${
|
|
283711
|
-
console.log(` ${
|
|
284176
|
+
${import_picocolors16.default.green("\u2713")} Registered ${import_picocolors16.default.bold(created.id)} \u2192 ${created.url}`);
|
|
284177
|
+
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)")}
|
|
283712
284178
|
`);
|
|
283713
284179
|
return;
|
|
283714
284180
|
}
|
|
@@ -283716,7 +284182,7 @@ async function webhooks(argv) {
|
|
|
283716
284182
|
const apiKey = requireKey2(mode2);
|
|
283717
284183
|
const { data } = await api(apiKey, "GET", "");
|
|
283718
284184
|
if (data.length === 0) {
|
|
283719
|
-
console.log(
|
|
284185
|
+
console.log(import_picocolors16.default.dim(" No webhook endpoints. ") + brand("ablo webhooks create <url>"));
|
|
283720
284186
|
return;
|
|
283721
284187
|
}
|
|
283722
284188
|
console.log();
|
|
@@ -283727,40 +284193,40 @@ async function webhooks(argv) {
|
|
|
283727
284193
|
if (sub === "roll") {
|
|
283728
284194
|
const id = positional(rest);
|
|
283729
284195
|
if (!id) {
|
|
283730
|
-
console.error(
|
|
284196
|
+
console.error(import_picocolors16.default.red(" Usage: ") + brand("ablo webhooks roll <id>"));
|
|
283731
284197
|
process.exit(1);
|
|
283732
284198
|
}
|
|
283733
284199
|
const apiKey = requireKey2(mode2);
|
|
283734
284200
|
const rolled = await api(apiKey, "POST", `/${id}/roll_secret`);
|
|
283735
284201
|
const file = writeSecretToEnv(rolled.secret);
|
|
283736
284202
|
console.log(`
|
|
283737
|
-
${
|
|
284203
|
+
${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)")}
|
|
283738
284204
|
`);
|
|
283739
284205
|
return;
|
|
283740
284206
|
}
|
|
283741
284207
|
if (sub === "enable") {
|
|
283742
284208
|
const id = positional(rest);
|
|
283743
284209
|
if (!id) {
|
|
283744
|
-
console.error(
|
|
284210
|
+
console.error(import_picocolors16.default.red(" Usage: ") + brand("ablo webhooks enable <id>"));
|
|
283745
284211
|
process.exit(1);
|
|
283746
284212
|
}
|
|
283747
284213
|
const apiKey = requireKey2(mode2);
|
|
283748
284214
|
const e2 = await api(apiKey, "POST", `/${id}/enable`);
|
|
283749
|
-
console.log(` ${
|
|
284215
|
+
console.log(` ${import_picocolors16.default.green("\u2713")} Re-enabled ${import_picocolors16.default.bold(e2.id)}`);
|
|
283750
284216
|
return;
|
|
283751
284217
|
}
|
|
283752
284218
|
if (sub === "rm" || sub === "delete") {
|
|
283753
284219
|
const id = positional(rest);
|
|
283754
284220
|
if (!id) {
|
|
283755
|
-
console.error(
|
|
284221
|
+
console.error(import_picocolors16.default.red(" Usage: ") + brand("ablo webhooks rm <id>"));
|
|
283756
284222
|
process.exit(1);
|
|
283757
284223
|
}
|
|
283758
284224
|
const apiKey = requireKey2(mode2);
|
|
283759
284225
|
await api(apiKey, "DELETE", `/${id}`);
|
|
283760
|
-
console.log(` ${
|
|
284226
|
+
console.log(` ${import_picocolors16.default.green("\u2713")} Removed ${import_picocolors16.default.bold(id)}`);
|
|
283761
284227
|
return;
|
|
283762
284228
|
}
|
|
283763
|
-
console.log(` ${
|
|
284229
|
+
console.log(` ${import_picocolors16.default.bold("Usage:")}`);
|
|
283764
284230
|
console.log(` ${brand("ablo webhooks create <url>")} Register an endpoint; writes ${ENV_KEY}`);
|
|
283765
284231
|
console.log(` ${brand("ablo webhooks list")} List endpoints + delivery health`);
|
|
283766
284232
|
console.log(` ${brand("ablo webhooks roll <id>")} Mint a fresh signing secret`);
|
|
@@ -283772,7 +284238,7 @@ async function webhooks(argv) {
|
|
|
283772
284238
|
// src/cli/check.ts
|
|
283773
284239
|
init_cjs_shims();
|
|
283774
284240
|
init_errors();
|
|
283775
|
-
var
|
|
284241
|
+
var import_picocolors17 = __toESM(require_picocolors(), 1);
|
|
283776
284242
|
init_src();
|
|
283777
284243
|
var import_schema6 = require("@abloatai/ablo/schema");
|
|
283778
284244
|
init_push();
|
|
@@ -283810,13 +284276,13 @@ async function check(argv) {
|
|
|
283810
284276
|
try {
|
|
283811
284277
|
args = parseCheckArgs(argv);
|
|
283812
284278
|
} catch (err) {
|
|
283813
|
-
console.error(
|
|
284279
|
+
console.error(import_picocolors17.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
283814
284280
|
process.exit(1);
|
|
283815
284281
|
}
|
|
283816
284282
|
const dbUrl = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
|
|
283817
284283
|
if (!dbUrl) {
|
|
283818
284284
|
console.error(
|
|
283819
|
-
|
|
284285
|
+
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.`)
|
|
283820
284286
|
);
|
|
283821
284287
|
process.exit(1);
|
|
283822
284288
|
}
|
|
@@ -283831,7 +284297,7 @@ async function check(argv) {
|
|
|
283831
284297
|
[args.appSchema]
|
|
283832
284298
|
);
|
|
283833
284299
|
} catch (err) {
|
|
283834
|
-
console.error(
|
|
284300
|
+
console.error(import_picocolors17.default.red(` Couldn't read the database: ${err instanceof Error ? err.message : String(err)}`));
|
|
283835
284301
|
await sql.end({ timeout: 2 });
|
|
283836
284302
|
process.exit(1);
|
|
283837
284303
|
}
|
|
@@ -283846,7 +284312,7 @@ async function check(argv) {
|
|
|
283846
284312
|
set.add(r2.column_name);
|
|
283847
284313
|
}
|
|
283848
284314
|
console.log(`
|
|
283849
|
-
${brand("ablo")} ${
|
|
284315
|
+
${brand("ablo")} ${import_picocolors17.default.dim("check")} ${import_picocolors17.default.dim(`schema "${args.appSchema}"`)}
|
|
283850
284316
|
`);
|
|
283851
284317
|
const declaredTables = /* @__PURE__ */ new Set();
|
|
283852
284318
|
let errors = 0;
|
|
@@ -283856,7 +284322,7 @@ async function check(argv) {
|
|
|
283856
284322
|
declaredTables.add(table);
|
|
283857
284323
|
const present = colsByTable.get(table);
|
|
283858
284324
|
if (!present) {
|
|
283859
|
-
console.log(` ${
|
|
284325
|
+
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")}`);
|
|
283860
284326
|
errors++;
|
|
283861
284327
|
continue;
|
|
283862
284328
|
}
|
|
@@ -283878,26 +284344,26 @@ async function check(argv) {
|
|
|
283878
284344
|
if (!present.has(col)) problems.push(`missing column "${col}" (field ${fieldName})`);
|
|
283879
284345
|
}
|
|
283880
284346
|
if (problems.length > 0) {
|
|
283881
|
-
console.log(` ${
|
|
283882
|
-
for (const p2 of problems) console.log(` ${
|
|
283883
|
-
for (const w2 of warns) console.log(` ${
|
|
284347
|
+
console.log(` ${import_picocolors17.default.red("\u2717")} ${import_picocolors17.default.bold(key)} ${import_picocolors17.default.dim("\u2192")} ${table}`);
|
|
284348
|
+
for (const p2 of problems) console.log(` ${import_picocolors17.default.red("\u2022")} ${p2}`);
|
|
284349
|
+
for (const w2 of warns) console.log(` ${import_picocolors17.default.yellow("\u2022")} ${w2}`);
|
|
283884
284350
|
errors++;
|
|
283885
284351
|
} else if (warns.length > 0) {
|
|
283886
|
-
console.log(` ${
|
|
283887
|
-
for (const w2 of warns) console.log(` ${
|
|
284352
|
+
console.log(` ${import_picocolors17.default.yellow("!")} ${import_picocolors17.default.bold(key)} ${import_picocolors17.default.dim("\u2192")} ${table}`);
|
|
284353
|
+
for (const w2 of warns) console.log(` ${import_picocolors17.default.yellow("\u2022")} ${w2}`);
|
|
283888
284354
|
warnings++;
|
|
283889
284355
|
} else {
|
|
283890
|
-
console.log(` ${
|
|
284356
|
+
console.log(` ${import_picocolors17.default.green("\u2713")} ${import_picocolors17.default.bold(key)} ${import_picocolors17.default.dim(`\u2192 ${table} (id, ${orgCol ?? "no org"} ok)`)}`);
|
|
283891
284357
|
}
|
|
283892
284358
|
}
|
|
283893
284359
|
const modelCount = Object.keys(schemaJson.models).length;
|
|
283894
284360
|
const ignored = [...colsByTable.keys()].filter((t) => !declaredTables.has(t)).length;
|
|
283895
284361
|
console.log(
|
|
283896
284362
|
`
|
|
283897
|
-
${modelCount} model${modelCount === 1 ? "" : "s"} \xB7 ${
|
|
284363
|
+
${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"}`)}` : "")
|
|
283898
284364
|
);
|
|
283899
284365
|
if (ignored > 0) {
|
|
283900
|
-
console.log(` ${
|
|
284366
|
+
console.log(` ${import_picocolors17.default.dim(`${ignored} other table${ignored === 1 ? "" : "s"} in your database \u2014 ignored by Ablo`)}`);
|
|
283901
284367
|
}
|
|
283902
284368
|
console.log();
|
|
283903
284369
|
process.exit(errors > 0 ? 1 : 0);
|
|
@@ -283905,7 +284371,7 @@ async function check(argv) {
|
|
|
283905
284371
|
|
|
283906
284372
|
// src/cli/upgrade.ts
|
|
283907
284373
|
init_cjs_shims();
|
|
283908
|
-
var
|
|
284374
|
+
var import_picocolors18 = __toESM(require_picocolors(), 1);
|
|
283909
284375
|
var import_ts_morph = __toESM(require_ts_morph(), 1);
|
|
283910
284376
|
var DEFAULT_GLOBS = ["app/**/*.{ts,tsx}", "src/**/*.{ts,tsx}", "ablo/**/*.{ts,tsx}", "lib/**/*.{ts,tsx}"];
|
|
283911
284377
|
var VERB_ARGS = {
|
|
@@ -283983,7 +284449,7 @@ async function upgrade(argv) {
|
|
|
283983
284449
|
project.addSourceFilesAtPaths(globs.length > 0 ? globs : DEFAULT_GLOBS);
|
|
283984
284450
|
const files = project.getSourceFiles();
|
|
283985
284451
|
if (files.length === 0) {
|
|
283986
|
-
console.log(
|
|
284452
|
+
console.log(import_picocolors18.default.yellow(' No .ts/.tsx files found. Pass a glob, e.g. `ablo upgrade "src/**/*.tsx"`.'));
|
|
283987
284453
|
return;
|
|
283988
284454
|
}
|
|
283989
284455
|
const edits = [];
|
|
@@ -284057,39 +284523,39 @@ async function upgrade(argv) {
|
|
|
284057
284523
|
const rel = (f) => f.replace(cwd + "/", "");
|
|
284058
284524
|
console.log();
|
|
284059
284525
|
if (edits.length === 0 && manual.length === 0) {
|
|
284060
|
-
console.log(
|
|
284526
|
+
console.log(import_picocolors18.default.green(" \u2713 Nothing to migrate \u2014 your code is already on the current API."));
|
|
284061
284527
|
return;
|
|
284062
284528
|
}
|
|
284063
284529
|
if (edits.length > 0) {
|
|
284064
|
-
console.log(
|
|
284530
|
+
console.log(import_picocolors18.default.bold(` ${write ? "Applied" : "Would apply"} ${edits.length} change${edits.length === 1 ? "" : "s"}:`));
|
|
284065
284531
|
for (const e2 of edits) {
|
|
284066
|
-
console.log(` ${
|
|
284067
|
-
console.log(` ${
|
|
284068
|
-
console.log(` ${
|
|
284532
|
+
console.log(` ${import_picocolors18.default.dim(`${rel(e2.file)}:${e2.line}`)} ${import_picocolors18.default.cyan(e2.rule)}`);
|
|
284533
|
+
console.log(` ${import_picocolors18.default.red("-")} ${e2.before}`);
|
|
284534
|
+
console.log(` ${import_picocolors18.default.green("+")} ${e2.after}`);
|
|
284069
284535
|
}
|
|
284070
284536
|
}
|
|
284071
284537
|
if (manual.length > 0) {
|
|
284072
284538
|
console.log();
|
|
284073
|
-
console.log(
|
|
284539
|
+
console.log(import_picocolors18.default.bold(import_picocolors18.default.yellow(` ${manual.length} spot${manual.length === 1 ? "" : "s"} need manual review (structural):`)));
|
|
284074
284540
|
for (const m2 of manual) {
|
|
284075
|
-
console.log(` ${
|
|
284076
|
-
console.log(` ${
|
|
284541
|
+
console.log(` ${import_picocolors18.default.dim(`${rel(m2.file)}:${m2.line}`)} ${import_picocolors18.default.yellow(m2.rule)}`);
|
|
284542
|
+
console.log(` ${import_picocolors18.default.dim(m2.snippet)}`);
|
|
284077
284543
|
console.log(` \u2192 ${m2.hint}`);
|
|
284078
284544
|
}
|
|
284079
284545
|
}
|
|
284080
284546
|
console.log();
|
|
284081
284547
|
if (write) {
|
|
284082
284548
|
await project.save();
|
|
284083
|
-
console.log(
|
|
284549
|
+
console.log(import_picocolors18.default.green(` \u2713 Wrote ${edits.length} change${edits.length === 1 ? "" : "s"}. Review the diff, run your typecheck.`));
|
|
284084
284550
|
} else {
|
|
284085
|
-
console.log(
|
|
284551
|
+
console.log(import_picocolors18.default.dim(" Dry run. Re-run with `--write` to apply the auto-fixes above (manual items are never auto-written)."));
|
|
284086
284552
|
}
|
|
284087
284553
|
}
|
|
284088
284554
|
|
|
284089
284555
|
// src/cli/pull.ts
|
|
284090
284556
|
init_cjs_shims();
|
|
284091
284557
|
init_errors();
|
|
284092
|
-
var
|
|
284558
|
+
var import_picocolors19 = __toESM(require_picocolors(), 1);
|
|
284093
284559
|
init_src();
|
|
284094
284560
|
var import_fs9 = require("fs");
|
|
284095
284561
|
init_theme();
|
|
@@ -284203,45 +284669,45 @@ async function pull(argv) {
|
|
|
284203
284669
|
try {
|
|
284204
284670
|
args = parsePullArgs(argv);
|
|
284205
284671
|
} catch (err) {
|
|
284206
|
-
console.error(
|
|
284672
|
+
console.error(import_picocolors19.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
284207
284673
|
process.exit(1);
|
|
284208
284674
|
}
|
|
284209
284675
|
const dbUrl = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
|
|
284210
284676
|
if (!dbUrl) {
|
|
284211
|
-
console.error(
|
|
284677
|
+
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.`));
|
|
284212
284678
|
process.exit(1);
|
|
284213
284679
|
}
|
|
284214
284680
|
if ((0, import_fs9.existsSync)(args.out) && !args.force) {
|
|
284215
284681
|
console.error(
|
|
284216
|
-
|
|
284682
|
+
import_picocolors19.default.red(` ${args.out} already exists.`) + import_picocolors19.default.dim(` Re-run with ${import_picocolors19.default.bold("--force")} to overwrite.`)
|
|
284217
284683
|
);
|
|
284218
284684
|
process.exit(1);
|
|
284219
284685
|
}
|
|
284220
284686
|
console.log(`
|
|
284221
|
-
${brand("ablo")} ${
|
|
284687
|
+
${brand("ablo")} ${import_picocolors19.default.dim("pull")} ${import_picocolors19.default.dim(`schema "${args.appSchema}"`)}
|
|
284222
284688
|
`);
|
|
284223
284689
|
let result;
|
|
284224
284690
|
try {
|
|
284225
284691
|
result = await buildSchemaSourceFromDb({ dbUrl, appSchema: args.appSchema, importPath: args.importPath });
|
|
284226
284692
|
} catch (err) {
|
|
284227
|
-
console.error(
|
|
284693
|
+
console.error(import_picocolors19.default.red(` Couldn't read the database: ${err instanceof Error ? err.message : String(err)}`));
|
|
284228
284694
|
process.exit(1);
|
|
284229
284695
|
}
|
|
284230
284696
|
if (result.models.length === 0) {
|
|
284231
284697
|
console.error(
|
|
284232
|
-
|
|
284698
|
+
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).`)
|
|
284233
284699
|
);
|
|
284234
284700
|
process.exit(1);
|
|
284235
284701
|
}
|
|
284236
284702
|
(0, import_fs9.writeFileSync)(args.out, result.source);
|
|
284237
|
-
console.log(` ${
|
|
284238
|
-
console.log(` ${
|
|
284703
|
+
console.log(` ${import_picocolors19.default.green("\u2713")} wrote ${import_picocolors19.default.bold(args.out)} ${import_picocolors19.default.dim(`(${result.models.length} models)`)}`);
|
|
284704
|
+
console.log(` ${import_picocolors19.default.dim(`models: ${result.models.join(", ")}`)}`);
|
|
284239
284705
|
if (result.skipped > 0) {
|
|
284240
|
-
console.log(` ${
|
|
284706
|
+
console.log(` ${import_picocolors19.default.dim(`${result.skipped} table(s) skipped \u2014 no id/organization_id`)}`);
|
|
284241
284707
|
}
|
|
284242
284708
|
console.log(
|
|
284243
284709
|
`
|
|
284244
|
-
${
|
|
284710
|
+
${import_picocolors19.default.dim("Introspection is lossy (enums, JSON shape, relations). Review the file, then")} ${import_picocolors19.default.bold("ablo check")}.
|
|
284245
284711
|
`
|
|
284246
284712
|
);
|
|
284247
284713
|
}
|
|
@@ -284249,7 +284715,7 @@ async function pull(argv) {
|
|
|
284249
284715
|
// src/cli/prismaPull.ts
|
|
284250
284716
|
init_cjs_shims();
|
|
284251
284717
|
init_errors();
|
|
284252
|
-
var
|
|
284718
|
+
var import_picocolors20 = __toESM(require_picocolors(), 1);
|
|
284253
284719
|
var import_fs10 = require("fs");
|
|
284254
284720
|
init_theme();
|
|
284255
284721
|
|
|
@@ -284534,55 +285000,55 @@ async function prismaPull(argv) {
|
|
|
284534
285000
|
try {
|
|
284535
285001
|
args = parsePrismaPullArgs(argv);
|
|
284536
285002
|
} catch (err) {
|
|
284537
|
-
console.error(
|
|
285003
|
+
console.error(import_picocolors20.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
284538
285004
|
process.exit(1);
|
|
284539
285005
|
}
|
|
284540
285006
|
if (!(0, import_fs10.existsSync)(args.schema)) {
|
|
284541
285007
|
console.error(
|
|
284542
|
-
|
|
285008
|
+
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>")}.`)
|
|
284543
285009
|
);
|
|
284544
285010
|
process.exit(1);
|
|
284545
285011
|
}
|
|
284546
285012
|
if ((0, import_fs10.existsSync)(args.out) && !args.force) {
|
|
284547
285013
|
console.error(
|
|
284548
|
-
|
|
285014
|
+
import_picocolors20.default.red(` ${args.out} already exists.`) + import_picocolors20.default.dim(` Re-run with ${import_picocolors20.default.bold("--force")} to overwrite.`)
|
|
284549
285015
|
);
|
|
284550
285016
|
process.exit(1);
|
|
284551
285017
|
}
|
|
284552
285018
|
console.log(`
|
|
284553
|
-
${brand("ablo")} ${
|
|
285019
|
+
${brand("ablo")} ${import_picocolors20.default.dim("pull prisma")} ${import_picocolors20.default.dim(args.schema)}
|
|
284554
285020
|
`);
|
|
284555
285021
|
let result;
|
|
284556
285022
|
try {
|
|
284557
285023
|
const src = (0, import_fs10.readFileSync)(args.schema, "utf8");
|
|
284558
285024
|
result = buildSchemaSourceFromPrisma({ src, importPath: args.importPath });
|
|
284559
285025
|
} catch (err) {
|
|
284560
|
-
console.error(
|
|
285026
|
+
console.error(import_picocolors20.default.red(` Couldn't parse the schema: ${err instanceof Error ? err.message : String(err)}`));
|
|
284561
285027
|
process.exit(1);
|
|
284562
285028
|
}
|
|
284563
285029
|
if (result.models.length === 0) {
|
|
284564
285030
|
console.error(
|
|
284565
|
-
|
|
285031
|
+
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")}).`)
|
|
284566
285032
|
);
|
|
284567
285033
|
process.exit(1);
|
|
284568
285034
|
}
|
|
284569
285035
|
(0, import_fs10.writeFileSync)(args.out, result.source);
|
|
284570
|
-
console.log(` ${
|
|
284571
|
-
console.log(` ${
|
|
285036
|
+
console.log(` ${import_picocolors20.default.green("\u2713")} wrote ${import_picocolors20.default.bold(args.out)} ${import_picocolors20.default.dim(`(${result.models.length} models)`)}`);
|
|
285037
|
+
console.log(` ${import_picocolors20.default.dim(`models: ${result.models.join(", ")}`)}`);
|
|
284572
285038
|
if (result.skipped.length > 0) {
|
|
284573
|
-
console.log(` ${
|
|
284574
|
-
for (const s of result.skipped) console.log(` ${
|
|
285039
|
+
console.log(` ${import_picocolors20.default.dim(`${result.skipped.length} model(s) skipped:`)}`);
|
|
285040
|
+
for (const s of result.skipped) console.log(` ${import_picocolors20.default.dim(`- ${s.name}: ${s.reason}`)}`);
|
|
284575
285041
|
}
|
|
284576
285042
|
console.log(
|
|
284577
285043
|
`
|
|
284578
|
-
${
|
|
285044
|
+
${import_picocolors20.default.dim("Enums and relations were preserved. Review the file, then")} ${import_picocolors20.default.bold("ablo check")}.
|
|
284579
285045
|
`
|
|
284580
285046
|
);
|
|
284581
285047
|
}
|
|
284582
285048
|
|
|
284583
285049
|
// src/cli/drizzlePull.ts
|
|
284584
285050
|
init_cjs_shims();
|
|
284585
|
-
var
|
|
285051
|
+
var import_picocolors21 = __toESM(require_picocolors(), 1);
|
|
284586
285052
|
init_errors();
|
|
284587
285053
|
var import_fs11 = require("fs");
|
|
284588
285054
|
var import_path6 = require("path");
|
|
@@ -284721,27 +285187,27 @@ async function drizzlePull(argv) {
|
|
|
284721
285187
|
try {
|
|
284722
285188
|
args = parseDrizzlePullArgs(argv);
|
|
284723
285189
|
} catch (err) {
|
|
284724
|
-
console.error(
|
|
285190
|
+
console.error(import_picocolors21.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
284725
285191
|
process.exit(1);
|
|
284726
285192
|
}
|
|
284727
285193
|
if (!args.schema) {
|
|
284728
285194
|
console.error(
|
|
284729
|
-
|
|
285195
|
+
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")}.`)
|
|
284730
285196
|
);
|
|
284731
285197
|
process.exit(1);
|
|
284732
285198
|
}
|
|
284733
285199
|
if (!(0, import_fs11.existsSync)(args.schema)) {
|
|
284734
|
-
console.error(
|
|
285200
|
+
console.error(import_picocolors21.default.red(` No file at ${import_picocolors21.default.bold(args.schema)}.`));
|
|
284735
285201
|
process.exit(1);
|
|
284736
285202
|
}
|
|
284737
285203
|
if ((0, import_fs11.existsSync)(args.out) && !args.force) {
|
|
284738
285204
|
console.error(
|
|
284739
|
-
|
|
285205
|
+
import_picocolors21.default.red(` ${args.out} already exists.`) + import_picocolors21.default.dim(` Re-run with ${import_picocolors21.default.bold("--force")} to overwrite.`)
|
|
284740
285206
|
);
|
|
284741
285207
|
process.exit(1);
|
|
284742
285208
|
}
|
|
284743
285209
|
console.log(`
|
|
284744
|
-
${brand("ablo")} ${
|
|
285210
|
+
${brand("ablo")} ${import_picocolors21.default.dim("pull drizzle")} ${import_picocolors21.default.dim(args.schema)}
|
|
284745
285211
|
`);
|
|
284746
285212
|
let result;
|
|
284747
285213
|
try {
|
|
@@ -284749,26 +285215,26 @@ async function drizzlePull(argv) {
|
|
|
284749
285215
|
result = await buildSchemaSourceFromDrizzle({ mod, importPath: args.importPath });
|
|
284750
285216
|
} catch (err) {
|
|
284751
285217
|
const msg = err instanceof Error ? err.message : String(err);
|
|
284752
|
-
const hint = msg.includes("Cannot find package 'drizzle-orm'") ?
|
|
284753
|
-
console.error(
|
|
285218
|
+
const hint = msg.includes("Cannot find package 'drizzle-orm'") ? import_picocolors21.default.dim(` (install ${import_picocolors21.default.bold("drizzle-orm")} in this project)`) : "";
|
|
285219
|
+
console.error(import_picocolors21.default.red(` Couldn't load the schema: ${msg}`) + hint);
|
|
284754
285220
|
process.exit(1);
|
|
284755
285221
|
}
|
|
284756
285222
|
if (result.models.length === 0) {
|
|
284757
285223
|
console.error(
|
|
284758
|
-
|
|
285224
|
+
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).`)
|
|
284759
285225
|
);
|
|
284760
285226
|
process.exit(1);
|
|
284761
285227
|
}
|
|
284762
285228
|
(0, import_fs11.writeFileSync)(args.out, result.source);
|
|
284763
|
-
console.log(` ${
|
|
284764
|
-
console.log(` ${
|
|
285229
|
+
console.log(` ${import_picocolors21.default.green("\u2713")} wrote ${import_picocolors21.default.bold(args.out)} ${import_picocolors21.default.dim(`(${result.models.length} models)`)}`);
|
|
285230
|
+
console.log(` ${import_picocolors21.default.dim(`models: ${result.models.join(", ")}`)}`);
|
|
284765
285231
|
if (result.skipped.length > 0) {
|
|
284766
|
-
console.log(` ${
|
|
284767
|
-
for (const s of result.skipped) console.log(` ${
|
|
285232
|
+
console.log(` ${import_picocolors21.default.dim(`${result.skipped.length} table(s) skipped:`)}`);
|
|
285233
|
+
for (const s of result.skipped) console.log(` ${import_picocolors21.default.dim(`- ${s.name}: ${s.reason}`)}`);
|
|
284768
285234
|
}
|
|
284769
285235
|
console.log(
|
|
284770
285236
|
`
|
|
284771
|
-
${
|
|
285237
|
+
${import_picocolors21.default.dim("Enums and relations were preserved. Review the file, then")} ${import_picocolors21.default.bold("ablo check")}.
|
|
284772
285238
|
`
|
|
284773
285239
|
);
|
|
284774
285240
|
}
|
|
@@ -284778,7 +285244,7 @@ init_theme();
|
|
|
284778
285244
|
|
|
284779
285245
|
// src/cli/renderError.ts
|
|
284780
285246
|
init_cjs_shims();
|
|
284781
|
-
var
|
|
285247
|
+
var import_picocolors22 = __toESM(require_picocolors(), 1);
|
|
284782
285248
|
init_errors();
|
|
284783
285249
|
init_theme();
|
|
284784
285250
|
var RECOVERY_HINT = {
|
|
@@ -284800,15 +285266,15 @@ function isStringArray(v2) {
|
|
|
284800
285266
|
function renderKnownDetails(details, line) {
|
|
284801
285267
|
if (!details) return;
|
|
284802
285268
|
const { retryAfterSeconds, missingIds, requiredCapability, unexecutable, errors } = details;
|
|
284803
|
-
if (typeof retryAfterSeconds === "number") line(` ${
|
|
285269
|
+
if (typeof retryAfterSeconds === "number") line(` ${import_picocolors22.default.dim("retry")} after ${retryAfterSeconds}s`);
|
|
284804
285270
|
if (isStringArray(missingIds) && missingIds.length > 0) {
|
|
284805
285271
|
const shown = missingIds.slice(0, 5).join(", ");
|
|
284806
285272
|
const more = missingIds.length > 5 ? ` (+${missingIds.length - 5} more)` : "";
|
|
284807
|
-
line(` ${
|
|
285273
|
+
line(` ${import_picocolors22.default.dim("missing")} ${shown}${more}`);
|
|
284808
285274
|
}
|
|
284809
|
-
if (typeof requiredCapability === "string") line(` ${
|
|
285275
|
+
if (typeof requiredCapability === "string") line(` ${import_picocolors22.default.dim("needs")} ${requiredCapability}`);
|
|
284810
285276
|
if (Array.isArray(unexecutable) && unexecutable.length > 0) {
|
|
284811
|
-
line(` ${
|
|
285277
|
+
line(` ${import_picocolors22.default.dim("blocked")} ${unexecutable.length} change(s) can't be applied \u2014 see \`unexecutable\` (--verbose)`);
|
|
284812
285278
|
}
|
|
284813
285279
|
if (Array.isArray(errors)) {
|
|
284814
285280
|
for (const e2 of errors.slice(0, 8)) {
|
|
@@ -284816,7 +285282,7 @@ function renderKnownDetails(details, line) {
|
|
|
284816
285282
|
const rec = e2;
|
|
284817
285283
|
const where = typeof rec.param === "string" ? `${rec.param}: ` : "";
|
|
284818
285284
|
const msg = typeof rec.message === "string" ? rec.message : "";
|
|
284819
|
-
if (msg) line(` ${
|
|
285285
|
+
if (msg) line(` ${import_picocolors22.default.dim("\xB7")} ${where}${msg}`);
|
|
284820
285286
|
}
|
|
284821
285287
|
}
|
|
284822
285288
|
}
|
|
@@ -284827,22 +285293,22 @@ function renderCliError(err, opts = {}) {
|
|
|
284827
285293
|
});
|
|
284828
285294
|
const verbose = opts.verbose ?? (process.argv.includes("--verbose") || process.env.ABLO_VERBOSE === "1");
|
|
284829
285295
|
if (err instanceof AbloError) {
|
|
284830
|
-
const codeTag = err.code ? ` ${
|
|
285296
|
+
const codeTag = err.code ? ` ${import_picocolors22.default.dim(`[${err.code}]`)}` : "";
|
|
284831
285297
|
line("");
|
|
284832
|
-
line(` ${brand("ablo")} ${
|
|
285298
|
+
line(` ${brand("ablo")} ${import_picocolors22.default.red("\u2717")} ${import_picocolors22.default.bold(titleForType(err.type))}${codeTag}`);
|
|
284833
285299
|
line("");
|
|
284834
285300
|
line(` ${err.message}`);
|
|
284835
|
-
if (err.param) line(` ${
|
|
285301
|
+
if (err.param) line(` ${import_picocolors22.default.dim("field")} ${err.param}`);
|
|
284836
285302
|
renderKnownDetails(err.details, line);
|
|
284837
285303
|
const hint = err.code ? RECOVERY_HINT[classifyRecovery(err.code)] : void 0;
|
|
284838
|
-
if (hint) line(` ${
|
|
284839
|
-
if (err.docUrl) line(` ${
|
|
284840
|
-
if (err.requestId) line(` ${
|
|
285304
|
+
if (hint) line(` ${import_picocolors22.default.dim(hint)}`);
|
|
285305
|
+
if (err.docUrl) line(` ${import_picocolors22.default.dim("docs")} ${err.docUrl}`);
|
|
285306
|
+
if (err.requestId) line(` ${import_picocolors22.default.dim("ref")} ${err.requestId}`);
|
|
284841
285307
|
if (verbose) {
|
|
284842
285308
|
if (err.details && Object.keys(err.details).length > 0) {
|
|
284843
|
-
line(` ${
|
|
285309
|
+
line(` ${import_picocolors22.default.dim("details")} ${JSON.stringify(err.details)}`);
|
|
284844
285310
|
}
|
|
284845
|
-
if (err.stack) line(
|
|
285311
|
+
if (err.stack) line(import_picocolors22.default.dim(err.stack));
|
|
284846
285312
|
}
|
|
284847
285313
|
line("");
|
|
284848
285314
|
process.exitCode = 1;
|
|
@@ -284850,16 +285316,16 @@ function renderCliError(err, opts = {}) {
|
|
|
284850
285316
|
}
|
|
284851
285317
|
const message = err instanceof Error ? err.message : String(err);
|
|
284852
285318
|
line("");
|
|
284853
|
-
line(` ${brand("ablo")} ${
|
|
284854
|
-
if (verbose && err instanceof Error && err.stack) line(
|
|
284855
|
-
else line(` ${
|
|
285319
|
+
line(` ${brand("ablo")} ${import_picocolors22.default.red("\u2717")} ${message}`);
|
|
285320
|
+
if (verbose && err instanceof Error && err.stack) line(import_picocolors22.default.dim(err.stack));
|
|
285321
|
+
else line(` ${import_picocolors22.default.dim("Run with --verbose for the full error.")}`);
|
|
284856
285322
|
line("");
|
|
284857
285323
|
process.exitCode = 1;
|
|
284858
285324
|
}
|
|
284859
285325
|
|
|
284860
285326
|
// src/cli/index.ts
|
|
284861
285327
|
var LOGO = `
|
|
284862
|
-
${brand("ablo")} ${
|
|
285328
|
+
${brand("ablo")} ${import_picocolors23.default.dim("sync engine")}
|
|
284863
285329
|
`;
|
|
284864
285330
|
var SUBCOMMAND_USAGE = {
|
|
284865
285331
|
connect: CONNECT_USAGE,
|
|
@@ -284894,7 +285360,7 @@ async function main() {
|
|
|
284894
285360
|
const devArgs = process.argv.slice(3);
|
|
284895
285361
|
const oneShot = devArgs.includes("--no-watch");
|
|
284896
285362
|
console.log(
|
|
284897
|
-
|
|
285363
|
+
import_picocolors23.default.dim(
|
|
284898
285364
|
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."
|
|
284899
285365
|
)
|
|
284900
285366
|
);
|
|
@@ -284923,14 +285389,14 @@ async function main() {
|
|
|
284923
285389
|
const guard = guardActiveProjectKey();
|
|
284924
285390
|
if (!guard.ok && guard.available.length > 0 && !rest.includes("--url")) {
|
|
284925
285391
|
console.error(
|
|
284926
|
-
` ${
|
|
285392
|
+
` ${import_picocolors23.default.yellow("\u26A0")} active project ${import_picocolors23.default.bold(guard.activeProfile)} has no stored key ${import_picocolors23.default.dim(
|
|
284927
285393
|
`(you have keys for: ${guard.available.join(", ")})`
|
|
284928
285394
|
)}`
|
|
284929
285395
|
);
|
|
284930
285396
|
const loginCmd = guard.activeProfile === "default" ? "ablo login" : `ablo login --project ${guard.activeProfile}`;
|
|
284931
285397
|
console.error(
|
|
284932
|
-
|
|
284933
|
-
` Mint one with ${
|
|
285398
|
+
import_picocolors23.default.dim(
|
|
285399
|
+
` Mint one with ${import_picocolors23.default.bold(loginCmd)}, or switch with ${import_picocolors23.default.bold("ablo projects use <slug>")}.`
|
|
284934
285400
|
)
|
|
284935
285401
|
);
|
|
284936
285402
|
process.exitCode = 1;
|
|
@@ -284948,13 +285414,13 @@ async function main() {
|
|
|
284948
285414
|
await generate(process.argv.slice(3));
|
|
284949
285415
|
} else if (command === "schema") {
|
|
284950
285416
|
console.error(
|
|
284951
|
-
` ${
|
|
285417
|
+
` ${import_picocolors23.default.red("\u2717")} \`ablo schema push\` was renamed to \`${brand("ablo push")}\`.`
|
|
284952
285418
|
);
|
|
284953
285419
|
console.error(` Run \`ablo push${process.argv.slice(4).join(" ") ? " " + process.argv.slice(4).join(" ") : ""}\` instead.`);
|
|
284954
285420
|
process.exitCode = 1;
|
|
284955
285421
|
} else {
|
|
284956
285422
|
console.log(LOGO);
|
|
284957
|
-
console.log(` ${
|
|
285423
|
+
console.log(` ${import_picocolors23.default.bold("Usage:")}`);
|
|
284958
285424
|
console.log(` npx ablo init Scaffold ablo/ directory + starter schema`);
|
|
284959
285425
|
console.log(` npx ablo init --yes [--framework nextjs] Non-interactive (agents/CI): no prompts, flag-driven`);
|
|
284960
285426
|
console.log(` [--auth apikey] [--storage replication|endpoint] [--project <slug>] [--no-project]`);
|
|
@@ -284978,8 +285444,9 @@ async function main() {
|
|
|
284978
285444
|
console.log(` npx ablo pull drizzle <module> Generate schema.ts from a Drizzle schema (keeps enums + relations)`);
|
|
284979
285445
|
console.log(` npx ablo check Check your existing database fits the schema (read-only, creates no tables)`);
|
|
284980
285446
|
console.log(` npx ablo connect Connect a real database \u2014 prints the logical-replication setup SQL (the one way)`);
|
|
284981
|
-
console.log(` npx ablo connect
|
|
284982
|
-
console.log(` npx ablo connect
|
|
285447
|
+
console.log(` npx ablo connect check Validate DATABASE_URL is replication-ready (wal_level, publication, role, replica identity)`);
|
|
285448
|
+
console.log(` npx ablo connect scan Read-only audit for leftover Ablo sync infra in a customer DB`);
|
|
285449
|
+
console.log(` npx ablo connect deregister Remove this project's data source \u2014 Ablo stops reading/writing it`);
|
|
284983
285450
|
console.log(` npx ablo migrate Provision your synced-model tables in your own Postgres (optional escape hatch \u2014 \`connect\` is the way)`);
|
|
284984
285451
|
console.log(` npx ablo migrate --dry-run Print the SQL without executing (preview)`);
|
|
284985
285452
|
console.log(` npx ablo push Upload your schema definition to Ablo (metadata only \u2014 rows stay in your DB)`);
|
|
@@ -284990,10 +285457,10 @@ async function main() {
|
|
|
284990
285457
|
console.log(` npx ablo generate Emit TypeScript types from your schema`);
|
|
284991
285458
|
console.log(` npx ablo generate --out path.ts Write generated types to a path`);
|
|
284992
285459
|
console.log();
|
|
284993
|
-
console.log(` ${
|
|
285460
|
+
console.log(` ${import_picocolors23.default.bold("Schema workflow:")}`);
|
|
284994
285461
|
console.log(` The server holds its own copy of your schema \u2014 edit ${brand("ablo/schema.ts")}, then`);
|
|
284995
285462
|
console.log(` run ${brand("ablo push")} (or keep ${brand("ablo dev")} running) before the server will accept`);
|
|
284996
|
-
console.log(` writes to new or changed models. Skip it and writes fail with ${
|
|
285463
|
+
console.log(` writes to new or changed models. Skip it and writes fail with ${import_picocolors23.default.yellow("server_execute_unknown_model")}.`);
|
|
284997
285464
|
console.log();
|
|
284998
285465
|
}
|
|
284999
285466
|
}
|
|
@@ -285044,7 +285511,7 @@ async function ensureInitProject(opts) {
|
|
|
285044
285511
|
const ensured = await ensureProject(slug);
|
|
285045
285512
|
if (ensured) {
|
|
285046
285513
|
console.log(
|
|
285047
|
-
` ${
|
|
285514
|
+
` ${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.`
|
|
285048
285515
|
);
|
|
285049
285516
|
}
|
|
285050
285517
|
}
|
|
@@ -285086,7 +285553,7 @@ async function chooseBool(flagValue, fallback, interactive, prompt) {
|
|
|
285086
285553
|
async function init(args = []) {
|
|
285087
285554
|
const opts = parseInitArgs(args);
|
|
285088
285555
|
const interactive = Boolean(process.stdin.isTTY) && !opts.yes && !process.env.CI;
|
|
285089
|
-
Ie(`${brand("ablo")} ${
|
|
285556
|
+
Ie(`${brand("ablo")} ${import_picocolors23.default.dim("sync engine")}`);
|
|
285090
285557
|
if (!(0, import_fs12.existsSync)("package.json")) {
|
|
285091
285558
|
xe("No package.json found. Run this from your project root.");
|
|
285092
285559
|
process.exit(1);
|
|
@@ -285164,7 +285631,7 @@ async function init(args = []) {
|
|
|
285164
285631
|
if (pullExisting) {
|
|
285165
285632
|
const dbUrl = process.env.DATABASE_URL ?? process.env.ABLO_DATABASE_URL;
|
|
285166
285633
|
if (!dbUrl) {
|
|
285167
|
-
schemaNote =
|
|
285634
|
+
schemaNote = import_picocolors23.default.dim(" (no DATABASE_URL \u2014 wrote starter; run `ablo pull` later)");
|
|
285168
285635
|
} else {
|
|
285169
285636
|
try {
|
|
285170
285637
|
const pulled = await buildSchemaSourceFromDb({
|
|
@@ -285174,12 +285641,12 @@ async function init(args = []) {
|
|
|
285174
285641
|
});
|
|
285175
285642
|
if (pulled.models.length > 0) {
|
|
285176
285643
|
schemaSource = pulled.source;
|
|
285177
|
-
schemaNote =
|
|
285644
|
+
schemaNote = import_picocolors23.default.dim(` (pulled ${pulled.models.length} models)`);
|
|
285178
285645
|
} else {
|
|
285179
|
-
schemaNote =
|
|
285646
|
+
schemaNote = import_picocolors23.default.dim(" (no adoptable tables \u2014 wrote starter)");
|
|
285180
285647
|
}
|
|
285181
285648
|
} catch {
|
|
285182
|
-
schemaNote =
|
|
285649
|
+
schemaNote = import_picocolors23.default.dim(" (pull failed \u2014 wrote starter)");
|
|
285183
285650
|
}
|
|
285184
285651
|
}
|
|
285185
285652
|
}
|
|
@@ -285205,14 +285672,14 @@ async function init(args = []) {
|
|
|
285205
285672
|
const existing = (0, import_fs12.readFileSync)(envFile, "utf-8");
|
|
285206
285673
|
if (!existing.includes("ABLO_")) {
|
|
285207
285674
|
(0, import_fs12.writeFileSync)(envFile, existing + "\n" + envBody);
|
|
285208
|
-
created.push(`${envFile} ${
|
|
285675
|
+
created.push(`${envFile} ${import_picocolors23.default.dim("(appended)")}`);
|
|
285209
285676
|
} else {
|
|
285210
|
-
created.push(`${envFile} ${
|
|
285677
|
+
created.push(`${envFile} ${import_picocolors23.default.dim("(already configured)")}`);
|
|
285211
285678
|
}
|
|
285212
285679
|
}
|
|
285213
285680
|
if (wireRealKey && resolvedKey) {
|
|
285214
285681
|
wireEnvLocal(resolvedKey);
|
|
285215
|
-
created.push(`.env.local ${
|
|
285682
|
+
created.push(`.env.local ${import_picocolors23.default.dim("(ABLO_API_KEY set from your login)")}`);
|
|
285216
285683
|
}
|
|
285217
285684
|
if (agent) {
|
|
285218
285685
|
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "agent.ts"), generateAgent());
|
|
@@ -285227,17 +285694,17 @@ async function init(args = []) {
|
|
|
285227
285694
|
}
|
|
285228
285695
|
const providersPath = (0, import_path7.join)(layout.appBase, "providers.tsx");
|
|
285229
285696
|
(0, import_fs12.writeFileSync)(providersPath, generateProviders());
|
|
285230
|
-
created.push(`${providersPath} ${
|
|
285697
|
+
created.push(`${providersPath} ${import_picocolors23.default.dim(`(wrap ${(0, import_path7.join)(layout.appBase, "layout.tsx")} in <Providers>)`)}`);
|
|
285231
285698
|
const sessionDir = (0, import_path7.join)(layout.appBase, "api", "ablo-session");
|
|
285232
285699
|
(0, import_fs12.mkdirSync)(sessionDir, { recursive: true });
|
|
285233
285700
|
(0, import_fs12.writeFileSync)((0, import_path7.join)(sessionDir, "route.ts"), generateSessionRoute());
|
|
285234
|
-
created.push(`${(0, import_path7.join)(sessionDir, "route.ts")} ${
|
|
285701
|
+
created.push(`${(0, import_path7.join)(sessionDir, "route.ts")} ${import_picocolors23.default.dim("(wire your auth)")}`);
|
|
285235
285702
|
}
|
|
285236
285703
|
if (framework !== "vanilla") {
|
|
285237
285704
|
(0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "TaskList.tsx"), generateComponent());
|
|
285238
285705
|
created.push(`${abloDir}/TaskList.tsx`);
|
|
285239
285706
|
}
|
|
285240
|
-
Me(created.map((f) => `${
|
|
285707
|
+
Me(created.map((f) => `${import_picocolors23.default.green("\u2713")} ${f}`).join("\n"), "Created");
|
|
285241
285708
|
const pm = detectPackageManager();
|
|
285242
285709
|
if (opts.install) {
|
|
285243
285710
|
const s = Y2();
|
|
@@ -285246,47 +285713,47 @@ async function init(args = []) {
|
|
|
285246
285713
|
(0, import_child_process3.execSync)(`${pm} add @abloatai/ablo`, { stdio: "ignore" });
|
|
285247
285714
|
s.stop("Installed @abloatai/ablo");
|
|
285248
285715
|
} catch {
|
|
285249
|
-
s.stop(`${
|
|
285716
|
+
s.stop(`${import_picocolors23.default.yellow("!")} Couldn't auto-install \u2014 run ${import_picocolors23.default.bold(`${pm} install @abloatai/ablo`)}`);
|
|
285250
285717
|
}
|
|
285251
285718
|
}
|
|
285252
285719
|
const steps = [
|
|
285253
|
-
`Get a ${
|
|
285254
|
-
`Run ${
|
|
285255
|
-
`Set ${
|
|
285256
|
-
`Run ${
|
|
285720
|
+
`Get a ${import_picocolors23.default.bold("sk_test_")} key at ${import_picocolors23.default.cyan("https://abloatai.com")}`,
|
|
285721
|
+
`Run ${import_picocolors23.default.bold("npx ablo login")} (or add ${import_picocolors23.default.bold("ABLO_API_KEY")} to ${import_picocolors23.default.bold(envFile)})`,
|
|
285722
|
+
`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`,
|
|
285723
|
+
`Run ${import_picocolors23.default.bold("npx ablo dev")} \u2014 pushes your schema definition and watches for changes`,
|
|
285257
285724
|
...storage === "replication" ? [
|
|
285258
|
-
`Connect your database \u2014 ${
|
|
285259
|
-
`Verify it \u2014 ${
|
|
285260
|
-
`Register it \u2014 ${
|
|
285725
|
+
`Connect your database \u2014 ${import_picocolors23.default.bold("npx ablo connect")} prints the one-time logical-replication setup SQL to run on your Postgres`,
|
|
285726
|
+
`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`,
|
|
285727
|
+
`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`
|
|
285261
285728
|
] : [
|
|
285262
|
-
`Provision your DB: ${
|
|
285729
|
+
`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")}`
|
|
285263
285730
|
],
|
|
285264
285731
|
...framework === "nextjs" ? [
|
|
285265
|
-
`Wrap ${
|
|
285732
|
+
`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"))}`
|
|
285266
285733
|
] : [],
|
|
285267
|
-
`Run ${
|
|
285734
|
+
`Run ${import_picocolors23.default.bold(`${pm} run dev`)} and open two browser tabs \u2014 changes sync in real-time`,
|
|
285268
285735
|
...agent ? [
|
|
285269
|
-
`Run ${
|
|
285270
|
-
`Run ${
|
|
285736
|
+
`Run ${import_picocolors23.default.bold(`npx tsx ${abloDir}/agent.ts`)} \u2014 an AI teammate edits the same tasks`,
|
|
285737
|
+
`Run ${import_picocolors23.default.bold("npx ablo logs")} to watch human + agent commits stream by`
|
|
285271
285738
|
] : []
|
|
285272
285739
|
];
|
|
285273
285740
|
Me(steps.map((s, i) => `${i + 1}. ${s}`).join("\n"), "Next steps");
|
|
285274
285741
|
const existingKey = resolveApiKey("sandbox");
|
|
285275
285742
|
if (existingKey) {
|
|
285276
285743
|
await ensureInitProject(opts);
|
|
285277
|
-
Se(`Already authorized ${
|
|
285744
|
+
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`);
|
|
285278
285745
|
return;
|
|
285279
285746
|
}
|
|
285280
285747
|
if (interactive && opts.login) {
|
|
285281
285748
|
const loginNow = await ye({ message: "Log in now? (opens your browser)", initialValue: true });
|
|
285282
285749
|
if (!pD(loginNow) && loginNow) {
|
|
285283
|
-
Se(`${
|
|
285750
|
+
Se(`${import_picocolors23.default.dim("Docs:")} https://abloatai.com/docs`);
|
|
285284
285751
|
await login();
|
|
285285
285752
|
await ensureInitProject(opts);
|
|
285286
285753
|
return;
|
|
285287
285754
|
}
|
|
285288
285755
|
}
|
|
285289
|
-
Se(`Run ${
|
|
285756
|
+
Se(`Run ${import_picocolors23.default.bold("npx ablo login")} when ready. ${import_picocolors23.default.dim("Docs:")} https://abloatai.com/docs`);
|
|
285290
285757
|
}
|
|
285291
285758
|
function generateSchema() {
|
|
285292
285759
|
return `import { defineSchema, model, relation, z } from '@abloatai/ablo/schema';
|