@datarails-ai/dr-cli 1.6.94 → 1.6.95
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/dist/cli.js +509 -80
- package/dist/scopes-map.json +1 -1
- package/dist/skills/create-connector-lib/SKILL.md +6 -2
- package/dist/skills/datarails-connectors-onboard-connector/SKILL.md +20 -9
- package/dist/skills/dr-cli/SKILL.md +4 -1
- package/dist/skills/dr-cli/reference/filebox-documents.md +32 -2
- package/dist/skills/dr-cli/reference/reconciliation-mec.md +14 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "@datarails-ai/dr-cli",
|
|
11
|
-
version: "1.6.
|
|
11
|
+
version: "1.6.95",
|
|
12
12
|
description: "Datarails CLI",
|
|
13
13
|
type: "module",
|
|
14
14
|
engines: {
|
|
@@ -67,7 +67,7 @@ var package_default = {
|
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
// src/buildInfo.ts
|
|
70
|
-
var BUILD_TIME = "2026-10-
|
|
70
|
+
var BUILD_TIME = "2026-10-02T15:08:38.411Z";
|
|
71
71
|
function formatBuildTime(iso) {
|
|
72
72
|
if (iso === "") return "";
|
|
73
73
|
const d = new Date(iso);
|
|
@@ -4853,13 +4853,13 @@ function formatExpLine(claims) {
|
|
|
4853
4853
|
} else {
|
|
4854
4854
|
return String(raw);
|
|
4855
4855
|
}
|
|
4856
|
-
const
|
|
4856
|
+
const stamp2 = formatUtc(unix);
|
|
4857
4857
|
const deltaMs = unix * 1e3 - Date.now();
|
|
4858
4858
|
const roundedMs = roundToMinuteMs(deltaMs);
|
|
4859
4859
|
if (roundedMs < 0) {
|
|
4860
|
-
return `${
|
|
4860
|
+
return `${stamp2} (expired ${formatGoDuration(-roundedMs)} ago)`;
|
|
4861
4861
|
}
|
|
4862
|
-
return `${
|
|
4862
|
+
return `${stamp2} (in ${formatGoDuration(roundedMs)})`;
|
|
4863
4863
|
}
|
|
4864
4864
|
function formatClaimValue(key, v) {
|
|
4865
4865
|
if (key === "exp" || key === "iat" || key === "nbf") {
|
|
@@ -19305,9 +19305,9 @@ var PERIOD_LABEL_RE = /^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*[
|
|
|
19305
19305
|
function findPeriodLabelRefs(formula) {
|
|
19306
19306
|
const bad = [];
|
|
19307
19307
|
for (const m of formula.matchAll(BRACKET_REF_RE)) {
|
|
19308
|
-
const
|
|
19309
|
-
if (PERIOD_LABEL_RE.test(
|
|
19310
|
-
bad.push(
|
|
19308
|
+
const ref2 = (m[1] ?? "").trim();
|
|
19309
|
+
if (PERIOD_LABEL_RE.test(ref2)) {
|
|
19310
|
+
bad.push(ref2);
|
|
19311
19311
|
}
|
|
19312
19312
|
}
|
|
19313
19313
|
return bad;
|
|
@@ -20374,7 +20374,7 @@ function toWireFormula(input) {
|
|
|
20374
20374
|
}
|
|
20375
20375
|
const wire = padded.replace(STASH_RE, (_m, i) => stash[Number(i)]);
|
|
20376
20376
|
const hasMetricOperand = (wire.match(/'[^']*'/g) ?? []).some(
|
|
20377
|
-
(
|
|
20377
|
+
(ref2) => !INTERNAL_FORMULA_TOKENS.includes(ref2.slice(1, -1))
|
|
20378
20378
|
);
|
|
20379
20379
|
if (!hasMetricOperand) {
|
|
20380
20380
|
throw new Error(`--formula must reference at least one metric, e.g. [Revenue]`);
|
|
@@ -25544,8 +25544,8 @@ async function fetchTemplate(client, id, signal) {
|
|
|
25544
25544
|
throw err;
|
|
25545
25545
|
}
|
|
25546
25546
|
}
|
|
25547
|
-
function resolveFieldRef(template,
|
|
25548
|
-
const trimmed =
|
|
25547
|
+
function resolveFieldRef(template, ref2) {
|
|
25548
|
+
const trimmed = ref2.trim();
|
|
25549
25549
|
if (/^\d+$/.test(trimmed)) {
|
|
25550
25550
|
const id = Number(trimmed);
|
|
25551
25551
|
const byId = template.fields.find((f) => f.id === id);
|
|
@@ -36423,6 +36423,249 @@ function matchesAny(buf, sigs) {
|
|
|
36423
36423
|
return sigs.some((sig) => startsWith(buf, sig));
|
|
36424
36424
|
}
|
|
36425
36425
|
|
|
36426
|
+
// src/commands/_dataQueryWait.ts
|
|
36427
|
+
var DQ_WAIT_POLL_INTERVAL_MS = 5e3;
|
|
36428
|
+
var DQ_WAIT_POLL_MAX_INTERVAL_MS = 3e4;
|
|
36429
|
+
var DQ_WAIT_DEFAULT_TIMEOUT_SEC = 1800;
|
|
36430
|
+
var DQ_WAIT_TIMEOUT_MAX_SEC = 86400;
|
|
36431
|
+
var DQ_WAIT_SETUP_OVERHEAD_MS = 6e4;
|
|
36432
|
+
function dataQuerySyncCallTimeoutMs(wait, timeoutSec) {
|
|
36433
|
+
if (!wait) return void 0;
|
|
36434
|
+
return timeoutSec * 1e3 + DQ_WAIT_SETUP_OVERHEAD_MS;
|
|
36435
|
+
}
|
|
36436
|
+
function parseDataQueryWaitTimeout(raw) {
|
|
36437
|
+
const n = Number(raw ?? String(DQ_WAIT_DEFAULT_TIMEOUT_SEC));
|
|
36438
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
|
|
36439
|
+
throw new Error(`--timeout must be a positive integer number of seconds (got "${raw}")`);
|
|
36440
|
+
}
|
|
36441
|
+
if (n > DQ_WAIT_TIMEOUT_MAX_SEC) {
|
|
36442
|
+
throw new Error(
|
|
36443
|
+
`--timeout must be at most ${DQ_WAIT_TIMEOUT_MAX_SEC} seconds (24 hours; got "${raw}") \u2014 for longer runs, re-check later with 'dr filebox versions <id>' instead of waiting.`
|
|
36444
|
+
);
|
|
36445
|
+
}
|
|
36446
|
+
return n;
|
|
36447
|
+
}
|
|
36448
|
+
function asRecord(v) {
|
|
36449
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
36450
|
+
}
|
|
36451
|
+
function stamp(v) {
|
|
36452
|
+
return JSON.stringify(v ?? null);
|
|
36453
|
+
}
|
|
36454
|
+
function belongsTo(v, period) {
|
|
36455
|
+
return period.tags.every(
|
|
36456
|
+
(want) => (v.tags ?? []).some((t) => t.id === want.id && want.inPeriod(t.value))
|
|
36457
|
+
);
|
|
36458
|
+
}
|
|
36459
|
+
function maxId(rows) {
|
|
36460
|
+
return rows.reduce((m, v) => Math.max(m, v.id), 0);
|
|
36461
|
+
}
|
|
36462
|
+
function ref(v) {
|
|
36463
|
+
return { id: v.id, version: v.version };
|
|
36464
|
+
}
|
|
36465
|
+
function buildDataQuerySyncBaseline(periods, versions, dataQueryRaw) {
|
|
36466
|
+
const dataQuery = asRecord(dataQueryRaw);
|
|
36467
|
+
return {
|
|
36468
|
+
periods: periods.map((p) => {
|
|
36469
|
+
const rows = versions.filter((v) => belongsTo(v, p));
|
|
36470
|
+
return {
|
|
36471
|
+
maxId: maxId(rows),
|
|
36472
|
+
lastSyncById: new Map(rows.map((v) => [v.id, stamp(v.last_sync)]))
|
|
36473
|
+
};
|
|
36474
|
+
}),
|
|
36475
|
+
maxVersionId: maxId(versions),
|
|
36476
|
+
updateTime: stamp(dataQuery["update_time"])
|
|
36477
|
+
};
|
|
36478
|
+
}
|
|
36479
|
+
function classifyDataQuerySyncProgress(periods, baseline, versions, dataQueryRaw) {
|
|
36480
|
+
const dataQuery = asRecord(dataQueryRaw);
|
|
36481
|
+
const out = { periods: [], unmatchedNewVersions: [] };
|
|
36482
|
+
periods.forEach((p, i) => {
|
|
36483
|
+
const base = baseline.periods[i];
|
|
36484
|
+
const rows = versions.filter((v) => belongsTo(v, p));
|
|
36485
|
+
const created = rows.filter((v) => v.id > base.maxId);
|
|
36486
|
+
if (created.length > 0) {
|
|
36487
|
+
const newest = created.reduce((a, b) => b.id > a.id ? b : a);
|
|
36488
|
+
out.periods.push({ period: p.label, result: "new_version", version: ref(newest) });
|
|
36489
|
+
return;
|
|
36490
|
+
}
|
|
36491
|
+
const confirmed = rows.find(
|
|
36492
|
+
(v) => base.lastSyncById.has(v.id) && stamp(v.last_sync) !== base.lastSyncById.get(v.id)
|
|
36493
|
+
);
|
|
36494
|
+
if (confirmed !== void 0) {
|
|
36495
|
+
out.periods.push({ period: p.label, result: "no_change", version: ref(confirmed) });
|
|
36496
|
+
return;
|
|
36497
|
+
}
|
|
36498
|
+
out.periods.push({ period: p.label, result: "pending" });
|
|
36499
|
+
});
|
|
36500
|
+
out.unmatchedNewVersions = versions.filter((v) => v.id > baseline.maxVersionId && !periods.some((p) => belongsTo(v, p))).map((v) => v.id);
|
|
36501
|
+
if (dataQuery["status"] === "ERROR" && stamp(dataQuery["update_time"]) !== baseline.updateTime) {
|
|
36502
|
+
const msg = dataQuery["last_sync_error"];
|
|
36503
|
+
out.error = typeof msg === "string" && msg !== "" ? msg : "sync failed (no error text)";
|
|
36504
|
+
}
|
|
36505
|
+
return out;
|
|
36506
|
+
}
|
|
36507
|
+
async function captureDataQuerySyncBaseline(client, fileboxId, periods, signal) {
|
|
36508
|
+
try {
|
|
36509
|
+
const [versions, dataQuery] = await Promise.all([
|
|
36510
|
+
listVersions(client, fileboxId, {}, { signal }),
|
|
36511
|
+
getDataQueryRaw(client, fileboxId, { signal })
|
|
36512
|
+
]);
|
|
36513
|
+
return buildDataQuerySyncBaseline(periods, versions, dataQuery);
|
|
36514
|
+
} catch (err) {
|
|
36515
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
36516
|
+
throw new Error(
|
|
36517
|
+
`--wait: could not read filebox ${fileboxId}'s versions before syncing (${msg}); the sync was NOT triggered`
|
|
36518
|
+
);
|
|
36519
|
+
}
|
|
36520
|
+
}
|
|
36521
|
+
var defaultSleep8 = (ms, signal) => new Promise((resolve3, reject) => {
|
|
36522
|
+
if (signal?.aborted) {
|
|
36523
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
36524
|
+
return;
|
|
36525
|
+
}
|
|
36526
|
+
const onAbort = () => {
|
|
36527
|
+
clearTimeout(t);
|
|
36528
|
+
reject(signal?.reason ?? new Error("aborted"));
|
|
36529
|
+
};
|
|
36530
|
+
const t = setTimeout(() => {
|
|
36531
|
+
signal?.removeEventListener("abort", onAbort);
|
|
36532
|
+
resolve3();
|
|
36533
|
+
}, ms);
|
|
36534
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
36535
|
+
});
|
|
36536
|
+
async function waitForDataQuerySync(cfg) {
|
|
36537
|
+
let interval = cfg.pollIntervalMs ?? DQ_WAIT_POLL_INTERVAL_MS;
|
|
36538
|
+
const maxInterval = cfg.maxPollIntervalMs ?? DQ_WAIT_POLL_MAX_INTERVAL_MS;
|
|
36539
|
+
const sleep4 = cfg.sleep ?? defaultSleep8;
|
|
36540
|
+
const now = cfg.now ?? Date.now;
|
|
36541
|
+
const start = now();
|
|
36542
|
+
const deadline = start + cfg.timeoutSec * 1e3;
|
|
36543
|
+
if (!cfg.asJson) {
|
|
36544
|
+
const n = cfg.periods.length;
|
|
36545
|
+
cfg.out.write(
|
|
36546
|
+
`Waiting for the sync of ${n} period${plural(n)} on filebox ${cfg.fileboxId} (timeout ${cfg.timeoutSec}s)\u2026
|
|
36547
|
+
`
|
|
36548
|
+
);
|
|
36549
|
+
}
|
|
36550
|
+
let progress;
|
|
36551
|
+
let outcome;
|
|
36552
|
+
for (; ; ) {
|
|
36553
|
+
try {
|
|
36554
|
+
const [versions, dataQuery] = await Promise.all([
|
|
36555
|
+
listVersions(cfg.client, cfg.fileboxId, {}, { signal: cfg.signal }),
|
|
36556
|
+
getDataQueryRaw(cfg.client, cfg.fileboxId, { signal: cfg.signal })
|
|
36557
|
+
]);
|
|
36558
|
+
progress = classifyDataQuerySyncProgress(cfg.periods, cfg.baseline, versions, dataQuery);
|
|
36559
|
+
} catch (err) {
|
|
36560
|
+
if (cfg.signal.aborted) {
|
|
36561
|
+
outcome = "cancelled";
|
|
36562
|
+
break;
|
|
36563
|
+
}
|
|
36564
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
36565
|
+
cfg.errOut.write(`warning: sync status poll failed (${msg}); retrying
|
|
36566
|
+
`);
|
|
36567
|
+
}
|
|
36568
|
+
if (progress?.error !== void 0) {
|
|
36569
|
+
outcome = "failed";
|
|
36570
|
+
break;
|
|
36571
|
+
}
|
|
36572
|
+
if (progress !== void 0 && progress.periods.every((p) => p.result !== "pending")) {
|
|
36573
|
+
outcome = "completed";
|
|
36574
|
+
break;
|
|
36575
|
+
}
|
|
36576
|
+
if (now() >= deadline) {
|
|
36577
|
+
outcome = "timed_out";
|
|
36578
|
+
break;
|
|
36579
|
+
}
|
|
36580
|
+
try {
|
|
36581
|
+
await sleep4(interval, cfg.signal);
|
|
36582
|
+
} catch {
|
|
36583
|
+
outcome = "cancelled";
|
|
36584
|
+
break;
|
|
36585
|
+
}
|
|
36586
|
+
interval = Math.min(interval * 1.5, maxInterval);
|
|
36587
|
+
}
|
|
36588
|
+
const final = progress ?? {
|
|
36589
|
+
periods: cfg.periods.map((p) => ({ period: p.label, result: "pending" })),
|
|
36590
|
+
unmatchedNewVersions: []
|
|
36591
|
+
};
|
|
36592
|
+
const waitedSec = Math.round((now() - start) / 1e3);
|
|
36593
|
+
renderSummary2(cfg, final, outcome, waitedSec);
|
|
36594
|
+
if (outcome !== "completed") {
|
|
36595
|
+
throw new Error(failureMessage(cfg.fileboxId, final, outcome, waitedSec));
|
|
36596
|
+
}
|
|
36597
|
+
}
|
|
36598
|
+
function renderSummary2(cfg, progress, outcome, waitedSec) {
|
|
36599
|
+
if (cfg.asJson) {
|
|
36600
|
+
writeJSONIndented(cfg.out, {
|
|
36601
|
+
filebox_id: cfg.fileboxId,
|
|
36602
|
+
outcome,
|
|
36603
|
+
periods: progress.periods.map((p) => ({
|
|
36604
|
+
period: p.period,
|
|
36605
|
+
result: p.result,
|
|
36606
|
+
version_id: p.version?.id ?? null,
|
|
36607
|
+
version: p.version?.version ?? null
|
|
36608
|
+
})),
|
|
36609
|
+
unmatched_new_versions: progress.unmatchedNewVersions,
|
|
36610
|
+
error: progress.error ?? null,
|
|
36611
|
+
waited_seconds: waitedSec
|
|
36612
|
+
});
|
|
36613
|
+
return;
|
|
36614
|
+
}
|
|
36615
|
+
const count = (r) => progress.periods.filter((p) => p.result === r).length;
|
|
36616
|
+
const parts = [`${count("new_version")} new version${plural(count("new_version"))}`];
|
|
36617
|
+
parts.push(`${count("no_change")} unchanged`);
|
|
36618
|
+
if (count("pending") > 0) parts.push(`${count("pending")} unfinished`);
|
|
36619
|
+
const headline = {
|
|
36620
|
+
completed: "Sync finished",
|
|
36621
|
+
failed: "Sync failed",
|
|
36622
|
+
timed_out: `Sync did not finish within ${waitedSec}s`,
|
|
36623
|
+
cancelled: "Sync wait cancelled"
|
|
36624
|
+
}[outcome];
|
|
36625
|
+
cfg.out.write(`${headline} for filebox ${cfg.fileboxId}: ${parts.join(", ")}.
|
|
36626
|
+
`);
|
|
36627
|
+
if (progress.error !== void 0) {
|
|
36628
|
+
cfg.out.write(` error: ${progress.error}
|
|
36629
|
+
`);
|
|
36630
|
+
}
|
|
36631
|
+
for (const p of progress.periods) {
|
|
36632
|
+
if (p.result === "new_version") {
|
|
36633
|
+
cfg.out.write(` ${p.period}: new version #${p.version.id} (v${p.version.version})
|
|
36634
|
+
`);
|
|
36635
|
+
} else if (p.result === "no_change") {
|
|
36636
|
+
cfg.out.write(
|
|
36637
|
+
` ${p.period}: unchanged \u2014 identical to version #${p.version.id} (v${p.version.version}), so no new version was written
|
|
36638
|
+
`
|
|
36639
|
+
);
|
|
36640
|
+
} else if (outcome === "timed_out") {
|
|
36641
|
+
cfg.out.write(
|
|
36642
|
+
` ${p.period}: no new or re-confirmed version yet \u2014 still running, or the sync returned no rows (check 'dr filebox data-query get ${cfg.fileboxId}')
|
|
36643
|
+
`
|
|
36644
|
+
);
|
|
36645
|
+
} else {
|
|
36646
|
+
cfg.out.write(` ${p.period}: not finished
|
|
36647
|
+
`);
|
|
36648
|
+
}
|
|
36649
|
+
}
|
|
36650
|
+
for (const id of progress.unmatchedNewVersions) {
|
|
36651
|
+
cfg.out.write(
|
|
36652
|
+
` note: new version #${id} matches none of the requested periods \u2014 check its date tag with 'dr filebox versions ${cfg.fileboxId}'
|
|
36653
|
+
`
|
|
36654
|
+
);
|
|
36655
|
+
}
|
|
36656
|
+
}
|
|
36657
|
+
function failureMessage(fileboxId, progress, outcome, waitedSec) {
|
|
36658
|
+
const pending = progress.periods.filter((p) => p.result === "pending").map((p) => p.period);
|
|
36659
|
+
const pendingList = pending.join(", ") || "(none)";
|
|
36660
|
+
if (outcome === "failed") {
|
|
36661
|
+
return `data-query sync on filebox ${fileboxId} failed: ${progress.error}`;
|
|
36662
|
+
}
|
|
36663
|
+
if (outcome === "cancelled") {
|
|
36664
|
+
return `data-query sync wait cancelled after ${waitedSec}s with ${pending.length} period(s) unfinished: ${pendingList}. The sync continues server-side; re-check with 'dr filebox versions ${fileboxId}' instead of re-triggering.`;
|
|
36665
|
+
}
|
|
36666
|
+
return `data-query sync on filebox ${fileboxId} did not finish within ${waitedSec}s \u2014 ${pending.length} period(s) unfinished: ${pendingList}. Increase --timeout, or re-check with 'dr filebox versions ${fileboxId}'; do not blindly re-trigger.`;
|
|
36667
|
+
}
|
|
36668
|
+
|
|
36426
36669
|
// src/commands/filebox.ts
|
|
36427
36670
|
async function fileToBlob(filePath) {
|
|
36428
36671
|
const buf = await fsPromises.readFile(filePath);
|
|
@@ -36732,7 +36975,7 @@ ${JSON.stringify(result)}
|
|
|
36732
36975
|
`
|
|
36733
36976
|
);
|
|
36734
36977
|
}
|
|
36735
|
-
function
|
|
36978
|
+
function asRecord2(v) {
|
|
36736
36979
|
return v !== null && typeof v === "object" ? v : void 0;
|
|
36737
36980
|
}
|
|
36738
36981
|
function numOf(r, k) {
|
|
@@ -36742,9 +36985,9 @@ function strOf(r, k) {
|
|
|
36742
36985
|
return r !== void 0 && typeof r[k] === "string" ? r[k] : void 0;
|
|
36743
36986
|
}
|
|
36744
36987
|
function extractUploadedVersionMeta(result) {
|
|
36745
|
-
const r =
|
|
36746
|
-
const dv =
|
|
36747
|
-
const docObj =
|
|
36988
|
+
const r = asRecord2(result);
|
|
36989
|
+
const dv = asRecord2(r?.["document_version"]);
|
|
36990
|
+
const docObj = asRecord2(r?.["document"]);
|
|
36748
36991
|
return {
|
|
36749
36992
|
id: numOf(r, "id") ?? numOf(dv, "id") ?? numOf(docObj, "latest_version_id") ?? 0,
|
|
36750
36993
|
version: numOf(r, "version") ?? numOf(dv, "version") ?? 0,
|
|
@@ -38106,6 +38349,10 @@ async function runFileboxDataQueryGet(cfg) {
|
|
|
38106
38349
|
`);
|
|
38107
38350
|
cfg.out.write(` Last sync: ${orElse(renderCreateTime(q.last_sync), "never")}
|
|
38108
38351
|
`);
|
|
38352
|
+
if (q.last_general_sync !== void 0) {
|
|
38353
|
+
cfg.out.write(` Last start: ${orElse(renderCreateTime(q.last_general_sync), "never")}
|
|
38354
|
+
`);
|
|
38355
|
+
}
|
|
38109
38356
|
if (q.last_sync_error !== null && q.last_sync_error !== void 0 && q.last_sync_error !== "") {
|
|
38110
38357
|
cfg.out.write(` Last error: ${truncateField2(q.last_sync_error, 100)}
|
|
38111
38358
|
`);
|
|
@@ -38300,6 +38547,12 @@ function isoMondayOfUTC(d) {
|
|
|
38300
38547
|
monday.setUTCDate(d.getUTCDate() - isoDow);
|
|
38301
38548
|
return monday;
|
|
38302
38549
|
}
|
|
38550
|
+
function samePeriodAs(subType, stored, resolvedValue) {
|
|
38551
|
+
if (typeof stored !== "number" || !Number.isFinite(stored)) return false;
|
|
38552
|
+
const d = new Date(stored * 1e3);
|
|
38553
|
+
const parsed2 = { year: d.getUTCFullYear(), month0: d.getUTCMonth(), day: d.getUTCDate() };
|
|
38554
|
+
return resolveDateForSubType(subType, parsed2).value === resolvedValue;
|
|
38555
|
+
}
|
|
38303
38556
|
function resolveDateForSubType(subType, parsed2) {
|
|
38304
38557
|
const { year, month0, day } = parsed2;
|
|
38305
38558
|
switch ((subType ?? "").toLowerCase()) {
|
|
@@ -38347,9 +38600,15 @@ async function runFileboxDataQuerySync(cfg) {
|
|
|
38347
38600
|
const trimmed = t.trim();
|
|
38348
38601
|
if (trimmed !== "") rawTags.push(trimmed);
|
|
38349
38602
|
}
|
|
38603
|
+
if (cfg.wait !== void 0 && rawTags.length > 0) {
|
|
38604
|
+
throw new Error(
|
|
38605
|
+
"--wait cannot be combined with --tag; use --date to name the periods to wait for"
|
|
38606
|
+
);
|
|
38607
|
+
}
|
|
38350
38608
|
const parsedDates = cfg.dateArgs.map(parseCalendarDate);
|
|
38351
38609
|
const dateTargets = [];
|
|
38352
38610
|
const periodLabels = [];
|
|
38611
|
+
const waitPeriods = [];
|
|
38353
38612
|
if (parsedDates.length > 0) {
|
|
38354
38613
|
const details = await getDetails(cfg.client, cfg.id, {
|
|
38355
38614
|
signal: cfg.signal
|
|
@@ -38372,15 +38631,27 @@ async function runFileboxDataQuerySync(cfg) {
|
|
|
38372
38631
|
}
|
|
38373
38632
|
for (const parsed2 of parsedDates) {
|
|
38374
38633
|
const resolved = dateTags.map((tag) => {
|
|
38375
|
-
const { value, label } = resolveDateForSubType(tagSubType(tag), parsed2);
|
|
38376
|
-
return { id: tag.id, name: tag.name, value, label };
|
|
38634
|
+
const { value, label: label2 } = resolveDateForSubType(tagSubType(tag), parsed2);
|
|
38635
|
+
return { id: tag.id, name: tag.name, value, label: label2 };
|
|
38377
38636
|
});
|
|
38378
38637
|
dateTargets.push(
|
|
38379
38638
|
JSON.stringify(resolved.map(({ id, name, value }) => ({ id, name, value })))
|
|
38380
38639
|
);
|
|
38381
|
-
|
|
38640
|
+
const label = resolved.map((r) => r.label).join(" + ");
|
|
38641
|
+
periodLabels.push(label);
|
|
38642
|
+
waitPeriods.push({
|
|
38643
|
+
label,
|
|
38644
|
+
tags: dateTags.map((tag, i) => ({
|
|
38645
|
+
id: tag.id,
|
|
38646
|
+
inPeriod: (stored) => samePeriodAs(tagSubType(tag), stored, resolved[i].value)
|
|
38647
|
+
}))
|
|
38648
|
+
});
|
|
38382
38649
|
}
|
|
38383
38650
|
}
|
|
38651
|
+
if (waitPeriods.length === 0) {
|
|
38652
|
+
waitPeriods.push({ label: "latest sync", tags: [] });
|
|
38653
|
+
}
|
|
38654
|
+
const baseline = cfg.wait === void 0 ? void 0 : await captureDataQuerySyncBaseline(cfg.client, cfg.id, waitPeriods, cfg.signal);
|
|
38384
38655
|
const multipleTags = [...rawTags, ...dateTargets];
|
|
38385
38656
|
const body = {};
|
|
38386
38657
|
if (multipleTags.length > 0) {
|
|
@@ -38401,10 +38672,33 @@ async function runFileboxDataQuerySync(cfg) {
|
|
|
38401
38672
|
summary.push(`tags=${rawTags.join(",")}`);
|
|
38402
38673
|
}
|
|
38403
38674
|
const tagInfo = summary.length > 0 ? ` (${summary.join("; ")})` : "";
|
|
38404
|
-
cfg.
|
|
38675
|
+
if (cfg.wait === void 0 || baseline === void 0) {
|
|
38676
|
+
cfg.out.write(`Sync queued for filebox ${cfg.id}${tagInfo} \u2014 server: ${detail}
|
|
38405
38677
|
`);
|
|
38406
|
-
|
|
38678
|
+
cfg.out.write(
|
|
38679
|
+
`Check progress with: dr filebox data-query get ${cfg.id} (or re-run with --wait)
|
|
38680
|
+
`
|
|
38681
|
+
);
|
|
38682
|
+
return;
|
|
38683
|
+
}
|
|
38684
|
+
const progressOut = cfg.wait.asJson ? cfg.wait.err ?? process.stderr : cfg.out;
|
|
38685
|
+
progressOut.write(`Sync queued for filebox ${cfg.id}${tagInfo} \u2014 server: ${detail}
|
|
38407
38686
|
`);
|
|
38687
|
+
await waitForDataQuerySync({
|
|
38688
|
+
client: cfg.client,
|
|
38689
|
+
fileboxId: cfg.id,
|
|
38690
|
+
periods: waitPeriods,
|
|
38691
|
+
baseline,
|
|
38692
|
+
timeoutSec: cfg.wait.timeoutSec,
|
|
38693
|
+
asJson: cfg.wait.asJson,
|
|
38694
|
+
out: cfg.out,
|
|
38695
|
+
errOut: cfg.wait.err ?? process.stderr,
|
|
38696
|
+
signal: cfg.signal,
|
|
38697
|
+
pollIntervalMs: cfg.wait.pollIntervalMs,
|
|
38698
|
+
maxPollIntervalMs: cfg.wait.maxPollIntervalMs,
|
|
38699
|
+
sleep: cfg.wait.sleep,
|
|
38700
|
+
now: cfg.wait.now
|
|
38701
|
+
});
|
|
38408
38702
|
}
|
|
38409
38703
|
async function runFileboxDownload(cfg) {
|
|
38410
38704
|
if (cfg.fileboxId <= 0) {
|
|
@@ -39651,22 +39945,39 @@ function registerFileboxCommand(root) {
|
|
|
39651
39945
|
`Advanced: raw server-shape tag JSON, e.g. '[{"id":6,"name":"Month","value":1782907200}]'. Prefer --date \u2014 this is an unvalidated escape hatch for tag dimensions --date doesn't cover (e.g. LIST tags). Repeatable.`,
|
|
39652
39946
|
collectStrings,
|
|
39653
39947
|
[]
|
|
39654
|
-
).
|
|
39655
|
-
|
|
39656
|
-
|
|
39657
|
-
|
|
39658
|
-
|
|
39659
|
-
|
|
39660
|
-
|
|
39661
|
-
|
|
39662
|
-
|
|
39663
|
-
|
|
39664
|
-
|
|
39948
|
+
).option(
|
|
39949
|
+
"--wait",
|
|
39950
|
+
"After queueing, wait until every --date period has finished, and say whether each wrote a new version or was unchanged (identical output writes no version). Exits non-zero on a failed sync or a timeout. Not combinable with --tag.",
|
|
39951
|
+
false
|
|
39952
|
+
).option(
|
|
39953
|
+
"--timeout <seconds>",
|
|
39954
|
+
`With --wait: how long to wait (default ${DQ_WAIT_DEFAULT_TIMEOUT_SEC}, max 86400)`
|
|
39955
|
+
).option("--json", "With --wait: print the result as one JSON object", false).action(
|
|
39956
|
+
async (idStr, opts) => {
|
|
39957
|
+
const id = parseFileboxIdArg(idStr);
|
|
39958
|
+
const wait = opts.wait === true;
|
|
39959
|
+
if (!wait && (opts.timeout !== void 0 || opts.json === true)) {
|
|
39960
|
+
throw new Error("--timeout and --json only apply with --wait");
|
|
39961
|
+
}
|
|
39962
|
+
const timeoutSec = parseDataQueryWaitTimeout(opts.timeout);
|
|
39963
|
+
const setup = await setupCall(opts.server, opts.account, {
|
|
39964
|
+
timeoutMs: dataQuerySyncCallTimeoutMs(wait, timeoutSec)
|
|
39665
39965
|
});
|
|
39666
|
-
|
|
39667
|
-
|
|
39966
|
+
try {
|
|
39967
|
+
await runFileboxDataQuerySync({
|
|
39968
|
+
client: setup.client,
|
|
39969
|
+
id,
|
|
39970
|
+
tags: opts.tag ?? [],
|
|
39971
|
+
dateArgs: opts.date ?? [],
|
|
39972
|
+
signal: setup.signal,
|
|
39973
|
+
out: process.stdout,
|
|
39974
|
+
wait: wait ? { timeoutSec, asJson: opts.json === true } : void 0
|
|
39975
|
+
});
|
|
39976
|
+
} finally {
|
|
39977
|
+
setup.cleanup();
|
|
39978
|
+
}
|
|
39668
39979
|
}
|
|
39669
|
-
|
|
39980
|
+
);
|
|
39670
39981
|
addServerFlag(dqSyncCmd);
|
|
39671
39982
|
addAccountFlag(dqSyncCmd);
|
|
39672
39983
|
registerFileboxAdminCommands(fb, setupCall, addServerFlag, addAccountFlag);
|
|
@@ -42915,6 +43226,31 @@ async function runReconciliationAccountsSettings(cfg) {
|
|
|
42915
43226
|
}
|
|
42916
43227
|
renderAccountsSettings(cfg.out, items);
|
|
42917
43228
|
}
|
|
43229
|
+
var SETTINGS_BATCH_MAX_BYTES = 256 * 1024;
|
|
43230
|
+
function settingsKey(s) {
|
|
43231
|
+
return JSON.stringify([s.entity ?? "", s.account_group ?? "", s.name]);
|
|
43232
|
+
}
|
|
43233
|
+
function settingsFieldChanges(have, want) {
|
|
43234
|
+
const fields = ["frequency", "threshold_from", "threshold_to"];
|
|
43235
|
+
return fields.filter((f) => have[f] !== want[f]).map((f) => `${f} ${String(have[f])} \u2192 ${String(want[f])}`);
|
|
43236
|
+
}
|
|
43237
|
+
function chunkByJsonBytes(rows, maxBytes) {
|
|
43238
|
+
const chunks = [];
|
|
43239
|
+
let current = [];
|
|
43240
|
+
let size = 2;
|
|
43241
|
+
for (const row of rows) {
|
|
43242
|
+
const rowBytes = Buffer.byteLength(JSON.stringify(row));
|
|
43243
|
+
if (current.length > 0 && size + rowBytes + 1 > maxBytes) {
|
|
43244
|
+
chunks.push(current);
|
|
43245
|
+
current = [];
|
|
43246
|
+
size = 2;
|
|
43247
|
+
}
|
|
43248
|
+
size += current.length === 0 ? rowBytes : rowBytes + 1;
|
|
43249
|
+
current.push(row);
|
|
43250
|
+
}
|
|
43251
|
+
if (current.length > 0) chunks.push(current);
|
|
43252
|
+
return chunks;
|
|
43253
|
+
}
|
|
42918
43254
|
function settingsActionId(settings) {
|
|
42919
43255
|
return createHash8("sha256").update(JSON.stringify(settings)).digest("hex").slice(0, 12);
|
|
42920
43256
|
}
|
|
@@ -42926,6 +43262,32 @@ async function runReconciliationAccountsUpdateSettings(cfg) {
|
|
|
42926
43262
|
};
|
|
42927
43263
|
await preflightConfirmToken(action, cfg.token);
|
|
42928
43264
|
const n = cfg.settings.length;
|
|
43265
|
+
const current = await getAccountSettings(cfg.client, {}, { signal: cfg.signal });
|
|
43266
|
+
const currentByKey = new Map(current.map((s) => [settingsKey(s), s]));
|
|
43267
|
+
const changed = [];
|
|
43268
|
+
const preview = [];
|
|
43269
|
+
for (const want of cfg.settings) {
|
|
43270
|
+
const have = currentByKey.get(settingsKey(want));
|
|
43271
|
+
let what;
|
|
43272
|
+
if (have === void 0) {
|
|
43273
|
+
what = `new (frequency ${want.frequency})`;
|
|
43274
|
+
} else {
|
|
43275
|
+
const diffs = settingsFieldChanges(have, want);
|
|
43276
|
+
if (diffs.length === 0) continue;
|
|
43277
|
+
what = diffs.join(", ");
|
|
43278
|
+
}
|
|
43279
|
+
changed.push(want);
|
|
43280
|
+
if (preview.length < 5) {
|
|
43281
|
+
preview.push(` ${want.entity ?? ""} / ${want.account_group ?? ""} / ${want.name}: ${what}`);
|
|
43282
|
+
}
|
|
43283
|
+
}
|
|
43284
|
+
const unchanged = n - changed.length;
|
|
43285
|
+
if (changed.length === 0) {
|
|
43286
|
+
cfg.out.write(`Nothing to change: all ${n} row${plural(n)} already match the tenant.
|
|
43287
|
+
`);
|
|
43288
|
+
return;
|
|
43289
|
+
}
|
|
43290
|
+
const chunks = chunkByJsonBytes(changed, cfg.maxBatchBytes ?? SETTINGS_BATCH_MAX_BYTES);
|
|
42929
43291
|
await twoStepConfirm({
|
|
42930
43292
|
out: cfg.out,
|
|
42931
43293
|
action,
|
|
@@ -42933,19 +43295,42 @@ async function runReconciliationAccountsUpdateSettings(cfg) {
|
|
|
42933
43295
|
server: cfg.serverFlag,
|
|
42934
43296
|
confirmCmd: cfg.confirmCmd,
|
|
42935
43297
|
describe: async () => {
|
|
42936
|
-
const
|
|
42937
|
-
const
|
|
42938
|
-
|
|
42939
|
-
|
|
42940
|
-
|
|
43298
|
+
const byFrequency = /* @__PURE__ */ new Map();
|
|
43299
|
+
for (const s of changed) {
|
|
43300
|
+
byFrequency.set(s.frequency, (byFrequency.get(s.frequency) ?? 0) + 1);
|
|
43301
|
+
}
|
|
43302
|
+
const lines = [
|
|
43303
|
+
`rows: ${n}`,
|
|
43304
|
+
`to change: ${changed.length} (${chunks.length} PATCH request${plural(chunks.length)})`,
|
|
43305
|
+
`unchanged: ${unchanged} (already match the tenant \u2014 skipped)`,
|
|
43306
|
+
`by frequency: ${[...byFrequency].map(([f, c]) => `${f} ${c}`).join(", ")}`,
|
|
43307
|
+
"changes:",
|
|
43308
|
+
...preview
|
|
42941
43309
|
];
|
|
43310
|
+
if (changed.length > preview.length) {
|
|
43311
|
+
lines.push(` \u2026 (+${changed.length - preview.length} more)`);
|
|
43312
|
+
}
|
|
43313
|
+
return lines;
|
|
42942
43314
|
},
|
|
42943
43315
|
execute: async () => {
|
|
42944
|
-
|
|
42945
|
-
|
|
42946
|
-
|
|
42947
|
-
|
|
42948
|
-
|
|
43316
|
+
extendForBatch(cfg.extendTimeout, chunks.length);
|
|
43317
|
+
let applied = 0;
|
|
43318
|
+
for (const chunk of chunks) {
|
|
43319
|
+
try {
|
|
43320
|
+
await updateAccountSettings(cfg.client, chunk, { signal: cfg.signal });
|
|
43321
|
+
} catch (err) {
|
|
43322
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
43323
|
+
throw new Error(
|
|
43324
|
+
`PATCH failed after ${applied} of ${changed.length} rows were written (rows ${applied + 1}\u2013${applied + chunk.length} may be partially applied): ${msg}
|
|
43325
|
+
Re-run the same command without --confirm-token: rows that landed plan as unchanged, so the new plan covers only the rest.`
|
|
43326
|
+
);
|
|
43327
|
+
}
|
|
43328
|
+
applied += chunk.length;
|
|
43329
|
+
}
|
|
43330
|
+
cfg.out.write(
|
|
43331
|
+
`Updated ${applied} account setting${plural(applied)} (${unchanged} unchanged).
|
|
43332
|
+
`
|
|
43333
|
+
);
|
|
42949
43334
|
}
|
|
42950
43335
|
});
|
|
42951
43336
|
}
|
|
@@ -43384,10 +43769,29 @@ verify the date matches the accounts' reporting period (try \`dr reconciliation
|
|
|
43384
43769
|
account_id: id,
|
|
43385
43770
|
members: applyAssignments(byId.get(id), changesByAccount.get(id))
|
|
43386
43771
|
}));
|
|
43387
|
-
extendForBatch(cfg.extendTimeout, requests);
|
|
43772
|
+
extendForBatch(cfg.extendTimeout, requests * 2 - 1);
|
|
43388
43773
|
let applied = 0;
|
|
43389
43774
|
for (let i = 0; i < items.length; i += chunkSize) {
|
|
43390
43775
|
const chunk = items.slice(i, i + chunkSize);
|
|
43776
|
+
if (i > 0) {
|
|
43777
|
+
let drift;
|
|
43778
|
+
try {
|
|
43779
|
+
drift = await chunkDrift(cfg, byId, chunk);
|
|
43780
|
+
} catch (err) {
|
|
43781
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
43782
|
+
throw new Error(
|
|
43783
|
+
`re-read failed after ${applied} of ${items.length} account${plural(items.length)} were written; nothing more was sent: ${msg}
|
|
43784
|
+
Re-run the same command without --confirm-token: rows that landed plan as unchanged, so the new plan covers only the rest.`
|
|
43785
|
+
);
|
|
43786
|
+
}
|
|
43787
|
+
if (drift.length > 0) {
|
|
43788
|
+
throw new Error(
|
|
43789
|
+
`halted after ${applied} of ${items.length} account${plural(items.length)} were written; nothing more was sent:
|
|
43790
|
+
` + drift.join("\n ") + `
|
|
43791
|
+
The tenant changed since the plan. Re-run the same command without --confirm-token: rows that landed plan as unchanged. If accounts are gone, the trial balance was rebuilt and their ids changed \u2014 wait for it to settle and rebuild the file from \`dr reconciliation accounts list\`.`
|
|
43792
|
+
);
|
|
43793
|
+
}
|
|
43794
|
+
}
|
|
43391
43795
|
try {
|
|
43392
43796
|
await updateAccountMembers(cfg.client, chunk, { signal: cfg.signal });
|
|
43393
43797
|
} catch (err) {
|
|
@@ -43411,6 +43815,28 @@ Re-run the same command without --confirm-token: rows that landed plan as unchan
|
|
|
43411
43815
|
}
|
|
43412
43816
|
});
|
|
43413
43817
|
}
|
|
43818
|
+
async function chunkDrift(cfg, planned, chunk) {
|
|
43819
|
+
const now = await getAccounts(cfg.client, { date: cfg.date }, { signal: cfg.signal });
|
|
43820
|
+
const nowById = new Map(now.map((a) => [a.account_id, a]));
|
|
43821
|
+
const drift = [];
|
|
43822
|
+
for (const { account_id: id } of chunk) {
|
|
43823
|
+
const before = planned.get(id);
|
|
43824
|
+
const after = nowById.get(id);
|
|
43825
|
+
const label = `${before.account_name} (${id})`;
|
|
43826
|
+
if (after === void 0) {
|
|
43827
|
+
drift.push(`${label}: no longer at --date`);
|
|
43828
|
+
} else if (!sameMemberSlots(before.members, after.members)) {
|
|
43829
|
+
drift.push(`${label}: member slots changed since the plan`);
|
|
43830
|
+
}
|
|
43831
|
+
}
|
|
43832
|
+
return drift;
|
|
43833
|
+
}
|
|
43834
|
+
function sameMemberSlots(a, b) {
|
|
43835
|
+
const shape = (ms) => JSON.stringify(
|
|
43836
|
+
ms.map((m) => [m.type, m.user_id, m.due_date, m.is_sign_off, m.sign_off_timestamp]).sort()
|
|
43837
|
+
);
|
|
43838
|
+
return shape(a) === shape(b);
|
|
43839
|
+
}
|
|
43414
43840
|
function applyAssignments(account, changes) {
|
|
43415
43841
|
const byType = new Map(changes.map((c) => [c.row.type, c]));
|
|
43416
43842
|
const assigned = (type, dueDate) => {
|
|
@@ -43734,39 +44160,42 @@ function wireReconciliation(program) {
|
|
|
43734
44160
|
).requiredOption(
|
|
43735
44161
|
"--file <path>",
|
|
43736
44162
|
"JSON file with the array of settings (output of `dr reconciliation accounts settings --json`)"
|
|
43737
|
-
).option(
|
|
44163
|
+
).option("--timeout <seconds>", BATCH_TIMEOUT_FLAG_HELP).option(
|
|
43738
44164
|
"--confirm-token <token>",
|
|
43739
44165
|
"Token from a prior plan run \u2014 required to actually update",
|
|
43740
44166
|
""
|
|
43741
|
-
).action(
|
|
43742
|
-
|
|
43743
|
-
|
|
43744
|
-
|
|
43745
|
-
|
|
43746
|
-
|
|
43747
|
-
|
|
43748
|
-
|
|
43749
|
-
|
|
43750
|
-
|
|
43751
|
-
|
|
43752
|
-
|
|
43753
|
-
|
|
43754
|
-
|
|
43755
|
-
|
|
43756
|
-
|
|
43757
|
-
|
|
43758
|
-
|
|
43759
|
-
|
|
43760
|
-
|
|
43761
|
-
|
|
43762
|
-
|
|
43763
|
-
|
|
43764
|
-
|
|
43765
|
-
|
|
43766
|
-
|
|
43767
|
-
|
|
44167
|
+
).action(
|
|
44168
|
+
async (opts) => {
|
|
44169
|
+
const filePath = opts.file ?? "";
|
|
44170
|
+
const raw = await fs18.readFile(filePath, "utf8");
|
|
44171
|
+
const settings = parseReconciliationSettingsFile(raw);
|
|
44172
|
+
await preflightConfirmToken(
|
|
44173
|
+
{
|
|
44174
|
+
resource: "recon-settings",
|
|
44175
|
+
id: settingsActionId(settings),
|
|
44176
|
+
verb: "update"
|
|
44177
|
+
},
|
|
44178
|
+
opts.confirmToken
|
|
44179
|
+
);
|
|
44180
|
+
const serverKey = resolveServer(opts.server);
|
|
44181
|
+
const confirmCmd = `dr reconciliation accounts update-settings --file ${JSON.stringify(filePath)}` + (opts.timeout !== void 0 ? ` --timeout ${opts.timeout}` : "") + ` --server ${serverKey}`;
|
|
44182
|
+
const setup = await setupCall(opts.server, opts.account, batchSetupOptions(opts.timeout));
|
|
44183
|
+
try {
|
|
44184
|
+
await runReconciliationAccountsUpdateSettings({
|
|
44185
|
+
client: setup.client,
|
|
44186
|
+
extendTimeout: setup.extendTimeout,
|
|
44187
|
+
settings,
|
|
44188
|
+
token: opts.confirmToken,
|
|
44189
|
+
confirmCmd,
|
|
44190
|
+
serverFlag: serverKey,
|
|
44191
|
+
signal: setup.signal,
|
|
44192
|
+
out: process.stdout
|
|
44193
|
+
});
|
|
44194
|
+
} finally {
|
|
44195
|
+
setup.cleanup();
|
|
44196
|
+
}
|
|
43768
44197
|
}
|
|
43769
|
-
|
|
44198
|
+
);
|
|
43770
44199
|
addServerFlag(updateSettingsCmd);
|
|
43771
44200
|
addAccountFlag(updateSettingsCmd);
|
|
43772
44201
|
const setAssigneeCmd = accounts.command("set-assignee").summary(
|
|
@@ -58473,7 +58902,7 @@ var EXTRACT_POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
|
58473
58902
|
function isTerminalAiStatus(status2) {
|
|
58474
58903
|
return status2 === "PROCESSED" || status2 === "FAILED";
|
|
58475
58904
|
}
|
|
58476
|
-
async function
|
|
58905
|
+
async function defaultSleep9(ms, signal) {
|
|
58477
58906
|
if (signal?.aborted === true) return;
|
|
58478
58907
|
await new Promise((resolve3) => {
|
|
58479
58908
|
const timer = setTimeout(resolve3, ms);
|
|
@@ -58490,7 +58919,7 @@ async function defaultSleep8(ms, signal) {
|
|
|
58490
58919
|
async function pollExtractionStatus(client, contractFileIds, options = {}) {
|
|
58491
58920
|
const interval = options.pollIntervalMs ?? EXTRACT_POLL_INTERVAL_MS;
|
|
58492
58921
|
const timeout = options.timeoutMs ?? EXTRACT_POLL_TIMEOUT_MS;
|
|
58493
|
-
const sleep4 = options.sleep ??
|
|
58922
|
+
const sleep4 = options.sleep ?? defaultSleep9;
|
|
58494
58923
|
const now = options.now ?? Date.now;
|
|
58495
58924
|
const resolved = /* @__PURE__ */ new Map();
|
|
58496
58925
|
if (contractFileIds.length === 0) return { resolved, pending: [] };
|
|
@@ -60269,15 +60698,15 @@ async function extractAsync(cfg, parts) {
|
|
|
60269
60698
|
return { results, sourceFiles, outcomes: ordered2, abortedMidRun };
|
|
60270
60699
|
}
|
|
60271
60700
|
function describeUnresolvedExtraction(contractFileId, lastError, commandAborted) {
|
|
60272
|
-
const
|
|
60701
|
+
const ref2 = `contract_file_id ${contractFileId}`;
|
|
60273
60702
|
if (lastError !== void 0 && !isAbortError(lastError)) {
|
|
60274
60703
|
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
60275
|
-
return `extraction status could not be read (${
|
|
60704
|
+
return `extraction status could not be read (${ref2}): ${msg}. The upload succeeded \u2014 re-check the status rather than re-uploading.`;
|
|
60276
60705
|
}
|
|
60277
60706
|
if (commandAborted || lastError !== void 0 && isAbortError(lastError)) {
|
|
60278
|
-
return `the command budget expired before extraction finished (${
|
|
60707
|
+
return `the command budget expired before extraction finished (${ref2}). Raise --timeout, then re-check the status rather than re-uploading.`;
|
|
60279
60708
|
}
|
|
60280
|
-
return `still extracting when --poll-timeout expired (${
|
|
60709
|
+
return `still extracting when --poll-timeout expired (${ref2}). Raise --poll-timeout, then re-check rather than re-uploading.`;
|
|
60281
60710
|
}
|
|
60282
60711
|
async function extractSync(cfg, parts) {
|
|
60283
60712
|
const allResults = [];
|
package/dist/scopes-map.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"cliVersion": "1.6.
|
|
3
|
+
"cliVersion": "1.6.95",
|
|
4
4
|
"_contract": "Resolve a command path through `aliases` (canonical prefix + alias segment -> canonical path) before any lookup. A path listed in `pendingCommands`, or whose first segment is a key of `pending`, is UNDECIDED, not unrestricted: fail closed or skip it, and never read a path's absence from `commands` as 'no requirement'. An entry carrying `denied` is UNAVAILABLE to any AGENT-SCOPED credential — one whose `scopes` claim carries at least one `resource:action` entry; a claim that is absent, empty, capability-only or wholly unparseable is UNRESTRICTED, and this mark does not apply to it — whatever else that entry says: it is not a ceiling and it is not always-available, its `denied.reason` says why and `denied.until` names the ticket that lifts it, and the absence of `requires` on it must NEVER be read as 'no requirement'. Only an entry in `commands` with an empty `requires` means always-available.",
|
|
5
5
|
"commands": {
|
|
6
6
|
"login": {
|
|
@@ -60,7 +60,9 @@ parameter fails in `connect()` before the vendor is called.
|
|
|
60
60
|
synthetic. Per-customer values (columns, category or custom-field names) live only in thin files.
|
|
61
61
|
Before committing, grep the lib diff for the customer's name, domain and org id.
|
|
62
62
|
- **Reuse before adding.** Use the SDK kit first (`from dr_integrations_sdk import kit`: logger, query
|
|
63
|
-
vars, HTTP client, CSV writer, paging, auth, dates
|
|
63
|
+
vars, HTTP client, CSV writer, paging, auth, dates; on kit 1.1 also `config.is_unlimited`,
|
|
64
|
+
`http.json`, `http.same_origin`, `paging.gather_or_cancel`, `runtime.Deadline` and
|
|
65
|
+
`CsvWriter(block_bytes=)`), then `_core` helpers and an existing connection
|
|
64
66
|
that fits; never a second copy. Name modules after what they do, never after who uses them. Hand the
|
|
65
67
|
user a list of lib improvements you noticed (duplicated helpers, code that belongs in `_core`, a
|
|
66
68
|
hand-written copy of a kit helper), done in their own PR.
|
|
@@ -81,7 +83,9 @@ parameter fails in `connect()` before the vendor is called.
|
|
|
81
83
|
the header-once, one-block-per-page writer and takes `csv.writer` dialect kwargs
|
|
82
84
|
(`lineterminator="\n"`, `quoting=`), but writes each value as `csv.writer` does (`str()`, `None` as an
|
|
83
85
|
empty cell), so the engine still pre-formats every cell the way the landed file shows (`true`, not
|
|
84
|
-
`True`).
|
|
86
|
+
`True`). Kit 1.1's `CsvWriter(cell=kit.csv.cell)` is a generic formatter, not a parity one: use it
|
|
87
|
+
only when its output matches the landed file byte for byte, else pass the engine's own function as
|
|
88
|
+
`cell=`.
|
|
85
89
|
- **Credentials resolve at runtime from the datasource** (`get_credentials`), never from code, tests
|
|
86
90
|
or logs (`tech/security.md` §4). Check a migrated credential's shape without printing it; a stored
|
|
87
91
|
header value may carry its scheme (`Basic <base64>`).
|
|
@@ -194,22 +194,32 @@ dataquery that defines its own `_get_logger`, `_query_vars`, `_safe_snippet`, `_
|
|
|
194
194
|
call below before it is reviewed. A template gives you the structure; the logger, config, retry, CSV
|
|
195
195
|
and paging code comes from
|
|
196
196
|
`from dr_integrations_sdk import kit` (import the package, not `kit.csv`/`kit.logging` by name — they
|
|
197
|
-
would shadow the stdlib).
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
would shadow the stdlib). Every template is built on it (`sample_http_vendor`, `sample_streaming_fanout`,
|
|
198
|
+
`lib/sample_vendor`), so a copy starts with no hand-written helpers. Reference: dr-connectors-runner
|
|
199
|
+
`docs/connector-kit.md`.
|
|
200
200
|
|
|
201
201
|
| Need | Kit |
|
|
202
202
|
|---|---|
|
|
203
203
|
| a logger whose lines survive the sandbox | `kit.logging.get_logger("<vendor>__<dq>")` |
|
|
204
204
|
| the merged config channels, required variables | `kit.config.query_vars(req)`, `kit.config.require(vars, *names, context=)` (raises `PermanentError` naming each missing one) |
|
|
205
|
+
| *(1.1)* a cap variable that may mean "no cap" | `kit.config.is_unlimited(value)`: `None`, `""`, `0` and the string `"0"` all mean unlimited |
|
|
205
206
|
| vendor text in a log line or an error | `kit.redact.safe_snippet` / `log_safe` (redacts credentials); `one_line` for ids and cursors |
|
|
206
207
|
| HTTP with retry and 4xx classification | `kit.http.client(...)`, an `httpx.AsyncClient` (every httpx kwarg passes through): retries 429 (honouring `Retry-After`), 500/502/503/504 and transport errors; 401/403 raise `AuthError`, any other 4xx or an unfollowed redirect `PermanentError`; a 429/5xx still failing after the last attempt raises `httpx.HTTPStatusError` (runtime retry). `raise_for_status=False` hands 4xx back to you |
|
|
208
|
+
| *(1.1)* a JSON body you must not misread as an empty page | `kit.http.json(response, expect=list)`: `PermanentError` with a redacted snippet when the body is not JSON or not that type |
|
|
209
|
+
| *(1.1)* a vendor next link fetched with the credential | `kit.paging.cursor_pages(page, same_origin=base_url)` refuses a link to another scheme, host or port before fetching it; `kit.http.same_origin(url, base)` / `require_same_origin` for links you follow yourself; *(1.2)* `kit.http.client(base_url=..., same_origin_only=True)` refuses every request off that origin, redirect hops included |
|
|
207
210
|
| the CSV header once, one block per page | `kit.csv.CsvWriter(fields).blocks(req, pages)`; `.rows` counts rows written |
|
|
211
|
+
| *(1.1)* many small pages | `CsvWriter(fields, block_bytes=kit.csv.BLOCK_BYTES)` coalesces them into ~4 MB blocks, far under Azure's 50,000-block limit |
|
|
212
|
+
| *(1.1)* typed values as CSV cells | `CsvWriter(fields, cell=kit.csv.cell)`: bool `true`/`false`, `Decimal` without exponent, tz-aware datetime as naive UTC, bytes hex, dict/list compact JSON. Not on a Workato migration: there the cell shape comes from the landed file (see *create-connector-lib*) |
|
|
208
213
|
| paging that fails loudly | `kit.paging.cursor_pages` / `offset_pages`; `kit.paging.waves(items, worker, concurrency=)` |
|
|
214
|
+
| *(1.1)* a handful of concurrent vendor calls | `kit.paging.gather_or_cancel(awaitables)`: cancels the siblings on the first failure and re-raises it unwrapped. Never `asyncio.TaskGroup`: its `ExceptionGroup` turns a `PermanentError` into 5 retries |
|
|
215
|
+
| *(1.1)* the hook's time budget | `deadline = kit.runtime.Deadline("<vendor> <dq>")` in `fetch`: *(1.2)* it counts from the runner's own start of the hook and stops 60 s short of the timeout the runner kills at, watching CPU time too. `deadline.check(progress)` per page, `await deadline.sleep(s, progress)` for poll waits, and `kit.http.client(deadline=deadline)` for retry waits. Each raises `PermanentError` naming the progress |
|
|
209
216
|
| credentials and tokens | `kit.auth.credential(creds, *keys, vendor=)`, `client_credentials` / `password_grant` → `Token.header()`, `basic_header`, `bearer_header` |
|
|
210
217
|
| a date window | `kit.dates.date_window(vars, start_keys=, end_keys=, required=)`, `DateWindow.chunks(days)`, `parse_date` |
|
|
211
218
|
|
|
212
|
-
**
|
|
219
|
+
**Kit 1.2 is live in every production ring** (runner image `connectors-runner-base` v1.35.0, since
|
|
220
|
+
2026-10-02), so every row above is usable there and the kit is never a reason to fall back to copied
|
|
221
|
+
helpers. The *(1.1)* / *(1.2)* marks say which kit version a row needs, for an environment whose runner
|
|
222
|
+
image is older.
|
|
213
223
|
At runtime the kit comes from the **runner image**, not from `.sdk-ref` (that pins only what CI tests
|
|
214
224
|
against). An image with the kit prints
|
|
215
225
|
`INFO:dr.sdk: hook=… correlation_id=… kit=<version>` as each hook's first log line. On a dev or test
|
|
@@ -309,9 +319,10 @@ Two more sandbox ceilings worth knowing, both undocumented elsewhere:
|
|
|
309
319
|
ThreadPoolExecutor **never reaps its threads** — so an unbounded pool can claim ~256 MB of the
|
|
310
320
|
512 MB and never give it back. That is why a brief concurrency spike used to poison every *later*
|
|
311
321
|
phase of a run with `RuntimeError: can't start new thread` and SSL allocation failures long after
|
|
312
|
-
the spike finished.
|
|
313
|
-
`
|
|
314
|
-
|
|
322
|
+
the spike finished. **The runner now caps it at 8 threads** before every hook
|
|
323
|
+
(`sandbox_entry.py` `SANDBOX_EXECUTOR_THREADS`, DR-53651), so a connector no longer installs its own
|
|
324
|
+
`set_default_executor(...)`; remove one when you touch the file.
|
|
325
|
+
- **`RLIMIT_CPU = 2700s`** sits right next to the memory limit in `sandbox_entry.py`. Exceeding it
|
|
315
326
|
SIGKILLs the hook (runner reports `sandbox exited -24`) with no `MemoryError` to explain it. Async
|
|
316
327
|
I/O waiting is free, but TLS handshakes, gzip and JSON parsing are not — log
|
|
317
328
|
`time.process_time()` alongside wall clock so you can see how close you are.
|
|
@@ -871,8 +882,8 @@ unmerged connector has no version to ingest, bind, or run.
|
|
|
871
882
|
| Confused about publish vs ingest | CI publishes to JFrog on merge (cuts the git tag `vN`); the CLI ingests a published version into a DC | `dr connectors publish <path/to/dataqueries/<dq>.py> --server DEV --account <customer-user-email> --json` reads the newest git tag (`git fetch --tags` + max `vN`, or `--tag vN`) — **no local compose** — then the control-plane **pulls that tag from JFrog** (no upload) and registers it (needs `NODE_EXTRA_CA_CERTS`); returns `{name, version: "vN", digest}`. |
|
|
872
883
|
| Run `Failed`, `connect` completion `verdict: ok` but `fetch` completion `verdict: retry`, error `'async for' requires an object with __aiter__ method, got function` | Passed the async-generator **function** to `write_output` — `write_output(req, gen)` instead of **calling** it `write_output(req, gen())`. `write_output` does `async for chunk in <arg>`, which needs an async **iterator**, not a function. (The connect gate passing but fetch dying instantly is the tell.) | Call the generator: `write_output(req, gen())`. The golden template already shows `write_output(req, rows())` / `write_output(req, blocks())` — match it exactly; the missing `()` is easy to drop. |
|
|
873
884
|
| Vendor **4xx** (e.g. `402 Payment Required`, `401`, `403`, bad-symbol `404`) makes the `fetch` completion `verdict: retry` and the run **retries forever**; the HTTP status shows ONLY in the completion store, never in the Inngest run `output` (which is `null`). | A bare `r.raise_for_status()` raises `httpx.HTTPStatusError` for **every** status ≥ 400, and the runtime classifies that as **transient → retry**. But a plan/auth/bad-request 4xx will never succeed on retry. | Use `kit.http.client` (skill §1): 401/403 raise `AuthError` and any other 4xx `PermanentError`, each naming the status and a redacted body; 429 and 5xx are retried, then left to the runtime's retry (genuinely transient). Concrete case: FMP's free tier answers **402** when `limit` exceeds its historical allowance (dev key caps statements at the last **5** annual records) — a small `limit` or a paid key fixes that one; the PermanentError stops the infinite loop for the general case. The golden `sample_http_vendor` template is built on this client. |
|
|
874
|
-
| Connector works perfectly in local/dev testing, then in production `fetch` dies with `MemoryError`, `MemoryError: Unable to allocate output buffer`, an SSL/crypto allocation failure, or `RuntimeError: can't start new thread` — often within a second of "fetch start", and deterministically on every one of the 5 attempts | The sandbox caps **virtual address space at `RLIMIT_AS` = 512 MB** (`dr-connectors-runner/src/runner/sandbox_entry.py`), and the runner pod's limit is also 512Mi. The guard is `if sys.platform != "win32"`, so **the ceiling does not exist on a Windows dev box** — a buffering connector cannot fail locally. Two causes, usually together: (a) the fetch accumulates the result set (a `rows` list, `asyncio.gather` over every parent, a full CSV string + its `.encode()` copy) — journal_lines measured **378 MB live to emit 67 MB**, 243 MB of it row dicts at 846 B/row; (b) asyncio's **default** DNS ThreadPoolExecutor grew to `min(32, node_cores + 4)` threads × 8 MB stack ≈ **256 MB of address space that is never reclaimed**, which is why a brief concurrency spike poisons every later phase of the run. | Stream: do the fetching **inside** the generator passed to `write_output`, replace gather-over-everything with a bounded worker pool feeding a bounded `asyncio.Queue`, format rows to CSV bytes immediately (never hold dicts), and re-batch to ~4 MB blocks.
|
|
875
|
-
| Hook dies with no traceback and no `MemoryError`; runner reports `sandbox exited -24` | **`RLIMIT_CPU =
|
|
885
|
+
| Connector works perfectly in local/dev testing, then in production `fetch` dies with `MemoryError`, `MemoryError: Unable to allocate output buffer`, an SSL/crypto allocation failure, or `RuntimeError: can't start new thread` — often within a second of "fetch start", and deterministically on every one of the 5 attempts | The sandbox caps **virtual address space at `RLIMIT_AS` = 512 MB** (`dr-connectors-runner/src/runner/sandbox_entry.py`), and the runner pod's limit is also 512Mi. The guard is `if sys.platform != "win32"`, so **the ceiling does not exist on a Windows dev box** — a buffering connector cannot fail locally. Two causes, usually together: (a) the fetch accumulates the result set (a `rows` list, `asyncio.gather` over every parent, a full CSV string + its `.encode()` copy) — journal_lines measured **378 MB live to emit 67 MB**, 243 MB of it row dicts at 846 B/row; (b) asyncio's **default** DNS ThreadPoolExecutor grew to `min(32, node_cores + 4)` threads × 8 MB stack ≈ **256 MB of address space that is never reclaimed**, which is why a brief concurrency spike poisons every later phase of the run. | Stream: do the fetching **inside** the generator passed to `write_output`, replace gather-over-everything with a bounded worker pool feeding a bounded `asyncio.Queue`, format rows to CSV bytes immediately (never hold dicts), and re-batch to ~4 MB blocks (`kit.csv.CsvWriter(..., block_bytes=kit.csv.BLOCK_BYTES)` on kit 1.1). The executor cap (b) is now set by the runner (8 threads); a connector installs none. Full rules in §1 *Memory*; working template `samples/sample_streaming_fanout/`. Reproduce locally in `python:3.12-slim` under the same rlimits — never trust a Windows run for this. |
|
|
886
|
+
| Hook dies with no traceback and no `MemoryError`; runner reports `sandbox exited -24` | **`RLIMIT_CPU = 2700s`** (`sandbox_entry.py`, right beside the memory limit) — 2700 seconds of **CPU** time, summed over all threads, then SIGXCPU/SIGKILL. Async I/O waiting is free, but TLS handshakes, gzip and JSON parsing across thousands of requests are not. | Cut CPU per row (fewer/larger vendor pages, `onlyData` where the vendor supports it, no per-row dict churn) and log `time.process_time()` next to wall clock so proximity to the cap is visible before it kills a run. |
|
|
876
887
|
| A sync `Completed` and the file **looks** complete, but the customer's totals are short | A page-limit "safety cap" (`_LINE_PAGE_LIMIT`, `_HEADER_PAGE_LIMIT`, …) tripped and the code **logged a warning and returned what it had**, so a partial result landed as if whole. Same failure class as a missing offset follow-up: journal_lines silently dropped **113,171 rows** of one customer's month this way. | Caps must `raise PermanentError`, never warn-and-return — a failed sync is recoverable, a silently partial landing gets reconciled against and believed. Audit every `log.warning` that is followed by `return`. |
|
|
877
888
|
|
|
878
889
|
## Env quick reference (dev germany)
|
|
@@ -611,7 +611,10 @@ network traffic or hand-edits JSON. Full recipes in the relevant reference file.
|
|
|
611
611
|
date to whatever granularity the filebox's own DATE tag actually uses —
|
|
612
612
|
no need to know if it's yearly/quarterly/monthly/daily/weekly. `--tag` is
|
|
613
613
|
an unvalidated escape hatch that forwards straight to the server; a
|
|
614
|
-
malformed value 500s with no error body.
|
|
614
|
+
malformed value 500s with no error body. To know when it finished, add
|
|
615
|
+
`--wait` — never poll `status` or the version count: an unchanged result
|
|
616
|
+
writes no new version and still reads `SYNCED`. See
|
|
617
|
+
reference/filebox-documents.md.
|
|
615
618
|
- **"User has no table permissions" is almost always a misread.** To see which
|
|
616
619
|
tables a user can access, use `dr users permissions get <id>` — never the raw
|
|
617
620
|
`GET /api/it/users/<id>` object's `permissions` field. That field is Django
|
|
@@ -169,8 +169,12 @@ set-version-include` (below).
|
|
|
169
169
|
only allow-listed keys are accepted (currently `is_bank_integration`); unknown
|
|
170
170
|
keys are rejected client-side (the server silently accepts dead keys).
|
|
171
171
|
- `filebox data-query get|set|clear|sync <fb_id>` — read/write/delete/poke the
|
|
172
|
-
Workato data-query. `get` shows `sync_interval` (cron)
|
|
173
|
-
|
|
172
|
+
Workato data-query. `get` shows `sync_interval` (cron), `last_sync` and
|
|
173
|
+
`Last start` (when the latest sync started); `set --payload-file` PUTs a
|
|
174
|
+
create/update body; `clear` is two-step. `last_sync` doubles as the
|
|
175
|
+
auto-sync debounce, so it can read minutes in the future. Older servers do
|
|
176
|
+
not return either timestamp: `last_sync` then reads `never` and `Last start`
|
|
177
|
+
is omitted.
|
|
174
178
|
- `filebox data-query sync <fb_id> [--date YYYY-MM-DD]` — trigger an async
|
|
175
179
|
sync, optionally scoped to specific period(s). `--date` is repeatable and
|
|
176
180
|
takes one plain calendar date — the CLI reads the filebox's own DATE
|
|
@@ -195,6 +199,32 @@ set-version-include` (below).
|
|
|
195
199
|
constant — varies per tenant) and the epoch just needs to fall within the
|
|
196
200
|
target period (the server normalizes it). Prefer `--date`; reach for raw
|
|
197
201
|
`--tag` only when it doesn't cover your case.
|
|
202
|
+
- **Waiting for a sync: `filebox data-query sync <fb_id> --date … --wait`.**
|
|
203
|
+
Never poll `status` or the version count yourself — both lie:
|
|
204
|
+
- With `sync_only_changes` (the default), a sync whose output is identical
|
|
205
|
+
to the stored latest version for that period writes **no new version**.
|
|
206
|
+
`status` still goes to `SYNCED`, and the version count never moves. This
|
|
207
|
+
is not an error.
|
|
208
|
+
- Right after a sync is queued, `status` can still show the previous run's
|
|
209
|
+
value (or `-`) until the sync starts. A loop that waits for "not
|
|
210
|
+
SYNCING" exits at once.
|
|
211
|
+
- A multi-period sync flips `status` to `SYNCED` after each period, so
|
|
212
|
+
"status is SYNCED" can mean one period of several.
|
|
213
|
+
- A premature exit followed by `filebox download` silently returns the
|
|
214
|
+
**previous** version.
|
|
215
|
+
|
|
216
|
+
`--wait` snapshots each `--date` period's versions before triggering, then
|
|
217
|
+
polls until every period either gained a new version (**new version**) or
|
|
218
|
+
had its latest version re-confirmed — the server bumps that version's
|
|
219
|
+
`last_sync` instead of writing one (**unchanged**). It exits non-zero when
|
|
220
|
+
the data-query goes to `ERROR` (with the server's `last_sync_error`) or on
|
|
221
|
+
`--timeout <seconds>` (default 1800). `--json` prints one object:
|
|
222
|
+
`{outcome, periods: [{period, result: new_version|no_change|pending,
|
|
223
|
+
version_id, version}], unmatched_new_versions, error, waited_seconds}`.
|
|
224
|
+
Without `--date`, it waits for the first period to finish. It cannot be
|
|
225
|
+
combined with `--tag`. One blind spot: a sync that returns **no rows**
|
|
226
|
+
writes no version and re-confirms none, so it ends in a timeout whose
|
|
227
|
+
message says so — check `dr filebox data-query get <fb_id>`.
|
|
198
228
|
- `filebox data-query set-sync-interval <fb_id> --cron "<expr>"` — change how
|
|
199
229
|
often the data-query polls (Workato-backed **and** connector-platform /
|
|
200
230
|
`DatarailsConnector` fileboxes both use this cron). **Use this for sync
|
|
@@ -22,7 +22,14 @@ Account assignment / sign-off (all take the bank-account UUID via
|
|
|
22
22
|
|
|
23
23
|
- `reconciliation accounts update-settings --file <p>` — admin-only PATCH.
|
|
24
24
|
Two-step confirm; the token binds to a hash of the file's rows, so editing
|
|
25
|
-
the file between plan and confirm invalidates it.
|
|
25
|
+
the file between plan and confirm invalidates it. Rows are matched to the
|
|
26
|
+
tenant by `(entity, account_group, name)`; only rows that differ are sent,
|
|
27
|
+
in PATCHes of at most 256 KB (one whole-tenant PATCH near 1 MB returns
|
|
28
|
+
HTTP 500). The plan reports rows to change, rows skipped as already
|
|
29
|
+
matching, and the changes by target frequency (`Exclude` is how an account
|
|
30
|
+
is excluded from reconciliation). If a PATCH fails, the error says how many
|
|
31
|
+
rows landed; re-run the same file without a token and the new plan covers
|
|
32
|
+
only the rest.
|
|
26
33
|
- `reconciliation accounts set-assignee <account_uuid> --date <YYYY-MM-DD>
|
|
27
34
|
--type Preparer|Reviewer --user-id <n> [--user-name <s> --user-email <s>]` —
|
|
28
35
|
resets any prior sign-off; name/email are auto-fetched if not supplied.
|
|
@@ -38,7 +45,12 @@ Account assignment / sign-off (all take the bank-account UUID via
|
|
|
38
45
|
resets. Every account is checked before anything is written, and the
|
|
39
46
|
write goes out in PATCHes of 100 accounts. If one fails, the error says
|
|
40
47
|
how many accounts landed; re-run the same file without a token and the
|
|
41
|
-
new plan covers only the rest.
|
|
48
|
+
new plan covers only the rest. Before each PATCH after the first, the
|
|
49
|
+
accounts are re-read; if any account in the next chunk is gone (a
|
|
50
|
+
trial-balance rebuild re-mints account ids) or its member slots changed
|
|
51
|
+
since the plan, the run halts, says how many accounts landed, and names
|
|
52
|
+
the drifted accounts. Gone accounts mean the file must be rebuilt from
|
|
53
|
+
`accounts list` once the rebuild settles. Do not script a loop of single
|
|
42
54
|
`set-assignee` calls for this.
|
|
43
55
|
- `reconciliation accounts clear-assignee <account_uuid> --date <YYYY-MM-DD>
|
|
44
56
|
--type Preparer|Reviewer` — unassigns (preserves due_date, resets sign-off).
|