@datarails-ai/dr-cli 1.6.104 → 1.6.106
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 +964 -235
- package/dist/scopes-map.json +21 -1
- package/dist/skills/dr-cli/SKILL.md +14 -5
- package/dist/skills/dr-cli/reference/metrics.md +73 -3
- package/dist/skills/dr-switch-user/SKILL.md +2 -1
- package/package.json +2 -2
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.106",
|
|
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.
|
|
43
|
+
"@datarails-ai/dr-appspace-cli": "0.11.3",
|
|
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-
|
|
72
|
+
var BUILD_TIME = "2026-10-07T14:49:41.993Z";
|
|
73
73
|
function formatBuildTime(iso) {
|
|
74
74
|
if (iso === "") return "";
|
|
75
75
|
const d = new Date(iso);
|
|
@@ -1165,13 +1165,8 @@ function exchangeForSession(store, serverKey, email, session, host, opts) {
|
|
|
1165
1165
|
inFlightExchanges.set(key, entry);
|
|
1166
1166
|
return joinExchange(entry, opts.signal);
|
|
1167
1167
|
}
|
|
1168
|
-
async function
|
|
1169
|
-
const
|
|
1170
|
-
if (stored === void 0) {
|
|
1171
|
-
throw new NotLoggedInError(`not logged in: ${serverKey}:${email}`);
|
|
1172
|
-
}
|
|
1173
|
-
const keyed = usesApiKey(stored);
|
|
1174
|
-
const unsaved = keyed ? unsavedExchanges.get(`${serverKey}:${email}`) : void 0;
|
|
1168
|
+
async function exchangeKeyedSession(store, serverKey, email, stored, opts) {
|
|
1169
|
+
const unsaved = unsavedExchanges.get(`${serverKey}:${email}`);
|
|
1175
1170
|
const session = unsaved !== void 0 && unsaved.apiKey === stored.apiKey ? {
|
|
1176
1171
|
...stored,
|
|
1177
1172
|
jwtAccess: unsaved.jwtAccess,
|
|
@@ -1184,16 +1179,40 @@ async function ensureJwt(store, serverKey, email, opts) {
|
|
|
1184
1179
|
// The store still holds whatever the failed save meant to remove.
|
|
1185
1180
|
jwtRefresh: void 0
|
|
1186
1181
|
} : stored;
|
|
1187
|
-
if (!opts.force &&
|
|
1188
|
-
session,
|
|
1189
|
-
keyed ? apiKeyStaleBuffer(session, `${serverKey}:${email}`, opts.buffer) : opts.buffer
|
|
1190
|
-
)) {
|
|
1182
|
+
if (!opts.force && mintedByStoredApiKey(session) && session.jwtAccess !== void 0 && session.jwtAccess !== "" && !jwtExpiringSoon(session, apiKeyStaleBuffer(session, `${serverKey}:${email}`, opts.buffer))) {
|
|
1191
1183
|
return session;
|
|
1192
1184
|
}
|
|
1193
1185
|
const host = opts.host ?? resolve(serverKey).host;
|
|
1194
|
-
|
|
1195
|
-
|
|
1186
|
+
return exchangeForSession(store, serverKey, email, session, host, opts);
|
|
1187
|
+
}
|
|
1188
|
+
async function ensureApiKeyJwt(store, serverKey, email, options) {
|
|
1189
|
+
const stored = options?.session ?? await store.getSession(serverKey, email);
|
|
1190
|
+
if (stored === void 0) {
|
|
1191
|
+
throw new NotLoggedInError(`not logged in: ${serverKey}:${email}`);
|
|
1192
|
+
}
|
|
1193
|
+
if (!usesApiKey(stored)) {
|
|
1194
|
+
throw new Error(`${serverKey}:${email} is not an api-key identity`);
|
|
1195
|
+
}
|
|
1196
|
+
return exchangeKeyedSession(store, serverKey, email, stored, {
|
|
1197
|
+
buffer: options?.buffer ?? 0,
|
|
1198
|
+
force: false,
|
|
1199
|
+
signal: options?.signal,
|
|
1200
|
+
host: options?.host
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
async function ensureJwt(store, serverKey, email, opts) {
|
|
1204
|
+
const stored = await store.getSession(serverKey, email);
|
|
1205
|
+
if (stored === void 0) {
|
|
1206
|
+
throw new NotLoggedInError(`not logged in: ${serverKey}:${email}`);
|
|
1196
1207
|
}
|
|
1208
|
+
if (usesApiKey(stored)) {
|
|
1209
|
+
return exchangeKeyedSession(store, serverKey, email, stored, opts);
|
|
1210
|
+
}
|
|
1211
|
+
const session = stored;
|
|
1212
|
+
if (!opts.force && session.jwtAccess !== void 0 && session.jwtAccess !== "" && !jwtExpiringSoon(session, opts.buffer)) {
|
|
1213
|
+
return session;
|
|
1214
|
+
}
|
|
1215
|
+
const host = opts.host ?? resolve(serverKey).host;
|
|
1197
1216
|
if (session.impersonatorEmail !== void 0 && session.impersonatorEmail !== "") {
|
|
1198
1217
|
return recoverImpersonatedJwt(store, serverKey, email, session, host, opts);
|
|
1199
1218
|
}
|
|
@@ -2101,7 +2120,8 @@ var AuthorizedClient = class _AuthorizedClient {
|
|
|
2101
2120
|
* How this identity gets a usable JWT again. `dr login` is refused over an
|
|
2102
2121
|
* api-key credential, and it re-authenticates the operator rather than the
|
|
2103
2122
|
* impersonated user, so naming it for either sends the reader at a command
|
|
2104
|
-
* that cannot help — the same loop `runWhoami`
|
|
2123
|
+
* that cannot help — the same loop `runWhoami` avoids by never naming
|
|
2124
|
+
* `dr login` for either.
|
|
2105
2125
|
*
|
|
2106
2126
|
* `cause` splits the keyed answer, which is the only one that differs: a
|
|
2107
2127
|
* token that never arrived is not a token whose `organization_id` is wrong,
|
|
@@ -4807,7 +4827,7 @@ function declaredWithValueBelow(flag, cmd) {
|
|
|
4807
4827
|
// src/commands/whoami.ts
|
|
4808
4828
|
function registerWhoamiCommand(root) {
|
|
4809
4829
|
const cmd = root.command("whoami").description(
|
|
4810
|
-
"whoami decodes the locally-stored JWT for a server and prints the identity claims (user_id, username, email, expiration).
|
|
4830
|
+
"whoami decodes the locally-stored JWT for a server and prints the identity claims (user_id, username, email, expiration). Offline, except that an api-key identity holding no token its current key minted is exchanged once to obtain one."
|
|
4811
4831
|
).summary("Show the identity encoded in the stored JWT for a server").option("--json", "Output the full claims map as JSON", false).option("--verbose", "Print every claim in a table, sorted alphabetically", false).action(
|
|
4812
4832
|
async (opts) => {
|
|
4813
4833
|
const fileStore = await resolveFileStore(opts.server);
|
|
@@ -4845,19 +4865,34 @@ async function runWhoami(cfg) {
|
|
|
4845
4865
|
}
|
|
4846
4866
|
emitApiKeyExpiryWarning(session, `${cfg.serverKey}:${email}`);
|
|
4847
4867
|
const foreignJwt = usesApiKey(session) && session.jwtAccess !== void 0 && session.jwtAccess !== "" && !mintedByStoredApiKey(session);
|
|
4868
|
+
let active = session;
|
|
4848
4869
|
if (session.jwtAccess === void 0 || session.jwtAccess === "" || foreignJwt) {
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4870
|
+
if (usesApiKey(session)) {
|
|
4871
|
+
try {
|
|
4872
|
+
active = await ensureApiKeyJwt(cfg.store, cfg.serverKey, email, { session });
|
|
4873
|
+
} catch (err) {
|
|
4874
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
4875
|
+
throw new Error(`cannot show identity for ${cfg.serverKey}:${email}: ${reason}`, {
|
|
4876
|
+
cause: err
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4879
|
+
} else {
|
|
4880
|
+
const missing = `no JWT stored for ${cfg.serverKey}:${email}`;
|
|
4881
|
+
throw new Error(
|
|
4882
|
+
isImpersonatedSession(session) ? `${missing}; re-run dr login-as` : `${missing}; run dr login --server ${cfg.serverKey} to refresh`
|
|
4883
|
+
);
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
if (active.jwtAccess === void 0 || active.jwtAccess === "") {
|
|
4887
|
+
throw new Error(`no JWT stored for ${cfg.serverKey}:${email}`);
|
|
4853
4888
|
}
|
|
4854
4889
|
try {
|
|
4855
|
-
setTelemetryTarget(cfg.serverKey, deriveIdentity(
|
|
4890
|
+
setTelemetryTarget(cfg.serverKey, deriveIdentity(active));
|
|
4856
4891
|
} catch {
|
|
4857
4892
|
}
|
|
4858
4893
|
let claims;
|
|
4859
4894
|
try {
|
|
4860
|
-
claims = decodeJwtClaims(
|
|
4895
|
+
claims = decodeJwtClaims(active.jwtAccess);
|
|
4861
4896
|
} catch (err) {
|
|
4862
4897
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4863
4898
|
throw new Error(`decode JWT for ${cfg.serverKey}: ${msg}`);
|
|
@@ -4867,7 +4902,7 @@ async function runWhoami(cfg) {
|
|
|
4867
4902
|
host = resolve(cfg.serverKey).host;
|
|
4868
4903
|
} catch {
|
|
4869
4904
|
}
|
|
4870
|
-
const impersonatorEmail =
|
|
4905
|
+
const impersonatorEmail = active.impersonatorEmail !== void 0 && active.impersonatorEmail !== "" ? active.impersonatorEmail : void 0;
|
|
4871
4906
|
if (cfg.asJson) {
|
|
4872
4907
|
writeWhoamiJSON(cfg.out, cfg.serverKey, host, claims, impersonatorEmail);
|
|
4873
4908
|
return;
|
|
@@ -5360,18 +5395,18 @@ async function openBrowser(url) {
|
|
|
5360
5395
|
// src/commands/_serverPicker.ts
|
|
5361
5396
|
async function readLine(stdin) {
|
|
5362
5397
|
return new Promise((resolve3, reject) => {
|
|
5363
|
-
let
|
|
5398
|
+
let data4 = "";
|
|
5364
5399
|
const onData = (chunk) => {
|
|
5365
|
-
|
|
5366
|
-
const i =
|
|
5400
|
+
data4 += chunk.toString();
|
|
5401
|
+
const i = data4.indexOf("\n");
|
|
5367
5402
|
if (i >= 0) {
|
|
5368
5403
|
cleanup();
|
|
5369
|
-
resolve3(
|
|
5404
|
+
resolve3(data4.slice(0, i + 1));
|
|
5370
5405
|
}
|
|
5371
5406
|
};
|
|
5372
5407
|
const onEnd = () => {
|
|
5373
5408
|
cleanup();
|
|
5374
|
-
resolve3(
|
|
5409
|
+
resolve3(data4);
|
|
5375
5410
|
};
|
|
5376
5411
|
const onError = (err) => {
|
|
5377
5412
|
cleanup();
|
|
@@ -5495,7 +5530,7 @@ async function readFirstLineFromPipe(stdin) {
|
|
|
5495
5530
|
stdin.setEncoding?.("utf8");
|
|
5496
5531
|
stdin.resume?.();
|
|
5497
5532
|
return new Promise((resolve3, reject) => {
|
|
5498
|
-
let
|
|
5533
|
+
let data4 = "";
|
|
5499
5534
|
let done = false;
|
|
5500
5535
|
const finish = (line) => {
|
|
5501
5536
|
if (done) return;
|
|
@@ -5504,11 +5539,11 @@ async function readFirstLineFromPipe(stdin) {
|
|
|
5504
5539
|
resolve3(line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
5505
5540
|
};
|
|
5506
5541
|
const onData = (chunk) => {
|
|
5507
|
-
|
|
5508
|
-
const i =
|
|
5509
|
-
if (i >= 0) finish(
|
|
5542
|
+
data4 += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
5543
|
+
const i = data4.indexOf("\n");
|
|
5544
|
+
if (i >= 0) finish(data4.slice(0, i));
|
|
5510
5545
|
};
|
|
5511
|
-
const onEnd = () => finish(
|
|
5546
|
+
const onEnd = () => finish(data4);
|
|
5512
5547
|
const onError = (err) => {
|
|
5513
5548
|
if (done) return;
|
|
5514
5549
|
done = true;
|
|
@@ -5657,6 +5692,10 @@ async function runApiKeyLogin(cfg) {
|
|
|
5657
5692
|
apiKey: cfg.apiKey,
|
|
5658
5693
|
jwtAccess: tok.access,
|
|
5659
5694
|
jwtExpiresAt: tok.expiresAt,
|
|
5695
|
+
// Records which key minted this token, as every exchange does. Without it
|
|
5696
|
+
// `mintedByStoredApiKey` reads the token as unknown provenance, so `whoami`
|
|
5697
|
+
// and the next command would exchange again instead of using it.
|
|
5698
|
+
jwtApiKeyFingerprint: apiKeyFingerprint(cfg.apiKey),
|
|
5660
5699
|
savedAt: /* @__PURE__ */ new Date(),
|
|
5661
5700
|
cookieless: true
|
|
5662
5701
|
};
|
|
@@ -12672,25 +12711,25 @@ function buildAggregateBody(dimensions, metrics, filters) {
|
|
|
12672
12711
|
}
|
|
12673
12712
|
return body;
|
|
12674
12713
|
}
|
|
12675
|
-
function groupRows(
|
|
12676
|
-
if (!Array.isArray(
|
|
12677
|
-
return
|
|
12714
|
+
function groupRows(data4) {
|
|
12715
|
+
if (!Array.isArray(data4)) return data4;
|
|
12716
|
+
return data4.filter((r) => !(isPlainObject(r) && ("col_keys" in r || "row_keys" in r)));
|
|
12678
12717
|
}
|
|
12679
12718
|
function readTotals(metadata) {
|
|
12680
12719
|
if (!isPlainObject(metadata)) return null;
|
|
12681
12720
|
const totals = metadata["totals"];
|
|
12682
12721
|
return isPlainObject(totals) && Object.keys(totals).length > 0 ? totals : null;
|
|
12683
12722
|
}
|
|
12684
|
-
function asRowList(
|
|
12685
|
-
if (Array.isArray(
|
|
12686
|
-
if (
|
|
12687
|
-
return
|
|
12723
|
+
function asRowList(data4) {
|
|
12724
|
+
if (Array.isArray(data4)) {
|
|
12725
|
+
if (data4.every((x) => isPlainObject(x))) {
|
|
12726
|
+
return data4;
|
|
12688
12727
|
}
|
|
12689
12728
|
return null;
|
|
12690
12729
|
}
|
|
12691
|
-
if (isPlainObject(
|
|
12730
|
+
if (isPlainObject(data4)) {
|
|
12692
12731
|
for (const key of ["rows", "data", "results", "items"]) {
|
|
12693
|
-
const v =
|
|
12732
|
+
const v = data4[key];
|
|
12694
12733
|
if (Array.isArray(v) && v.every((x) => isPlainObject(x))) {
|
|
12695
12734
|
return v;
|
|
12696
12735
|
}
|
|
@@ -12723,10 +12762,10 @@ function writeTable5(out, rows) {
|
|
|
12723
12762
|
out.write(parts.join("") + "\n");
|
|
12724
12763
|
}
|
|
12725
12764
|
}
|
|
12726
|
-
function renderAggregateResult(out,
|
|
12727
|
-
const rows = asRowList(
|
|
12765
|
+
function renderAggregateResult(out, data4) {
|
|
12766
|
+
const rows = asRowList(data4);
|
|
12728
12767
|
if (rows === null) {
|
|
12729
|
-
out.write(JSON.stringify(
|
|
12768
|
+
out.write(JSON.stringify(data4, null, 2) + "\n");
|
|
12730
12769
|
return;
|
|
12731
12770
|
}
|
|
12732
12771
|
if (rows.length === 0) {
|
|
@@ -15442,6 +15481,108 @@ function registerDynamicRangesCommand(root) {
|
|
|
15442
15481
|
// src/commands/functions.ts
|
|
15443
15482
|
import { promises as fs11 } from "fs";
|
|
15444
15483
|
|
|
15484
|
+
// src/api/_cacheKeyPolling.ts
|
|
15485
|
+
var CacheKeyHttpError = class extends Error {
|
|
15486
|
+
status;
|
|
15487
|
+
path;
|
|
15488
|
+
bodyText;
|
|
15489
|
+
constructor(method, path11, status2, bodyText) {
|
|
15490
|
+
super(`${method} ${path11}: HTTP ${status2}: ${truncate5(bodyText, 200)}`);
|
|
15491
|
+
this.name = "CacheKeyHttpError";
|
|
15492
|
+
this.status = status2;
|
|
15493
|
+
this.path = path11;
|
|
15494
|
+
this.bodyText = bodyText;
|
|
15495
|
+
}
|
|
15496
|
+
};
|
|
15497
|
+
async function putOnce(client, path11, body, options) {
|
|
15498
|
+
const res = await client.do("PUT", path11, body, options);
|
|
15499
|
+
const text2 = await res.text();
|
|
15500
|
+
if (res.status !== 409 && (res.status < 200 || res.status >= 300)) {
|
|
15501
|
+
throw new CacheKeyHttpError("PUT", path11, res.status, text2);
|
|
15502
|
+
}
|
|
15503
|
+
if (text2 === "" || text2 === '""') {
|
|
15504
|
+
return { status: res.status, parsed: null };
|
|
15505
|
+
}
|
|
15506
|
+
try {
|
|
15507
|
+
return { status: res.status, parsed: JSON.parse(text2) };
|
|
15508
|
+
} catch (err) {
|
|
15509
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
15510
|
+
throw new Error(`PUT ${path11}: decode: ${msg}`);
|
|
15511
|
+
}
|
|
15512
|
+
}
|
|
15513
|
+
function extractCacheKey(body) {
|
|
15514
|
+
if (body === null || typeof body !== "object") return "";
|
|
15515
|
+
const top = body["cache_key"];
|
|
15516
|
+
if (typeof top === "string" && top !== "") return top;
|
|
15517
|
+
const detail = body["detail"];
|
|
15518
|
+
if (detail !== null && typeof detail === "object") {
|
|
15519
|
+
const nested = detail["cache_key"];
|
|
15520
|
+
if (typeof nested === "string" && nested !== "") return nested;
|
|
15521
|
+
}
|
|
15522
|
+
return "";
|
|
15523
|
+
}
|
|
15524
|
+
function withCacheKey(path11, key) {
|
|
15525
|
+
return `${path11}?cache_key=${encodeURIComponent(key)}`;
|
|
15526
|
+
}
|
|
15527
|
+
var CACHE_KEY_POLL_INTERVAL_MS = 500;
|
|
15528
|
+
var abortableSleep = (ms, signal) => new Promise((resolve3, reject) => {
|
|
15529
|
+
if (signal?.aborted) {
|
|
15530
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
15531
|
+
return;
|
|
15532
|
+
}
|
|
15533
|
+
const onAbort = () => {
|
|
15534
|
+
clearTimeout(t);
|
|
15535
|
+
reject(signal?.reason ?? new Error("aborted"));
|
|
15536
|
+
};
|
|
15537
|
+
const t = setTimeout(() => {
|
|
15538
|
+
signal?.removeEventListener("abort", onAbort);
|
|
15539
|
+
resolve3();
|
|
15540
|
+
}, ms);
|
|
15541
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
15542
|
+
});
|
|
15543
|
+
async function putWithCacheKeyPolling(client, path11, payload, options = {}) {
|
|
15544
|
+
const interval = options.pollIntervalMs ?? CACHE_KEY_POLL_INTERVAL_MS;
|
|
15545
|
+
const sleep4 = options.sleep ?? abortableSleep;
|
|
15546
|
+
const label = options.label ?? "data";
|
|
15547
|
+
const body = JSON.stringify(payload);
|
|
15548
|
+
const first = await putOnce(client, path11, body, options);
|
|
15549
|
+
if (first.status !== 409) {
|
|
15550
|
+
return first.parsed;
|
|
15551
|
+
}
|
|
15552
|
+
const cacheKey = extractCacheKey(first.parsed);
|
|
15553
|
+
if (cacheKey === "") {
|
|
15554
|
+
throw new Error(
|
|
15555
|
+
`PUT ${path11}: 409 without cache_key: ${truncate5(JSON.stringify(first.parsed ?? ""), 200)}`
|
|
15556
|
+
);
|
|
15557
|
+
}
|
|
15558
|
+
const pollPath = withCacheKey(path11, cacheKey);
|
|
15559
|
+
for (; ; ) {
|
|
15560
|
+
try {
|
|
15561
|
+
await sleep4(interval, options.signal);
|
|
15562
|
+
} catch (err) {
|
|
15563
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
15564
|
+
throw new Error(`${label} polling timed out: ${reason}`);
|
|
15565
|
+
}
|
|
15566
|
+
let result;
|
|
15567
|
+
try {
|
|
15568
|
+
result = await putOnce(client, pollPath, body, options);
|
|
15569
|
+
} catch (err) {
|
|
15570
|
+
if (options.signal?.aborted) {
|
|
15571
|
+
const reason = options.signal.reason instanceof Error ? options.signal.reason.message : String(options.signal.reason ?? "aborted");
|
|
15572
|
+
throw new Error(`${label} polling timed out: ${reason}`);
|
|
15573
|
+
}
|
|
15574
|
+
throw err;
|
|
15575
|
+
}
|
|
15576
|
+
if (result.status !== 409) {
|
|
15577
|
+
return result.parsed;
|
|
15578
|
+
}
|
|
15579
|
+
}
|
|
15580
|
+
}
|
|
15581
|
+
function truncate5(s, n) {
|
|
15582
|
+
if (s.length <= n) return s;
|
|
15583
|
+
return s.slice(0, n) + "\u2026";
|
|
15584
|
+
}
|
|
15585
|
+
|
|
15445
15586
|
// src/api/functions.ts
|
|
15446
15587
|
function normalize(raw) {
|
|
15447
15588
|
return {
|
|
@@ -15485,6 +15626,16 @@ async function getDetail(client, id, options) {
|
|
|
15485
15626
|
default_agg_config: out.default_agg_config ?? null
|
|
15486
15627
|
};
|
|
15487
15628
|
}
|
|
15629
|
+
var DEFAULT_AGG_CONFIG = {
|
|
15630
|
+
YTD: { aggregate_by: "month", fnc: "sum", is_end_of_period: true },
|
|
15631
|
+
QTD: { aggregate_by: "month", fnc: "sum", is_end_of_period: true },
|
|
15632
|
+
RM: { rolling_month_before: 0, is_end_of_period: true },
|
|
15633
|
+
SA: { aggregate_by: "month", fnc: "sum", is_end_of_period: true },
|
|
15634
|
+
YTG: { aggregate_by: "month", fnc: "sum", is_end_of_period: true },
|
|
15635
|
+
QTG: { aggregate_by: "month", fnc: "sum", is_end_of_period: true },
|
|
15636
|
+
MTD: { aggregate_by: "month", fnc: "sum", is_end_of_period: true },
|
|
15637
|
+
MTG: { aggregate_by: "month", fnc: "sum", is_end_of_period: true }
|
|
15638
|
+
};
|
|
15488
15639
|
async function create6(client, args, options) {
|
|
15489
15640
|
const body = {
|
|
15490
15641
|
id: -1,
|
|
@@ -15495,6 +15646,10 @@ async function create6(client, args, options) {
|
|
|
15495
15646
|
calculated_values: [],
|
|
15496
15647
|
template: args.template
|
|
15497
15648
|
};
|
|
15649
|
+
if (args.dateField !== void 0) {
|
|
15650
|
+
body.default_agg_datefield = args.dateField;
|
|
15651
|
+
body.default_agg_config = DEFAULT_AGG_CONFIG;
|
|
15652
|
+
}
|
|
15498
15653
|
const out = await client.postJSON("/api/functions/", body, options);
|
|
15499
15654
|
if (out === null) {
|
|
15500
15655
|
return { id: 0, name: "", aggfun: "", value_field: 0, template: 0 };
|
|
@@ -15511,7 +15666,9 @@ async function getFull(client, id, options) {
|
|
|
15511
15666
|
value_field: 0,
|
|
15512
15667
|
template: 0,
|
|
15513
15668
|
calculated_values: [],
|
|
15514
|
-
is_fx_aware: false
|
|
15669
|
+
is_fx_aware: false,
|
|
15670
|
+
default_agg_datefield: null,
|
|
15671
|
+
default_agg_config: null
|
|
15515
15672
|
};
|
|
15516
15673
|
}
|
|
15517
15674
|
const links = (out.calculated_values ?? []).map((cv) => ({
|
|
@@ -15519,7 +15676,13 @@ async function getFull(client, id, options) {
|
|
|
15519
15676
|
field: cv.field ?? null,
|
|
15520
15677
|
side: cv.side ?? null
|
|
15521
15678
|
}));
|
|
15522
|
-
return {
|
|
15679
|
+
return {
|
|
15680
|
+
...normalize(out),
|
|
15681
|
+
calculated_values: links,
|
|
15682
|
+
is_fx_aware: out.is_fx_aware === true,
|
|
15683
|
+
default_agg_datefield: out.default_agg_datefield ?? null,
|
|
15684
|
+
default_agg_config: out.default_agg_config ?? null
|
|
15685
|
+
};
|
|
15523
15686
|
}
|
|
15524
15687
|
async function update2(client, id, body, options) {
|
|
15525
15688
|
const out = await client.putJSON(
|
|
@@ -15538,6 +15701,140 @@ async function rename2(client, id, name, options) {
|
|
|
15538
15701
|
async function deleteById3(client, id, options) {
|
|
15539
15702
|
await client.deleteJSON(`/api/functions/${id}`, options);
|
|
15540
15703
|
}
|
|
15704
|
+
async function data2(client, id, body, options = {}) {
|
|
15705
|
+
const out = await putWithCacheKeyPolling(client, `/api/functions/${id}/data/v2`, body, {
|
|
15706
|
+
label: "function data",
|
|
15707
|
+
...options
|
|
15708
|
+
});
|
|
15709
|
+
return out ?? { query_results: [] };
|
|
15710
|
+
}
|
|
15711
|
+
async function drilldown(client, id, body, options = {}) {
|
|
15712
|
+
return putWithCacheKeyPolling(client, `/api/functions/${id}/drilldown/v4`, body, {
|
|
15713
|
+
label: "function drilldown",
|
|
15714
|
+
...options
|
|
15715
|
+
});
|
|
15716
|
+
}
|
|
15717
|
+
function parseDrilldown(body) {
|
|
15718
|
+
if (!Array.isArray(body)) {
|
|
15719
|
+
throw new Error(`unexpected drill-down response (not an array): ${preview(body)}`);
|
|
15720
|
+
}
|
|
15721
|
+
const first = body[0];
|
|
15722
|
+
if (first !== null && typeof first === "object" && "error" in first) {
|
|
15723
|
+
const e = first;
|
|
15724
|
+
if (e["error"] === "too_large_data") {
|
|
15725
|
+
return {
|
|
15726
|
+
kind: "too_large",
|
|
15727
|
+
rows_count: Number(e["rows_count"] ?? 0),
|
|
15728
|
+
cols_count: Number(e["cols_count"] ?? 0)
|
|
15729
|
+
};
|
|
15730
|
+
}
|
|
15731
|
+
throw new Error(`drill-down error: ${preview(first)}`);
|
|
15732
|
+
}
|
|
15733
|
+
if (body.length === 0) {
|
|
15734
|
+
return { kind: "rows", rows: [], schema: { fields: [], empty_fields: [] } };
|
|
15735
|
+
}
|
|
15736
|
+
const last = body[body.length - 1];
|
|
15737
|
+
const isSchema = last !== null && typeof last === "object" && Array.isArray(last["fields"]);
|
|
15738
|
+
if (!isSchema) {
|
|
15739
|
+
throw new Error("unexpected drill-down response: no trailing {fields, empty_fields} schema");
|
|
15740
|
+
}
|
|
15741
|
+
const raw = last;
|
|
15742
|
+
const schema = {
|
|
15743
|
+
fields: raw["fields"],
|
|
15744
|
+
empty_fields: Array.isArray(raw["empty_fields"]) ? raw["empty_fields"] : []
|
|
15745
|
+
};
|
|
15746
|
+
return { kind: "rows", rows: body.slice(0, -1), schema };
|
|
15747
|
+
}
|
|
15748
|
+
async function exportDrilldownCsv(client, id, body, options = {}) {
|
|
15749
|
+
const interval = options.pollIntervalMs ?? CACHE_KEY_POLL_INTERVAL_MS;
|
|
15750
|
+
const sleep4 = options.sleep ?? abortableSleep;
|
|
15751
|
+
const path11 = `/api/functions/${id}/drilldown/export_to_csv`;
|
|
15752
|
+
const payload = JSON.stringify(body);
|
|
15753
|
+
let res = await putOnce(client, path11, payload, options);
|
|
15754
|
+
while (res.status === 409) {
|
|
15755
|
+
const key = extractCacheKey(res.parsed);
|
|
15756
|
+
if (key === "") {
|
|
15757
|
+
throw new Error(`PUT ${path11}: 409 without cache_key: ${preview(res.parsed)}`);
|
|
15758
|
+
}
|
|
15759
|
+
for (; ; ) {
|
|
15760
|
+
try {
|
|
15761
|
+
await sleep4(interval, options.signal);
|
|
15762
|
+
} catch (err) {
|
|
15763
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
15764
|
+
throw new Error(`drill-down CSV export polling timed out: ${reason}`);
|
|
15765
|
+
}
|
|
15766
|
+
const status2 = await client.postJSON(
|
|
15767
|
+
"/api/cache/cache_keys_status",
|
|
15768
|
+
{ cache_keys: [key] },
|
|
15769
|
+
options
|
|
15770
|
+
);
|
|
15771
|
+
const entry = status2?.[key];
|
|
15772
|
+
if (entry !== null && typeof entry === "object") {
|
|
15773
|
+
const e = entry;
|
|
15774
|
+
const status3 = typeof e["status"] === "string" ? e["status"].toUpperCase() : "";
|
|
15775
|
+
const hasError = e["error"] !== void 0 && e["error"] !== null && e["error"] !== false;
|
|
15776
|
+
if (status3 === "FAILURE" || hasError) {
|
|
15777
|
+
throw new Error(
|
|
15778
|
+
`drill-down CSV export failed on the server (cache key ${key}, status ${status3 === "" ? "-" : status3}${hasError ? `, error ${preview(e["error"])}` : ""}); retry, or narrow the drill-down with more --filter flags`
|
|
15779
|
+
);
|
|
15780
|
+
}
|
|
15781
|
+
if (e["is_ready"] === true) break;
|
|
15782
|
+
}
|
|
15783
|
+
}
|
|
15784
|
+
res = await putOnce(client, withCacheKey(path11, key), payload, options);
|
|
15785
|
+
}
|
|
15786
|
+
const parsed2 = res.parsed;
|
|
15787
|
+
const reportId = parsed2 !== null && typeof parsed2 === "object" ? parsed2["drill_down_report_id"] : void 0;
|
|
15788
|
+
if (typeof reportId !== "number") {
|
|
15789
|
+
throw new Error(`PUT ${path11}: no drill_down_report_id in ${preview(parsed2)}`);
|
|
15790
|
+
}
|
|
15791
|
+
const reportPath = `/api/dashboards/report/${reportId}?report_type=drill_down_report`;
|
|
15792
|
+
const report = await client.do("GET", reportPath, void 0, options);
|
|
15793
|
+
if (report.status < 200 || report.status >= 300) {
|
|
15794
|
+
const text2 = await report.text();
|
|
15795
|
+
throw new Error(`GET ${reportPath}: HTTP ${report.status}: ${text2.slice(0, 200)}`);
|
|
15796
|
+
}
|
|
15797
|
+
return { reportId, bytes: new Uint8Array(await report.arrayBuffer()) };
|
|
15798
|
+
}
|
|
15799
|
+
function preview(v) {
|
|
15800
|
+
const s = JSON.stringify(v ?? null);
|
|
15801
|
+
return s.length > 200 ? s.slice(0, 200) + "\u2026" : s;
|
|
15802
|
+
}
|
|
15803
|
+
|
|
15804
|
+
// src/commands/_excelSerial.ts
|
|
15805
|
+
var EXCEL_UNIX_EPOCH_OFFSET_DAYS = 25569;
|
|
15806
|
+
var EXCEL_MAX_SERIAL = 2958465;
|
|
15807
|
+
var MIN_EPOCH_SECONDS = 1e8;
|
|
15808
|
+
var MAX_EPOCH_SECONDS = 1e11;
|
|
15809
|
+
var SERIAL_DATE_FORMS = "a full date (YYYY-MM-DD, M/D/YYYY or an ISO datetime \u2014 a month alone is ambiguous), an Excel serial (e.g. 45688), or Unix epoch seconds";
|
|
15810
|
+
function toExcelSerial(raw) {
|
|
15811
|
+
const s = raw.trim();
|
|
15812
|
+
if (/^\d+(\.\d+)?$/.test(s)) {
|
|
15813
|
+
const n = Number(s);
|
|
15814
|
+
if (n <= EXCEL_MAX_SERIAL) return n;
|
|
15815
|
+
if (Number.isInteger(n) && n >= MIN_EPOCH_SECONDS && n < MAX_EPOCH_SECONDS) {
|
|
15816
|
+
return epochSecondsToSerial(n);
|
|
15817
|
+
}
|
|
15818
|
+
return null;
|
|
15819
|
+
}
|
|
15820
|
+
if (/^\d{4}[-/]\d{1,2}$/.test(s)) return null;
|
|
15821
|
+
const epoch = toEpochSeconds(s);
|
|
15822
|
+
if (epoch === null) return null;
|
|
15823
|
+
return epochSecondsToSerial(Number(epoch));
|
|
15824
|
+
}
|
|
15825
|
+
function epochSecondsToSerial(sec) {
|
|
15826
|
+
return Math.floor(sec / 86400) + EXCEL_UNIX_EPOCH_OFFSET_DAYS;
|
|
15827
|
+
}
|
|
15828
|
+
function excelSerialToIso(n) {
|
|
15829
|
+
if (!Number.isFinite(n)) return String(n);
|
|
15830
|
+
const ms = (Math.floor(n) - EXCEL_UNIX_EPOCH_OFFSET_DAYS) * 864e5;
|
|
15831
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
15832
|
+
}
|
|
15833
|
+
function epochSecondsToIso(sec) {
|
|
15834
|
+
if (!Number.isFinite(sec)) return String(sec);
|
|
15835
|
+
const iso = new Date(sec * 1e3).toISOString();
|
|
15836
|
+
return sec % 86400 === 0 ? iso.slice(0, 10) : iso.replace(/\.\d{3}Z$/, "Z");
|
|
15837
|
+
}
|
|
15541
15838
|
|
|
15542
15839
|
// src/commands/functions.ts
|
|
15543
15840
|
var VALID_AGGS2 = ["SUM", "COUNT", "AVG", "MIN", "MAX"];
|
|
@@ -15588,19 +15885,37 @@ async function runFunctionsGet(cfg) {
|
|
|
15588
15885
|
renderFunctionTable(cfg.out, [fn]);
|
|
15589
15886
|
renderFunctionFilters(cfg.out, fn);
|
|
15590
15887
|
}
|
|
15888
|
+
async function resolveDateField(client, templateId, token, signal) {
|
|
15889
|
+
const index = await loadTemplateFields(client, templateId, { signal });
|
|
15890
|
+
const field = resolveField(index, token, "--date-field");
|
|
15891
|
+
if (!isDateField(field)) {
|
|
15892
|
+
throw new Error(
|
|
15893
|
+
`--date-field "${token}": "${field.name}" is a ${field.type} field; the default aggregation date field must be a Date field`
|
|
15894
|
+
);
|
|
15895
|
+
}
|
|
15896
|
+
return field;
|
|
15897
|
+
}
|
|
15591
15898
|
async function runFunctionsCreate(cfg) {
|
|
15592
15899
|
validateName(cfg.name);
|
|
15593
15900
|
const aggUpper = validateAgg(cfg.aggOriginal);
|
|
15901
|
+
const dateField = cfg.dateField === void 0 ? void 0 : await resolveDateField(cfg.client, cfg.template, cfg.dateField, cfg.signal);
|
|
15594
15902
|
const created = await create6(
|
|
15595
15903
|
cfg.client,
|
|
15596
15904
|
{
|
|
15597
15905
|
name: cfg.name,
|
|
15598
15906
|
template: cfg.template,
|
|
15599
15907
|
valueField: cfg.valueField,
|
|
15600
|
-
aggfun: aggUpper
|
|
15908
|
+
aggfun: aggUpper,
|
|
15909
|
+
...dateField !== void 0 ? { dateField: dateField.id } : {}
|
|
15601
15910
|
},
|
|
15602
15911
|
{ signal: cfg.signal }
|
|
15603
15912
|
);
|
|
15913
|
+
if (dateField === void 0) {
|
|
15914
|
+
(cfg.err ?? process.stderr).write(
|
|
15915
|
+
`warning: no --date-field given \u2014 period aggregations (DR.YTD/QTD/MTD/\u2026, functions data --agg YTD) will not work for function ${created.id}; set one with: dr functions update ${created.id} --date-field <Date field>
|
|
15916
|
+
`
|
|
15917
|
+
);
|
|
15918
|
+
}
|
|
15604
15919
|
if (cfg.asJson) {
|
|
15605
15920
|
writeJSONIndented(cfg.out, toJsonShape(created));
|
|
15606
15921
|
return;
|
|
@@ -15615,13 +15930,14 @@ async function runFunctionsUpdate(cfg) {
|
|
|
15615
15930
|
const hasField = cfg.field !== void 0;
|
|
15616
15931
|
const hasAgg = cfg.aggOriginal !== void 0;
|
|
15617
15932
|
const hasTemplate = cfg.template !== void 0;
|
|
15618
|
-
|
|
15619
|
-
|
|
15933
|
+
const hasDateField = cfg.dateField !== void 0;
|
|
15934
|
+
if (!hasName && !hasField && !hasAgg && !hasTemplate && !hasDateField) {
|
|
15935
|
+
throw new Error("nothing to update (pass --name/--field/--agg/--template/--date-field)");
|
|
15620
15936
|
}
|
|
15621
15937
|
if (hasName) validateName(cfg.name);
|
|
15622
15938
|
const aggUpper = hasAgg ? validateAgg(cfg.aggOriginal) : void 0;
|
|
15623
15939
|
let result;
|
|
15624
|
-
if (!hasField && !hasAgg && !hasTemplate) {
|
|
15940
|
+
if (!hasField && !hasAgg && !hasTemplate && !hasDateField) {
|
|
15625
15941
|
await rename2(cfg.client, cfg.id, cfg.name, {
|
|
15626
15942
|
signal: cfg.signal
|
|
15627
15943
|
});
|
|
@@ -15630,6 +15946,13 @@ async function runFunctionsUpdate(cfg) {
|
|
|
15630
15946
|
const current = await getFull(cfg.client, cfg.id, {
|
|
15631
15947
|
signal: cfg.signal
|
|
15632
15948
|
});
|
|
15949
|
+
const template = hasTemplate ? cfg.template : current.template;
|
|
15950
|
+
if (template !== current.template && current.default_agg_datefield !== null && !hasDateField) {
|
|
15951
|
+
throw new Error(
|
|
15952
|
+
`function ${cfg.id} has default date field ${current.default_agg_datefield}, which belongs to the old template ${current.template}; pass --date-field <Date field of template ${template}> together with --template`
|
|
15953
|
+
);
|
|
15954
|
+
}
|
|
15955
|
+
const dateField = hasDateField ? await resolveDateField(cfg.client, template, cfg.dateField, cfg.signal) : void 0;
|
|
15633
15956
|
result = await update2(
|
|
15634
15957
|
cfg.client,
|
|
15635
15958
|
cfg.id,
|
|
@@ -15637,11 +15960,16 @@ async function runFunctionsUpdate(cfg) {
|
|
|
15637
15960
|
name: hasName ? cfg.name : current.name,
|
|
15638
15961
|
value_field: hasField ? cfg.field : current.value_field,
|
|
15639
15962
|
aggfun: hasAgg ? aggUpper : current.aggfun,
|
|
15640
|
-
template
|
|
15963
|
+
template,
|
|
15641
15964
|
calculated_values: current.calculated_values,
|
|
15642
15965
|
// Re-sent so the full PUT can't silently reset the currency-conversion
|
|
15643
15966
|
// flag (same class of trap as calculated_values above).
|
|
15644
|
-
is_fx_aware: current.is_fx_aware
|
|
15967
|
+
is_fx_aware: current.is_fx_aware,
|
|
15968
|
+
...dateField !== void 0 ? {
|
|
15969
|
+
default_agg_datefield: dateField.id,
|
|
15970
|
+
// Keep a config the function already has; otherwise the platform default.
|
|
15971
|
+
default_agg_config: current.default_agg_config ?? DEFAULT_AGG_CONFIG
|
|
15972
|
+
} : {}
|
|
15645
15973
|
},
|
|
15646
15974
|
{ signal: cfg.signal }
|
|
15647
15975
|
);
|
|
@@ -15784,6 +16112,365 @@ async function runFunctionsDelete(cfg) {
|
|
|
15784
16112
|
}
|
|
15785
16113
|
});
|
|
15786
16114
|
}
|
|
16115
|
+
var PERIOD_AGGS = [
|
|
16116
|
+
"DEFAULT",
|
|
16117
|
+
"YTD",
|
|
16118
|
+
"YTG",
|
|
16119
|
+
"QTD",
|
|
16120
|
+
"QTG",
|
|
16121
|
+
"MTD",
|
|
16122
|
+
"MTG"
|
|
16123
|
+
];
|
|
16124
|
+
function validatePeriodAgg(orig) {
|
|
16125
|
+
const upper = orig.trim().toUpperCase();
|
|
16126
|
+
if (!PERIOD_AGGS.includes(upper)) {
|
|
16127
|
+
throw new Error(`invalid --agg "${orig}" (valid: ${PERIOD_AGGS.join(", ")})`);
|
|
16128
|
+
}
|
|
16129
|
+
return upper;
|
|
16130
|
+
}
|
|
16131
|
+
function parseEvalFilterFlags(raw, flag) {
|
|
16132
|
+
return raw.map((r) => {
|
|
16133
|
+
const idx = r.indexOf("=");
|
|
16134
|
+
const name = idx > 0 ? r.slice(0, idx).trim() : "";
|
|
16135
|
+
const values = idx > 0 ? r.slice(idx + 1).split(",").map((v) => v.trim()).filter((v) => v !== "") : [];
|
|
16136
|
+
if (name === "" || values.length === 0) {
|
|
16137
|
+
throw new Error(`invalid ${flag} "${r}": expected NAME=V1[,V2\u2026]`);
|
|
16138
|
+
}
|
|
16139
|
+
return { flag, name, values };
|
|
16140
|
+
});
|
|
16141
|
+
}
|
|
16142
|
+
var NUMERIC_FIELD_TYPES = /* @__PURE__ */ new Set(["integer", "float", "number", "numeric", "monetary"]);
|
|
16143
|
+
function resolveEvalFilters(index, specs) {
|
|
16144
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16145
|
+
return specs.map((s) => {
|
|
16146
|
+
const field = resolveField(index, s.name, s.flag);
|
|
16147
|
+
if (seen.has(field.id)) {
|
|
16148
|
+
throw new Error(
|
|
16149
|
+
`field "${field.name}" is given more than once; put all its values in one ${s.flag} "${field.name}=V1,V2"`
|
|
16150
|
+
);
|
|
16151
|
+
}
|
|
16152
|
+
seen.add(field.id);
|
|
16153
|
+
const isDate = isDateField(field);
|
|
16154
|
+
if (s.flag === "--date" && !isDate) {
|
|
16155
|
+
throw new Error(
|
|
16156
|
+
`--date "${s.name}=\u2026": "${field.name}" is a ${field.type} field, not Date; use --filter`
|
|
16157
|
+
);
|
|
16158
|
+
}
|
|
16159
|
+
let values;
|
|
16160
|
+
if (isDate) {
|
|
16161
|
+
values = s.values.map((v) => {
|
|
16162
|
+
const serial = toExcelSerial(v);
|
|
16163
|
+
if (serial === null) {
|
|
16164
|
+
throw new Error(
|
|
16165
|
+
`invalid date "${v}" for Date field "${field.name}" (${s.flag}): expected ${SERIAL_DATE_FORMS}`
|
|
16166
|
+
);
|
|
16167
|
+
}
|
|
16168
|
+
return serial;
|
|
16169
|
+
});
|
|
16170
|
+
} else if (NUMERIC_FIELD_TYPES.has(field.type.trim().toLowerCase())) {
|
|
16171
|
+
values = s.values.map((v) => /^-?\d+(\.\d+)?$/.test(v) ? Number(v) : v);
|
|
16172
|
+
} else {
|
|
16173
|
+
values = [...s.values];
|
|
16174
|
+
}
|
|
16175
|
+
return { field, isDate, values, isExcluded: s.flag === "--exclude" };
|
|
16176
|
+
});
|
|
16177
|
+
}
|
|
16178
|
+
function readableQueryError(msg) {
|
|
16179
|
+
const decoded = String(decodeHtmlEntitiesDeep(msg));
|
|
16180
|
+
const inner = [...decoded.matchAll(/\[\s*'((?:[^'\\]|\\.)*)'\s*\]/g)].map((m) => m[1]);
|
|
16181
|
+
return inner.length > 0 ? inner.join("; ") : decoded;
|
|
16182
|
+
}
|
|
16183
|
+
function rethrowEvaluatorError(err, id, what) {
|
|
16184
|
+
if (err instanceof CacheKeyHttpError) {
|
|
16185
|
+
let msg = `function ${id} ${what}: HTTP ${err.status}: ${err.bodyText}`;
|
|
16186
|
+
if (err.status === 400 && err.bodyText.includes("DateAgg")) {
|
|
16187
|
+
msg += `
|
|
16188
|
+
hint: period aggregations need a default date field \u2014 dr functions update ${id} --date-field <Date field>`;
|
|
16189
|
+
}
|
|
16190
|
+
throw new Error(msg);
|
|
16191
|
+
}
|
|
16192
|
+
throw err;
|
|
16193
|
+
}
|
|
16194
|
+
function formatEvalNumber(v) {
|
|
16195
|
+
if (v === null || v === void 0) return "-";
|
|
16196
|
+
if (typeof v !== "number") return typeof v === "string" ? v : JSON.stringify(v);
|
|
16197
|
+
if (!Number.isFinite(v) || Number.isInteger(v)) return String(v);
|
|
16198
|
+
if (Math.abs(v) < 1) return String(Number(v.toPrecision(10)));
|
|
16199
|
+
if (Math.abs(v) >= 1e15) return String(v);
|
|
16200
|
+
return String(Math.round(v * 1e4) / 1e4);
|
|
16201
|
+
}
|
|
16202
|
+
async function runFunctionsData(cfg) {
|
|
16203
|
+
if (cfg.id <= 0) throw new Error("function id must be positive");
|
|
16204
|
+
const agg = validatePeriodAgg(cfg.aggOriginal);
|
|
16205
|
+
const specs = [
|
|
16206
|
+
...parseEvalFilterFlags(cfg.filters, "--filter"),
|
|
16207
|
+
...parseEvalFilterFlags(cfg.dates, "--date")
|
|
16208
|
+
];
|
|
16209
|
+
const fn = await getDetail(cfg.client, cfg.id, { signal: cfg.signal });
|
|
16210
|
+
const resolved = specs.length === 0 ? [] : resolveEvalFilters(
|
|
16211
|
+
await loadTemplateFields(cfg.client, fn.template, { signal: cfg.signal }),
|
|
16212
|
+
specs
|
|
16213
|
+
);
|
|
16214
|
+
const body = {
|
|
16215
|
+
queries: [
|
|
16216
|
+
{
|
|
16217
|
+
client_key: "q1",
|
|
16218
|
+
agg_type: agg,
|
|
16219
|
+
fields: resolved.map((r) => ({ name: r.field.name, values: r.values }))
|
|
16220
|
+
}
|
|
16221
|
+
],
|
|
16222
|
+
global_filters: [],
|
|
16223
|
+
function_filters: []
|
|
16224
|
+
};
|
|
16225
|
+
let res;
|
|
16226
|
+
try {
|
|
16227
|
+
res = await data2(cfg.client, cfg.id, body, {
|
|
16228
|
+
signal: cfg.signal,
|
|
16229
|
+
pollIntervalMs: cfg.pollIntervalMs,
|
|
16230
|
+
sleep: cfg.sleep
|
|
16231
|
+
});
|
|
16232
|
+
} catch (err) {
|
|
16233
|
+
rethrowEvaluatorError(err, cfg.id, "data");
|
|
16234
|
+
}
|
|
16235
|
+
if (cfg.asJson) writeJSONIndented(cfg.out, res);
|
|
16236
|
+
const results = Array.isArray(res.query_results) ? res.query_results : [];
|
|
16237
|
+
const errors = results.filter((q) => q.error !== void 0 && q.error !== null);
|
|
16238
|
+
if (errors.length > 0) {
|
|
16239
|
+
throw new Error(
|
|
16240
|
+
`function ${cfg.id} data: ` + errors.map((q) => readableQueryError(String(q.error?.msg ?? ""))).join("; ")
|
|
16241
|
+
);
|
|
16242
|
+
}
|
|
16243
|
+
if (cfg.asJson) return;
|
|
16244
|
+
const values = results.flatMap((q) => Array.isArray(q.res) ? q.res : []);
|
|
16245
|
+
if (values.length === 0) {
|
|
16246
|
+
cfg.out.write("(no data: nothing matches these filters)\n");
|
|
16247
|
+
return;
|
|
16248
|
+
}
|
|
16249
|
+
const rows = [{ cells: [...resolved.map((r) => r.field.name), "VALUE"] }];
|
|
16250
|
+
for (const r of values) {
|
|
16251
|
+
const k = Array.isArray(r.k) ? r.k : [];
|
|
16252
|
+
const keyCells = k.length === resolved.length ? k.map(
|
|
16253
|
+
(kv, i) => resolved[i]?.isDate === true && typeof kv === "number" ? excelSerialToIso(kv) : formatEvalNumber(kv)
|
|
16254
|
+
) : resolved.map((_, i) => i === 0 ? JSON.stringify(k) : "");
|
|
16255
|
+
rows.push({ cells: [...keyCells, formatEvalNumber(r.v)] });
|
|
16256
|
+
}
|
|
16257
|
+
writeTable11(cfg.out, rows);
|
|
16258
|
+
}
|
|
16259
|
+
function aggregateColumn(aggfun, values) {
|
|
16260
|
+
const present = values.filter((v) => v !== null && v !== void 0 && v !== "");
|
|
16261
|
+
const nums = present.map((v) => Number(v)).filter((n) => Number.isFinite(n));
|
|
16262
|
+
switch (aggfun.trim().toUpperCase()) {
|
|
16263
|
+
case "COUNT":
|
|
16264
|
+
return present.length;
|
|
16265
|
+
case "AVG":
|
|
16266
|
+
return nums.length === 0 ? null : nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
16267
|
+
case "MIN":
|
|
16268
|
+
return nums.length === 0 ? null : Math.min(...nums);
|
|
16269
|
+
case "MAX":
|
|
16270
|
+
return nums.length === 0 ? null : Math.max(...nums);
|
|
16271
|
+
default:
|
|
16272
|
+
return nums.reduce((a, b) => a + b, 0);
|
|
16273
|
+
}
|
|
16274
|
+
}
|
|
16275
|
+
function drilldownCell(v, isDate) {
|
|
16276
|
+
if (v === null || v === void 0) return "";
|
|
16277
|
+
if (isDate && typeof v === "number") return epochSecondsToIso(v);
|
|
16278
|
+
if (typeof v === "number") return formatEvalNumber(v);
|
|
16279
|
+
if (typeof v === "string") return v;
|
|
16280
|
+
return JSON.stringify(v);
|
|
16281
|
+
}
|
|
16282
|
+
function drilldownRowsToCsv(rows, schema) {
|
|
16283
|
+
const cols = schema.fields.length > 0 ? schema.fields : Object.keys(rows[0] ?? {}).map((name) => ({ name }));
|
|
16284
|
+
const dateCols = new Set(
|
|
16285
|
+
schema.fields.filter((f) => (f.type ?? "").toLowerCase() === "date").map((f) => f.name)
|
|
16286
|
+
);
|
|
16287
|
+
const cell = (v) => {
|
|
16288
|
+
if (v === null || v === void 0) return "";
|
|
16289
|
+
if (typeof v === "string") return v;
|
|
16290
|
+
return typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
16291
|
+
};
|
|
16292
|
+
const lines = [formatCsvRow(cols.map((c) => c.name))];
|
|
16293
|
+
for (const r of rows) {
|
|
16294
|
+
lines.push(
|
|
16295
|
+
formatCsvRow(
|
|
16296
|
+
cols.map((c) => {
|
|
16297
|
+
const v = r[c.name];
|
|
16298
|
+
return dateCols.has(c.name) && typeof v === "number" ? epochSecondsToIso(v) : cell(v);
|
|
16299
|
+
})
|
|
16300
|
+
)
|
|
16301
|
+
);
|
|
16302
|
+
}
|
|
16303
|
+
return lines.join("\n") + "\n";
|
|
16304
|
+
}
|
|
16305
|
+
async function runFunctionsDrilldown(cfg) {
|
|
16306
|
+
if (cfg.id <= 0) throw new Error("function id must be positive");
|
|
16307
|
+
if (cfg.forceCsv && cfg.outPath === void 0) {
|
|
16308
|
+
throw new Error("--csv needs --out <file.csv>");
|
|
16309
|
+
}
|
|
16310
|
+
if (!Number.isInteger(cfg.limit) || cfg.limit < 0) {
|
|
16311
|
+
throw new Error("--limit must be a non-negative integer (0 = all rows)");
|
|
16312
|
+
}
|
|
16313
|
+
const agg = validatePeriodAgg(cfg.aggOriginal);
|
|
16314
|
+
const specs = [
|
|
16315
|
+
...parseEvalFilterFlags(cfg.filters, "--filter"),
|
|
16316
|
+
...parseEvalFilterFlags(cfg.excludes, "--exclude"),
|
|
16317
|
+
...parseEvalFilterFlags(cfg.dates, "--date")
|
|
16318
|
+
];
|
|
16319
|
+
const fn = await getDetail(cfg.client, cfg.id, { signal: cfg.signal });
|
|
16320
|
+
const index = await loadTemplateFields(cfg.client, fn.template, { signal: cfg.signal });
|
|
16321
|
+
const resolved = resolveEvalFilters(index, specs);
|
|
16322
|
+
const valueFieldName = index.fields.find((f) => f.id === fn.value_field)?.name ?? `field ${fn.value_field}`;
|
|
16323
|
+
const aggLabel = `${fn.aggfun === "" ? "SUM" : fn.aggfun.toUpperCase()}(${valueFieldName})`;
|
|
16324
|
+
const body = {
|
|
16325
|
+
agg_type: [agg],
|
|
16326
|
+
global_filters: [],
|
|
16327
|
+
function_filters: [
|
|
16328
|
+
resolved.map((r) => ({
|
|
16329
|
+
values: r.values,
|
|
16330
|
+
name: r.field.name,
|
|
16331
|
+
type: r.field.type,
|
|
16332
|
+
field: r.field.id,
|
|
16333
|
+
function_id: cfg.id,
|
|
16334
|
+
is_excluded: r.isExcluded,
|
|
16335
|
+
show_in_graph: false
|
|
16336
|
+
}))
|
|
16337
|
+
],
|
|
16338
|
+
table_filters: []
|
|
16339
|
+
};
|
|
16340
|
+
const writeFile6 = cfg.writeFile ?? ((p, d) => fs11.writeFile(p, d));
|
|
16341
|
+
const seams = { signal: cfg.signal, pollIntervalMs: cfg.pollIntervalMs, sleep: cfg.sleep };
|
|
16342
|
+
const exportCsv2 = async (reason) => {
|
|
16343
|
+
const outPath = cfg.outPath;
|
|
16344
|
+
let exp;
|
|
16345
|
+
try {
|
|
16346
|
+
exp = await exportDrilldownCsv(cfg.client, cfg.id, body, seams);
|
|
16347
|
+
} catch (err) {
|
|
16348
|
+
rethrowEvaluatorError(err, cfg.id, "drilldown export");
|
|
16349
|
+
}
|
|
16350
|
+
await writeFile6(outPath, exp.bytes);
|
|
16351
|
+
const table2 = parseCsvRows(new TextDecoder().decode(exp.bytes).replace(/^/, ""));
|
|
16352
|
+
const header = table2[0] ?? [];
|
|
16353
|
+
const col = header.findIndex((h) => h.trim() === valueFieldName);
|
|
16354
|
+
const dataRows = table2.slice(1);
|
|
16355
|
+
const aggValue2 = col < 0 ? null : aggregateColumn(
|
|
16356
|
+
fn.aggfun,
|
|
16357
|
+
dataRows.map((r) => r[col])
|
|
16358
|
+
);
|
|
16359
|
+
if (cfg.asJson) {
|
|
16360
|
+
writeJSONIndented(cfg.out, {
|
|
16361
|
+
function_id: cfg.id,
|
|
16362
|
+
out: outPath,
|
|
16363
|
+
source: "export_to_csv",
|
|
16364
|
+
report_id: exp.reportId,
|
|
16365
|
+
bytes: exp.bytes.length,
|
|
16366
|
+
rows: dataRows.length,
|
|
16367
|
+
columns: header.length,
|
|
16368
|
+
aggregate: { label: aggLabel, value: aggValue2 }
|
|
16369
|
+
});
|
|
16370
|
+
return;
|
|
16371
|
+
}
|
|
16372
|
+
cfg.out.write(
|
|
16373
|
+
`Wrote ${dataRows.length} rows \xD7 ${header.length} columns to ${outPath} (server CSV export${reason}, report ${exp.reportId}, ${exp.bytes.length} bytes).
|
|
16374
|
+
`
|
|
16375
|
+
);
|
|
16376
|
+
cfg.out.write(
|
|
16377
|
+
col < 0 ? `(value field "${valueFieldName}" not found in the CSV header; no aggregate)
|
|
16378
|
+
` : `${aggLabel} over these rows: ${formatEvalNumber(aggValue2)}
|
|
16379
|
+
`
|
|
16380
|
+
);
|
|
16381
|
+
};
|
|
16382
|
+
if (cfg.forceCsv) {
|
|
16383
|
+
await exportCsv2("");
|
|
16384
|
+
return;
|
|
16385
|
+
}
|
|
16386
|
+
let raw;
|
|
16387
|
+
try {
|
|
16388
|
+
raw = await drilldown(cfg.client, cfg.id, body, seams);
|
|
16389
|
+
} catch (err) {
|
|
16390
|
+
rethrowEvaluatorError(err, cfg.id, "drilldown");
|
|
16391
|
+
}
|
|
16392
|
+
const parsed2 = parseDrilldown(raw);
|
|
16393
|
+
if (parsed2.kind === "too_large") {
|
|
16394
|
+
if (cfg.outPath !== void 0) {
|
|
16395
|
+
await exportCsv2(`; inline answer too large: ${parsed2.rows_count} rows`);
|
|
16396
|
+
return;
|
|
16397
|
+
}
|
|
16398
|
+
if (cfg.asJson) writeJSONIndented(cfg.out, raw);
|
|
16399
|
+
throw new Error(
|
|
16400
|
+
`drill-down for function ${cfg.id} is too large to return inline (${parsed2.rows_count} rows \xD7 ${parsed2.cols_count} columns); re-run with --out <file.csv> to export it as CSV, or narrow it with more --filter flags`
|
|
16401
|
+
);
|
|
16402
|
+
}
|
|
16403
|
+
const { rows, schema } = parsed2;
|
|
16404
|
+
const aggValue = aggregateColumn(
|
|
16405
|
+
fn.aggfun,
|
|
16406
|
+
rows.map((r) => r[valueFieldName])
|
|
16407
|
+
);
|
|
16408
|
+
if (cfg.outPath !== void 0) {
|
|
16409
|
+
await writeFile6(cfg.outPath, drilldownRowsToCsv(rows, schema));
|
|
16410
|
+
if (cfg.asJson) {
|
|
16411
|
+
writeJSONIndented(cfg.out, {
|
|
16412
|
+
function_id: cfg.id,
|
|
16413
|
+
out: cfg.outPath,
|
|
16414
|
+
source: "inline",
|
|
16415
|
+
rows: rows.length,
|
|
16416
|
+
columns: schema.fields.length,
|
|
16417
|
+
aggregate: { label: aggLabel, value: aggValue }
|
|
16418
|
+
});
|
|
16419
|
+
return;
|
|
16420
|
+
}
|
|
16421
|
+
cfg.out.write(
|
|
16422
|
+
`Wrote ${rows.length} rows \xD7 ${schema.fields.length} columns to ${cfg.outPath} (inline drill-down).
|
|
16423
|
+
`
|
|
16424
|
+
);
|
|
16425
|
+
cfg.out.write(`${aggLabel} over these rows: ${formatEvalNumber(aggValue)}
|
|
16426
|
+
`);
|
|
16427
|
+
return;
|
|
16428
|
+
}
|
|
16429
|
+
if (cfg.asJson) {
|
|
16430
|
+
writeJSONIndented(cfg.out, raw);
|
|
16431
|
+
return;
|
|
16432
|
+
}
|
|
16433
|
+
cfg.out.write(
|
|
16434
|
+
`${rows.length} rows \xD7 ${schema.fields.length} columns (${schema.empty_fields.length} empty)
|
|
16435
|
+
`
|
|
16436
|
+
);
|
|
16437
|
+
cfg.out.write(`${aggLabel} over these rows: ${formatEvalNumber(aggValue)}
|
|
16438
|
+
`);
|
|
16439
|
+
if (rows.length === 0) return;
|
|
16440
|
+
const byName = new Map(schema.fields.map((f) => [f.name, f]));
|
|
16441
|
+
let cols;
|
|
16442
|
+
if (cfg.columns !== void 0 && cfg.columns.length > 0) {
|
|
16443
|
+
cols = cfg.columns.map((c) => {
|
|
16444
|
+
if (byName.has(c)) return c;
|
|
16445
|
+
const loose = schema.fields.filter((f) => f.name.toLowerCase() === c.toLowerCase());
|
|
16446
|
+
if (loose.length === 1) return loose[0].name;
|
|
16447
|
+
throw new Error(
|
|
16448
|
+
`unknown --columns entry "${c}"; drill-down columns: ${schema.fields.map((f) => f.name).join(", ")}`
|
|
16449
|
+
);
|
|
16450
|
+
});
|
|
16451
|
+
} else {
|
|
16452
|
+
cols = [.../* @__PURE__ */ new Set([...resolved.map((r) => r.field.name), valueFieldName])].filter(
|
|
16453
|
+
(c) => byName.has(c)
|
|
16454
|
+
);
|
|
16455
|
+
}
|
|
16456
|
+
const shown = cfg.limit === 0 ? rows : rows.slice(0, cfg.limit);
|
|
16457
|
+
cfg.out.write("\n");
|
|
16458
|
+
const table = [{ cells: cols }];
|
|
16459
|
+
for (const r of shown) {
|
|
16460
|
+
table.push({
|
|
16461
|
+
cells: cols.map(
|
|
16462
|
+
(c) => drilldownCell(r[c], (byName.get(c)?.type ?? "").toLowerCase() === "date")
|
|
16463
|
+
)
|
|
16464
|
+
});
|
|
16465
|
+
}
|
|
16466
|
+
writeTable11(cfg.out, table);
|
|
16467
|
+
if (shown.length < rows.length) {
|
|
16468
|
+
cfg.out.write(
|
|
16469
|
+
`(showing ${shown.length} of ${rows.length} rows \u2014 --limit 0 for all, --columns to pick columns, --out <file.csv> for every column)
|
|
16470
|
+
`
|
|
16471
|
+
);
|
|
16472
|
+
}
|
|
16473
|
+
}
|
|
15787
16474
|
function writeTable11(out, rows) {
|
|
15788
16475
|
if (rows.length === 0) return;
|
|
15789
16476
|
const widths = [];
|
|
@@ -15859,6 +16546,10 @@ function parseIntFlag(raw, flagName) {
|
|
|
15859
16546
|
}
|
|
15860
16547
|
return n;
|
|
15861
16548
|
}
|
|
16549
|
+
function collectFlag(value, previous) {
|
|
16550
|
+
return [...previous, value];
|
|
16551
|
+
}
|
|
16552
|
+
var DRILLDOWN_EXPORT_TIMEOUT_MS = 3e5;
|
|
15862
16553
|
function registerFunctionsCommand(root) {
|
|
15863
16554
|
const fn = root.command("functions").description("Operations on Datarails template functions");
|
|
15864
16555
|
const listCmd = fn.command("list").description("List functions on this tenant").option("--json", "Output raw JSON instead of a table", false).action(async (opts) => {
|
|
@@ -15893,7 +16584,10 @@ function registerFunctionsCommand(root) {
|
|
|
15893
16584
|
});
|
|
15894
16585
|
addServerFlag(getCmd);
|
|
15895
16586
|
addAccountFlag(getCmd);
|
|
15896
|
-
const createCmd = fn.command("create").description("Create a new function").requiredOption("--name <name>", "Function name (no whitespace; required)").requiredOption("--template <id>", "Template ID (required)").requiredOption("--field <id>", "Value field ID (required)").option("--agg <kind>", "Aggregation: SUM, COUNT, AVG, MIN, MAX (default: SUM)", "SUM").option(
|
|
16587
|
+
const createCmd = fn.command("create").description("Create a new function").requiredOption("--name <name>", "Function name (no whitespace; required)").requiredOption("--template <id>", "Template ID (required)").requiredOption("--field <id>", "Value field ID (required)").option("--agg <kind>", "Aggregation: SUM, COUNT, AVG, MIN, MAX (default: SUM)", "SUM").option(
|
|
16588
|
+
"--date-field <id|name>",
|
|
16589
|
+
"Default aggregation date field (a Date field of the template). Needed for DR.YTD/QTD/MTD/\u2026 and `functions data --agg YTD`; without it a warning is printed"
|
|
16590
|
+
).option("--json", "Output raw JSON instead of a table", false).action(
|
|
15897
16591
|
async (opts) => {
|
|
15898
16592
|
const template = parseIntFlag(opts.template, "--template");
|
|
15899
16593
|
const valueField = parseIntFlag(opts.field, "--field");
|
|
@@ -15905,6 +16599,7 @@ function registerFunctionsCommand(root) {
|
|
|
15905
16599
|
template,
|
|
15906
16600
|
valueField,
|
|
15907
16601
|
aggOriginal: opts.agg ?? "SUM",
|
|
16602
|
+
dateField: opts.dateField,
|
|
15908
16603
|
asJson: opts.json === true,
|
|
15909
16604
|
signal: setup.signal,
|
|
15910
16605
|
out: process.stdout
|
|
@@ -15918,7 +16613,10 @@ function registerFunctionsCommand(root) {
|
|
|
15918
16613
|
addAccountFlag(createCmd);
|
|
15919
16614
|
const updateCmd = fn.command("update").description(
|
|
15920
16615
|
"Update a function (name-only routes to the rename endpoint; structural changes read-modify-write a full PUT)"
|
|
15921
|
-
).argument("<id>", "function id").option("--name <name>", "New function name (no whitespace)").option("--field <id>", "New value field ID").option("--agg <kind>", "New aggregation: SUM, COUNT, AVG, MIN, MAX").option("--template <id>", "New template ID").option(
|
|
16616
|
+
).argument("<id>", "function id").option("--name <name>", "New function name (no whitespace)").option("--field <id>", "New value field ID").option("--agg <kind>", "New aggregation: SUM, COUNT, AVG, MIN, MAX").option("--template <id>", "New template ID").option(
|
|
16617
|
+
"--date-field <id|name>",
|
|
16618
|
+
"Set the default aggregation date field (a Date field) used by DR.YTD/QTD/MTD/\u2026"
|
|
16619
|
+
).option("--json", "Output raw JSON instead of a table", false).action(
|
|
15922
16620
|
async (idStr, opts) => {
|
|
15923
16621
|
const id = parseFunctionIdArg(idStr);
|
|
15924
16622
|
const field = opts.field !== void 0 ? parseIntFlag(opts.field, "--field") : void 0;
|
|
@@ -15932,6 +16630,7 @@ function registerFunctionsCommand(root) {
|
|
|
15932
16630
|
field,
|
|
15933
16631
|
aggOriginal: opts.agg,
|
|
15934
16632
|
template,
|
|
16633
|
+
dateField: opts.dateField,
|
|
15935
16634
|
asJson: opts.json === true,
|
|
15936
16635
|
signal: setup.signal,
|
|
15937
16636
|
out: process.stdout
|
|
@@ -15977,6 +16676,106 @@ function registerFunctionsCommand(root) {
|
|
|
15977
16676
|
);
|
|
15978
16677
|
addServerFlag(setFiltersCmd);
|
|
15979
16678
|
addAccountFlag(setFiltersCmd);
|
|
16679
|
+
const dataCmd = fn.command("data").description(
|
|
16680
|
+
"Evaluate a function the way the Excel add-in does (PUT /api/functions/<id>/data/v2): one value per filter-value combination; dates are sent as Excel serials"
|
|
16681
|
+
).argument("<id>", "function id").option(
|
|
16682
|
+
"--filter <NAME=V1,V2>",
|
|
16683
|
+
"Dimension filter by field name (or id); repeatable. Several values return one result per value. A value on a Date field is converted like --date",
|
|
16684
|
+
collectFlag,
|
|
16685
|
+
[]
|
|
16686
|
+
).option(
|
|
16687
|
+
"--date <NAME=DATE[,DATE]>",
|
|
16688
|
+
'Date filter, e.g. "Reporting Date=2025-01-31" (YYYY-MM-DD, M/D/YYYY, an ISO datetime, an Excel serial, or epoch seconds; a bare month is refused); repeatable',
|
|
16689
|
+
collectFlag,
|
|
16690
|
+
[]
|
|
16691
|
+
).option(
|
|
16692
|
+
"--agg <period>",
|
|
16693
|
+
"Period aggregation: DEFAULT, YTD, YTG, QTD, QTG, MTD, MTG",
|
|
16694
|
+
"DEFAULT"
|
|
16695
|
+
).option("--timeout <seconds>", "Whole-command budget in seconds (default 30)").option("--json", "Print the server body verbatim", false).action(
|
|
16696
|
+
async (idStr, opts) => {
|
|
16697
|
+
const id = parseFunctionIdArg(idStr);
|
|
16698
|
+
validatePeriodAgg(opts.agg ?? "DEFAULT");
|
|
16699
|
+
const setup = await setupCall(opts.server, opts.account, batchSetupOptions(opts.timeout));
|
|
16700
|
+
try {
|
|
16701
|
+
await runFunctionsData({
|
|
16702
|
+
client: setup.client,
|
|
16703
|
+
id,
|
|
16704
|
+
filters: opts.filter,
|
|
16705
|
+
dates: opts.date,
|
|
16706
|
+
aggOriginal: opts.agg ?? "DEFAULT",
|
|
16707
|
+
asJson: opts.json === true,
|
|
16708
|
+
signal: setup.signal,
|
|
16709
|
+
out: process.stdout
|
|
16710
|
+
});
|
|
16711
|
+
} finally {
|
|
16712
|
+
setup.cleanup();
|
|
16713
|
+
}
|
|
16714
|
+
}
|
|
16715
|
+
);
|
|
16716
|
+
addServerFlag(dataCmd);
|
|
16717
|
+
addAccountFlag(dataCmd);
|
|
16718
|
+
const drilldownCmd = fn.command("drilldown").description(
|
|
16719
|
+
"The rows behind a function value (PUT /api/functions/<id>/drilldown/v4); with --out, a result too large to return inline is exported as CSV automatically"
|
|
16720
|
+
).argument("<id>", "function id").option(
|
|
16721
|
+
"--filter <NAME=V1,V2>",
|
|
16722
|
+
"Dimension filter by field name (or id); repeatable",
|
|
16723
|
+
collectFlag,
|
|
16724
|
+
[]
|
|
16725
|
+
).option(
|
|
16726
|
+
"--exclude <NAME=V1,V2>",
|
|
16727
|
+
"Exclude these values of a field; repeatable",
|
|
16728
|
+
collectFlag,
|
|
16729
|
+
[]
|
|
16730
|
+
).option(
|
|
16731
|
+
"--date <NAME=DATE[,DATE]>",
|
|
16732
|
+
'Date filter, e.g. "Reporting Date=2025-01-31"; repeatable',
|
|
16733
|
+
collectFlag,
|
|
16734
|
+
[]
|
|
16735
|
+
).option(
|
|
16736
|
+
"--agg <period>",
|
|
16737
|
+
"Period aggregation: DEFAULT, YTD, YTG, QTD, QTG, MTD, MTG",
|
|
16738
|
+
"DEFAULT"
|
|
16739
|
+
).option(
|
|
16740
|
+
"--columns <names>",
|
|
16741
|
+
"Comma-separated columns to print (default: the filtered fields and the value field)"
|
|
16742
|
+
).option("--limit <n>", "Rows to print; 0 = all", "20").option(
|
|
16743
|
+
"--out <file.csv>",
|
|
16744
|
+
"Write every row and column to a CSV file; a result over the inline cap is fetched through the server CSV export"
|
|
16745
|
+
).option("--csv", "Always use the server CSV export (needs --out)", false).option(
|
|
16746
|
+
"--timeout <seconds>",
|
|
16747
|
+
"Whole-command budget in seconds (default 30, or 300 with --out)"
|
|
16748
|
+
).option("--json", "Print the server body verbatim (with --out: a JSON summary)", false).action(
|
|
16749
|
+
async (idStr, opts) => {
|
|
16750
|
+
const id = parseFunctionIdArg(idStr);
|
|
16751
|
+
validatePeriodAgg(opts.agg ?? "DEFAULT");
|
|
16752
|
+
const limit = parseIntFlag(opts.limit ?? "20", "--limit");
|
|
16753
|
+
const columns = opts.columns === void 0 ? void 0 : opts.columns.split(",").map((c) => c.trim()).filter((c) => c !== "");
|
|
16754
|
+
const setupOpts = opts.timeout !== void 0 ? batchSetupOptions(opts.timeout) : opts.out !== void 0 ? { timeoutMs: DRILLDOWN_EXPORT_TIMEOUT_MS } : {};
|
|
16755
|
+
const setup = await setupCall(opts.server, opts.account, setupOpts);
|
|
16756
|
+
try {
|
|
16757
|
+
await runFunctionsDrilldown({
|
|
16758
|
+
client: setup.client,
|
|
16759
|
+
id,
|
|
16760
|
+
filters: opts.filter,
|
|
16761
|
+
excludes: opts.exclude,
|
|
16762
|
+
dates: opts.date,
|
|
16763
|
+
aggOriginal: opts.agg ?? "DEFAULT",
|
|
16764
|
+
columns,
|
|
16765
|
+
limit,
|
|
16766
|
+
outPath: opts.out,
|
|
16767
|
+
forceCsv: opts.csv === true,
|
|
16768
|
+
asJson: opts.json === true,
|
|
16769
|
+
signal: setup.signal,
|
|
16770
|
+
out: process.stdout
|
|
16771
|
+
});
|
|
16772
|
+
} finally {
|
|
16773
|
+
setup.cleanup();
|
|
16774
|
+
}
|
|
16775
|
+
}
|
|
16776
|
+
);
|
|
16777
|
+
addServerFlag(drilldownCmd);
|
|
16778
|
+
addAccountFlag(drilldownCmd);
|
|
15980
16779
|
const deleteCmd = fn.command("delete").description("Delete a function by ID (two-step confirm)").argument("<id>", "function id (positive integer)").option(
|
|
15981
16780
|
"--confirm-token <token>",
|
|
15982
16781
|
"Token from a prior plan run \u2014 required to actually delete",
|
|
@@ -16025,7 +16824,7 @@ async function get7(client, alias, options) {
|
|
|
16025
16824
|
}
|
|
16026
16825
|
return out;
|
|
16027
16826
|
}
|
|
16028
|
-
async function
|
|
16827
|
+
async function data3(client, alias, params, options) {
|
|
16029
16828
|
const path11 = `/finance-os/api/semantictables/v1/${rawPathSegmentEscape(alias)}/data`;
|
|
16030
16829
|
const body = {
|
|
16031
16830
|
limit: params.rows,
|
|
@@ -16087,7 +16886,7 @@ async function runSemanticTablesData(cfg) {
|
|
|
16087
16886
|
}
|
|
16088
16887
|
const parsedFilters = parseAggregateFilters(cfg.filters, cfg.excludes);
|
|
16089
16888
|
const select = parseSelectFields(cfg.fields);
|
|
16090
|
-
const env = await
|
|
16889
|
+
const env = await data3(
|
|
16091
16890
|
cfg.client,
|
|
16092
16891
|
cfg.alias,
|
|
16093
16892
|
{
|
|
@@ -17420,7 +18219,7 @@ async function saveMappings(client, documentId, body, options = {}) {
|
|
|
17420
18219
|
if (first.status === 200) {
|
|
17421
18220
|
return toResult(first.text);
|
|
17422
18221
|
}
|
|
17423
|
-
const cacheKey =
|
|
18222
|
+
const cacheKey = extractCacheKey2(first.text);
|
|
17424
18223
|
if (cacheKey === "") {
|
|
17425
18224
|
throw new Error(
|
|
17426
18225
|
`POST ${path11}: HTTP ${first.status} without a cache_key to join: ${truncateBody(first.text)}`
|
|
@@ -17448,7 +18247,7 @@ async function postOnce(client, path11, payload, signal) {
|
|
|
17448
18247
|
}
|
|
17449
18248
|
throw new Error(`POST ${path11}: HTTP ${res.status}: ${truncateBody(text2)}`);
|
|
17450
18249
|
}
|
|
17451
|
-
function
|
|
18250
|
+
function extractCacheKey2(text2) {
|
|
17452
18251
|
try {
|
|
17453
18252
|
const parsed2 = JSON.parse(text2);
|
|
17454
18253
|
return typeof parsed2.cache_key === "string" ? parsed2.cache_key : "";
|
|
@@ -18230,14 +19029,14 @@ async function runLutData(cfg) {
|
|
|
18230
19029
|
if (cfg.lutId <= 0) {
|
|
18231
19030
|
throw new Error("lut id must be positive");
|
|
18232
19031
|
}
|
|
18233
|
-
const
|
|
19032
|
+
const data4 = await getMappedData(cfg.client, cfg.documentId, cfg.lutId, {
|
|
18234
19033
|
signal: cfg.signal
|
|
18235
19034
|
});
|
|
18236
19035
|
if (cfg.asJson) {
|
|
18237
|
-
writeJSONIndented(cfg.out,
|
|
19036
|
+
writeJSONIndented(cfg.out, data4);
|
|
18238
19037
|
return;
|
|
18239
19038
|
}
|
|
18240
|
-
const keys2 = Object.keys(
|
|
19039
|
+
const keys2 = Object.keys(data4);
|
|
18241
19040
|
if (keys2.length === 0) {
|
|
18242
19041
|
cfg.out.write(`(LUT ${cfg.lutId} on document ${cfg.documentId} is empty or unmapped)
|
|
18243
19042
|
`);
|
|
@@ -18246,7 +19045,7 @@ async function runLutData(cfg) {
|
|
|
18246
19045
|
const sortedKeys = [...keys2].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
18247
19046
|
const rows = [{ cells: ["KEY", "VALUE"] }];
|
|
18248
19047
|
for (const k of sortedKeys) {
|
|
18249
|
-
rows.push({ cells: [k, renderLutValue(
|
|
19048
|
+
rows.push({ cells: [k, renderLutValue(data4[k])] });
|
|
18250
19049
|
}
|
|
18251
19050
|
writeTable15(cfg.out, rows);
|
|
18252
19051
|
cfg.out.write(`
|
|
@@ -20337,14 +21136,14 @@ function renderDataflow(out, items) {
|
|
|
20337
21136
|
];
|
|
20338
21137
|
const group = groups[col] ?? [];
|
|
20339
21138
|
for (const it of group) {
|
|
20340
|
-
const
|
|
21139
|
+
const data4 = extractData(it.data);
|
|
20341
21140
|
rows.push({
|
|
20342
21141
|
cells: [
|
|
20343
21142
|
String(it.order),
|
|
20344
21143
|
it.type,
|
|
20345
|
-
String(
|
|
20346
|
-
|
|
20347
|
-
|
|
21144
|
+
String(data4.entityId ?? 0),
|
|
21145
|
+
data4.name ?? "",
|
|
21146
|
+
data4.statusType ?? "",
|
|
20348
21147
|
String(it.connections.length),
|
|
20349
21148
|
String(it.children.length)
|
|
20350
21149
|
]
|
|
@@ -20445,7 +21244,7 @@ async function getData2(client, params, options) {
|
|
|
20445
21244
|
}
|
|
20446
21245
|
return out;
|
|
20447
21246
|
}
|
|
20448
|
-
async function
|
|
21247
|
+
async function drilldown2(client, params, options) {
|
|
20449
21248
|
const path11 = "/finance-os/api/metrics/v1/drill_down";
|
|
20450
21249
|
const body = {
|
|
20451
21250
|
metric_ids: params.metric_ids
|
|
@@ -21711,8 +22510,8 @@ function detectApiTruncation(env) {
|
|
|
21711
22510
|
}
|
|
21712
22511
|
return false;
|
|
21713
22512
|
}
|
|
21714
|
-
function readCountUniqueRows(
|
|
21715
|
-
const rows = Array.isArray(
|
|
22513
|
+
function readCountUniqueRows(data4, field) {
|
|
22514
|
+
const rows = Array.isArray(data4) ? data4 : null;
|
|
21716
22515
|
if (rows === null) return null;
|
|
21717
22516
|
let best = null;
|
|
21718
22517
|
for (const row of rows) {
|
|
@@ -25264,12 +26063,12 @@ async function runMetricsList(cfg) {
|
|
|
25264
26063
|
if (cfg.raw) {
|
|
25265
26064
|
const env2 = await listRaw2(cfg.client, { signal: cfg.signal });
|
|
25266
26065
|
envelopeErr(env2.success, env2.error);
|
|
25267
|
-
const
|
|
26066
|
+
const data4 = decodeHtmlEntitiesDeep(env2.data);
|
|
25268
26067
|
if (cfg.asJson) {
|
|
25269
|
-
writeJSONIndented(cfg.out,
|
|
26068
|
+
writeJSONIndented(cfg.out, data4);
|
|
25270
26069
|
return;
|
|
25271
26070
|
}
|
|
25272
|
-
renderMetricsRawTable(cfg.out,
|
|
26071
|
+
renderMetricsRawTable(cfg.out, data4);
|
|
25273
26072
|
return;
|
|
25274
26073
|
}
|
|
25275
26074
|
const env = await listSemantic(cfg.client, {
|
|
@@ -25367,7 +26166,7 @@ async function runMetricsDrilldown(cfg) {
|
|
|
25367
26166
|
throw new Error(`--id list capped at 50 (got ${cfg.ids.length})`);
|
|
25368
26167
|
}
|
|
25369
26168
|
const dimFilters = parseDimensionFilters(cfg.filters);
|
|
25370
|
-
const env = await
|
|
26169
|
+
const env = await drilldown2(
|
|
25371
26170
|
cfg.client,
|
|
25372
26171
|
{
|
|
25373
26172
|
metric_ids: cfg.ids,
|
|
@@ -25814,83 +26613,13 @@ async function getTemplateFields(client, templateId, options) {
|
|
|
25814
26613
|
}
|
|
25815
26614
|
return result;
|
|
25816
26615
|
}
|
|
25817
|
-
async function putDataOnce(client, path11, body, options) {
|
|
25818
|
-
const res = await client.do("PUT", path11, body, options);
|
|
25819
|
-
const text2 = await res.text();
|
|
25820
|
-
if (res.status === 401 || res.status === 403) {
|
|
25821
|
-
throw new Error(`PUT ${path11}: HTTP ${res.status}: ${truncate5(text2, 200)}`);
|
|
25822
|
-
}
|
|
25823
|
-
if (res.status !== 200 && res.status !== 409 && (res.status < 200 || res.status >= 300)) {
|
|
25824
|
-
throw new Error(`PUT ${path11}: HTTP ${res.status}: ${truncate5(text2, 200)}`);
|
|
25825
|
-
}
|
|
25826
|
-
if (text2 === "" || text2 === '""') {
|
|
25827
|
-
return { status: res.status, parsed: null };
|
|
25828
|
-
}
|
|
25829
|
-
try {
|
|
25830
|
-
return { status: res.status, parsed: JSON.parse(text2) };
|
|
25831
|
-
} catch (err) {
|
|
25832
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
25833
|
-
throw new Error(`PUT ${path11}: decode: ${msg}`);
|
|
25834
|
-
}
|
|
25835
|
-
}
|
|
25836
|
-
var WIDGET_DATA_POLL_INTERVAL_MS = 500;
|
|
25837
|
-
var defaultSleep6 = (ms, signal) => new Promise((resolve3, reject) => {
|
|
25838
|
-
if (signal?.aborted) {
|
|
25839
|
-
reject(signal.reason ?? new Error("aborted"));
|
|
25840
|
-
return;
|
|
25841
|
-
}
|
|
25842
|
-
const onAbort = () => {
|
|
25843
|
-
clearTimeout(t);
|
|
25844
|
-
reject(signal?.reason ?? new Error("aborted"));
|
|
25845
|
-
};
|
|
25846
|
-
const t = setTimeout(() => {
|
|
25847
|
-
signal?.removeEventListener("abort", onAbort);
|
|
25848
|
-
resolve3();
|
|
25849
|
-
}, ms);
|
|
25850
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
25851
|
-
});
|
|
25852
26616
|
async function putDataWithPolling(client, dashboardId, id, payload, options = {}) {
|
|
25853
|
-
|
|
25854
|
-
|
|
25855
|
-
|
|
25856
|
-
|
|
25857
|
-
|
|
25858
|
-
|
|
25859
|
-
return first.parsed;
|
|
25860
|
-
}
|
|
25861
|
-
const task = first.parsed !== null && typeof first.parsed === "object" ? first.parsed : {};
|
|
25862
|
-
const cacheKey = typeof task["cache_key"] === "string" ? task["cache_key"] : "";
|
|
25863
|
-
if (cacheKey === "") {
|
|
25864
|
-
throw new Error(
|
|
25865
|
-
`PUT ${path11}: 409 without cache_key: ${truncate5(JSON.stringify(first.parsed ?? ""), 200)}`
|
|
25866
|
-
);
|
|
25867
|
-
}
|
|
25868
|
-
const pollPath = `${path11}?cache_key=${encodeURIComponent(cacheKey)}`;
|
|
25869
|
-
for (; ; ) {
|
|
25870
|
-
try {
|
|
25871
|
-
await sleep4(interval, options.signal);
|
|
25872
|
-
} catch (err) {
|
|
25873
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
25874
|
-
throw new Error(`widget data polling timed out: ${reason}`);
|
|
25875
|
-
}
|
|
25876
|
-
let result;
|
|
25877
|
-
try {
|
|
25878
|
-
result = await putDataOnce(client, pollPath, body, options);
|
|
25879
|
-
} catch (err) {
|
|
25880
|
-
if (options.signal?.aborted) {
|
|
25881
|
-
const reason = options.signal.reason instanceof Error ? options.signal.reason.message : String(options.signal.reason ?? "aborted");
|
|
25882
|
-
throw new Error(`widget data polling timed out: ${reason}`);
|
|
25883
|
-
}
|
|
25884
|
-
throw err;
|
|
25885
|
-
}
|
|
25886
|
-
if (result.status === 200) {
|
|
25887
|
-
return result.parsed;
|
|
25888
|
-
}
|
|
25889
|
-
}
|
|
25890
|
-
}
|
|
25891
|
-
function truncate5(s, n) {
|
|
25892
|
-
if (s.length <= n) return s;
|
|
25893
|
-
return s.slice(0, n) + "\u2026";
|
|
26617
|
+
return putWithCacheKeyPolling(
|
|
26618
|
+
client,
|
|
26619
|
+
`/api/dashboards/${dashboardId}/widgets/${id}/data`,
|
|
26620
|
+
payload,
|
|
26621
|
+
{ ...options, label: "widget data" }
|
|
26622
|
+
);
|
|
25894
26623
|
}
|
|
25895
26624
|
|
|
25896
26625
|
// src/commands/_shellQuote.ts
|
|
@@ -29967,16 +30696,16 @@ function validateCalcFields(entries) {
|
|
|
29967
30696
|
if (isLikelyValidCalcFormula(entry.formula)) continue;
|
|
29968
30697
|
const name = entry.name === "" ? "(unnamed)" : entry.name;
|
|
29969
30698
|
const trimmed = entry.formula.trim();
|
|
29970
|
-
const
|
|
29971
|
-
throw new Error(formatCalcFieldError(name,
|
|
30699
|
+
const preview2 = trimmed.slice(0, 80);
|
|
30700
|
+
throw new Error(formatCalcFieldError(name, preview2, trimmed));
|
|
29972
30701
|
}
|
|
29973
30702
|
const arityViolations = findArityViolations(entries);
|
|
29974
30703
|
if (arityViolations.length > 0) {
|
|
29975
30704
|
throw new Error(formatArityViolation(arityViolations[0]));
|
|
29976
30705
|
}
|
|
29977
30706
|
}
|
|
29978
|
-
function formatCalcFieldError(name,
|
|
29979
|
-
const head = `calculated_fields["${name}"]: formula "${
|
|
30707
|
+
function formatCalcFieldError(name, preview2, full) {
|
|
30708
|
+
const head = `calculated_fields["${name}"]: formula "${preview2}"`;
|
|
29980
30709
|
if (full === "") {
|
|
29981
30710
|
return `${head} is empty \u2014 every calc field needs a formula.`;
|
|
29982
30711
|
}
|
|
@@ -34763,7 +35492,7 @@ async function captureDataQuerySyncBaseline(client, fileboxId, periods, signal)
|
|
|
34763
35492
|
);
|
|
34764
35493
|
}
|
|
34765
35494
|
}
|
|
34766
|
-
var
|
|
35495
|
+
var defaultSleep6 = (ms, signal) => new Promise((resolve3, reject) => {
|
|
34767
35496
|
if (signal?.aborted) {
|
|
34768
35497
|
reject(signal.reason ?? new Error("aborted"));
|
|
34769
35498
|
return;
|
|
@@ -34781,7 +35510,7 @@ var defaultSleep7 = (ms, signal) => new Promise((resolve3, reject) => {
|
|
|
34781
35510
|
async function waitForDataQuerySync(cfg) {
|
|
34782
35511
|
let interval = cfg.pollIntervalMs ?? DQ_WAIT_POLL_INTERVAL_MS;
|
|
34783
35512
|
const maxInterval = cfg.maxPollIntervalMs ?? DQ_WAIT_POLL_MAX_INTERVAL_MS;
|
|
34784
|
-
const sleep4 = cfg.sleep ??
|
|
35513
|
+
const sleep4 = cfg.sleep ?? defaultSleep6;
|
|
34785
35514
|
const now = cfg.now ?? Date.now;
|
|
34786
35515
|
const start = now();
|
|
34787
35516
|
const deadline = start + cfg.timeoutSec * 1e3;
|
|
@@ -38542,7 +39271,7 @@ function syncCallTimeoutMs(waitMs) {
|
|
|
38542
39271
|
if (waitMs <= 0) return void 0;
|
|
38543
39272
|
return waitMs + SYNC_SETUP_OVERHEAD_MS;
|
|
38544
39273
|
}
|
|
38545
|
-
var
|
|
39274
|
+
var defaultSleep7 = (ms, signal) => new Promise((resolve3, reject) => {
|
|
38546
39275
|
if (signal?.aborted) {
|
|
38547
39276
|
reject(signal.reason ?? new Error("aborted"));
|
|
38548
39277
|
return;
|
|
@@ -38562,7 +39291,7 @@ async function runIntegrationsSync(cfg) {
|
|
|
38562
39291
|
throw new Error("integration id must be positive");
|
|
38563
39292
|
}
|
|
38564
39293
|
const interval = cfg.pollIntervalMs ?? INTEGRATIONS_SYNC_POLL_INTERVAL_MS;
|
|
38565
|
-
const sleep4 = cfg.sleep ??
|
|
39294
|
+
const sleep4 = cfg.sleep ?? defaultSleep7;
|
|
38566
39295
|
const resp = await resync(cfg.client, cfg.id, {
|
|
38567
39296
|
signal: cfg.signal
|
|
38568
39297
|
});
|
|
@@ -44235,7 +44964,7 @@ async function runReconciliationAccountsUpdateSettings(cfg) {
|
|
|
44235
44964
|
const current = await getAccountSettings(cfg.client, {}, { signal: cfg.signal });
|
|
44236
44965
|
const currentByKey = new Map(current.map((s) => [settingsKey(s), s]));
|
|
44237
44966
|
const changed = [];
|
|
44238
|
-
const
|
|
44967
|
+
const preview2 = [];
|
|
44239
44968
|
for (const want of cfg.settings) {
|
|
44240
44969
|
const have = currentByKey.get(settingsKey(want));
|
|
44241
44970
|
let what;
|
|
@@ -44247,8 +44976,8 @@ async function runReconciliationAccountsUpdateSettings(cfg) {
|
|
|
44247
44976
|
what = diffs.join(", ");
|
|
44248
44977
|
}
|
|
44249
44978
|
changed.push(want);
|
|
44250
|
-
if (
|
|
44251
|
-
|
|
44979
|
+
if (preview2.length < 5) {
|
|
44980
|
+
preview2.push(` ${want.entity ?? ""} / ${want.account_group ?? ""} / ${want.name}: ${what}`);
|
|
44252
44981
|
}
|
|
44253
44982
|
}
|
|
44254
44983
|
const unchanged = n - changed.length;
|
|
@@ -44275,10 +45004,10 @@ async function runReconciliationAccountsUpdateSettings(cfg) {
|
|
|
44275
45004
|
`unchanged: ${unchanged} (already match the tenant \u2014 skipped)`,
|
|
44276
45005
|
`by frequency: ${[...byFrequency].map(([f, c]) => `${f} ${c}`).join(", ")}`,
|
|
44277
45006
|
"changes:",
|
|
44278
|
-
...
|
|
45007
|
+
...preview2
|
|
44279
45008
|
];
|
|
44280
|
-
if (changed.length >
|
|
44281
|
-
lines.push(` \u2026 (+${changed.length -
|
|
45009
|
+
if (changed.length > preview2.length) {
|
|
45010
|
+
lines.push(` \u2026 (+${changed.length - preview2.length} more)`);
|
|
44282
45011
|
}
|
|
44283
45012
|
return lines;
|
|
44284
45013
|
},
|
|
@@ -44704,16 +45433,16 @@ verify the date matches the accounts' reporting period (try \`dr reconciliation
|
|
|
44704
45433
|
describe: async () => {
|
|
44705
45434
|
let signOffsReset = 0;
|
|
44706
45435
|
let bootstraps = 0;
|
|
44707
|
-
const
|
|
45436
|
+
const preview2 = [];
|
|
44708
45437
|
for (const id of accountIds) {
|
|
44709
45438
|
const account = byId.get(id);
|
|
44710
45439
|
if (account.members.length === 0) bootstraps += 1;
|
|
44711
45440
|
for (const r of changesByAccount.get(id)) {
|
|
44712
45441
|
const current = account.members.find((m) => m.type === r.row.type);
|
|
44713
45442
|
if (current?.is_sign_off === true) signOffsReset += 1;
|
|
44714
|
-
if (
|
|
45443
|
+
if (preview2.length < 5) {
|
|
44715
45444
|
const from = current === void 0 ? "(no slot)" : `${current.name} (user ${current.user_id})`;
|
|
44716
|
-
|
|
45445
|
+
preview2.push(
|
|
44717
45446
|
` ${account.account_name} (${id}) ${r.row.type}: ${from} \u2192 ${r.name} (user ${r.row.userId})`
|
|
44718
45447
|
);
|
|
44719
45448
|
}
|
|
@@ -44730,8 +45459,8 @@ verify the date matches the accounts' reporting period (try \`dr reconciliation
|
|
|
44730
45459
|
`no member slots yet: ${bootstraps} account${plural(bootstraps)} \u2014 both slots are initialized, the unlisted role as Unassigned`
|
|
44731
45460
|
);
|
|
44732
45461
|
}
|
|
44733
|
-
lines.push("changes:", ...
|
|
44734
|
-
if (changeCount >
|
|
45462
|
+
lines.push("changes:", ...preview2);
|
|
45463
|
+
if (changeCount > preview2.length) lines.push(` \u2026 (+${changeCount - preview2.length} more)`);
|
|
44735
45464
|
return lines;
|
|
44736
45465
|
},
|
|
44737
45466
|
execute: async () => {
|
|
@@ -44829,13 +45558,13 @@ verify the date matches the accounts' reporting period (try \`dr reconciliation
|
|
|
44829
45558
|
confirmCmd: cfg.confirmCmd,
|
|
44830
45559
|
describe: async () => {
|
|
44831
45560
|
let signOffsReset = 0;
|
|
44832
|
-
const
|
|
45561
|
+
const preview2 = [];
|
|
44833
45562
|
for (const id of accountIds) {
|
|
44834
45563
|
const account = byId.get(id);
|
|
44835
45564
|
for (const { row, current } of clearsByAccount.get(id)) {
|
|
44836
45565
|
if (current.is_sign_off) signOffsReset += 1;
|
|
44837
|
-
if (
|
|
44838
|
-
|
|
45566
|
+
if (preview2.length < 5) {
|
|
45567
|
+
preview2.push(
|
|
44839
45568
|
` ${account.account_name} (${id}) ${row.type}: ${current.name} (user ${current.user_id}) \u2192 Unassigned`
|
|
44840
45569
|
);
|
|
44841
45570
|
}
|
|
@@ -44847,9 +45576,9 @@ verify the date matches the accounts' reporting period (try \`dr reconciliation
|
|
|
44847
45576
|
`unchanged: ${unchanged.length} (slot already Unassigned, or the account has no member slots \u2014 skipped)`,
|
|
44848
45577
|
`sign-offs reset: ${signOffsReset}`,
|
|
44849
45578
|
"changes:",
|
|
44850
|
-
...
|
|
45579
|
+
...preview2
|
|
44851
45580
|
];
|
|
44852
|
-
if (clearCount >
|
|
45581
|
+
if (clearCount > preview2.length) lines.push(` \u2026 (+${clearCount - preview2.length} more)`);
|
|
44853
45582
|
return lines;
|
|
44854
45583
|
},
|
|
44855
45584
|
execute: async () => {
|
|
@@ -46051,9 +46780,9 @@ function sanitizeProse(input, maxLen) {
|
|
|
46051
46780
|
}
|
|
46052
46781
|
return s;
|
|
46053
46782
|
}
|
|
46054
|
-
function applyProse(
|
|
46783
|
+
function applyProse(data4, overlay, warn2 = () => {
|
|
46055
46784
|
}) {
|
|
46056
|
-
const out = JSON.parse(JSON.stringify(
|
|
46785
|
+
const out = JSON.parse(JSON.stringify(data4));
|
|
46057
46786
|
const rowProse = overlay.rowProse ?? {};
|
|
46058
46787
|
const byId = new Map(
|
|
46059
46788
|
out.rows.filter((r) => "id" in r).map((r) => [r.id, r])
|
|
@@ -46080,11 +46809,11 @@ function applyProse(data3, overlay, warn2 = () => {
|
|
|
46080
46809
|
import { readFile as readFile6 } from "fs/promises";
|
|
46081
46810
|
import { join as join12 } from "path";
|
|
46082
46811
|
var MARKER = "<!--__DATA_BLOCK__-->";
|
|
46083
|
-
function renderReportHtml(
|
|
46812
|
+
function renderReportHtml(data4, templateHtml) {
|
|
46084
46813
|
if (!templateHtml.includes(MARKER)) {
|
|
46085
46814
|
throw new Error(`report template is missing the ${MARKER} marker`);
|
|
46086
46815
|
}
|
|
46087
|
-
const json = JSON.stringify(
|
|
46816
|
+
const json = JSON.stringify(data4).replace(/</g, "\\u003c");
|
|
46088
46817
|
const block = `<script>window.__DATA__ = ${json};</script>`;
|
|
46089
46818
|
return templateHtml.replace(MARKER, block);
|
|
46090
46819
|
}
|
|
@@ -46109,25 +46838,25 @@ function validateReportType(raw) {
|
|
|
46109
46838
|
throw new Error("--report-type must be 'P&L' or 'BS'");
|
|
46110
46839
|
}
|
|
46111
46840
|
async function runSessionCreate(cfg) {
|
|
46112
|
-
const
|
|
46113
|
-
cfg.out.write(`${
|
|
46841
|
+
const data4 = await createSession(cfg.client, cfg.baseUrl, { signal: cfg.signal });
|
|
46842
|
+
cfg.out.write(`${data4.session_id ?? ""}
|
|
46114
46843
|
`);
|
|
46115
46844
|
}
|
|
46116
46845
|
async function runSessionGet(cfg) {
|
|
46117
|
-
const
|
|
46846
|
+
const data4 = await getSession(cfg.client, cfg.baseUrl, cfg.sessionId, {
|
|
46118
46847
|
signal: cfg.signal
|
|
46119
46848
|
});
|
|
46120
46849
|
if (cfg.asJson) {
|
|
46121
|
-
writeJSONIndented(cfg.out,
|
|
46850
|
+
writeJSONIndented(cfg.out, data4);
|
|
46122
46851
|
return;
|
|
46123
46852
|
}
|
|
46124
|
-
const config =
|
|
46853
|
+
const config = data4.config ?? {};
|
|
46125
46854
|
const rows = [
|
|
46126
|
-
["session_id", String(
|
|
46127
|
-
["created_at", String(
|
|
46128
|
-
["reconcile_status", String(
|
|
46129
|
-
["investigation_status", String(
|
|
46130
|
-
["reconcile_error", String(
|
|
46855
|
+
["session_id", String(data4.session_id ?? "-")],
|
|
46856
|
+
["created_at", String(data4.created_at ?? "-")],
|
|
46857
|
+
["reconcile_status", String(data4.reconcile_status ?? "-")],
|
|
46858
|
+
["investigation_status", String(data4.investigation_status ?? "-")],
|
|
46859
|
+
["reconcile_error", String(data4.reconcile_error ?? "-")],
|
|
46131
46860
|
["report_type", String(config.report_type ?? "-")],
|
|
46132
46861
|
["period", String(config.period ?? "-")]
|
|
46133
46862
|
];
|
|
@@ -46151,12 +46880,12 @@ async function runSessionDelete(cfg) {
|
|
|
46151
46880
|
server: cfg.serverFlag,
|
|
46152
46881
|
confirmCmd: `dr validate onboarding session delete ${cfg.sessionId} --server ${cfg.serverFlag}`,
|
|
46153
46882
|
describe: async () => {
|
|
46154
|
-
const
|
|
46883
|
+
const data4 = await getSession(cfg.client, cfg.baseUrl, cfg.sessionId, {
|
|
46155
46884
|
signal: cfg.signal
|
|
46156
46885
|
});
|
|
46157
46886
|
return [
|
|
46158
|
-
`created_at: ${String(
|
|
46159
|
-
`reconcile_status: ${String(
|
|
46887
|
+
`created_at: ${String(data4.created_at ?? "-")}`,
|
|
46888
|
+
`reconcile_status: ${String(data4.reconcile_status ?? "-")}`
|
|
46160
46889
|
];
|
|
46161
46890
|
},
|
|
46162
46891
|
execute: async () => {
|
|
@@ -46198,14 +46927,14 @@ async function runUpload(cfg) {
|
|
|
46198
46927
|
}
|
|
46199
46928
|
}
|
|
46200
46929
|
async function runReconcile(cfg) {
|
|
46201
|
-
const
|
|
46930
|
+
const data4 = await startReconcile(
|
|
46202
46931
|
cfg.client,
|
|
46203
46932
|
cfg.baseUrl,
|
|
46204
46933
|
cfg.sessionId,
|
|
46205
46934
|
{ report_type: cfg.reportType, period: cfg.period, csm_notes: cfg.csmNotes },
|
|
46206
46935
|
{ signal: cfg.signal }
|
|
46207
46936
|
);
|
|
46208
|
-
if (
|
|
46937
|
+
if (data4.status === "already_running") {
|
|
46209
46938
|
cfg.out.write(`reconcile already running for session ${cfg.sessionId}
|
|
46210
46939
|
`);
|
|
46211
46940
|
} else {
|
|
@@ -46216,12 +46945,12 @@ async function runReconcile(cfg) {
|
|
|
46216
46945
|
`);
|
|
46217
46946
|
}
|
|
46218
46947
|
async function runStatus(cfg) {
|
|
46219
|
-
const
|
|
46948
|
+
const data4 = await getSession(cfg.client, cfg.baseUrl, cfg.sessionId, {
|
|
46220
46949
|
signal: cfg.signal
|
|
46221
46950
|
});
|
|
46222
|
-
const recon =
|
|
46223
|
-
const invest =
|
|
46224
|
-
const err =
|
|
46951
|
+
const recon = data4.reconcile_status ?? "pending";
|
|
46952
|
+
const invest = data4.investigation_status ?? "pending";
|
|
46953
|
+
const err = data4.reconcile_error ?? null;
|
|
46225
46954
|
const summary = summaryStatus(recon, invest);
|
|
46226
46955
|
if (cfg.asJson) {
|
|
46227
46956
|
writeJSONIndented(cfg.out, {
|
|
@@ -46235,13 +46964,13 @@ async function runStatus(cfg) {
|
|
|
46235
46964
|
cfg.out.write(`reconcile: ${recon} | investigation: ${invest}
|
|
46236
46965
|
`);
|
|
46237
46966
|
if (err !== null && err !== "") {
|
|
46238
|
-
const
|
|
46239
|
-
cfg.out.write(`error: ${
|
|
46967
|
+
const preview2 = err.length <= ERROR_PREVIEW_MAX ? err : `${err.slice(0, ERROR_PREVIEW_MAX)}...`;
|
|
46968
|
+
cfg.out.write(`error: ${preview2}
|
|
46240
46969
|
`);
|
|
46241
46970
|
}
|
|
46242
46971
|
}
|
|
46243
46972
|
async function runResult(cfg) {
|
|
46244
|
-
const
|
|
46973
|
+
const data4 = await getResult(
|
|
46245
46974
|
cfg.client,
|
|
46246
46975
|
cfg.baseUrl,
|
|
46247
46976
|
cfg.sessionId,
|
|
@@ -46249,21 +46978,21 @@ async function runResult(cfg) {
|
|
|
46249
46978
|
{ signal: cfg.signal }
|
|
46250
46979
|
);
|
|
46251
46980
|
if (cfg.asJson) {
|
|
46252
|
-
writeJSONIndented(cfg.out,
|
|
46981
|
+
writeJSONIndented(cfg.out, data4);
|
|
46253
46982
|
return;
|
|
46254
46983
|
}
|
|
46255
46984
|
if (cfg.reconcile) {
|
|
46256
|
-
writeJSONIndented(cfg.out,
|
|
46985
|
+
writeJSONIndented(cfg.out, data4);
|
|
46257
46986
|
return;
|
|
46258
46987
|
}
|
|
46259
|
-
const report =
|
|
46988
|
+
const report = data4.report;
|
|
46260
46989
|
const markdown = report !== null && report !== void 0 && typeof report === "object" ? report.markdown : void 0;
|
|
46261
46990
|
if (typeof markdown === "string" && markdown !== "") {
|
|
46262
46991
|
cfg.out.write(`${markdown}
|
|
46263
46992
|
`);
|
|
46264
46993
|
return;
|
|
46265
46994
|
}
|
|
46266
|
-
writeJSONIndented(cfg.out,
|
|
46995
|
+
writeJSONIndented(cfg.out, data4);
|
|
46267
46996
|
}
|
|
46268
46997
|
async function runReport(cfg) {
|
|
46269
46998
|
let result;
|
|
@@ -46336,24 +47065,24 @@ async function runReport(cfg) {
|
|
|
46336
47065
|
tenant: cfg.org || null,
|
|
46337
47066
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
46338
47067
|
};
|
|
46339
|
-
let
|
|
47068
|
+
let data4 = buildReportData(result, meta, balanceCheck);
|
|
46340
47069
|
if (cfg.emitData) {
|
|
46341
47070
|
if (cfg.outPath) {
|
|
46342
|
-
await fs19.writeFile(cfg.outPath, JSON.stringify(
|
|
47071
|
+
await fs19.writeFile(cfg.outPath, JSON.stringify(data4, null, 2), "utf8");
|
|
46343
47072
|
cfg.out.write(`wrote data block to ${cfg.outPath}
|
|
46344
47073
|
`);
|
|
46345
47074
|
} else {
|
|
46346
|
-
writeJSONIndented(cfg.out,
|
|
47075
|
+
writeJSONIndented(cfg.out, data4);
|
|
46347
47076
|
}
|
|
46348
47077
|
return;
|
|
46349
47078
|
}
|
|
46350
47079
|
if (cfg.prosePath) {
|
|
46351
47080
|
const overlay = JSON.parse(await fs19.readFile(cfg.prosePath, "utf8"));
|
|
46352
|
-
|
|
47081
|
+
data4 = applyProse(data4, overlay, (m) => process.stderr.write(`${m}
|
|
46353
47082
|
`));
|
|
46354
47083
|
}
|
|
46355
47084
|
const template = await readReportTemplate();
|
|
46356
|
-
const html = renderReportHtml(
|
|
47085
|
+
const html = renderReportHtml(data4, template);
|
|
46357
47086
|
const outPath = cfg.outPath || `./${cfg.sessionId}-report.html`;
|
|
46358
47087
|
await fs19.writeFile(outPath, html, "utf8");
|
|
46359
47088
|
cfg.out.write(`${outPath}
|
|
@@ -49718,9 +50447,9 @@ async function runMecMemberGrantBulk(cfg) {
|
|
|
49718
50447
|
signal: cfg.signal
|
|
49719
50448
|
});
|
|
49720
50449
|
const wirePayload = { ...cfg.payload, role: role.id };
|
|
49721
|
-
let
|
|
50450
|
+
let preview2 = null;
|
|
49722
50451
|
try {
|
|
49723
|
-
|
|
50452
|
+
preview2 = await withMec(
|
|
49724
50453
|
() => grantUsersBulkCheck(cfg.client, wirePayload, { signal: cfg.signal })
|
|
49725
50454
|
);
|
|
49726
50455
|
} catch {
|
|
@@ -49739,8 +50468,8 @@ async function runMecMemberGrantBulk(cfg) {
|
|
|
49739
50468
|
"",
|
|
49740
50469
|
"WARNING: this cascades table permissions for linked LUT templates."
|
|
49741
50470
|
];
|
|
49742
|
-
if (
|
|
49743
|
-
lines.push(`pre-check response: ${JSON.stringify(
|
|
50471
|
+
if (preview2 !== null) {
|
|
50472
|
+
lines.push(`pre-check response: ${JSON.stringify(preview2).slice(0, 200)}`);
|
|
49744
50473
|
}
|
|
49745
50474
|
return lines;
|
|
49746
50475
|
},
|
|
@@ -52564,9 +53293,9 @@ async function runMemberGrantBulk(cfg) {
|
|
|
52564
53293
|
signal: cfg.signal
|
|
52565
53294
|
});
|
|
52566
53295
|
const wirePayload = { ...cfg.payload, role: role.id };
|
|
52567
|
-
let
|
|
53296
|
+
let preview2 = null;
|
|
52568
53297
|
try {
|
|
52569
|
-
|
|
53298
|
+
preview2 = await withWorkflowV2(
|
|
52570
53299
|
() => grantUsersBulkCheck(cfg.client, wirePayload, { signal: cfg.signal })
|
|
52571
53300
|
);
|
|
52572
53301
|
} catch {
|
|
@@ -52585,8 +53314,8 @@ async function runMemberGrantBulk(cfg) {
|
|
|
52585
53314
|
"",
|
|
52586
53315
|
"WARNING: this cascades table permissions for linked LUT templates."
|
|
52587
53316
|
];
|
|
52588
|
-
if (
|
|
52589
|
-
lines.push(`pre-check response: ${JSON.stringify(
|
|
53317
|
+
if (preview2 !== null) {
|
|
53318
|
+
lines.push(`pre-check response: ${JSON.stringify(preview2).slice(0, 200)}`);
|
|
52590
53319
|
}
|
|
52591
53320
|
return lines;
|
|
52592
53321
|
},
|
|
@@ -54186,12 +54915,12 @@ function parseCsvImportText(text2) {
|
|
|
54186
54915
|
`--file: unexpected CSV header \u2014 expected "${EXPECTED_HEADER5.join(",")}", got "${header.slice(0, 3).join(",")}"`
|
|
54187
54916
|
);
|
|
54188
54917
|
}
|
|
54189
|
-
const
|
|
54190
|
-
if (
|
|
54918
|
+
const data4 = rows.slice(1).filter((r) => r.some((c) => c.trim() !== ""));
|
|
54919
|
+
if (data4.length === 0) {
|
|
54191
54920
|
throw new Error("--file: CSV has no data rows (header only)");
|
|
54192
54921
|
}
|
|
54193
54922
|
const out = [];
|
|
54194
|
-
|
|
54923
|
+
data4.forEach((row, idx) => {
|
|
54195
54924
|
const lineNo = idx + 2;
|
|
54196
54925
|
const detailedRule = (row[0] ?? "").trim();
|
|
54197
54926
|
const category = (row[2] ?? "").trim();
|
|
@@ -58953,9 +59682,9 @@ async function runCpList(cfg) {
|
|
|
58953
59682
|
}
|
|
58954
59683
|
async function runCpGet(cfg) {
|
|
58955
59684
|
const bucket = await getBucket(cfg.client, cfg.id, { signal: cfg.signal });
|
|
58956
|
-
const
|
|
59685
|
+
const data4 = cfg.noCpData ? void 0 : await getCpData(cfg.client, cfg.id, { signal: cfg.signal });
|
|
58957
59686
|
if (cfg.asJson) {
|
|
58958
|
-
writeJSONIndented(cfg.out, { bucket, cp_data:
|
|
59687
|
+
writeJSONIndented(cfg.out, { bucket, cp_data: data4 ?? null });
|
|
58959
59688
|
return;
|
|
58960
59689
|
}
|
|
58961
59690
|
cfg.out.write(`id: ${bucket.id}
|
|
@@ -58968,11 +59697,11 @@ async function runCpGet(cfg) {
|
|
|
58968
59697
|
`);
|
|
58969
59698
|
cfg.out.write(`is_demo: ${bucket.is_demo}
|
|
58970
59699
|
`);
|
|
58971
|
-
if (
|
|
58972
|
-
cfg.out.write(`cp_data.owner_user_id: ${
|
|
59700
|
+
if (data4 !== void 0) {
|
|
59701
|
+
cfg.out.write(`cp_data.owner_user_id: ${data4.owner_user_id}
|
|
58973
59702
|
`);
|
|
58974
59703
|
cfg.out.write(
|
|
58975
|
-
`cp_data.level_dimension_tags: ${JSON.stringify(
|
|
59704
|
+
`cp_data.level_dimension_tags: ${JSON.stringify(data4.level_dimension_tags)}
|
|
58976
59705
|
`
|
|
58977
59706
|
);
|
|
58978
59707
|
}
|
|
@@ -60059,7 +60788,7 @@ var EXTRACT_POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
|
60059
60788
|
function isTerminalAiStatus(status2) {
|
|
60060
60789
|
return status2 === "PROCESSED" || status2 === "FAILED";
|
|
60061
60790
|
}
|
|
60062
|
-
async function
|
|
60791
|
+
async function defaultSleep8(ms, signal) {
|
|
60063
60792
|
if (signal?.aborted === true) return;
|
|
60064
60793
|
await new Promise((resolve3) => {
|
|
60065
60794
|
const timer = setTimeout(resolve3, ms);
|
|
@@ -60076,7 +60805,7 @@ async function defaultSleep9(ms, signal) {
|
|
|
60076
60805
|
async function pollExtractionStatus(client, contractFileIds, options = {}) {
|
|
60077
60806
|
const interval = options.pollIntervalMs ?? EXTRACT_POLL_INTERVAL_MS;
|
|
60078
60807
|
const timeout = options.timeoutMs ?? EXTRACT_POLL_TIMEOUT_MS;
|
|
60079
|
-
const sleep4 = options.sleep ??
|
|
60808
|
+
const sleep4 = options.sleep ?? defaultSleep8;
|
|
60080
60809
|
const now = options.now ?? Date.now;
|
|
60081
60810
|
const resolved = /* @__PURE__ */ new Map();
|
|
60082
60811
|
if (contractFileIds.length === 0) return { resolved, pending: [] };
|
|
@@ -62702,16 +63431,16 @@ function describeBulkCreate(draft, contracts, batchCount, serverKey) {
|
|
|
62702
63431
|
}
|
|
62703
63432
|
if (d.category !== void 0) out.push(`default category: ${d.category}`);
|
|
62704
63433
|
}
|
|
62705
|
-
const
|
|
62706
|
-
if (
|
|
63434
|
+
const preview2 = contracts.slice(0, DESCRIBE_PREVIEW_ROWS3);
|
|
63435
|
+
if (preview2.length > 0) {
|
|
62707
63436
|
out.push("preview:");
|
|
62708
|
-
for (let i = 0; i <
|
|
62709
|
-
const c =
|
|
63437
|
+
for (let i = 0; i < preview2.length; i++) {
|
|
63438
|
+
const c = preview2[i];
|
|
62710
63439
|
const vendor = c.vendor_name !== void 0 && c.vendor_name !== null && c.vendor_name !== "" ? c.vendor_name : c.vendor_id !== void 0 && c.vendor_id !== null ? c.vendor_id : "?";
|
|
62711
63440
|
out.push(` [${i}] ${vendor} \u2014 ${c.value} ${c.currency}`);
|
|
62712
63441
|
}
|
|
62713
|
-
if (contracts.length >
|
|
62714
|
-
out.push(` \u2026 (${contracts.length -
|
|
63442
|
+
if (contracts.length > preview2.length) {
|
|
63443
|
+
out.push(` \u2026 (${contracts.length - preview2.length} more)`);
|
|
62715
63444
|
}
|
|
62716
63445
|
}
|
|
62717
63446
|
return out;
|