@datarails-ai/dr-cli 1.6.106 → 1.6.108

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 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.106",
11
+ version: "1.6.108",
12
12
  description: "Datarails CLI",
13
13
  type: "module",
14
14
  engines: {
@@ -40,7 +40,7 @@ var package_default = {
40
40
  postinstall: "node scripts/postinstall.mjs"
41
41
  },
42
42
  dependencies: {
43
- "@datarails-ai/dr-appspace-cli": "0.11.3",
43
+ "@datarails-ai/dr-appspace-cli": "0.11.4",
44
44
  "@modelcontextprotocol/sdk": "^1.29.0",
45
45
  "@napi-rs/keyring": "^1.3.0",
46
46
  commander: "^12.1.0",
@@ -69,7 +69,7 @@ var package_default = {
69
69
  };
70
70
 
71
71
  // src/buildInfo.ts
72
- var BUILD_TIME = "2026-10-07T14:49:41.993Z";
72
+ var BUILD_TIME = "2026-10-07T17:12:24.139Z";
73
73
  function formatBuildTime(iso) {
74
74
  if (iso === "") return "";
75
75
  const d = new Date(iso);
@@ -5338,7 +5338,7 @@ async function poll(host, path11, deviceCode, options, terminalHints, onSuccess)
5338
5338
  const interval = options.interval ?? 2e3;
5339
5339
  const maxWait = options.maxWait ?? 6e5;
5340
5340
  const now = options.now ?? Date.now;
5341
- const sleep4 = options.sleep ?? defaultSleep;
5341
+ const sleep3 = options.sleep ?? defaultSleep;
5342
5342
  const signal = options.signal;
5343
5343
  const base = host.replace(/\/+$/, "");
5344
5344
  const deadline = now() + maxWait;
@@ -5366,7 +5366,7 @@ async function poll(host, path11, deviceCode, options, terminalHints, onSuccess)
5366
5366
  }
5367
5367
  const errCode = tryParseErrorCode(text2);
5368
5368
  if (errCode === "authorization_pending" || errCode === "slow_down") {
5369
- await sleep4(interval, signal);
5369
+ await sleep3(interval, signal);
5370
5370
  continue;
5371
5371
  }
5372
5372
  if (errCode) {
@@ -12284,7 +12284,7 @@ var defaultSleep2 = (ms, signal) => new Promise((resolve3, reject) => {
12284
12284
  async function pollTask(client, label, path11, options = {}) {
12285
12285
  const interval = options.pollIntervalMs ?? AGGREGATE_POLL_INTERVAL_MS;
12286
12286
  const timeout = options.timeoutMs ?? AGGREGATE_POLL_TIMEOUT_MS;
12287
- const sleep4 = options.sleep ?? defaultSleep2;
12287
+ const sleep3 = options.sleep ?? defaultSleep2;
12288
12288
  const now = options.now ?? Date.now;
12289
12289
  const inner = new AbortController();
12290
12290
  const onOuterAbort = () => inner.abort(options.signal?.reason);
@@ -12326,7 +12326,7 @@ async function pollTask(client, label, path11, options = {}) {
12326
12326
  return result.env;
12327
12327
  }
12328
12328
  try {
12329
- await sleep4(interval, inner.signal);
12329
+ await sleep3(interval, inner.signal);
12330
12330
  } catch {
12331
12331
  const reason = inner.signal.reason instanceof Error ? inner.signal.reason.message : String(inner.signal.reason ?? "aborted");
12332
12332
  throw new Error(`${label} polling timed out: ${reason}`);
@@ -15542,7 +15542,7 @@ var abortableSleep = (ms, signal) => new Promise((resolve3, reject) => {
15542
15542
  });
15543
15543
  async function putWithCacheKeyPolling(client, path11, payload, options = {}) {
15544
15544
  const interval = options.pollIntervalMs ?? CACHE_KEY_POLL_INTERVAL_MS;
15545
- const sleep4 = options.sleep ?? abortableSleep;
15545
+ const sleep3 = options.sleep ?? abortableSleep;
15546
15546
  const label = options.label ?? "data";
15547
15547
  const body = JSON.stringify(payload);
15548
15548
  const first = await putOnce(client, path11, body, options);
@@ -15558,7 +15558,7 @@ async function putWithCacheKeyPolling(client, path11, payload, options = {}) {
15558
15558
  const pollPath = withCacheKey(path11, cacheKey);
15559
15559
  for (; ; ) {
15560
15560
  try {
15561
- await sleep4(interval, options.signal);
15561
+ await sleep3(interval, options.signal);
15562
15562
  } catch (err) {
15563
15563
  const reason = err instanceof Error ? err.message : String(err);
15564
15564
  throw new Error(`${label} polling timed out: ${reason}`);
@@ -15747,7 +15747,7 @@ function parseDrilldown(body) {
15747
15747
  }
15748
15748
  async function exportDrilldownCsv(client, id, body, options = {}) {
15749
15749
  const interval = options.pollIntervalMs ?? CACHE_KEY_POLL_INTERVAL_MS;
15750
- const sleep4 = options.sleep ?? abortableSleep;
15750
+ const sleep3 = options.sleep ?? abortableSleep;
15751
15751
  const path11 = `/api/functions/${id}/drilldown/export_to_csv`;
15752
15752
  const payload = JSON.stringify(body);
15753
15753
  let res = await putOnce(client, path11, payload, options);
@@ -15758,7 +15758,7 @@ async function exportDrilldownCsv(client, id, body, options = {}) {
15758
15758
  }
15759
15759
  for (; ; ) {
15760
15760
  try {
15761
- await sleep4(interval, options.signal);
15761
+ await sleep3(interval, options.signal);
15762
15762
  } catch (err) {
15763
15763
  const reason = err instanceof Error ? err.message : String(err);
15764
15764
  throw new Error(`drill-down CSV export polling timed out: ${reason}`);
@@ -18210,7 +18210,7 @@ var LUT_SAVE_POLL_TIMEOUT_MS = 3e5;
18210
18210
  async function saveMappings(client, documentId, body, options = {}) {
18211
18211
  const interval = options.pollIntervalMs ?? LUT_SAVE_POLL_INTERVAL_MS;
18212
18212
  const timeout = options.timeoutMs ?? LUT_SAVE_POLL_TIMEOUT_MS;
18213
- const sleep4 = options.sleep ?? defaultPollSleep;
18213
+ const sleep3 = options.sleep ?? defaultPollSleep;
18214
18214
  const now = options.now ?? Date.now;
18215
18215
  const deadline = now() + timeout;
18216
18216
  const path11 = `/api/documents/${documentId}/web_update_lut`;
@@ -18232,7 +18232,7 @@ async function saveMappings(client, documentId, body, options = {}) {
18232
18232
  `lut save-mappings: the server is still writing after ${Math.round(timeout / 1e3)}s (task ${cacheKey}). The write is still running server-side \u2014 re-run the same command to rejoin it, or raise --timeout.`
18233
18233
  );
18234
18234
  }
18235
- await sleep4(interval, options.signal);
18235
+ await sleep3(interval, options.signal);
18236
18236
  const tick = await postOnce(client, pollPath, payload, options.signal);
18237
18237
  if (tick.status === 200) {
18238
18238
  return toResult(tick.text);
@@ -18530,7 +18530,7 @@ async function exportDocument(client, id, destinationOrgIds, override, reqOption
18530
18530
  async function pollDocumentJob(client, basePath, label, requestId, options = {}) {
18531
18531
  const interval = options.pollIntervalMs ?? AUTO_CALC_POLL_INTERVAL_MS;
18532
18532
  const timeout = options.timeoutMs ?? AUTO_CALC_POLL_TIMEOUT_MS;
18533
- const sleep4 = options.sleep ?? defaultSleep3;
18533
+ const sleep3 = options.sleep ?? defaultSleep3;
18534
18534
  const now = options.now ?? Date.now;
18535
18535
  const deadline = now() + timeout;
18536
18536
  for (; ; ) {
@@ -18558,7 +18558,7 @@ async function pollDocumentJob(client, basePath, label, requestId, options = {})
18558
18558
  `${label} polling timed out after ${timeout}ms (request ${requestId} still processing)`
18559
18559
  );
18560
18560
  }
18561
- await sleep4(interval, options.signal);
18561
+ await sleep3(interval, options.signal);
18562
18562
  }
18563
18563
  }
18564
18564
 
@@ -21783,7 +21783,7 @@ async function waitForReAggregation(cfg) {
21783
21783
  let interval = cfg.pollIntervalMs ?? REAGG_POLL_INTERVAL_MS;
21784
21784
  const maxInterval = cfg.maxPollIntervalMs ?? REAGG_POLL_MAX_INTERVAL_MS;
21785
21785
  const timeout = cfg.timeoutMs ?? REAGG_POLL_TIMEOUT_MS;
21786
- const sleep4 = cfg.sleep ?? defaultSleep4;
21786
+ const sleep3 = cfg.sleep ?? defaultSleep4;
21787
21787
  const now = cfg.now ?? Date.now;
21788
21788
  const accepted = [...cfg.accepted];
21789
21789
  const observedProcessing = /* @__PURE__ */ new Set();
@@ -21818,7 +21818,7 @@ async function waitForReAggregation(cfg) {
21818
21818
  break;
21819
21819
  }
21820
21820
  try {
21821
- await sleep4(interval, cfg.signal);
21821
+ await sleep3(interval, cfg.signal);
21822
21822
  } catch {
21823
21823
  aborted = true;
21824
21824
  break;
@@ -24425,7 +24425,7 @@ async function waitForSuggestionRun(r, cfg, runId) {
24425
24425
  let interval = cfg.pollIntervalMs ?? SUGGESTIONS_POLL_INTERVAL_MS;
24426
24426
  const maxInterval = cfg.maxPollIntervalMs ?? SUGGESTIONS_POLL_MAX_INTERVAL_MS;
24427
24427
  const timeout = cfg.timeoutMs ?? SUGGESTIONS_POLL_TIMEOUT_MS;
24428
- const sleep4 = cfg.sleep ?? defaultSleep5;
24428
+ const sleep3 = cfg.sleep ?? defaultSleep5;
24429
24429
  const now = cfg.now ?? Date.now;
24430
24430
  const errOut = cfg.errOut ?? process.stderr;
24431
24431
  const start = now();
@@ -24455,7 +24455,7 @@ async function waitForSuggestionRun(r, cfg, runId) {
24455
24455
  break;
24456
24456
  }
24457
24457
  try {
24458
- await sleep4(interval, cfg.signal);
24458
+ await sleep3(interval, cfg.signal);
24459
24459
  } catch {
24460
24460
  aborted = true;
24461
24461
  break;
@@ -30451,9 +30451,6 @@ function registerSkillCommand(root) {
30451
30451
  });
30452
30452
  }
30453
30453
 
30454
- // src/commands/datamappers.ts
30455
- import { setTimeout as sleep3 } from "timers/promises";
30456
-
30457
30454
  // src/api/datamappers.ts
30458
30455
  async function list20(client, tableId, options) {
30459
30456
  const out = await client.getJSON(`/api/templates/${tableId}/mappers`, options);
@@ -30487,14 +30484,6 @@ async function create12(client, tableId, body, options) {
30487
30484
  }
30488
30485
  return out;
30489
30486
  }
30490
- async function update8(client, id, body, options) {
30491
- const path11 = `/api/templates/mappers/${id}`;
30492
- const out = await client.putJSON(path11, body, options);
30493
- if (out === null) {
30494
- throw new Error(`PUT ${path11}: empty response from datamapper update`);
30495
- }
30496
- return out;
30497
- }
30498
30487
  async function deleteById12(client, tableId, id, options) {
30499
30488
  await client.deleteJSON(`/api/templates/${tableId}/mappers/${id}`, options);
30500
30489
  }
@@ -31218,34 +31207,69 @@ async function runDatamappersUpdate(cfg) {
31218
31207
  "merge"
31219
31208
  );
31220
31209
  }
31210
+ const err = cfg.err ?? process.stderr;
31221
31211
  const { decoded: decodedBody, changeCount } = decodeBodyEntitiesDeep(body);
31222
31212
  if (changeCount > 0) {
31223
- process.stderr.write(
31224
- `note: decoded ${changeCount} HTML entit${changeCount === 1 ? "y" : "ies"} (&quot; \u2192 ", &amp; \u2192 &, &lt; \u2192 <, &gt; \u2192 >) in the request body \u2014 mirrors the legacy webclient's ajaxService.success post-processing; the server's GET response carries the entities and the PUT-side parser doesn't decode them.
31213
+ err.write(
31214
+ `note: decoded ${changeCount} HTML entit${changeCount === 1 ? "y" : "ies"} (&quot; \u2192 ", &amp; \u2192 &, &lt; \u2192 <, &gt; \u2192 >) in the request body \u2014 mirrors the legacy webclient's ajaxService.success post-processing; the server's GET response carries the entities and the mapper compiler doesn't decode them.
31225
31215
  `
31226
31216
  );
31227
31217
  body = decodedBody;
31228
31218
  }
31229
31219
  validateCalcFields(extractCalcFields(body));
31230
- const err = cfg.err ?? process.stderr;
31231
- const scanBaseline = rescanValue === "latest" && cfg.verifyScanDispatch !== void 0 ? await captureScanBaseline(cfg.client, cfg.id, cfg.signal, err) : void 0;
31232
- let updated;
31220
+ const details = await getDetails2(cfg.client, cfg.id, { signal: cfg.signal });
31221
+ const templateId = details.template.id;
31222
+ const payloadVersion = body["document_version_id"];
31223
+ if (payloadVersion != null && !(Number.isInteger(payloadVersion) && payloadVersion > 0)) {
31224
+ throw new Error(
31225
+ `payload document_version_id must be a positive integer, got ${JSON.stringify(payloadVersion)}`
31226
+ );
31227
+ }
31228
+ const versionId = payloadVersion ?? details.from_version;
31229
+ if (typeof versionId !== "number") {
31230
+ throw new Error(
31231
+ `mapper ${cfg.id} has no source document version (typical after an org import or template push). Add "document_version_id" to the payload: an ACTIVE version of one of its documents (\`dr filebox versions <filebox_id>\`), with a sheet_name that exists in it`
31232
+ );
31233
+ }
31234
+ const current = await get14(cfg.client, templateId, cfg.id, { signal: cfg.signal });
31235
+ for (const key of ["id", "mapped_documents_ids", "previous_version", "mapping_date"]) {
31236
+ delete body[key];
31237
+ }
31238
+ const boundDocs = current.mapped_documents_ids ?? [];
31239
+ const payloadDocs = body["selected_document_ids"];
31240
+ const hasPayloadDocs = Array.isArray(payloadDocs) && payloadDocs.length > 0;
31241
+ if (boundDocs.length === 0 && !hasPayloadDocs) {
31242
+ throw new Error(
31243
+ `mapper ${cfg.id} is bound to no documents, so it is most likely an older version that an update or a UI publish replaced. Find the current one with \`dr datamappers list --table ${templateId}\`, or name the documents in the payload with "selected_document_ids"`
31244
+ );
31245
+ }
31246
+ body = {
31247
+ // GET returns the name HTML-escaped; sent back as is, it would be stored escaped twice.
31248
+ name: decodeHtmlEntitiesDeep(details.name),
31249
+ ordinal: current.ordinal,
31250
+ ...body,
31251
+ // Pinned after the spread: a null version or an empty doc list in the
31252
+ // payload would otherwise publish a version bound to nothing.
31253
+ document_version_id: versionId,
31254
+ selected_document_ids: hasPayloadDocs ? payloadDocs : boundDocs,
31255
+ datamapper_id: cfg.id
31256
+ };
31257
+ const publishPath = `/api/templates/${templateId}/mappers`;
31258
+ let published;
31233
31259
  try {
31234
- updated = await update8(cfg.client, cfg.id, body, {
31235
- signal: cfg.signal
31236
- });
31260
+ published = await create12(cfg.client, templateId, body, { signal: cfg.signal });
31237
31261
  } catch (err2) {
31238
31262
  const msg = err2 instanceof Error ? err2.message : String(err2);
31239
31263
  if (/HTTP 500/.test(msg)) {
31240
- const hint = "hint: the server logs an `ETLError: Custom Columns: The creation of '<field>' field has failed due to incorrect formula syntax.` line for this case but the response body is a bare 500 HTML page. Common causes the client-side guard cannot detect: (1) a [field] reference in a calc formula that does not resolve to any header `map_to`, calc-field `name`, or system field (`[FileBox_ID]`, `[System_Year]`, `[Posting Date]`, \u2026), (2) an unsupported function name (try copying the function spelling from another working calc field via `dr datamappers get`), (3) a string literal that's a plain-English placeholder (e.g. `\"Customer or Client name\"`) which parses but the ETL refuses. Iterate on calc-field formulas one at a time. If every formula looks correct and PUT still 500s, fall back to the workaround: `dr datamappers create from-file <table> --file <new.json>` then `dr datamappers delete <old_id> --table <table>` (note: this orphans references \u2014 LUTs/dashboards/functions pointing at the old mapper id break).";
31264
+ const hint = "hint: the server logs an `ETLError: Custom Columns: The creation of '<field>' field has failed due to incorrect formula syntax.` line for this case but the response body is a bare 500 HTML page. Common causes the client-side guard cannot detect: (1) a [field] reference in a calc formula that does not resolve to any header `map_to`, calc-field `name`, or system field (`[FileBox_ID]`, `[System_Year]`, `[Posting Date]`, \u2026), (2) an unsupported function name (try copying the function spelling from another working calc field via `dr datamappers get`), (3) a string literal that's a plain-English placeholder (e.g. `\"Customer or Client name\"`) which parses but the ETL refuses. Iterate on calc-field formulas one at a time.";
31241
31265
  const responseBody = extractResponseBodyFromError(msg);
31242
31266
  let logPath;
31243
31267
  try {
31244
31268
  logPath = await writeErrorLog({
31245
31269
  command: `dr datamappers update ${cfg.id} --payload-file ${payloadFile}`,
31246
31270
  server: cfg.serverFlag ?? "",
31247
- method: "PUT",
31248
- url: `/api/templates/mappers/${cfg.id}`,
31271
+ method: "POST",
31272
+ url: publishPath,
31249
31273
  status: 500,
31250
31274
  requestBody: body,
31251
31275
  responseBody,
@@ -31264,103 +31288,26 @@ ${hint}${logSuffix}`);
31264
31288
  throw err2;
31265
31289
  }
31266
31290
  if (cfg.asJson) {
31267
- writeJSONIndented(cfg.out, updated);
31291
+ writeJSONIndented(cfg.out, published);
31268
31292
  } else {
31269
- renderMappersTable(cfg.out, [updated]);
31270
- }
31271
- if (scanBaseline !== void 0) {
31272
- const opts = cfg.verifyScanDispatch ?? {};
31273
- const started2 = await waitForNewScan(cfg.client, scanBaseline, cfg.signal, opts);
31274
- if (!started2) {
31275
- const docIds = await mappedDocumentIds(
31276
- cfg.client,
31277
- scanBaseline.templateId,
31278
- cfg.id,
31279
- cfg.signal
31280
- );
31281
- err.write(
31282
- formatNoScanWarning({
31283
- mapperId: cfg.id,
31284
- templateId: scanBaseline.templateId,
31285
- timeoutMs: opts.timeoutMs ?? DEFAULT_SCAN_DISPATCH_TIMEOUT_MS,
31286
- docIds
31287
- })
31288
- );
31289
- }
31290
- }
31291
- }
31292
- var DEFAULT_SCAN_DISPATCH_TIMEOUT_MS = 3e4;
31293
- var DEFAULT_SCAN_DISPATCH_POLL_MS = 2e3;
31294
- function scanFingerprint(report) {
31295
- let maxScanId = 0;
31296
- for (const doc of report.documents ?? []) {
31297
- for (const v of doc.versions ?? []) {
31298
- if (typeof v.scan_id === "number" && v.scan_id > maxScanId) maxScanId = v.scan_id;
31299
- }
31300
- }
31301
- return `${report.scans_status?.last_updated ?? ""}|${maxScanId}`;
31302
- }
31303
- async function captureScanBaseline(client, mapperId, signal, err) {
31304
- try {
31305
- const details = await getDetails2(client, mapperId, { signal });
31306
- const templateId = details.template.id;
31307
- const report = await getStatus(client, templateId, { signal });
31308
- return { templateId, fingerprint: scanFingerprint(report) };
31309
- } catch (e) {
31310
- if (signal.aborted) throw e;
31311
- err.write(
31312
- "note: could not read the scan status before the update, so dr cannot confirm a rescan starts. Check with `dr scans status <template_id>` afterwards.\n"
31313
- );
31314
- return void 0;
31315
- }
31316
- }
31317
- async function waitForNewScan(client, baseline, signal, opts) {
31318
- const timeoutMs = opts.timeoutMs ?? DEFAULT_SCAN_DISPATCH_TIMEOUT_MS;
31319
- const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_SCAN_DISPATCH_POLL_MS;
31320
- const deadline = Date.now() + timeoutMs;
31321
- for (; ; ) {
31322
- try {
31323
- const report = await getStatus(client, baseline.templateId, { signal });
31324
- if (scanFingerprint(report) !== baseline.fingerprint) return true;
31325
- } catch (e) {
31326
- if (signal.aborted) throw e;
31327
- }
31328
- const remaining = deadline - Date.now();
31329
- if (remaining <= 0) return false;
31330
- await sleep3(Math.min(pollIntervalMs, remaining), void 0, { signal });
31293
+ renderMappersTable(cfg.out, [published]);
31331
31294
  }
31332
- }
31333
- async function mappedDocumentIds(client, templateId, mapperId, signal) {
31334
- try {
31335
- const mapper = await get14(client, templateId, mapperId, { signal });
31336
- return Array.isArray(mapper.mapped_documents_ids) ? mapper.mapped_documents_ids : void 0;
31337
- } catch (e) {
31338
- if (signal.aborted) throw e;
31339
- return void 0;
31340
- }
31341
- }
31342
- function formatNoScanWarning(p) {
31343
- const secs = Math.round(p.timeoutMs / 1e3);
31344
- const lines = [
31345
- `warning: mapper ${p.mapperId} was saved, but no new scan showed up on template ${p.templateId} within ${secs}s.`,
31346
- ' Either the scan is still queued, or this server predates the DR-54200 fix and ignored rescan="latest", so existing data still has the old config.',
31347
- ` Check first: dr scans status ${p.templateId}`
31348
- ];
31349
- if (p.docIds !== void 0 && p.docIds.length === 0) {
31350
- lines.push(" The mapper has no bound documents, so there is nothing to scan.");
31351
- } else {
31352
- const docFlags = p.docIds !== void 0 ? p.docIds.map((d) => `--doc ${d}`).join(" ") : "--doc <doc_id> [--doc <doc_id> ...]";
31353
- lines.push(
31354
- " If no new scan appears there, re-bind the mapper to its current documents with a rescan:"
31295
+ err.write(
31296
+ `note: mapper ${cfg.id} was published as new version ${published.id} on template ${templateId}; use ${published.id} for later updates.
31297
+ `
31298
+ );
31299
+ const declaredCalcNames = extractCalcFields(body).map((c) => c.name).filter((n) => n !== "");
31300
+ if (declaredCalcNames.length > 0 && rescanValue !== "") {
31301
+ const result = await verifyCalcFieldsMaterialise(
31302
+ cfg.client,
31303
+ templateId,
31304
+ declaredCalcNames,
31305
+ cfg.signal,
31306
+ cfg.verifyCalcFieldsOpts
31355
31307
  );
31356
- if (p.docIds === void 0) {
31357
- lines.push(
31358
- ` dr datamappers get ${p.mapperId} --table ${p.templateId} --json # read mapped_documents_ids`
31359
- );
31360
- }
31361
- lines.push(` dr datamappers bind ${p.mapperId} ${docFlags} --rescan latest`);
31308
+ const timeoutMs = cfg.verifyCalcFieldsOpts?.timeoutMs ?? 3e4;
31309
+ err.write(formatVerifyResult(templateId, result, timeoutMs));
31362
31310
  }
31363
- return lines.join("\n") + "\n";
31364
31311
  }
31365
31312
  async function buildConveniencePayload(client, tableId, name, versionId, sheet, flags, signal) {
31366
31313
  const fieldTypes = parseFieldOverrides(flags.fieldOverrides);
@@ -31973,8 +31920,12 @@ function registerDatamappersCommand(root) {
31973
31920
  addServerFlag(fromFileCmd);
31974
31921
  addAccountFlag(fromFileCmd);
31975
31922
  const updateCmd = dm.command("update").description(
31976
- "Update a datamapper in place (payload-file only \u2014 server accepts config+sheet_name+rescan). `--rescan latest` (the default) re-extracts the LATEST version of each document bound to the mapper. Servers without the DR-54200 fix save the config and start no scan, so after the update dr watches the template for a new scan and, when none shows up, prints how to check `dr scans status` and the `dr datamappers bind` fallback. For a full historical rescan (every calculated field re-executed against every document version ever uploaded), use `dr rescan table <template_id>` instead \u2014 that is far more expensive and higher-blast."
31977
- ).argument("<mapper_id>", "mapper id (positive integer)").option("--payload-file <path>", "Path to a JSON file with the PUT body (required)", "").option(
31923
+ 'Update a datamapper (payload-file only: config + sheet_name, optionally name). Publishes the payload as a new version of the mapper, the way the UI "Publish and scan" does: new columns are added to the table, the new version takes over the documents of the old one, and the old version is unbound. The new version has a NEW mapper id, which dr prints. `--rescan latest` (the default) re-extracts the LATEST version of each bound document, then dr checks that any calculated fields in the payload appear on the table. For a full historical rescan (every calculated field re-executed against every document version ever uploaded), use `dr rescan table <template_id>` instead \u2014 that is far more expensive and higher-blast.'
31924
+ ).argument("<mapper_id>", "mapper id (positive integer)").option(
31925
+ "--payload-file <path>",
31926
+ "Path to a JSON file with the mapper body: config, sheet_name, optional name (required)",
31927
+ ""
31928
+ ).option(
31978
31929
  "--field <NAME:TYPE>",
31979
31930
  'Set a column type via fields_to_change (Text|Float|Date|Integer). Repeatable. Mirrors the UI "Publish and scan" so a calc column referencing a LUT-injected column materialises instead of 500ing on inferred types. Note: the server blocks a type change (HTTP 400) when the template has more than one mapper.',
31980
31931
  collectArrayOpt,
@@ -31996,8 +31947,7 @@ function registerDatamappersCommand(root) {
31996
31947
  serverFlag: opts.server ?? "",
31997
31948
  asJson: opts.json === true,
31998
31949
  signal: setup.signal,
31999
- out: process.stdout,
32000
- verifyScanDispatch: {}
31950
+ out: process.stdout
32001
31951
  });
32002
31952
  } finally {
32003
31953
  setup.cleanup();
@@ -33732,7 +33682,7 @@ async function create15(client, body, reqOptions) {
33732
33682
  }
33733
33683
  return out;
33734
33684
  }
33735
- async function update9(client, id, body, reqOptions) {
33685
+ async function update8(client, id, body, reqOptions) {
33736
33686
  const path11 = `/api/v3/data_integrations/${id}/`;
33737
33687
  const out = await client.putJSON(path11, body, reqOptions);
33738
33688
  if (out === null) {
@@ -35510,7 +35460,7 @@ var defaultSleep6 = (ms, signal) => new Promise((resolve3, reject) => {
35510
35460
  async function waitForDataQuerySync(cfg) {
35511
35461
  let interval = cfg.pollIntervalMs ?? DQ_WAIT_POLL_INTERVAL_MS;
35512
35462
  const maxInterval = cfg.maxPollIntervalMs ?? DQ_WAIT_POLL_MAX_INTERVAL_MS;
35513
- const sleep4 = cfg.sleep ?? defaultSleep6;
35463
+ const sleep3 = cfg.sleep ?? defaultSleep6;
35514
35464
  const now = cfg.now ?? Date.now;
35515
35465
  const start = now();
35516
35466
  const deadline = start + cfg.timeoutSec * 1e3;
@@ -35552,7 +35502,7 @@ async function waitForDataQuerySync(cfg) {
35552
35502
  break;
35553
35503
  }
35554
35504
  try {
35555
- await sleep4(interval, cfg.signal);
35505
+ await sleep3(interval, cfg.signal);
35556
35506
  } catch {
35557
35507
  outcome = "cancelled";
35558
35508
  break;
@@ -36863,7 +36813,7 @@ async function runFileboxSetVersionFileStatus(cfg) {
36863
36813
  throw new Error("version id must be positive");
36864
36814
  }
36865
36815
  const err = cfg.err ?? process.stderr;
36866
- const sleep4 = cfg.sleep ?? defaultCycleTagSleep;
36816
+ const sleep3 = cfg.sleep ?? defaultCycleTagSleep;
36867
36817
  const settleMs = cfg.tagSettleDelayMs ?? CYCLE_TAG_SETTLE_DELAY_MS;
36868
36818
  const details = await getDetails(cfg.client, cfg.docId, { signal: cfg.signal });
36869
36819
  const tag = (details.tags_config ?? []).find(isFileStatusTag);
@@ -36956,7 +36906,7 @@ async function runFileboxSetVersionFileStatus(cfg) {
36956
36906
  }
36957
36907
  };
36958
36908
  const readBack = async () => {
36959
- await sleep4(settleMs, cfg.signal);
36909
+ await sleep3(settleMs, cfg.signal);
36960
36910
  return readVersionForConfirm(cfg.client, cfg.versionId, cfg.docId, cfg.signal);
36961
36911
  };
36962
36912
  await write();
@@ -39291,7 +39241,7 @@ async function runIntegrationsSync(cfg) {
39291
39241
  throw new Error("integration id must be positive");
39292
39242
  }
39293
39243
  const interval = cfg.pollIntervalMs ?? INTEGRATIONS_SYNC_POLL_INTERVAL_MS;
39294
- const sleep4 = cfg.sleep ?? defaultSleep7;
39244
+ const sleep3 = cfg.sleep ?? defaultSleep7;
39295
39245
  const resp = await resync(cfg.client, cfg.id, {
39296
39246
  signal: cfg.signal
39297
39247
  });
@@ -39340,7 +39290,7 @@ async function runIntegrationsSync(cfg) {
39340
39290
  return;
39341
39291
  }
39342
39292
  try {
39343
- await sleep4(interval, inner.signal);
39293
+ await sleep3(interval, inner.signal);
39344
39294
  } catch {
39345
39295
  cfg.out.write(
39346
39296
  `Timed out waiting for sync to finish \u2014 last status was ${orDash2(lastStatus)}
@@ -39430,7 +39380,7 @@ async function runIntegrationsUpdate(cfg) {
39430
39380
  throw new Error("--payload-file is required");
39431
39381
  }
39432
39382
  const body = await readJsonPayload(pf);
39433
- const updated = await update9(cfg.client, cfg.id, body, {
39383
+ const updated = await update8(cfg.client, cfg.id, body, {
39434
39384
  signal: cfg.signal
39435
39385
  });
39436
39386
  if (cfg.asJson) {
@@ -57217,7 +57167,7 @@ async function create16(client, body, options) {
57217
57167
  }
57218
57168
  return out;
57219
57169
  }
57220
- async function update10(client, id, body, options) {
57170
+ async function update9(client, id, body, options) {
57221
57171
  const out = await client.putJSON(`/api/templates/${id}/calcmodels`, body, options);
57222
57172
  if (out === null) {
57223
57173
  throw new Error(
@@ -57314,7 +57264,7 @@ async function runCalcmodelsUpdate(cfg) {
57314
57264
  );
57315
57265
  }
57316
57266
  validateCalcmodelBody(body);
57317
- const updated = await update10(cfg.client, cfg.id, body, {
57267
+ const updated = await update9(cfg.client, cfg.id, body, {
57318
57268
  signal: cfg.signal
57319
57269
  });
57320
57270
  if (cfg.asJson) {
@@ -57361,7 +57311,7 @@ async function runCalcmodelsSetFlow(cfg) {
57361
57311
  `new update_flow: ${flow}`
57362
57312
  ]),
57363
57313
  execute: async () => {
57364
- const updated = await update10(
57314
+ const updated = await update9(
57365
57315
  cfg.client,
57366
57316
  cfg.id,
57367
57317
  { ...model, update_flow: flow },
@@ -57391,7 +57341,7 @@ async function runCalcmodelsRecalculate(cfg) {
57391
57341
  `type: ${model.type}`
57392
57342
  ]),
57393
57343
  execute: async () => {
57394
- await update10(cfg.client, cfg.id, model, { signal: cfg.signal });
57344
+ await update9(cfg.client, cfg.id, model, { signal: cfg.signal });
57395
57345
  cfg.out.write(`recalculation queued \u2014 check status via dr calcmodels get ${cfg.id}
57396
57346
  `);
57397
57347
  }
@@ -57657,7 +57607,7 @@ async function create17(client, templateId, body, options) {
57657
57607
  }
57658
57608
  return out;
57659
57609
  }
57660
- async function update11(client, templateId, calcId, body, options) {
57610
+ async function update10(client, templateId, calcId, body, options) {
57661
57611
  const out = await client.putJSON(
57662
57612
  `/api/templates/${templateId}/calc_vals/${calcId}`,
57663
57613
  body,
@@ -57784,7 +57734,7 @@ async function runCalcValsUpdate(cfg) {
57784
57734
  `--file body.id (${String(bodyId)}) does not match URL id (${cfg.id}) \u2014 refusing to overwrite calc value #${cfg.id} with another value's body. Either fix the body's id field or point --file at the right file.`
57785
57735
  );
57786
57736
  }
57787
- updated = await update11(cfg.client, cfg.templateId, cfg.id, body, {
57737
+ updated = await update10(cfg.client, cfg.templateId, cfg.id, body, {
57788
57738
  signal: cfg.signal
57789
57739
  });
57790
57740
  } else {
@@ -57799,7 +57749,7 @@ async function runCalcValsUpdate(cfg) {
57799
57749
  // `--format ""` clears the mask (→ null); otherwise keep existing.
57800
57750
  format: cfg.format !== void 0 ? cfg.format === "" ? null : cfg.format : existing.format
57801
57751
  };
57802
- updated = await update11(cfg.client, cfg.templateId, cfg.id, body, {
57752
+ updated = await update10(cfg.client, cfg.templateId, cfg.id, body, {
57803
57753
  signal: cfg.signal
57804
57754
  });
57805
57755
  }
@@ -60805,7 +60755,7 @@ async function defaultSleep8(ms, signal) {
60805
60755
  async function pollExtractionStatus(client, contractFileIds, options = {}) {
60806
60756
  const interval = options.pollIntervalMs ?? EXTRACT_POLL_INTERVAL_MS;
60807
60757
  const timeout = options.timeoutMs ?? EXTRACT_POLL_TIMEOUT_MS;
60808
- const sleep4 = options.sleep ?? defaultSleep8;
60758
+ const sleep3 = options.sleep ?? defaultSleep8;
60809
60759
  const now = options.now ?? Date.now;
60810
60760
  const resolved = /* @__PURE__ */ new Map();
60811
60761
  if (contractFileIds.length === 0) return { resolved, pending: [] };
@@ -60855,7 +60805,7 @@ async function pollExtractionStatus(client, contractFileIds, options = {}) {
60855
60805
  options.onTick?.(resolved.size, contractFileIds.length);
60856
60806
  if (resolved.size >= contractFileIds.length) break;
60857
60807
  if (now() > deadline || inner.signal.aborted) break;
60858
- await sleep4(interval, inner.signal);
60808
+ await sleep3(interval, inner.signal);
60859
60809
  }
60860
60810
  } finally {
60861
60811
  clearTimeout(deadlineTimer);
@@ -63712,7 +63662,7 @@ function registerSpendControlCommand(root) {
63712
63662
  );
63713
63663
  addServerFlag(bulkCreate);
63714
63664
  addAccountFlag(bulkCreate);
63715
- const update12 = contracts.command("update").description("Partially update a contract by id (PATCH /contracts/{id})").argument("<contract_id>", "Contract UUID").requiredOption("--from <path>", "Path to JSON body matching ContractPartialUpdateSchema").option("--json", "Output raw JSON of the updated contract", false).action(async (contractId, opts) => {
63665
+ const update11 = contracts.command("update").description("Partially update a contract by id (PATCH /contracts/{id})").argument("<contract_id>", "Contract UUID").requiredOption("--from <path>", "Path to JSON body matching ContractPartialUpdateSchema").option("--json", "Output raw JSON of the updated contract", false).action(async (contractId, opts) => {
63716
63666
  const setup = await setupCall(opts.server, opts.account);
63717
63667
  try {
63718
63668
  await runContractUpdate({
@@ -63727,8 +63677,8 @@ function registerSpendControlCommand(root) {
63727
63677
  setup.cleanup();
63728
63678
  }
63729
63679
  });
63730
- addServerFlag(update12);
63731
- addAccountFlag(update12);
63680
+ addServerFlag(update11);
63681
+ addAccountFlag(update11);
63732
63682
  for (const spec of [
63733
63683
  {
63734
63684
  name: "archive",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "cliVersion": "1.6.106",
3
+ "cliVersion": "1.6.108",
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": {
@@ -556,8 +556,9 @@ returns the source rows behind the number.
556
556
  counts — and a token; re-run with `--confirm-token` to fire it) → poll `scans
557
557
  status` again. This is a **full historical** rescan (every calc field × every
558
558
  version). To only pick up a datamapper change on the live template, prefer
559
- `datamappers update <id> --rescan latest` (latest version of the mapper's bound
560
- documents only) — far cheaper and lower-blast.
559
+ `datamappers update <id> --payload-file <p> --rescan latest` (latest version of
560
+ the mapper's bound documents only) — far cheaper and lower-blast. It publishes a
561
+ new mapper version, so the mapper gets a **new id**, which dr prints.
561
562
 
562
563
  **Build a `create` body from plain-English intent** (calc models, datamappers,
563
564
  metrics): you resolve names → integer IDs yourself against the tenant
@@ -67,12 +67,23 @@ value-inference behavior. `NAME` is the target **table field** name (the
67
67
  ## Update / preview / bind / delete
68
68
 
69
69
  - `datamappers update`, `datamappers delete` (two-step confirm).
70
- - `datamappers update --rescan latest` — after the PUT, dr watches the
71
- template's scan status for up to 30s, because servers without the DR-54200 fix
72
- save the config but ignore `rescan: "latest"`. If no new scan shows up, it
73
- prints a warning with a ready `datamappers bind <id> --doc … --rescan latest`
74
- command. Run `scans status <template_id>` first: the scan may only be queued,
75
- and `bind` on a queued scan starts a second one. Don't re-run `update`.
70
+ - `datamappers update <id> --payload-file <p>` publishes the payload as a **new
71
+ version** of the mapper, the same request the UI's "Publish and scan" sends
72
+ (`POST /api/templates/<tid>/mappers` with `datamapper_id` = the old id). The
73
+ server adds any new columns to the table, binds the new version to the old
74
+ one's documents, unbinds the old one, and rescans (`--rescan latest` by
75
+ default). The payload needs `config` and `sheet_name`; dr fills in `name`,
76
+ `ordinal`, the bound documents and the source version from the old mapper,
77
+ and any of those keys in the payload wins.
78
+ - **The mapper gets a new id.** dr prints it (`note: mapper <old> was published
79
+ as new version <new>`). Use the new id for the next update, and keep an
80
+ old→new log if anything outside the table refers to the mapper id.
81
+ `update` refuses an old id that is bound to no documents, and points at
82
+ `datamappers list --table <tid>` for the current one.
83
+ - A mapper made by an org import or template push has no source version on
84
+ record, and `update` stops with an error. Add `"document_version_id"` to the
85
+ payload: an ACTIVE version of one of its documents, with a `sheet_name` that
86
+ exists in it.
76
87
  - `datamappers preview --payload-file <p>` — renders mapper output for sample
77
88
  input ("what would this extract?") before saving.
78
89
  - `datamappers bind <mapper_id> --doc <id> [--rescan latest|all]` — change which
@@ -82,15 +93,15 @@ value-inference behavior. `NAME` is the target **table field** name (the
82
93
 
83
94
  `datamappers update <id> --payload-file <p> [--field NAME:TYPE ...]`. Repeatable;
84
95
  `TYPE` is `Text|Float|Date|Integer`. Each `--field` becomes a `fields_to_change`
85
- entry on the PUT body, telling the server to set that column's type **explicitly**
96
+ entry in the body, telling the server to set that column's type **explicitly**
86
97
  instead of inferring it — this is exactly what the web UI's "Publish and scan"
87
- does (same `PUT api/templates/mappers/<id>` endpoint; the only payload difference
88
- is `fields_to_change`).
98
+ does (same `POST api/templates/<tid>/mappers` endpoint; the only payload
99
+ difference is `fields_to_change`).
89
100
 
90
101
  **When you need it:** adding a calc column that multiplies/divides by a
91
102
  **LUT-injected column**. LUT columns land as **Text**; the server infers Text for
92
103
  them, so a formula like `[Posting Amount] * [Sign]` fails to materialise and the
93
- PUT returns a bare `HTTP 500` (`ETLError: … incorrect formula syntax`) — even
104
+ publish returns a bare `HTTP 500` (`ETLError: … incorrect formula syntax`) — even
94
105
  though the formula is valid. Fixing the LUT column's type up-front lets it
95
106
  materialise:
96
107
 
@@ -120,12 +131,12 @@ valid formula the ETL compiler can't resolve (unknown/wrong-cased function,
120
131
  unresolvable `[field]` reference, a string-constant that happens to be quoted)
121
132
  still returns a bare 500.
122
133
 
123
- After `create`, the CLI polls the template's `fields[]` for up to 30s for every
134
+ After `create` and `update`, the CLI polls the template's `fields[]` for up to 30s for every
124
135
  declared calc field to materialise, and warns (pointing at `mapper-recipes get`
125
136
  for working formulas) if any are missing — this catches the EOMONTH failure
126
137
  mode. Skipped when `--no-rescan` is set.
127
138
 
128
- On a PUT 500, the CLI writes a `dr-cli-error-<timestamp>.md` to the cwd capturing
139
+ On a create or update 500, the CLI writes a `dr-cli-error-<timestamp>.md` to the cwd capturing
129
140
  URL + status + request/response bodies; the path is named in the stderr error.
130
141
 
131
142
  Formula syntax:
@@ -12,7 +12,7 @@ Write:
12
12
 
13
13
  - `integrations add` (payload-file), `integrations sync`,
14
14
  `integrations test-connection`, `integrations update`,
15
- `integrations delete` (two-step confirm).
15
+ `integrations delete` (two-step confirm, Tier A, see below).
16
16
  - `integrations revoke <id>` (two-step confirm; Tier A on prod) — Disconnect a
17
17
  `DatarailsConnector` OAuth data source: `POST /api/v2/data_integrations/<id>/revoke`.
18
18
  The grant is cleared locally first (`oauth_tokens`/`oauth_pending` dropped,
@@ -23,6 +23,38 @@ Write:
23
23
  `revoked` may leave the grant live at the vendor. Legacy vendors are refused.
24
24
  - `integrations share users` / `integrations share groups`.
25
25
 
26
+ ### Deleting a data source (`integrations delete <id>`)
27
+
28
+ `DELETE /api/v3/data_integrations/<id>/`. It cannot be undone and **cascades to queries and
29
+ documents**. Typical use: removing a validation or staging datasource that holds live customer
30
+ credentials once its bindings are gone.
31
+
32
+ 1. **Plan:** run it without a token, as the identity that owns the data source (on a staging org,
33
+ the impersonated staging user, never the operator):
34
+ `dr integrations delete <id> --server <S> --account <user>`. It prints `name`, `type`, `status`,
35
+ `org` (name + id) and `shared` (user/group counts), plus the exact confirm command with a token
36
+ that expires in **10 minutes** and is bound to this id and verb. Check the org id and the name
37
+ before going on: the plan does **not** list the queries and documents the cascade will remove.
38
+ Make sure nothing you want to keep still points at this data source.
39
+ 2. **Relay and get a "yes"**: name, org and why it is being deleted.
40
+ 3. **Confirm:** re-run the printed command with `--confirm-token <tok>`. On a prod tenant (`US`,
41
+ `US2`, `UK`, `CA`, `EU`, `STUDIO`, `DEMO`) this is **Tier A**: the binary shows a fresh word in a
42
+ desktop dialog (or a TTY prompt) and a human must type it. With no interactive channel it exits
43
+ non-zero; hand the printed line to the user rather than retrying. Success prints
44
+ `Deleted integration <id> — "<name>".`
45
+ 4. **Verify:** `dr integrations get <id> --server <S> --account <user>` returns `HTTP 404`.
46
+
47
+ Notes:
48
+
49
+ - `dr connectors` `Connector` rows that reference the data source (`--secret-ref`) are **not**
50
+ deleted, and dr-cli has no connector delete. Delete the `dr connectors` bindings first
51
+ (`dr connectors query delete`), then record the orphaned connector id.
52
+ - A `403` (`delete refused: …`) means the identity can't delete it: switch to the owner's account,
53
+ not to the operator.
54
+ - Deleting a validation data source on the Workato-migration staging orgs left the staging fileboxes
55
+ in place (their data-query rows pointed at the data source). Check one with `dr filebox get` after
56
+ the delete if you need them as evidence.
57
+
26
58
  ## IT-admin file-sync (`integrations it`)
27
59
 
28
60
  File-sync connectors only — OneDrive / SharePoint / Egnyte. Output redacts
@@ -149,8 +149,9 @@ except on `update`, which resolves the range's own table automatically.
149
149
  the blast radius (document + version counts) and get a token, then re-run with
150
150
  `--confirm-token` — token-only, no word gate even on prod (a rescan re-derives
151
151
  data, it doesn't delete it). To pick up a datamapper change on the live template
152
- **without** touching history, use `datamappers update <id> --rescan latest`
153
- instead (re-extracts the latest version of the mapper's bound documents); a full rescan will re-run pre-existing
152
+ **without** touching history, use `datamappers update <id> --payload-file <p>
153
+ --rescan latest` instead (re-extracts the latest version of the mapper's bound
154
+ documents; it publishes a new mapper version, so the mapper gets a new id); a full rescan will re-run pre-existing
154
155
  calc fields against old rows, so a latent divide-by-null there fails the whole
155
156
  scan. Refuses `UsageStats` on every tenant (system data, not customer-owned).
156
157
  - `dataflow get` — the dependency graph: inputs → mappers → LUTs → table →
@@ -431,27 +431,20 @@ this path that advice does not hold.) The generator omits it.
431
431
  `document_version_id` is required and is **not** in `dr datamappers get` output —
432
432
  read it from the upload response or `dr filebox versions <filebox_id>`.
433
433
 
434
- ### Changing a mapper that already exists: remap, don't update
434
+ ### Changing a mapper that already exists: remap
435
435
 
436
436
  Widening an existing feed mapper, or adding reporting-amount columns to a business
437
- table's mappers, means editing mappers that already exist. `dr datamappers update` is
438
- the obvious tool, and on many orgs **it cannot work.** A mapper created by an org
439
- import or template push has no source version on record. `PUT
440
- /api/templates/mappers/<id>` reads the mapper's source file, and for such a mapper it
441
- fails with a bare HTTP 500. The server log shows `AttributeError: 'NoneType' object has
442
- no attribute 'get_document_data'`. No request body fixes it, because the endpoint never
443
- reads a version from the body.
437
+ table's mappers, means editing mappers that already exist. `dr datamappers update
438
+ <id> --payload-file <p>` does this as a remap: it publishes the edited config as a new
439
+ version of the mapper, the way the UI's "Publish and scan" does, and fills in `name`,
440
+ `ordinal`, the bound documents and the source version from the old mapper.
444
441
 
445
- Remap instead. `dr datamappers create from-file <table_id>` with the existing
446
- mapper's id replaces it in place and records a real source version:
442
+ A mapper created by an org import or template push has no source version on record,
443
+ and `update` stops with an error saying so. Put a version in the payload:
447
444
 
448
445
  ```json
449
446
  {
450
- "name": "<existing name>",
451
- "datamapper_id": <existing mapper id>,
452
- "ordinal": <existing ordinal>,
453
447
  "document_version_id": <an ACTIVE version of one of its documents>,
454
- "selected_document_ids": [<its mapped_documents_ids>],
455
448
  "sheet_name": "<a sheet that exists in THAT version>",
456
449
  "config": { ... the edited config ... }
457
450
  }
@@ -459,7 +452,8 @@ mapper's id replaces it in place and records a real source version:
459
452
 
460
453
  - **The mapper gets a new id.** The old id stays behind bound to no documents, and
461
454
  lookups, which live on the table, are unaffected. Keep an old→new id log for rollback.
462
- - **`ordinal` must be the existing one.** A lower one is refused as a duplicate mapper.
455
+ - **`ordinal` must be the existing one.** `update` sends it for you; a lower one in the
456
+ payload is refused as a duplicate mapper.
463
457
  - **`sheet_name` must exist in the version you pass**, and a mapper's
464
458
  `preview_sheet_name` is often stale. Check with `dr filebox sheets <version_id>`; when
465
459
  the preview sheet is gone and the version has one sheet, use that one.
@@ -754,7 +748,7 @@ the package layout reaches every producer.
754
748
  | `#NAME?` on the label cell | `TEXT()` wrapped around `DR.MONTH()` | Step 4 |
755
749
  | Self-rate is not exactly 1 | ratio not anchored to the target row | Step 4 |
756
750
  | Custom code rejected by the Tables dialog | never registered as an org currency | Step 1 |
757
- | `datamappers update` 500s on an unchanged config | mapper has no source version (org import) | remap, Step 6 |
751
+ | `datamappers update`: mapper has no source document version | mapper made by an org import | `document_version_id` in the payload, Step 6 |
758
752
  | Remap 500s: `Cannot read sheet with name …` | chosen version is in `ERROR` status | use the newest ACTIVE version, Step 6 |
759
753
  | New lookup columns empty on every target row | lookup built while the FX table was still scanning | rebuild the lookup, Step 7 |
760
754
  | Opening balances have no currency amounts | calc-model rows skip the mappers | **Step 7c** |
@@ -152,8 +152,11 @@ goes merge → new vN → §3 → `dr connectors query repoint` → re-sync. Cut
152
152
 
153
153
  The staging datasource holds **live customer credentials**, so remove it right away:
154
154
  1. `dr connectors query list` → `dr connectors query delete <queryId>` for each binding.
155
- 2. `dr integrations delete <ds>` (two-step confirm). dr-cli has no connector delete, so the orphaned
156
- `Connector` row points at a deleted datasource. Record its id.
155
+ 2. `dr integrations delete <ds> $STG` prints a plan and a confirm token. Re-run it with
156
+ `--confirm-token <tok>`. Prod is Tier A, so a human types the word shown in the desktop dialog; with
157
+ no interactive channel, hand the line to the user. Verify with `dr integrations get <ds> $STG` → 404.
158
+ dr-cli has no connector delete, so the orphaned `Connector` row points at a deleted datasource.
159
+ Record its id. (dr-cli skill, `reference/integrations.md`, "Deleting a data source".)
157
160
  3. Keep the staging fileboxes as evidence until cut-over + ticket close, then delete the `<Customer>` tree.
158
161
  4. Delete the local `wk/` and `st/` downloads: they are customer data. Keep only `report.md`.
159
162
  5. Record the report, what was deleted and what was kept in the migration ticket.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datarails-ai/dr-cli",
3
- "version": "1.6.106",
3
+ "version": "1.6.108",
4
4
  "description": "Datarails CLI",
5
5
  "type": "module",
6
6
  "engines": {
@@ -17,7 +17,7 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@datarails-ai/dr-appspace-cli": "0.11.3",
20
+ "@datarails-ai/dr-appspace-cli": "0.11.4",
21
21
  "@modelcontextprotocol/sdk": "^1.29.0",
22
22
  "@napi-rs/keyring": "^1.3.0",
23
23
  "commander": "^12.1.0",