@headband/cli 0.6.0 → 0.6.1
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/README.md +14 -6
- package/dist/index.js +401 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -528,8 +528,11 @@ headband schema forget <index> [--yes] [--json]
|
|
|
528
528
|
- `apply` runs the migration files the server does not have yet, in order. A
|
|
529
529
|
new index is created and backfilled from every source row (rows
|
|
530
530
|
`shouldIndex` rejects are left out). A schema change builds a new index
|
|
531
|
-
beside the live one, backfills it and switches the name over.
|
|
532
|
-
index
|
|
531
|
+
beside the live one, backfills it and switches the name over. Rows your app
|
|
532
|
+
writes during the backfill still go to the live index, so `apply` then
|
|
533
|
+
compares every document of the new index with its source row and writes the
|
|
534
|
+
ones that differ, once before the switch and once after it (CLI 0.6.1).
|
|
535
|
+
Removing an index needs `--allow-destructive`. `--json` prints
|
|
533
536
|
`{appliedFiles, appliedOps, projectId}`.
|
|
534
537
|
- `apply` recognises migrations the server already has from its schema state,
|
|
535
538
|
so a fresh checkout or CI applies without `headband/migrations/.applied.json`.
|
|
@@ -600,7 +603,7 @@ changes to an outbox table, and the worker pushes them to Headband. The
|
|
|
600
603
|
headband sync init [--table <name>] [--write] [--apply] [--json]
|
|
601
604
|
headband sync worker [--loop] [--budget-ms <ms>] [--batch-size <n>] [--reconcile-every <n>] [--project <id>] [--json]
|
|
602
605
|
headband sync worker --scaffold docker [--force] [--json]
|
|
603
|
-
headband sync reconcile [--fix] [--json]
|
|
606
|
+
headband sync reconcile [--content] [--fix] [--json]
|
|
604
607
|
headband sync retry-dead-letters [--json]
|
|
605
608
|
headband sync status [--json]
|
|
606
609
|
```
|
|
@@ -617,7 +620,9 @@ headband sync status [--json]
|
|
|
617
620
|
`--json` sends progress to stderr and prints the report when the worker
|
|
618
621
|
stops. A row that fails is printed with its model, ids and error; a batch
|
|
619
622
|
that fails is retried row by row, so one bad row never fails the others.
|
|
620
|
-
Rows that keep failing move to the dead letters.
|
|
623
|
+
Rows that keep failing move to the dead letters. A batch that fails because
|
|
624
|
+
Headband or the network is unavailable goes back to the queue without
|
|
625
|
+
counting an attempt, so an outage never fills the dead letters (CLI 0.6.1). Each invocation is recorded
|
|
621
626
|
as one run under the connector `orm-sync` (`GET /v1/connectors/runs`); with
|
|
622
627
|
the org-wide login key that needs `--project <id>` or the config's
|
|
623
628
|
`projectId`. `--reconcile-every <n>` runs `reconcile` after every n-th batch.
|
|
@@ -626,8 +631,11 @@ headband sync status [--json]
|
|
|
626
631
|
service for it. The container needs `DATABASE_URL` and `HEADBAND_API_KEY`.
|
|
627
632
|
- `reconcile` compares the source rows that belong in each index (every row
|
|
628
633
|
`shouldIndex` keeps) with the documents in it, and reports how many are
|
|
629
|
-
missing and how many are extra (counts, not ids). `--
|
|
630
|
-
and
|
|
634
|
+
missing and how many are extra (counts, not ids). `--content` also compares
|
|
635
|
+
every document with its freshly resolved source row and counts the stale
|
|
636
|
+
ones; it reads the whole index, so it takes about as long as a backfill (CLI
|
|
637
|
+
0.6.1). `--fix` pushes the missing (and with `--content` the stale) and
|
|
638
|
+
deletes the extra. It exits
|
|
631
639
|
`1` when drift is left. `--json` prints `{indexes, drift}`. The index's ids
|
|
632
640
|
are read by cursor, so a Visor index past 1000 documents is compared id by
|
|
633
641
|
id too.
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
main: () => main,
|
|
36
36
|
parseArgv: () => parseArgv,
|
|
37
37
|
parseExpires: () => parseExpires,
|
|
38
|
+
parseIndexList: () => parseIndexList,
|
|
38
39
|
parsePermissions: () => parsePermissions,
|
|
39
40
|
run: () => run,
|
|
40
41
|
stdinIsInteractive: () => stdinIsInteractive,
|
|
@@ -1202,7 +1203,7 @@ function validateConfig(config, ormSchema) {
|
|
|
1202
1203
|
continue;
|
|
1203
1204
|
}
|
|
1204
1205
|
const hasPath = model.relations.some(
|
|
1205
|
-
(r) => r.fromModel ===
|
|
1206
|
+
(r) => r.fromModel === index.source && r.toModel === spec.model && r.kind === "many"
|
|
1206
1207
|
);
|
|
1207
1208
|
if (!hasPath) {
|
|
1208
1209
|
errors.push({
|
|
@@ -1488,6 +1489,67 @@ var ExportInterruptedError = class extends Error {
|
|
|
1488
1489
|
this.name = "ExportInterruptedError";
|
|
1489
1490
|
}
|
|
1490
1491
|
};
|
|
1492
|
+
function refusedWalk(error) {
|
|
1493
|
+
if (!(error instanceof ApiError) || error.code !== "feature_not_supported") return null;
|
|
1494
|
+
if (!/primary key/i.test(error.message)) return null;
|
|
1495
|
+
const pk = /add `([^`]+)`/.exec(error.message)?.[1] ?? null;
|
|
1496
|
+
const named = ["filterableAttributes", "sortableAttributes", "displayedAttributes"].filter(
|
|
1497
|
+
(name) => error.message.includes(name)
|
|
1498
|
+
);
|
|
1499
|
+
return { pk, lists: named.length ? named : ["filterableAttributes", "sortableAttributes"] };
|
|
1500
|
+
}
|
|
1501
|
+
var DECLARE_WAIT_MS = 15 * 6e4;
|
|
1502
|
+
var DECLARE_POLL_MS = 3e3;
|
|
1503
|
+
async function declareExportKey(client, index, io, opts = {}) {
|
|
1504
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1505
|
+
const now = opts.now ?? Date.now;
|
|
1506
|
+
const probe = () => client.get(`/v1/documents?index=${encodeURIComponent(index)}&limit=1&cursor=`);
|
|
1507
|
+
let refused;
|
|
1508
|
+
try {
|
|
1509
|
+
await probe();
|
|
1510
|
+
return false;
|
|
1511
|
+
} catch (error) {
|
|
1512
|
+
refused = refusedWalk(error);
|
|
1513
|
+
if (!refused) throw error;
|
|
1514
|
+
if (!refused.pk) throw error;
|
|
1515
|
+
}
|
|
1516
|
+
const { pk, lists } = refused;
|
|
1517
|
+
const path2 = `/v1/indexes/${encodeURIComponent(index)}/settings`;
|
|
1518
|
+
const current = await client.get(path2);
|
|
1519
|
+
const patch = {};
|
|
1520
|
+
for (const name of lists) {
|
|
1521
|
+
const list = Array.isArray(current?.[name]) ? current[name] : [];
|
|
1522
|
+
if (list.includes("*") || list.includes(pk)) continue;
|
|
1523
|
+
patch[name] = [...list, pk];
|
|
1524
|
+
}
|
|
1525
|
+
io.stderr(
|
|
1526
|
+
`Declaring \`${pk}\` in ${Object.keys(patch).join(" and ") || lists.join(" and ")} on ${index}, so the export can read past 1000 documents. Visor rebuilds the index for this change, which can take minutes on a large one.`
|
|
1527
|
+
);
|
|
1528
|
+
if (Object.keys(patch).length > 0) {
|
|
1529
|
+
try {
|
|
1530
|
+
await client.patch(path2, patch);
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
if (!(error instanceof ApiError) || error.status < 500) throw error;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
const deadline = now() + (opts.waitMs ?? DECLARE_WAIT_MS);
|
|
1536
|
+
for (; ; ) {
|
|
1537
|
+
try {
|
|
1538
|
+
await probe();
|
|
1539
|
+
io.stderr(`\`${pk}\` is declared. Exporting.`);
|
|
1540
|
+
return true;
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
const waiting = refusedWalk(error) || error instanceof ApiError && error.status >= 500;
|
|
1543
|
+
if (!waiting) throw error;
|
|
1544
|
+
}
|
|
1545
|
+
if (now() >= deadline) {
|
|
1546
|
+
throw new CliError(
|
|
1547
|
+
`\`${pk}\` is still not declared on ${index} after ${Math.round((opts.waitMs ?? DECLARE_WAIT_MS) / 6e4)} minutes. Visor may still be rebuilding it: run headband docs export again later.`
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
await sleep(DECLARE_POLL_MS);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1491
1553
|
function exportRefusalAdvice(error, index) {
|
|
1492
1554
|
if (!(error instanceof ApiError) || error.code !== "feature_not_supported") return null;
|
|
1493
1555
|
if (!/primary key/i.test(error.message)) return null;
|
|
@@ -1498,7 +1560,8 @@ function exportRefusalAdvice(error, index) {
|
|
|
1498
1560
|
);
|
|
1499
1561
|
const lists = missing.length ? missing.join(" and ") : "filterableAttributes and sortableAttributes";
|
|
1500
1562
|
return [
|
|
1501
|
-
`To export the whole index, declare ${key} on
|
|
1563
|
+
`To export the whole index, run it again with --declare-key: it declares ${key} on the index`,
|
|
1564
|
+
`once (Visor rebuilds the index for it), waits for it, and exports. Or declare it yourself:`,
|
|
1502
1565
|
` 1. headband indexes settings get ${index} > settings.json`,
|
|
1503
1566
|
` 2. Add ${key} to ${lists} in settings.json.`,
|
|
1504
1567
|
` 3. headband indexes settings set ${index} --file settings.json`,
|
|
@@ -1514,6 +1577,41 @@ function exportFailure(error, index) {
|
|
|
1514
1577
|
${advice}`);
|
|
1515
1578
|
}
|
|
1516
1579
|
|
|
1580
|
+
// src/content-drift.ts
|
|
1581
|
+
async function readRemoteDocuments(client, index, primaryKey) {
|
|
1582
|
+
const batchSize = await defaultExportBatchSize(client, index);
|
|
1583
|
+
const docs = /* @__PURE__ */ new Map();
|
|
1584
|
+
for await (const { results } of walkDocuments(client, { index, batchSize })) {
|
|
1585
|
+
for (const doc of results) {
|
|
1586
|
+
const id = doc?.[primaryKey];
|
|
1587
|
+
if (id !== void 0 && id !== null) docs.set(String(id), doc);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return docs;
|
|
1591
|
+
}
|
|
1592
|
+
function wire(value) {
|
|
1593
|
+
return value === void 0 ? null : JSON.parse(JSON.stringify(value));
|
|
1594
|
+
}
|
|
1595
|
+
function canonical(value) {
|
|
1596
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
1597
|
+
if (value !== null && typeof value === "object") {
|
|
1598
|
+
const entries = Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`);
|
|
1599
|
+
return `{${entries.join(",")}}`;
|
|
1600
|
+
}
|
|
1601
|
+
return JSON.stringify(value ?? null);
|
|
1602
|
+
}
|
|
1603
|
+
function documentsMatch(expected, remote, primaryKey) {
|
|
1604
|
+
if (!remote) return false;
|
|
1605
|
+
for (const [key, raw] of Object.entries(expected)) {
|
|
1606
|
+
if (key === primaryKey) {
|
|
1607
|
+
if (String(raw) !== String(remote[key])) return false;
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (canonical(wire(raw)) !== canonical(wire(remote[key]))) return false;
|
|
1611
|
+
}
|
|
1612
|
+
return true;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1517
1615
|
// ../../src/lib/density/catalog-constants.ts
|
|
1518
1616
|
function formatMemory(mb) {
|
|
1519
1617
|
return mb >= 1024 ? `${mb / 1024} GB` : `${mb} MB`;
|
|
@@ -2357,6 +2455,14 @@ var HeadbandClient = class {
|
|
|
2357
2455
|
if (opts?.projectId !== void 0) body.projectId = opts.projectId;
|
|
2358
2456
|
await this.request("POST", "/v1/indexes", body);
|
|
2359
2457
|
}
|
|
2458
|
+
/** The index (GET /v1/indexes/{uid}): its engine among the rest. */
|
|
2459
|
+
async getIndex(uid) {
|
|
2460
|
+
return this.request("GET", `/v1/indexes/${encodeURIComponent(uid)}`);
|
|
2461
|
+
}
|
|
2462
|
+
/** Deletes an index (DELETE /v1/indexes/{uid}): `schema apply` removes a reindex it could not set up. */
|
|
2463
|
+
async deleteIndex(uid) {
|
|
2464
|
+
await this.request("DELETE", `/v1/indexes/${encodeURIComponent(uid)}`);
|
|
2465
|
+
}
|
|
2360
2466
|
async patchSettings(uid, settings) {
|
|
2361
2467
|
await this.request("PATCH", `/v1/indexes/${uid}/settings`, settings);
|
|
2362
2468
|
}
|
|
@@ -2512,7 +2618,36 @@ async function createIndexIdempotent(ctx, uid, opts) {
|
|
|
2512
2618
|
ctx.io.stdout(`Index "${uid}" already exists. Continuing.`);
|
|
2513
2619
|
}
|
|
2514
2620
|
}
|
|
2621
|
+
var ES_EVERY_FIELD = [
|
|
2622
|
+
"searchableAttributes",
|
|
2623
|
+
"filterableAttributes",
|
|
2624
|
+
"sortableAttributes"
|
|
2625
|
+
];
|
|
2626
|
+
async function indexEngine(ctx, uid) {
|
|
2627
|
+
try {
|
|
2628
|
+
return (await ctx.client.getIndex(uid)).engine ?? null;
|
|
2629
|
+
} catch {
|
|
2630
|
+
return null;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
function settingsForEngine(ctx, uid, engine, settings) {
|
|
2634
|
+
if (engine !== "elasticsearch") return settings;
|
|
2635
|
+
const out = { ...settings };
|
|
2636
|
+
const left = [];
|
|
2637
|
+
for (const key of ES_EVERY_FIELD) {
|
|
2638
|
+
if (out[key] === void 0) continue;
|
|
2639
|
+
delete out[key];
|
|
2640
|
+
left.push(key);
|
|
2641
|
+
}
|
|
2642
|
+
if (left.length > 0) {
|
|
2643
|
+
ctx.io.stdout(
|
|
2644
|
+
`"${uid}" is on Elasticsearch, which searches, filters and sorts every field without these lists, so they are not sent: ${left.join(", ")}.`
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
return out;
|
|
2648
|
+
}
|
|
2515
2649
|
async function patchSettingsBeforeBackfill(ctx, uid, settings) {
|
|
2650
|
+
if (Object.keys(settings).length === 0) return false;
|
|
2516
2651
|
try {
|
|
2517
2652
|
await ctx.client.patchSettings(uid, settings);
|
|
2518
2653
|
return false;
|
|
@@ -2536,13 +2671,14 @@ async function patchSettingsAfterBackfill(ctx, uid, settings) {
|
|
|
2536
2671
|
function settingsRefused(uid, err) {
|
|
2537
2672
|
const message = err instanceof Error ? err.message : String(err);
|
|
2538
2673
|
const { status, code } = err ?? {};
|
|
2539
|
-
|
|
2674
|
+
const refused = new CliError(
|
|
2540
2675
|
`Index "${uid}" exists, but its settings were refused: ${message.replace(/\.?$/, ".")} Change the index's settings in headband.config.ts, then re-run "headband schema apply": it picks up the existing index.`,
|
|
2541
2676
|
{
|
|
2542
2677
|
...typeof status === "number" ? { exitCode: exitCodeForStatus(status) } : {},
|
|
2543
2678
|
...typeof code === "string" ? { code } : {}
|
|
2544
2679
|
}
|
|
2545
2680
|
);
|
|
2681
|
+
return Object.assign(refused, { refusal: message });
|
|
2546
2682
|
}
|
|
2547
2683
|
function postState(client, state) {
|
|
2548
2684
|
return client.postSchemaState(state);
|
|
@@ -2590,13 +2726,83 @@ async function backfill(ctx, def, targetUid, abortPoint, onPage) {
|
|
|
2590
2726
|
);
|
|
2591
2727
|
return { pushed, excluded };
|
|
2592
2728
|
}
|
|
2729
|
+
async function catchUp(ctx, def, targetUid, when) {
|
|
2730
|
+
if (!ctx.readRemoteDocuments) return;
|
|
2731
|
+
const remote = await ctx.readRemoteDocuments(targetUid, def.primaryKey);
|
|
2732
|
+
const keep = /* @__PURE__ */ new Set();
|
|
2733
|
+
let pushed = 0;
|
|
2734
|
+
let cursor = null;
|
|
2735
|
+
for (; ; ) {
|
|
2736
|
+
const page = await ctx.adapter.listIds(def.source, cursor, ctx.backfillBatchSize);
|
|
2737
|
+
if (page.ids.length === 0) break;
|
|
2738
|
+
const docs = await ctx.adapter.resolveDocs(def.name, page.ids);
|
|
2739
|
+
const toPush = [];
|
|
2740
|
+
for (const doc of docs) {
|
|
2741
|
+
const sourceRow = doc.__source ?? doc;
|
|
2742
|
+
delete doc.__source;
|
|
2743
|
+
if (def.shouldIndex && !def.shouldIndex(sourceRow)) continue;
|
|
2744
|
+
const key = String(doc[def.primaryKey]);
|
|
2745
|
+
keep.add(key);
|
|
2746
|
+
if (!documentsMatch(doc, remote.get(key), def.primaryKey)) toPush.push(doc);
|
|
2747
|
+
}
|
|
2748
|
+
if (toPush.length > 0) {
|
|
2749
|
+
const result = await ctx.client.bulkPush(targetUid, toPush, def.primaryKey);
|
|
2750
|
+
if (result?.partialFailure) {
|
|
2751
|
+
throw new CliError(
|
|
2752
|
+
`Catching up "${def.name}" ${when} stopped: ${partialFailureSummary(result.partialFailure)}. Re-run "headband schema apply" once resolved.`
|
|
2753
|
+
);
|
|
2754
|
+
}
|
|
2755
|
+
pushed += toPush.length;
|
|
2756
|
+
}
|
|
2757
|
+
cursor = page.nextCursor;
|
|
2758
|
+
if (cursor === null || page.ids.length < ctx.backfillBatchSize) break;
|
|
2759
|
+
}
|
|
2760
|
+
const extra = [...remote.keys()].filter((key) => !keep.has(key));
|
|
2761
|
+
for (let offset = 0; offset < extra.length; offset += ctx.backfillBatchSize) {
|
|
2762
|
+
const result = await ctx.client.deleteDocs(
|
|
2763
|
+
targetUid,
|
|
2764
|
+
extra.slice(offset, offset + ctx.backfillBatchSize)
|
|
2765
|
+
);
|
|
2766
|
+
if (result?.partialFailure) {
|
|
2767
|
+
throw new CliError(
|
|
2768
|
+
`Catching up "${def.name}" ${when} stopped: ${partialFailureSummary(result.partialFailure)}. Re-run "headband schema apply" once resolved.`
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
ctx.io.stdout(
|
|
2773
|
+
`Caught up "${def.name}" ${when}: ${pushed} changed or new document(s) written, ${extra.length} removed.`
|
|
2774
|
+
);
|
|
2775
|
+
}
|
|
2776
|
+
async function abandonReindex(ctx, index, physical, err) {
|
|
2777
|
+
let removed;
|
|
2778
|
+
try {
|
|
2779
|
+
await ctx.client.deleteIndex(physical);
|
|
2780
|
+
removed = `The new index "${physical}" was removed`;
|
|
2781
|
+
} catch (deleteErr) {
|
|
2782
|
+
const why = deleteErr instanceof Error ? deleteErr.message : String(deleteErr);
|
|
2783
|
+
removed = `The new index "${physical}" could not be removed (${why}): remove it with "headband indexes delete ${physical}"`;
|
|
2784
|
+
}
|
|
2785
|
+
const reason = err?.refusal ?? (err instanceof Error ? err.message : String(err));
|
|
2786
|
+
return new CliError(
|
|
2787
|
+
`The reindex of "${index}" stopped: the new index's settings were refused: ${String(reason).replace(/\.?$/, ".")} ${removed}, and "${index}" is unchanged. Change the index's settings in headband.config.ts, then re-run "headband schema apply".`,
|
|
2788
|
+
{
|
|
2789
|
+
...err instanceof CliError ? { exitCode: err.exitCode, code: err.code } : {}
|
|
2790
|
+
}
|
|
2791
|
+
);
|
|
2792
|
+
}
|
|
2593
2793
|
async function applyReindex(op, ctx, existing, expectedParentHash) {
|
|
2594
2794
|
const def = ctx.config.indexes.find((d) => d.name === op.index);
|
|
2595
2795
|
if (!def) {
|
|
2596
2796
|
throw new CliError(`Cannot reindex "${op.index}": no matching index in the loaded config.`);
|
|
2597
2797
|
}
|
|
2798
|
+
const settings = settingsForEngine(ctx, op.index, await indexEngine(ctx, op.index), op.settings);
|
|
2598
2799
|
await createIndexIdempotent(ctx, op.toPhysicalUid, { primaryKey: def.primaryKey });
|
|
2599
|
-
|
|
2800
|
+
let settingsAfter;
|
|
2801
|
+
try {
|
|
2802
|
+
settingsAfter = await patchSettingsBeforeBackfill(ctx, op.toPhysicalUid, settings);
|
|
2803
|
+
} catch (err) {
|
|
2804
|
+
throw await abandonReindex(ctx, op.index, op.toPhysicalUid, err);
|
|
2805
|
+
}
|
|
2600
2806
|
const version = (existing?.version ?? 0) + 1;
|
|
2601
2807
|
const schemaHashValue = docShapeHash(def);
|
|
2602
2808
|
const settingsHashValue = settingsHash(def);
|
|
@@ -2625,8 +2831,10 @@ async function applyReindex(op, ctx, existing, expectedParentHash) {
|
|
|
2625
2831
|
"before the alias flip",
|
|
2626
2832
|
(cursor) => writeState("migrating", cursor)
|
|
2627
2833
|
);
|
|
2628
|
-
if (settingsAfter) await patchSettingsAfterBackfill(ctx, op.toPhysicalUid,
|
|
2834
|
+
if (settingsAfter) await patchSettingsAfterBackfill(ctx, op.toPhysicalUid, settings);
|
|
2835
|
+
await catchUp(ctx, def, op.toPhysicalUid, "before the alias flip");
|
|
2629
2836
|
await ctx.client.patchAlias(op.index, op.toPhysicalUid);
|
|
2837
|
+
await catchUp(ctx, def, op.index, "after the alias flip");
|
|
2630
2838
|
await writeState("applied", null);
|
|
2631
2839
|
return {
|
|
2632
2840
|
indexUid: op.index,
|
|
@@ -2646,9 +2854,15 @@ async function applyOp(op, ctx, existing, expectedParentHash) {
|
|
|
2646
2854
|
);
|
|
2647
2855
|
}
|
|
2648
2856
|
await createIndexIdempotent(ctx, op.index, { primaryKey: def.primaryKey });
|
|
2649
|
-
const
|
|
2857
|
+
const settings = settingsForEngine(
|
|
2858
|
+
ctx,
|
|
2859
|
+
op.index,
|
|
2860
|
+
await indexEngine(ctx, op.index),
|
|
2861
|
+
op.settings
|
|
2862
|
+
);
|
|
2863
|
+
const settingsAfter = await patchSettingsBeforeBackfill(ctx, op.index, settings);
|
|
2650
2864
|
await backfill(ctx, def, op.index, "before recording the index as applied");
|
|
2651
|
-
if (settingsAfter) await patchSettingsAfterBackfill(ctx, op.index,
|
|
2865
|
+
if (settingsAfter) await patchSettingsAfterBackfill(ctx, op.index, settings);
|
|
2652
2866
|
const next = {
|
|
2653
2867
|
indexUid: op.index,
|
|
2654
2868
|
version: 1,
|
|
@@ -2664,7 +2878,13 @@ async function applyOp(op, ctx, existing, expectedParentHash) {
|
|
|
2664
2878
|
if (!existing) {
|
|
2665
2879
|
throw new CliError(`Cannot patch settings for "${op.index}": no applied schema state found.`);
|
|
2666
2880
|
}
|
|
2667
|
-
|
|
2881
|
+
const settings = settingsForEngine(
|
|
2882
|
+
ctx,
|
|
2883
|
+
op.index,
|
|
2884
|
+
await indexEngine(ctx, op.index),
|
|
2885
|
+
op.settings
|
|
2886
|
+
);
|
|
2887
|
+
if (Object.keys(settings).length > 0) await ctx.client.patchSettings(op.index, settings);
|
|
2668
2888
|
const def = ctx.config.indexes.find((d) => d.name === op.index);
|
|
2669
2889
|
const next = {
|
|
2670
2890
|
...existing,
|
|
@@ -2720,7 +2940,8 @@ async function runSchemaApply(deps) {
|
|
|
2720
2940
|
client: deps.client,
|
|
2721
2941
|
backfillBatchSize: deps.backfillBatchSize ?? DEFAULT_BACKFILL_BATCH_SIZE,
|
|
2722
2942
|
io: deps.io,
|
|
2723
|
-
projectId: deps.projectId
|
|
2943
|
+
projectId: deps.projectId,
|
|
2944
|
+
readRemoteDocuments: deps.readRemoteDocuments
|
|
2724
2945
|
};
|
|
2725
2946
|
let appliedOps = 0;
|
|
2726
2947
|
let appliedFiles = 0;
|
|
@@ -2917,6 +3138,12 @@ function pgOutboxDriver(exec, opts) {
|
|
|
2917
3138
|
);
|
|
2918
3139
|
return rows?.[0]?.status === "dead_letter" ? "dead_letter" : "pending";
|
|
2919
3140
|
},
|
|
3141
|
+
async release(ids, error) {
|
|
3142
|
+
await exec(
|
|
3143
|
+
`UPDATE ${table} SET last_error = $2, claimed_at = NULL WHERE id = ANY($1) AND status = 'pending'`,
|
|
3144
|
+
[ids, error]
|
|
3145
|
+
);
|
|
3146
|
+
},
|
|
2920
3147
|
async deadLetters(limit) {
|
|
2921
3148
|
const { rows } = await exec(
|
|
2922
3149
|
`SELECT * FROM ${table} WHERE status = 'dead_letter' ORDER BY id LIMIT $1`,
|
|
@@ -3154,6 +3381,7 @@ function describeRow(row) {
|
|
|
3154
3381
|
var emptyOutcome = () => ({
|
|
3155
3382
|
done: [],
|
|
3156
3383
|
retry: [],
|
|
3384
|
+
release: [],
|
|
3157
3385
|
deadLetter: [],
|
|
3158
3386
|
pushed: 0,
|
|
3159
3387
|
deleted: 0
|
|
@@ -3213,7 +3441,7 @@ async function runSyncWorker(opts) {
|
|
|
3213
3441
|
primaryKeyByIndex,
|
|
3214
3442
|
backoff
|
|
3215
3443
|
);
|
|
3216
|
-
return { ...rowOutcomes(batch, resolved, failures), pushed, deleted };
|
|
3444
|
+
return { ...rowOutcomes(batch, resolved, failures), release: [], pushed, deleted };
|
|
3217
3445
|
};
|
|
3218
3446
|
const processIsolated = async (batch) => {
|
|
3219
3447
|
const outcome = emptyOutcome();
|
|
@@ -3226,7 +3454,10 @@ async function runSyncWorker(opts) {
|
|
|
3226
3454
|
outcome.pushed += one.pushed;
|
|
3227
3455
|
outcome.deleted += one.deleted;
|
|
3228
3456
|
} catch (err) {
|
|
3229
|
-
outcome.retry.push([
|
|
3457
|
+
(isTransientBatchError(err) ? outcome.release : outcome.retry).push([
|
|
3458
|
+
row,
|
|
3459
|
+
errorMessage(err)
|
|
3460
|
+
]);
|
|
3230
3461
|
}
|
|
3231
3462
|
}
|
|
3232
3463
|
return outcome;
|
|
@@ -3245,12 +3476,14 @@ async function runSyncWorker(opts) {
|
|
|
3245
3476
|
try {
|
|
3246
3477
|
outcome = await processBatch(rows);
|
|
3247
3478
|
} catch (err) {
|
|
3248
|
-
|
|
3479
|
+
const transient = isTransientBatchError(err);
|
|
3480
|
+
if (rows.length > 1 && !transient) {
|
|
3249
3481
|
outcome = await processIsolated(rows);
|
|
3250
3482
|
} else {
|
|
3251
3483
|
wholeBatchFailed = true;
|
|
3252
3484
|
const message = errorMessage(err);
|
|
3253
|
-
|
|
3485
|
+
const failed = rows.map((row) => [row, message]);
|
|
3486
|
+
outcome = transient ? { ...emptyOutcome(), release: failed } : { ...emptyOutcome(), retry: failed };
|
|
3254
3487
|
log(
|
|
3255
3488
|
`[headband] a batch of ${rows.length} outbox row(s) failed and will be retried: ${message}`
|
|
3256
3489
|
);
|
|
@@ -3272,6 +3505,17 @@ async function runSyncWorker(opts) {
|
|
|
3272
3505
|
log(`[headband] ${describeRow(row)} failed: ${message}`);
|
|
3273
3506
|
}
|
|
3274
3507
|
}
|
|
3508
|
+
if (outcome.release.length > 0) {
|
|
3509
|
+
if (outbox.release) {
|
|
3510
|
+
await outbox.release(
|
|
3511
|
+
outcome.release.map(([row]) => row.id),
|
|
3512
|
+
outcome.release[0][1]
|
|
3513
|
+
);
|
|
3514
|
+
report.retried += outcome.release.length;
|
|
3515
|
+
} else {
|
|
3516
|
+
outcome.retry.push(...outcome.release);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3275
3519
|
for (const [row, message] of outcome.retry) {
|
|
3276
3520
|
const status = await outbox.markFailed(row.id, message);
|
|
3277
3521
|
if (status === "dead_letter") {
|
|
@@ -3288,7 +3532,7 @@ async function runSyncWorker(opts) {
|
|
|
3288
3532
|
}
|
|
3289
3533
|
}
|
|
3290
3534
|
}
|
|
3291
|
-
if (outcome.retry.length > 0) {
|
|
3535
|
+
if (outcome.retry.length > 0 || outcome.release.length > 0) {
|
|
3292
3536
|
consecutiveFailures += 1;
|
|
3293
3537
|
await backoff(consecutiveFailures);
|
|
3294
3538
|
} else {
|
|
@@ -3512,6 +3756,66 @@ async function runSyncWorkerCmd(deps) {
|
|
|
3512
3756
|
}
|
|
3513
3757
|
}
|
|
3514
3758
|
}
|
|
3759
|
+
async function reconcileContent(deps, def, pageSize) {
|
|
3760
|
+
const remote = await deps.readRemoteDocuments(def.name, def.primaryKey);
|
|
3761
|
+
const keep = /* @__PURE__ */ new Set();
|
|
3762
|
+
let missing = 0;
|
|
3763
|
+
let stale = 0;
|
|
3764
|
+
let cursor = null;
|
|
3765
|
+
for (; ; ) {
|
|
3766
|
+
const page = await deps.adapter.listIds(def.source, cursor, pageSize);
|
|
3767
|
+
if (page.ids.length === 0) break;
|
|
3768
|
+
const docs = await deps.adapter.resolveDocs(def.name, page.ids);
|
|
3769
|
+
const toPush = [];
|
|
3770
|
+
for (const doc of docs) {
|
|
3771
|
+
const sourceRow = doc.__source ?? doc;
|
|
3772
|
+
delete doc.__source;
|
|
3773
|
+
if (def.shouldIndex && !def.shouldIndex(sourceRow)) continue;
|
|
3774
|
+
const key = String(doc[def.primaryKey]);
|
|
3775
|
+
keep.add(key);
|
|
3776
|
+
const indexed = remote.get(key);
|
|
3777
|
+
if (!indexed) missing += 1;
|
|
3778
|
+
else if (!documentsMatch(doc, indexed, def.primaryKey)) stale += 1;
|
|
3779
|
+
else continue;
|
|
3780
|
+
toPush.push(doc);
|
|
3781
|
+
}
|
|
3782
|
+
if (deps.fix && toPush.length > 0) {
|
|
3783
|
+
const result = await deps.client.bulkPush(def.name, toPush, def.primaryKey);
|
|
3784
|
+
if (result?.partialFailure) {
|
|
3785
|
+
throw new CliError(
|
|
3786
|
+
`Repair push for "${def.name}" was incomplete: ${partialFailureSummary(result.partialFailure)}.`
|
|
3787
|
+
);
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
if (page.nextCursor === null) break;
|
|
3791
|
+
cursor = page.nextCursor;
|
|
3792
|
+
}
|
|
3793
|
+
const extraIds = [...remote.keys()].filter((key) => !keep.has(key));
|
|
3794
|
+
if (deps.fix) {
|
|
3795
|
+
for (let offset = 0; offset < extraIds.length; offset += pageSize) {
|
|
3796
|
+
const result = await deps.client.deleteDocs(
|
|
3797
|
+
def.name,
|
|
3798
|
+
extraIds.slice(offset, offset + pageSize)
|
|
3799
|
+
);
|
|
3800
|
+
if (result?.partialFailure) {
|
|
3801
|
+
throw new CliError(
|
|
3802
|
+
`Removing extra documents from "${def.name}" was incomplete: ${partialFailureSummary(result.partialFailure)}.`
|
|
3803
|
+
);
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
const drift = missing + extraIds.length + stale;
|
|
3808
|
+
return {
|
|
3809
|
+
index: def.name,
|
|
3810
|
+
sourceCount: keep.size,
|
|
3811
|
+
remoteCount: remote.size,
|
|
3812
|
+
missing,
|
|
3813
|
+
extra: extraIds.length,
|
|
3814
|
+
stale,
|
|
3815
|
+
drift,
|
|
3816
|
+
fixed: Boolean(deps.fix) && drift !== 0
|
|
3817
|
+
};
|
|
3818
|
+
}
|
|
3515
3819
|
async function indexableSourceIds(adapter, def, pageSize) {
|
|
3516
3820
|
const ids = /* @__PURE__ */ new Map();
|
|
3517
3821
|
let cursor = null;
|
|
@@ -3556,6 +3860,17 @@ async function runSyncReconcile(deps) {
|
|
|
3556
3860
|
const pageSize = deps.pageSize ?? RECONCILE_PAGE_SIZE;
|
|
3557
3861
|
const rows = [];
|
|
3558
3862
|
for (const def of deps.config.indexes) {
|
|
3863
|
+
if (deps.content && deps.readRemoteDocuments) {
|
|
3864
|
+
try {
|
|
3865
|
+
rows.push(await reconcileContent(deps, def, pageSize));
|
|
3866
|
+
continue;
|
|
3867
|
+
} catch (err) {
|
|
3868
|
+
if (err instanceof CliError) throw err;
|
|
3869
|
+
deps.io.stderr(
|
|
3870
|
+
`Could not read the documents of "${def.name}", comparing ids only: ${err instanceof Error ? err.message : String(err)}`
|
|
3871
|
+
);
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3559
3874
|
const source = await indexableSourceIds(deps.adapter, def, pageSize);
|
|
3560
3875
|
let remoteKeys = null;
|
|
3561
3876
|
let remoteCount = -1;
|
|
@@ -3619,6 +3934,7 @@ async function runSyncReconcile(deps) {
|
|
|
3619
3934
|
}
|
|
3620
3935
|
rows.push(row);
|
|
3621
3936
|
}
|
|
3937
|
+
const contentCompared = rows.some((r) => r.stale !== void 0 && r.stale !== null);
|
|
3622
3938
|
printTable(
|
|
3623
3939
|
deps.io,
|
|
3624
3940
|
rows.map((r) => ({
|
|
@@ -3626,9 +3942,10 @@ async function runSyncReconcile(deps) {
|
|
|
3626
3942
|
remoteCount: r.remoteCount === -1 ? "unknown" : r.remoteCount,
|
|
3627
3943
|
missing: r.missing ?? "",
|
|
3628
3944
|
extra: r.extra ?? "",
|
|
3945
|
+
stale: r.stale ?? "",
|
|
3629
3946
|
fixed: r.fixed ? "yes" : ""
|
|
3630
3947
|
})),
|
|
3631
|
-
["index", "sourceCount", "remoteCount", "missing", "extra", "drift", "fixed"]
|
|
3948
|
+
contentCompared ? ["index", "sourceCount", "remoteCount", "missing", "extra", "stale", "drift", "fixed"] : ["index", "sourceCount", "remoteCount", "missing", "extra", "drift", "fixed"]
|
|
3632
3949
|
);
|
|
3633
3950
|
const unfixed = rows.filter((r) => r.drift !== 0 && !r.fixed);
|
|
3634
3951
|
if (unfixed.length > 0 && !deps.fix) {
|
|
@@ -3798,13 +4115,14 @@ var syncWorker = [
|
|
|
3798
4115
|
"package name, and prints a docker-compose service."
|
|
3799
4116
|
].join("\n");
|
|
3800
4117
|
var syncReconcile = [
|
|
3801
|
-
"Usage: headband sync reconcile [--fix] [--json]",
|
|
4118
|
+
"Usage: headband sync reconcile [--content] [--fix] [--json]",
|
|
3802
4119
|
"",
|
|
3803
4120
|
"Per index, compares the source rows that belong in it (every row shouldIndex keeps) with",
|
|
3804
4121
|
"the documents in it, and prints how many documents are missing and how many are extra",
|
|
3805
4122
|
"(counts, not ids; --json gives the same counts). --fix pushes the missing ones and deletes",
|
|
3806
|
-
"the extra ones. Exits 1 when drift is left.
|
|
3807
|
-
"
|
|
4123
|
+
"the extra ones. Exits 1 when drift is left. --content also compares every document with its",
|
|
4124
|
+
"freshly resolved source row and counts the stale ones; with --fix it rewrites them. It reads",
|
|
4125
|
+
"the whole index, so it takes about as long as a backfill.",
|
|
3808
4126
|
"Needs the adapter export and DATABASE_URL."
|
|
3809
4127
|
].join("\n");
|
|
3810
4128
|
var syncRetry = [
|
|
@@ -3839,7 +4157,7 @@ var SUBCOMMAND_HELP = {
|
|
|
3839
4157
|
" headband sync init [--table <name>] [--write] [--apply] [--json]",
|
|
3840
4158
|
" headband sync worker [--loop] [--budget-ms <ms>] [--batch-size <n>] [--reconcile-every <n>] [--project <id>] [--json]",
|
|
3841
4159
|
" headband sync worker --scaffold docker [--force] [--json]",
|
|
3842
|
-
" headband sync reconcile [--fix] [--json]",
|
|
4160
|
+
" headband sync reconcile [--content] [--fix] [--json]",
|
|
3843
4161
|
" headband sync retry-dead-letters [--json]",
|
|
3844
4162
|
" headband sync status [--json]",
|
|
3845
4163
|
"",
|
|
@@ -4984,6 +5302,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
4984
5302
|
"--client",
|
|
4985
5303
|
"--instance",
|
|
4986
5304
|
"--instances",
|
|
5305
|
+
"--indexes",
|
|
4987
5306
|
"--db-url",
|
|
4988
5307
|
"--owner-url",
|
|
4989
5308
|
"--tls-mode",
|
|
@@ -5020,6 +5339,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
5020
5339
|
"--write",
|
|
5021
5340
|
"--force",
|
|
5022
5341
|
"--fix",
|
|
5342
|
+
"--content",
|
|
5023
5343
|
"--loop",
|
|
5024
5344
|
"--accept-broad-role",
|
|
5025
5345
|
"--rebuild",
|
|
@@ -5183,7 +5503,7 @@ Commands:
|
|
|
5183
5503
|
branch checkout <name> [--project <id>] [--json]
|
|
5184
5504
|
branch reset|rollback|restore|set-primary (coming with bucket storage)
|
|
5185
5505
|
keys list [--json]
|
|
5186
|
-
keys create <name> --type <admin|search> [--project <id>] [--branch <name>] [--instances <id,id>] [--permissions <a,b>] [--expires <30d|ISO date>] [--json]
|
|
5506
|
+
keys create <name> --type <admin|search> [--project <id>] [--branch <name>] [--instances <id,id>] [--indexes <uid,uid>] [--permissions <a,b>] [--expires <30d|ISO date>] [--json]
|
|
5187
5507
|
keys revoke <id> [--yes] [--json]
|
|
5188
5508
|
indexes list [--json] (index works in place of indexes)
|
|
5189
5509
|
indexes create <uid> [--primary-key <pk>] [--engine <engine>] [--instance <id>] [--json]
|
|
@@ -5201,7 +5521,7 @@ Commands:
|
|
|
5201
5521
|
instances connect <id> [--client <node|curl>] [--json]
|
|
5202
5522
|
warm <index>... | --project <id> [--branch <name>] [--level auto|stage|start] [--json]
|
|
5203
5523
|
docs push <file> --index <uid> [--json]
|
|
5204
|
-
docs export --index <uid> [--out <file>] [--format <jsonl|json>] [--batch <n>]
|
|
5524
|
+
docs export --index <uid> [--out <file>] [--format <jsonl|json>] [--batch <n>] [--declare-key]
|
|
5205
5525
|
docs delete <docId>... --index <uid> [--json]
|
|
5206
5526
|
search <index> [<query>] [--limit N] [--filter <expr>] [--mode <instant|retrieval>] [--json]
|
|
5207
5527
|
tasks list [--index <uid>] [--limit <n>] [--json]
|
|
@@ -5216,7 +5536,7 @@ Commands:
|
|
|
5216
5536
|
sync init [--table <name>] [--write] [--apply] [--json]
|
|
5217
5537
|
sync worker [--loop] [--budget-ms <ms>] [--batch-size <n>] [--reconcile-every <n>] [--project <id>] [--json]
|
|
5218
5538
|
sync worker --scaffold docker [--force] [--json]
|
|
5219
|
-
sync reconcile [--fix] [--json]
|
|
5539
|
+
sync reconcile [--content] [--fix] [--json]
|
|
5220
5540
|
sync retry-dead-letters [--json]
|
|
5221
5541
|
sync status [--json]
|
|
5222
5542
|
|
|
@@ -5252,13 +5572,14 @@ function printCommandHelp(command, io) {
|
|
|
5252
5572
|
keys: [
|
|
5253
5573
|
"Usage:",
|
|
5254
5574
|
" headband keys list [--json]",
|
|
5255
|
-
" headband keys create <name> --type <admin|search> [--project <id>] [--branch <name>] [--instances <id,id>] [--permissions <a,b>] [--expires <30d|ISO date>] [--json]",
|
|
5575
|
+
" headband keys create <name> --type <admin|search> [--project <id>] [--branch <name>] [--instances <id,id>] [--indexes <uid,uid>] [--permissions <a,b>] [--expires <30d|ISO date>] [--json]",
|
|
5256
5576
|
" headband keys revoke <id> [--yes] [--json]",
|
|
5257
5577
|
"",
|
|
5258
5578
|
"keys create mints a key for one branch: --branch (with --project, else the checkout's project),",
|
|
5259
5579
|
"else the checked-out branch, else the --project's primary branch. With none of these,",
|
|
5260
5580
|
"--type admin mints an org key, and --type search is refused: a search key needs a branch.",
|
|
5261
|
-
"--instances <id,id> limits the key to those dedicated engines (on a branch: its engines among them)."
|
|
5581
|
+
"--instances <id,id> limits the key to those dedicated engines (on a branch: its engines among them).",
|
|
5582
|
+
"--indexes <uid,uid> limits a search key to those indexes; any other index answers 404."
|
|
5262
5583
|
].join("\n"),
|
|
5263
5584
|
indexes: [
|
|
5264
5585
|
"Usage:",
|
|
@@ -5290,7 +5611,7 @@ function printCommandHelp(command, io) {
|
|
|
5290
5611
|
docs: [
|
|
5291
5612
|
"Usage:",
|
|
5292
5613
|
" headband docs push <file> --index <uid> [--json]",
|
|
5293
|
-
" headband docs export --index <uid> [--out <file>] [--format <jsonl|json>] [--batch <n>] [--json]",
|
|
5614
|
+
" headband docs export --index <uid> [--out <file>] [--format <jsonl|json>] [--batch <n>] [--declare-key] [--json]",
|
|
5294
5615
|
" headband docs delete <docId>... --index <uid> [--json]",
|
|
5295
5616
|
"",
|
|
5296
5617
|
"docs push sends the documents in a .json (an array) or .jsonl file, 1000 at a time.",
|
|
@@ -5300,6 +5621,9 @@ function printCommandHelp(command, io) {
|
|
|
5300
5621
|
" --out <file> to a file. The format follows the extension (.json is one array,",
|
|
5301
5622
|
" anything else JSONL) unless --format says otherwise.",
|
|
5302
5623
|
" --batch <n> documents per request. Default 1000, or 250 on Typesense.",
|
|
5624
|
+
" --declare-key a Visor index past 1000 documents whose primary key is not",
|
|
5625
|
+
" filterable and sortable (made before 2026-10-21): declare the",
|
|
5626
|
+
" key on it first (Visor rebuilds the index), wait, then export.",
|
|
5303
5627
|
" If the export stops on an error, the documents written so far are kept in",
|
|
5304
5628
|
" <file>.partial, and <file> is not written.",
|
|
5305
5629
|
"",
|
|
@@ -5510,7 +5834,12 @@ function parsePermissions(raw) {
|
|
|
5510
5834
|
}
|
|
5511
5835
|
function keyRow(key) {
|
|
5512
5836
|
const branch = key.branch;
|
|
5513
|
-
return {
|
|
5837
|
+
return {
|
|
5838
|
+
...key,
|
|
5839
|
+
branch: branch?.name ?? "",
|
|
5840
|
+
engines: enginesColumn(key.instanceIds),
|
|
5841
|
+
indexes: enginesColumn(key.indexUids)
|
|
5842
|
+
};
|
|
5514
5843
|
}
|
|
5515
5844
|
function enginesColumn(instanceIds) {
|
|
5516
5845
|
if (instanceIds === null) return "all";
|
|
@@ -5521,6 +5850,21 @@ function enginesColumn(instanceIds) {
|
|
|
5521
5850
|
}
|
|
5522
5851
|
return "none";
|
|
5523
5852
|
}
|
|
5853
|
+
function parseIndexList(raw) {
|
|
5854
|
+
const uids = [
|
|
5855
|
+
...new Set(
|
|
5856
|
+
raw.split(",").map((x) => x.trim()).filter(Boolean)
|
|
5857
|
+
)
|
|
5858
|
+
];
|
|
5859
|
+
if (uids.length === 0) throw new CliError("--indexes needs at least one index uid.");
|
|
5860
|
+
const bad = uids.find((uid) => !/^[A-Za-z0-9_-]{1,64}$/.test(uid));
|
|
5861
|
+
if (bad) {
|
|
5862
|
+
throw new CliError(
|
|
5863
|
+
`Not an index uid: ${bad}. Index uids are letters, numbers, hyphens and underscores.`
|
|
5864
|
+
);
|
|
5865
|
+
}
|
|
5866
|
+
return uids;
|
|
5867
|
+
}
|
|
5524
5868
|
function parseInstanceList(raw) {
|
|
5525
5869
|
const ids = [
|
|
5526
5870
|
...new Set(
|
|
@@ -5553,6 +5897,7 @@ async function runKeys(args, flags, io, confirm, cwd) {
|
|
|
5553
5897
|
"projectId",
|
|
5554
5898
|
"branch",
|
|
5555
5899
|
"engines",
|
|
5900
|
+
"indexes",
|
|
5556
5901
|
"environment",
|
|
5557
5902
|
"lastUsedAt",
|
|
5558
5903
|
"expiresAt"
|
|
@@ -5574,6 +5919,11 @@ async function runKeys(args, flags, io, confirm, cwd) {
|
|
|
5574
5919
|
}
|
|
5575
5920
|
const instances = stringFlag3(flags, "--instances");
|
|
5576
5921
|
if (instances !== void 0) body.instanceIds = parseInstanceList(instances);
|
|
5922
|
+
if (stringFlag3(flags, "--index") !== void 0) {
|
|
5923
|
+
throw new CliError("Unknown option --index for keys create. Did you mean --indexes?");
|
|
5924
|
+
}
|
|
5925
|
+
const indexes = stringFlag3(flags, "--indexes");
|
|
5926
|
+
if (indexes !== void 0) body.indexUids = parseIndexList(indexes);
|
|
5577
5927
|
const permissions = stringFlag3(flags, "--permissions");
|
|
5578
5928
|
if (permissions) body.permissions = parsePermissions(permissions);
|
|
5579
5929
|
const expires = stringFlag3(flags, "--expires");
|
|
@@ -5593,6 +5943,12 @@ async function runKeys(args, flags, io, confirm, cwd) {
|
|
|
5593
5943
|
typeof keyId === "string" && keyId ? `This API did not limit the new key to the chosen engines, so key ${keyId} reaches wider than asked. Revoke it now: headband keys revoke ${keyId}` : "This API did not limit the new key to the chosen engines, so it reaches wider than asked. Find it with `headband keys list` and revoke it with `headband keys revoke <id>`."
|
|
5594
5944
|
);
|
|
5595
5945
|
}
|
|
5946
|
+
if (indexes !== void 0 && !Array.isArray(response.data.key.indexUids)) {
|
|
5947
|
+
const keyId = response.data.key.id;
|
|
5948
|
+
throw new CliError(
|
|
5949
|
+
typeof keyId === "string" && keyId ? `This API did not limit the new key to the chosen indexes, so key ${keyId} reaches every index of its branch. Revoke it now: headband keys revoke ${keyId}` : "This API did not limit the new key to the chosen indexes, so it reaches every index of its branch. Find it with `headband keys list` and revoke it with `headband keys revoke <id>`."
|
|
5950
|
+
);
|
|
5951
|
+
}
|
|
5596
5952
|
if (boolFlag(flags, "--json")) {
|
|
5597
5953
|
printJson(io, response);
|
|
5598
5954
|
return;
|
|
@@ -5602,7 +5958,17 @@ async function runKeys(args, flags, io, confirm, cwd) {
|
|
|
5602
5958
|
printTable(
|
|
5603
5959
|
io,
|
|
5604
5960
|
[keyRow(response.data.key)],
|
|
5605
|
-
[
|
|
5961
|
+
[
|
|
5962
|
+
"id",
|
|
5963
|
+
"name",
|
|
5964
|
+
"type",
|
|
5965
|
+
"environment",
|
|
5966
|
+
"branch",
|
|
5967
|
+
"engines",
|
|
5968
|
+
"indexes",
|
|
5969
|
+
"keyPrefix",
|
|
5970
|
+
"expiresAt"
|
|
5971
|
+
]
|
|
5606
5972
|
);
|
|
5607
5973
|
return;
|
|
5608
5974
|
}
|
|
@@ -5738,6 +6104,7 @@ async function runDocsExport(flags, context, cwd) {
|
|
|
5738
6104
|
const format = exportFormat(flags, out);
|
|
5739
6105
|
const client = clientFromFlags(flags, cwd);
|
|
5740
6106
|
const batchSize = parseIntFlag(flags, "--batch") ?? await defaultExportBatchSize(client, index);
|
|
6107
|
+
if (boolFlag(flags, "--declare-key")) await declareExportKey(client, index, io);
|
|
5741
6108
|
if (!out) {
|
|
5742
6109
|
const writeLine = context.writeLine ?? ((line) => io.stdout(line));
|
|
5743
6110
|
try {
|
|
@@ -5940,7 +6307,8 @@ async function runSchema(args, flags, io, confirm, cwd) {
|
|
|
5940
6307
|
dir,
|
|
5941
6308
|
allowDestructive,
|
|
5942
6309
|
io: chatter,
|
|
5943
|
-
projectId
|
|
6310
|
+
projectId,
|
|
6311
|
+
readRemoteDocuments: (uid, primaryKey) => readRemoteDocuments(apiClient, uid, primaryKey)
|
|
5944
6312
|
});
|
|
5945
6313
|
if (json) printJson(io, { ...result, projectId: projectId ?? null });
|
|
5946
6314
|
else {
|
|
@@ -6120,8 +6488,10 @@ async function runSync(args, flags, io, confirm, cwd) {
|
|
|
6120
6488
|
client,
|
|
6121
6489
|
io: chatter,
|
|
6122
6490
|
fix: boolFlag(flags, "--fix"),
|
|
6491
|
+
content: boolFlag(flags, "--content"),
|
|
6123
6492
|
getStats: (uid) => apiClient.get(`/v1/indexes/${encode(uid)}/stats`),
|
|
6124
|
-
listRemoteIds: (uid, primaryKey) => listRemoteIds(apiClient, uid, primaryKey)
|
|
6493
|
+
listRemoteIds: (uid, primaryKey) => listRemoteIds(apiClient, uid, primaryKey),
|
|
6494
|
+
readRemoteDocuments: (uid, primaryKey) => readRemoteDocuments(apiClient, uid, primaryKey)
|
|
6125
6495
|
});
|
|
6126
6496
|
if (json) printJson(io, { indexes: rows, drift: reconcileHasDrift(rows) });
|
|
6127
6497
|
if (reconcileHasDrift(rows)) process.exitCode = 1;
|
|
@@ -6143,7 +6513,7 @@ async function runSync(args, flags, io, confirm, cwd) {
|
|
|
6143
6513
|
printCommandHelp("sync", io);
|
|
6144
6514
|
throw new UsageError("Unknown sync command");
|
|
6145
6515
|
}
|
|
6146
|
-
var CLI_VERSION = true ? "0.6.
|
|
6516
|
+
var CLI_VERSION = true ? "0.6.1" : "dev";
|
|
6147
6517
|
async function run(argv, context = { io: defaultIO }) {
|
|
6148
6518
|
if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version")) {
|
|
6149
6519
|
context.io.stdout(CLI_VERSION);
|
|
@@ -6277,6 +6647,7 @@ function exitAfterFlush(code) {
|
|
|
6277
6647
|
main,
|
|
6278
6648
|
parseArgv,
|
|
6279
6649
|
parseExpires,
|
|
6650
|
+
parseIndexList,
|
|
6280
6651
|
parsePermissions,
|
|
6281
6652
|
run,
|
|
6282
6653
|
stdinIsInteractive,
|