@thingd/cli 0.54.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +608 -33
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AAu8FA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0FvD"}
|
package/dist/interactive.js
CHANGED
|
@@ -69,6 +69,7 @@ let collections = [];
|
|
|
69
69
|
let streams = [];
|
|
70
70
|
let queues = [];
|
|
71
71
|
let objectsByCollection = new Map();
|
|
72
|
+
const collectionOptions = new Map();
|
|
72
73
|
const expandedSet = new Set(["cat:collections", "cat:streams", "cat:queues"]);
|
|
73
74
|
let cursorIndex = 0;
|
|
74
75
|
let maintenanceCursor = 0;
|
|
@@ -80,6 +81,8 @@ let totalActiveJobsCount = 0;
|
|
|
80
81
|
let totalDeadJobsCount = 0;
|
|
81
82
|
let totalLinksCount = 0;
|
|
82
83
|
let cloudError = null;
|
|
84
|
+
const eventsByStream = new Map();
|
|
85
|
+
const jobsByQueue = new Map();
|
|
83
86
|
let objectsHistory = [];
|
|
84
87
|
let eventsHistory = [];
|
|
85
88
|
let activeJobsHistory = [];
|
|
@@ -242,7 +245,21 @@ async function fetchResourcesFallback() {
|
|
|
242
245
|
objectsByCollection.clear();
|
|
243
246
|
await Promise.all(collections.map(async (col) => {
|
|
244
247
|
try {
|
|
245
|
-
const
|
|
248
|
+
const opts = collectionOptions.get(col);
|
|
249
|
+
const listOpts = {};
|
|
250
|
+
if (opts?.sortBy) {
|
|
251
|
+
listOpts.sortBy = { field: opts.sortBy, direction: opts.sortDir ?? "asc" };
|
|
252
|
+
}
|
|
253
|
+
if (opts?.limit) {
|
|
254
|
+
listOpts.limit = opts.limit;
|
|
255
|
+
}
|
|
256
|
+
if (opts?.offset) {
|
|
257
|
+
listOpts.offset = opts.offset;
|
|
258
|
+
}
|
|
259
|
+
if (opts?.filter) {
|
|
260
|
+
listOpts.filter = opts.filter;
|
|
261
|
+
}
|
|
262
|
+
const list = await db.listObjects(col, Object.keys(listOpts).length > 0 ? listOpts : undefined);
|
|
246
263
|
objectsByCollection.set(col, list.map((o) => o.id));
|
|
247
264
|
}
|
|
248
265
|
catch {
|
|
@@ -287,13 +304,50 @@ async function fetchResources() {
|
|
|
287
304
|
objectsByCollection.clear();
|
|
288
305
|
for (const col of collections) {
|
|
289
306
|
try {
|
|
290
|
-
const
|
|
307
|
+
const opts = collectionOptions.get(col);
|
|
308
|
+
const listOpts = {};
|
|
309
|
+
if (opts?.sortBy) {
|
|
310
|
+
listOpts.sortBy = { field: opts.sortBy, direction: opts.sortDir ?? "asc" };
|
|
311
|
+
}
|
|
312
|
+
if (opts?.limit) {
|
|
313
|
+
listOpts.limit = opts.limit;
|
|
314
|
+
}
|
|
315
|
+
if (opts?.offset) {
|
|
316
|
+
listOpts.offset = opts.offset;
|
|
317
|
+
}
|
|
318
|
+
if (opts?.filter) {
|
|
319
|
+
listOpts.filter = opts.filter;
|
|
320
|
+
}
|
|
321
|
+
const list = await db.listObjects(col, Object.keys(listOpts).length > 0 ? listOpts : undefined);
|
|
291
322
|
objectsByCollection.set(col, list.map((o) => o.id));
|
|
292
323
|
}
|
|
293
324
|
catch {
|
|
294
325
|
objectsByCollection.set(col, []);
|
|
295
326
|
}
|
|
296
327
|
}
|
|
328
|
+
// Populate events per stream
|
|
329
|
+
eventsByStream.clear();
|
|
330
|
+
await Promise.all(streams.map(async (s) => {
|
|
331
|
+
try {
|
|
332
|
+
const evts = await db.events.list(s, { limit: 20 });
|
|
333
|
+
eventsByStream.set(s, evts);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
eventsByStream.set(s, []);
|
|
337
|
+
}
|
|
338
|
+
}));
|
|
339
|
+
// Populate jobs per queue
|
|
340
|
+
jobsByQueue.clear();
|
|
341
|
+
await Promise.all(queues.map(async (q) => {
|
|
342
|
+
try {
|
|
343
|
+
const active = await db.queue(q).list();
|
|
344
|
+
const dead = await db.queue(q).dead();
|
|
345
|
+
jobsByQueue.set(q, { active, dead });
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
jobsByQueue.set(q, { active: [], dead: [] });
|
|
349
|
+
}
|
|
350
|
+
}));
|
|
297
351
|
// Calculate Deltas for Operations Throughput Rates
|
|
298
352
|
const prevObjects = objectsHistory.length > 0
|
|
299
353
|
? (objectsHistory[objectsHistory.length - 1] ?? totalObjects)
|
|
@@ -510,15 +564,40 @@ function buildTree() {
|
|
|
510
564
|
});
|
|
511
565
|
}
|
|
512
566
|
for (const stream of streams) {
|
|
567
|
+
const sOpen = expandedSet.has(`stream:${stream}`);
|
|
513
568
|
nodes.push({
|
|
514
569
|
id: `stream:${stream}`,
|
|
515
570
|
parentId: "cat:streams",
|
|
516
571
|
type: "stream",
|
|
517
|
-
label: `${pc.
|
|
572
|
+
label: `${sOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.green(stream)}`,
|
|
518
573
|
depth: 1,
|
|
519
|
-
expandable:
|
|
574
|
+
expandable: true,
|
|
520
575
|
ref: { name: stream },
|
|
521
576
|
});
|
|
577
|
+
if (sOpen) {
|
|
578
|
+
const evts = eventsByStream.get(stream) ?? [];
|
|
579
|
+
if (evts.length === 0) {
|
|
580
|
+
nodes.push({
|
|
581
|
+
id: `empty:evt:${stream}`,
|
|
582
|
+
parentId: `stream:${stream}`,
|
|
583
|
+
type: "status",
|
|
584
|
+
label: pc.dim("(no events)"),
|
|
585
|
+
depth: 2,
|
|
586
|
+
expandable: false,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
for (const evt of evts) {
|
|
590
|
+
nodes.push({
|
|
591
|
+
id: `evt:${stream}:${evt.id}`,
|
|
592
|
+
parentId: `stream:${stream}`,
|
|
593
|
+
type: "event",
|
|
594
|
+
label: `${pc.dim("·")} ${pc.dim(evt.type || "unknown")}`,
|
|
595
|
+
depth: 2,
|
|
596
|
+
expandable: false,
|
|
597
|
+
ref: { stream: stream, eventId: evt.id, eventData: evt },
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
522
601
|
}
|
|
523
602
|
}
|
|
524
603
|
// Queues
|
|
@@ -541,15 +620,87 @@ function buildTree() {
|
|
|
541
620
|
});
|
|
542
621
|
}
|
|
543
622
|
for (const q of queues) {
|
|
623
|
+
const qOpen = expandedSet.has(`queue:${q}`);
|
|
544
624
|
nodes.push({
|
|
545
625
|
id: `queue:${q}`,
|
|
546
626
|
parentId: "cat:queues",
|
|
547
627
|
type: "queue",
|
|
548
|
-
label: `${pc.
|
|
628
|
+
label: `${qOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.magenta(q)}`,
|
|
549
629
|
depth: 1,
|
|
550
|
-
expandable:
|
|
630
|
+
expandable: true,
|
|
551
631
|
ref: { name: q },
|
|
552
632
|
});
|
|
633
|
+
if (qOpen) {
|
|
634
|
+
const jobData = jobsByQueue.get(q);
|
|
635
|
+
const activeJobs = jobData?.active ?? [];
|
|
636
|
+
const deadJobs = jobData?.dead ?? [];
|
|
637
|
+
// Active jobs subcategory
|
|
638
|
+
const activeOpen = expandedSet.has(`queue:${q}:active`);
|
|
639
|
+
nodes.push({
|
|
640
|
+
id: `queue:${q}:active`,
|
|
641
|
+
parentId: `queue:${q}`,
|
|
642
|
+
type: "category",
|
|
643
|
+
label: `${activeOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.bold("Active")} ${pc.dim(`(${activeJobs.length})`)}`,
|
|
644
|
+
depth: 2,
|
|
645
|
+
expandable: true,
|
|
646
|
+
});
|
|
647
|
+
if (activeOpen) {
|
|
648
|
+
if (activeJobs.length === 0) {
|
|
649
|
+
nodes.push({
|
|
650
|
+
id: `empty:active:${q}`,
|
|
651
|
+
parentId: `queue:${q}:active`,
|
|
652
|
+
type: "status",
|
|
653
|
+
label: pc.dim("(no active jobs)"),
|
|
654
|
+
depth: 3,
|
|
655
|
+
expandable: false,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
for (const job of activeJobs) {
|
|
659
|
+
nodes.push({
|
|
660
|
+
id: `job:${q}:active:${job.id}`,
|
|
661
|
+
parentId: `queue:${q}:active`,
|
|
662
|
+
type: "job",
|
|
663
|
+
label: `${pc.cyan("●")} ${pc.dim(job.id.slice(0, 12))}`,
|
|
664
|
+
depth: 3,
|
|
665
|
+
expandable: false,
|
|
666
|
+
ref: { queue: q, jobId: job.id, status: "active", jobData: job },
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
// Dead jobs subcategory
|
|
671
|
+
const deadOpen = expandedSet.has(`queue:${q}:dead`);
|
|
672
|
+
nodes.push({
|
|
673
|
+
id: `queue:${q}:dead`,
|
|
674
|
+
parentId: `queue:${q}`,
|
|
675
|
+
type: "category",
|
|
676
|
+
label: `${deadOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.bold("Dead")} ${pc.dim(`(${deadJobs.length})`)}`,
|
|
677
|
+
depth: 2,
|
|
678
|
+
expandable: true,
|
|
679
|
+
});
|
|
680
|
+
if (deadOpen) {
|
|
681
|
+
if (deadJobs.length === 0) {
|
|
682
|
+
nodes.push({
|
|
683
|
+
id: `empty:dead:${q}`,
|
|
684
|
+
parentId: `queue:${q}:dead`,
|
|
685
|
+
type: "status",
|
|
686
|
+
label: pc.dim("(no dead jobs)"),
|
|
687
|
+
depth: 3,
|
|
688
|
+
expandable: false,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
for (const job of deadJobs) {
|
|
692
|
+
nodes.push({
|
|
693
|
+
id: `job:${q}:dead:${job.id}`,
|
|
694
|
+
parentId: `queue:${q}:dead`,
|
|
695
|
+
type: "job",
|
|
696
|
+
label: `${pc.red("○")} ${pc.dim(job.id.slice(0, 12))}`,
|
|
697
|
+
depth: 3,
|
|
698
|
+
expandable: false,
|
|
699
|
+
ref: { queue: q, jobId: job.id, status: "dead", jobData: job },
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
553
704
|
}
|
|
554
705
|
}
|
|
555
706
|
// Links
|
|
@@ -636,6 +787,27 @@ async function loadContent(node) {
|
|
|
636
787
|
const ref = node.ref;
|
|
637
788
|
const objs = objectsByCollection.get(ref.name) ?? [];
|
|
638
789
|
let res = `${pc.bold(ref.name)} ${pc.dim(`(${objs.length} objects)`)}\n\n`;
|
|
790
|
+
// Schema info
|
|
791
|
+
try {
|
|
792
|
+
const schemas = await db.schema(ref.name);
|
|
793
|
+
if (schemas.length > 0) {
|
|
794
|
+
const fields = schemas[0]?.fields ?? [];
|
|
795
|
+
if (fields.length > 0) {
|
|
796
|
+
res += `${pc.bold("Fields")}\n`;
|
|
797
|
+
for (const f of fields) {
|
|
798
|
+
const icon = f.nullable ? pc.dim("⊙") : pc.cyan("◎");
|
|
799
|
+
const sample = f.sampleValues.length > 0
|
|
800
|
+
? pc.dim(` e.g. ${String(f.sampleValues[0]).slice(0, 20)}`)
|
|
801
|
+
: "";
|
|
802
|
+
res += ` ${icon} ${f.name}: ${f.type}${sample}\n`;
|
|
803
|
+
}
|
|
804
|
+
res += "\n";
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
// Schema not available for this store
|
|
810
|
+
}
|
|
639
811
|
if (objs.length === 0) {
|
|
640
812
|
res += pc.dim("No objects in this collection.");
|
|
641
813
|
}
|
|
@@ -647,43 +819,59 @@ async function loadContent(node) {
|
|
|
647
819
|
}
|
|
648
820
|
else if (node.type === "stream" && node.ref) {
|
|
649
821
|
const ref = node.ref;
|
|
650
|
-
const
|
|
651
|
-
let res = `${pc.bold(ref.name)} ${pc.dim(`(${
|
|
652
|
-
|
|
653
|
-
|
|
822
|
+
const evts = eventsByStream.get(ref.name) ?? [];
|
|
823
|
+
let res = `${pc.bold(ref.name)} ${pc.dim(`(${evts.length} events shown)`)}\n\n`;
|
|
824
|
+
res += pc.dim("Expand to browse individual events, or press [c] to append.\n");
|
|
825
|
+
content = res;
|
|
826
|
+
}
|
|
827
|
+
else if (node.type === "event" && node.ref) {
|
|
828
|
+
const ref = node.ref;
|
|
829
|
+
const evt = ref.eventData;
|
|
830
|
+
let res = `${pc.bold(evt.type || "unknown")} ${pc.dim(evt.id)}\n`;
|
|
831
|
+
res += ` ${pc.dim("Stream:")} ${pc.green(ref.stream)}\n`;
|
|
832
|
+
res += ` ${pc.dim("Created:")} ${pc.dim(evt.createdAt || "—")}\n\n`;
|
|
833
|
+
const display = { ...evt };
|
|
834
|
+
for (const k of ["id", "stream", "sequence", "createdAt", "idempotencyKey"]) {
|
|
835
|
+
delete display[k];
|
|
836
|
+
}
|
|
837
|
+
if (Object.keys(display).length > 0) {
|
|
838
|
+
res += highlightJson(display);
|
|
654
839
|
}
|
|
655
840
|
else {
|
|
656
|
-
|
|
657
|
-
const ts = e.createdAt ? pc.dim(String(e.createdAt)) : "";
|
|
658
|
-
const type = pc.magenta(e.type || "unknown");
|
|
659
|
-
return ` ${ts} ${type}`;
|
|
660
|
-
});
|
|
661
|
-
res += lines.join("\n");
|
|
841
|
+
res += pc.dim("No payload.");
|
|
662
842
|
}
|
|
663
843
|
content = res;
|
|
664
844
|
}
|
|
665
845
|
else if (node.type === "queue" && node.ref) {
|
|
666
846
|
const ref = node.ref;
|
|
667
|
-
const
|
|
668
|
-
const
|
|
847
|
+
const jobData = jobsByQueue.get(ref.name);
|
|
848
|
+
const active = jobData?.active ?? [];
|
|
849
|
+
const dead = jobData?.dead ?? [];
|
|
669
850
|
let res = `${pc.bold(ref.name)}\n\n`;
|
|
670
851
|
res += `${pc.cyan("Active")} ${pc.dim(`(${active.length})`)}\n`;
|
|
671
|
-
|
|
672
|
-
|
|
852
|
+
res += `${pc.red("Dead")} ${pc.dim(`(${dead.length})`)}\n\n`;
|
|
853
|
+
res += pc.dim("Expand Active or Dead to browse jobs, [c] to push new job.");
|
|
854
|
+
content = res;
|
|
855
|
+
}
|
|
856
|
+
else if (node.type === "job" && node.ref) {
|
|
857
|
+
const ref = node.ref;
|
|
858
|
+
const job = ref.jobData;
|
|
859
|
+
let res = `${pc.bold(job.id)} ${pc.yellow(job.status || ref.status)}\n`;
|
|
860
|
+
res += ` ${pc.dim("Queue:")} ${pc.magenta(ref.queue)}\n`;
|
|
861
|
+
res += ` ${pc.dim("Attempts:")} ${job.attempts}/${job.maxAttempts}\n`;
|
|
862
|
+
res += ` ${pc.dim("Created:")} ${pc.dim(job.createdAt || "—")}\n`;
|
|
863
|
+
if (job.lastError) {
|
|
864
|
+
res += ` ${pc.dim("Error:")} ${pc.red(job.lastError)}\n`;
|
|
865
|
+
}
|
|
866
|
+
res += "\n";
|
|
867
|
+
if (job.payload && Object.keys(job.payload).length > 0) {
|
|
868
|
+
res += highlightJson(job.payload);
|
|
673
869
|
}
|
|
674
870
|
else {
|
|
675
|
-
|
|
676
|
-
res += ` ${pc.cyan("●")} ${j.id} ${pc.yellow(j.status)} ${pc.dim(`${j.attempts}/${j.maxAttempts}`)}\n`;
|
|
677
|
-
}
|
|
871
|
+
res += pc.dim("No payload.");
|
|
678
872
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
res += pc.dim(" No dead jobs\n");
|
|
682
|
-
}
|
|
683
|
-
else {
|
|
684
|
-
for (const j of dead) {
|
|
685
|
-
res += ` ${pc.red("○")} ${j.id} ${pc.dim(`${j.attempts}/${j.maxAttempts}`)}\n`;
|
|
686
|
-
}
|
|
873
|
+
if (ref.status === "dead") {
|
|
874
|
+
res += `\n\n${pc.dim("[e] Retry (ack) [d] Nack (remove from dead letter)")}`;
|
|
687
875
|
}
|
|
688
876
|
content = res;
|
|
689
877
|
}
|
|
@@ -903,7 +1091,7 @@ function draw() {
|
|
|
903
1091
|
help = ` ${pc.dim("↑↓")} nav ${pc.dim("enter")} connect ${pc.dim("q")} quit `;
|
|
904
1092
|
}
|
|
905
1093
|
else {
|
|
906
|
-
help = ` ${pc.dim("↑↓")} nav ${pc.dim("←→")} toggle ${pc.dim("c")} create ${pc.dim("e")} edit ${pc.dim("d")} delete ${pc.dim("/")} search ${pc.dim("n")} neighbors ${pc.dim("i")} info ${pc.dim("r")} refresh ${pc.dim("s")} switch ${pc.dim("l")} logout ${pc.dim("q")} quit `;
|
|
1094
|
+
help = ` ${pc.dim("↑↓")} nav ${pc.dim("←→")} toggle ${pc.dim("c")} create ${pc.dim("e")} edit ${pc.dim("d")} delete ${pc.dim("/")} search ${pc.dim("n")} neighbors ${pc.dim("N")} nlq ${pc.dim("a")} agg ${pc.dim("t")} ts ${pc.dim("o")} options ${pc.dim("b")} batch ${pc.dim("i")} info ${pc.dim("r")} refresh ${pc.dim("s")} switch ${pc.dim("l")} logout ${pc.dim("q")} quit `;
|
|
907
1095
|
}
|
|
908
1096
|
buf += `${pc.dim("─".repeat(W))}\n`;
|
|
909
1097
|
buf += padToWidth(help, W);
|
|
@@ -1220,6 +1408,41 @@ async function handleEdit(selected) {
|
|
|
1220
1408
|
}
|
|
1221
1409
|
});
|
|
1222
1410
|
}
|
|
1411
|
+
else if (selected.type === "job" && selected.ref) {
|
|
1412
|
+
const ref = selected.ref;
|
|
1413
|
+
if (ref.status === "dead") {
|
|
1414
|
+
openForm(`Retry Dead Job: ${ref.jobId.slice(0, 12)}`, [
|
|
1415
|
+
{
|
|
1416
|
+
id: "action",
|
|
1417
|
+
label: "Action",
|
|
1418
|
+
value: "ack",
|
|
1419
|
+
options: ["ack", "nack"],
|
|
1420
|
+
},
|
|
1421
|
+
{
|
|
1422
|
+
id: "error",
|
|
1423
|
+
label: "Error message (for nack)",
|
|
1424
|
+
placeholder: "Optional error",
|
|
1425
|
+
},
|
|
1426
|
+
], async (vals) => {
|
|
1427
|
+
const action = vals.action || "";
|
|
1428
|
+
if (action === "ack") {
|
|
1429
|
+
await db.queue(ref.queue).ack(ref.jobId);
|
|
1430
|
+
}
|
|
1431
|
+
else if (action === "nack") {
|
|
1432
|
+
await db.queue(ref.queue).nack(ref.jobId, { error: vals.error || "Rejected" });
|
|
1433
|
+
}
|
|
1434
|
+
else {
|
|
1435
|
+
throw new Error("Action must be 'ack' or 'nack'.");
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
else {
|
|
1440
|
+
// Active job — nack to fail it back to ready
|
|
1441
|
+
openForm(`Nack Job: ${ref.jobId.slice(0, 12)}`, [{ id: "error", label: "Error message", placeholder: "Optional" }], async (vals) => {
|
|
1442
|
+
await db.queue(ref.queue).nack(ref.jobId, { error: vals.error || "Nacked" });
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1223
1446
|
else {
|
|
1224
1447
|
openForm("Edit Not Supported", [{ id: "msg", label: "Error", value: "Editing is only available for Objects and Queues." }], async () => { });
|
|
1225
1448
|
}
|
|
@@ -1284,6 +1507,33 @@ async function handleDelete(selected) {
|
|
|
1284
1507
|
}
|
|
1285
1508
|
});
|
|
1286
1509
|
}
|
|
1510
|
+
else if (selected.type === "job" && selected.ref) {
|
|
1511
|
+
const ref = selected.ref;
|
|
1512
|
+
if (ref.status === "dead") {
|
|
1513
|
+
openForm(`Remove Dead Job: ${ref.jobId.slice(0, 12)}`, [
|
|
1514
|
+
{
|
|
1515
|
+
id: "action",
|
|
1516
|
+
label: "Action",
|
|
1517
|
+
value: "nack",
|
|
1518
|
+
options: ["ack", "nack"],
|
|
1519
|
+
},
|
|
1520
|
+
{ id: "confirm", label: 'Type "yes" to confirm', placeholder: "yes" },
|
|
1521
|
+
], async (vals) => {
|
|
1522
|
+
if ((vals.confirm || "").toLowerCase() !== "yes") {
|
|
1523
|
+
throw new Error("Canceled");
|
|
1524
|
+
}
|
|
1525
|
+
if (vals.action === "ack") {
|
|
1526
|
+
await db.queue(ref.queue).ack(ref.jobId);
|
|
1527
|
+
}
|
|
1528
|
+
else {
|
|
1529
|
+
await db.queue(ref.queue).nack(ref.jobId, { error: "Removed from dead letter" });
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
else {
|
|
1534
|
+
openForm("Delete Not For Active Jobs", [{ id: "msg", label: "Info", value: "Use [e] to nack an active job back to ready state." }], async () => { });
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1287
1537
|
else {
|
|
1288
1538
|
openForm("Delete Not Supported", [
|
|
1289
1539
|
{
|
|
@@ -1461,6 +1711,316 @@ async function handleNeighbors(selected) {
|
|
|
1461
1711
|
draw();
|
|
1462
1712
|
});
|
|
1463
1713
|
}
|
|
1714
|
+
async function handleAggregate(selected) {
|
|
1715
|
+
const defaultCol = selected?.type === "collection"
|
|
1716
|
+
? (selected.ref?.name ?? "")
|
|
1717
|
+
: selected?.type === "object"
|
|
1718
|
+
? (selected.ref?.collection ?? "")
|
|
1719
|
+
: "";
|
|
1720
|
+
openForm("Aggregate", [
|
|
1721
|
+
{
|
|
1722
|
+
id: "function",
|
|
1723
|
+
label: "Function",
|
|
1724
|
+
value: "count",
|
|
1725
|
+
options: ["count", "sum", "avg", "min", "max"],
|
|
1726
|
+
},
|
|
1727
|
+
{
|
|
1728
|
+
id: "collection",
|
|
1729
|
+
label: "Collection",
|
|
1730
|
+
value: defaultCol,
|
|
1731
|
+
options: collections,
|
|
1732
|
+
allowCustom: true,
|
|
1733
|
+
},
|
|
1734
|
+
{ id: "field", label: "Field (for sum/avg/min/max)", placeholder: "field name" },
|
|
1735
|
+
{ id: "groupBy", label: "Group By (optional field)", placeholder: "field name" },
|
|
1736
|
+
{ id: "filter", label: "Filter (optional JSON)", placeholder: '{"status":"active"}' },
|
|
1737
|
+
], async (vals) => {
|
|
1738
|
+
const func = vals.function || "count";
|
|
1739
|
+
const collection = (vals.collection || "").trim();
|
|
1740
|
+
if (!collection) {
|
|
1741
|
+
throw new Error("Collection is required.");
|
|
1742
|
+
}
|
|
1743
|
+
const field = (vals.field || "").trim() || undefined;
|
|
1744
|
+
const groupBy = (vals.groupBy || "").trim() || undefined;
|
|
1745
|
+
const filter = vals.filter?.trim() ? JSON.parse(vals.filter.trim()) : undefined;
|
|
1746
|
+
const options = { groupBy, filter };
|
|
1747
|
+
const a = db.aggregate;
|
|
1748
|
+
const result = func === "count"
|
|
1749
|
+
? await a.count(collection, options)
|
|
1750
|
+
: func === "sum"
|
|
1751
|
+
? await a.sum(collection, field, options)
|
|
1752
|
+
: func === "avg"
|
|
1753
|
+
? await a.avg(collection, field, options)
|
|
1754
|
+
: func === "min"
|
|
1755
|
+
? await a.min(collection, field, options)
|
|
1756
|
+
: await a.max(collection, field, options);
|
|
1757
|
+
const lines = [
|
|
1758
|
+
` ${pc.bold("Aggregate")} ${pc.cyan(func)} ${pc.dim(`on ${collection}`)}`,
|
|
1759
|
+
"",
|
|
1760
|
+
` ${pc.dim("Total:")} ${pc.bold(String(result.total))}`,
|
|
1761
|
+
"",
|
|
1762
|
+
];
|
|
1763
|
+
if (result.groups && result.groups.length > 0) {
|
|
1764
|
+
lines.push(` ${pc.bold("Groups")}`);
|
|
1765
|
+
const maxVal = Math.max(...result.groups.map((g) => g.value));
|
|
1766
|
+
for (const g of result.groups) {
|
|
1767
|
+
const barLen = Math.max(1, Math.round((g.value / maxVal) * 20));
|
|
1768
|
+
const bar = pc.cyan("█".repeat(barLen));
|
|
1769
|
+
lines.push(` ${g.key}: ${bar} ${pc.dim(String(g.value))}`);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
viewerLines = lines;
|
|
1773
|
+
loadedItemId = "aggregate_result";
|
|
1774
|
+
draw();
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
async function handleTimeseries(selected) {
|
|
1778
|
+
const defaultCol = selected?.type === "collection"
|
|
1779
|
+
? (selected.ref?.name ?? "")
|
|
1780
|
+
: selected?.type === "object"
|
|
1781
|
+
? (selected.ref?.collection ?? "")
|
|
1782
|
+
: "";
|
|
1783
|
+
openForm("Time Series", [
|
|
1784
|
+
{
|
|
1785
|
+
id: "function",
|
|
1786
|
+
label: "Function",
|
|
1787
|
+
value: "count",
|
|
1788
|
+
options: ["count", "sum", "avg", "min", "max"],
|
|
1789
|
+
},
|
|
1790
|
+
{
|
|
1791
|
+
id: "collection",
|
|
1792
|
+
label: "Collection",
|
|
1793
|
+
value: defaultCol,
|
|
1794
|
+
options: collections,
|
|
1795
|
+
allowCustom: true,
|
|
1796
|
+
},
|
|
1797
|
+
{ id: "field", label: "Field (optional)", placeholder: "field name" },
|
|
1798
|
+
{
|
|
1799
|
+
id: "bucket",
|
|
1800
|
+
label: "Bucket",
|
|
1801
|
+
value: "day",
|
|
1802
|
+
options: ["hour", "day", "week", "month"],
|
|
1803
|
+
},
|
|
1804
|
+
{ id: "from", label: "From (ISO date, optional)", placeholder: "2024-01-01" },
|
|
1805
|
+
{ id: "to", label: "To (ISO date, optional)", placeholder: "2024-12-31" },
|
|
1806
|
+
{ id: "filter", label: "Filter (optional JSON)", placeholder: '{"status":"active"}' },
|
|
1807
|
+
], async (vals) => {
|
|
1808
|
+
const func = vals.function || "count";
|
|
1809
|
+
const collection = (vals.collection || "").trim();
|
|
1810
|
+
if (!collection) {
|
|
1811
|
+
throw new Error("Collection is required.");
|
|
1812
|
+
}
|
|
1813
|
+
const field = (vals.field || "").trim() || undefined;
|
|
1814
|
+
const bucket = (vals.bucket || "day");
|
|
1815
|
+
const from = (vals.from || "").trim() || undefined;
|
|
1816
|
+
const to = (vals.to || "").trim() || undefined;
|
|
1817
|
+
const filter = vals.filter?.trim() ? JSON.parse(vals.filter.trim()) : undefined;
|
|
1818
|
+
const result = await db.timeseries(collection, {
|
|
1819
|
+
function: func,
|
|
1820
|
+
field,
|
|
1821
|
+
bucket,
|
|
1822
|
+
from,
|
|
1823
|
+
to,
|
|
1824
|
+
filter,
|
|
1825
|
+
});
|
|
1826
|
+
const lines = [
|
|
1827
|
+
` ${pc.bold("Time Series")} ${pc.cyan(func)} ${pc.dim(`on ${collection}, ${bucket}ly`)}`,
|
|
1828
|
+
"",
|
|
1829
|
+
];
|
|
1830
|
+
if (result.buckets.length === 0) {
|
|
1831
|
+
lines.push(pc.dim("No data for the selected range."));
|
|
1832
|
+
}
|
|
1833
|
+
else {
|
|
1834
|
+
const maxVal = Math.max(...result.buckets.map((b) => b.value));
|
|
1835
|
+
for (const b of result.buckets) {
|
|
1836
|
+
const barLen = Math.max(1, Math.round((b.value / maxVal) * 20));
|
|
1837
|
+
const bar = pc.green("█".repeat(barLen));
|
|
1838
|
+
lines.push(` ${b.label}: ${bar} ${pc.dim(String(b.value))}`);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
viewerLines = lines;
|
|
1842
|
+
loadedItemId = "timeseries_result";
|
|
1843
|
+
draw();
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
async function handleNlq(selected) {
|
|
1847
|
+
const defaultCol = selected?.type === "collection"
|
|
1848
|
+
? (selected.ref?.name ?? "")
|
|
1849
|
+
: selected?.type === "object"
|
|
1850
|
+
? (selected.ref?.collection ?? "")
|
|
1851
|
+
: "";
|
|
1852
|
+
openForm("Natural Language Query", [
|
|
1853
|
+
{ id: "question", label: "Question", placeholder: "How many active users?" },
|
|
1854
|
+
{
|
|
1855
|
+
id: "collection",
|
|
1856
|
+
label: "Collection (optional)",
|
|
1857
|
+
value: defaultCol,
|
|
1858
|
+
options: collections,
|
|
1859
|
+
allowCustom: true,
|
|
1860
|
+
},
|
|
1861
|
+
], async (vals) => {
|
|
1862
|
+
const question = (vals.question || "").trim();
|
|
1863
|
+
if (!question) {
|
|
1864
|
+
throw new Error("Question is required.");
|
|
1865
|
+
}
|
|
1866
|
+
const collection = (vals.collection || "").trim() || undefined;
|
|
1867
|
+
const result = await db.nlq.query(question, {
|
|
1868
|
+
collection,
|
|
1869
|
+
});
|
|
1870
|
+
const intent = result.intent;
|
|
1871
|
+
const lines = [
|
|
1872
|
+
` ${pc.bold("NLQ Result")}`,
|
|
1873
|
+
` ${pc.dim(`Question: ${question}`)}`,
|
|
1874
|
+
"",
|
|
1875
|
+
` ${result.answer}`,
|
|
1876
|
+
"",
|
|
1877
|
+
];
|
|
1878
|
+
if (intent) {
|
|
1879
|
+
lines.push(` ${pc.dim("Interpreted as:")} ${intent.action} ${pc.dim("on")} ${pc.cyan(intent.collection)}`);
|
|
1880
|
+
if (intent.function) {
|
|
1881
|
+
lines.push(` ${pc.dim("Function:")} ${intent.function}`);
|
|
1882
|
+
}
|
|
1883
|
+
if (intent.field) {
|
|
1884
|
+
lines.push(` ${pc.dim("Field:")} ${intent.field}`);
|
|
1885
|
+
}
|
|
1886
|
+
if (intent.query) {
|
|
1887
|
+
lines.push(` ${pc.dim("Query:")} ${intent.query}`);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
if (result.data !== undefined && result.data !== null) {
|
|
1891
|
+
lines.push("", highlightJson(result.data));
|
|
1892
|
+
}
|
|
1893
|
+
viewerLines = lines;
|
|
1894
|
+
loadedItemId = "nlq_result";
|
|
1895
|
+
draw();
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
async function handleCollectionOptions(selected) {
|
|
1899
|
+
const colName = selected?.type === "collection"
|
|
1900
|
+
? (selected.ref?.name ?? "")
|
|
1901
|
+
: selected?.type === "object"
|
|
1902
|
+
? (selected.ref?.collection ?? "")
|
|
1903
|
+
: "";
|
|
1904
|
+
if (!colName) {
|
|
1905
|
+
viewerLines = [pc.yellow("Select a collection first, then press [o] to set listing options.")];
|
|
1906
|
+
loadedItemId = "options_info";
|
|
1907
|
+
draw();
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
const current = collectionOptions.get(colName) ?? {};
|
|
1911
|
+
openForm(`Listing Options: ${colName}`, [
|
|
1912
|
+
{
|
|
1913
|
+
id: "sortBy",
|
|
1914
|
+
label: "Sort By",
|
|
1915
|
+
value: current.sortBy ?? "",
|
|
1916
|
+
options: ["", "id", "created_at", "updated_at", "version"],
|
|
1917
|
+
allowCustom: true,
|
|
1918
|
+
},
|
|
1919
|
+
{
|
|
1920
|
+
id: "sortDir",
|
|
1921
|
+
label: "Sort Direction",
|
|
1922
|
+
value: current.sortDir ?? "asc",
|
|
1923
|
+
options: ["asc", "desc"],
|
|
1924
|
+
},
|
|
1925
|
+
{ id: "limit", label: "Limit", value: String(current.limit ?? ""), placeholder: "50" },
|
|
1926
|
+
{ id: "offset", label: "Offset", value: String(current.offset ?? ""), placeholder: "0" },
|
|
1927
|
+
{
|
|
1928
|
+
id: "filter",
|
|
1929
|
+
label: "Filter (JSON)",
|
|
1930
|
+
value: current.filter ? JSON.stringify(current.filter) : "",
|
|
1931
|
+
placeholder: '{"status":"active"}',
|
|
1932
|
+
},
|
|
1933
|
+
], async (vals) => {
|
|
1934
|
+
const opts = {};
|
|
1935
|
+
if (vals.sortBy) {
|
|
1936
|
+
opts.sortBy = vals.sortBy;
|
|
1937
|
+
opts.sortDir = vals.sortDir || "asc";
|
|
1938
|
+
}
|
|
1939
|
+
if (vals.limit) {
|
|
1940
|
+
opts.limit = parseInt(vals.limit, 10) || 50;
|
|
1941
|
+
}
|
|
1942
|
+
if (vals.offset) {
|
|
1943
|
+
opts.offset = parseInt(vals.offset, 10) || 0;
|
|
1944
|
+
}
|
|
1945
|
+
if (vals.filter?.trim()) {
|
|
1946
|
+
opts.filter = JSON.parse(vals.filter.trim());
|
|
1947
|
+
}
|
|
1948
|
+
if (Object.keys(opts).length > 0) {
|
|
1949
|
+
collectionOptions.set(colName, opts);
|
|
1950
|
+
}
|
|
1951
|
+
else {
|
|
1952
|
+
collectionOptions.delete(colName);
|
|
1953
|
+
}
|
|
1954
|
+
await fetchResources();
|
|
1955
|
+
const tree = buildTree();
|
|
1956
|
+
const idx = tree.findIndex((n) => n.id === `col:${colName}`);
|
|
1957
|
+
if (idx !== -1) {
|
|
1958
|
+
cursorIndex = idx;
|
|
1959
|
+
}
|
|
1960
|
+
const n = tree[cursorIndex];
|
|
1961
|
+
if (n) {
|
|
1962
|
+
scheduleLoad(n);
|
|
1963
|
+
}
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
async function handleBatchOps(selected) {
|
|
1967
|
+
const colName = selected?.type === "collection"
|
|
1968
|
+
? (selected.ref?.name ?? "")
|
|
1969
|
+
: selected?.type === "object"
|
|
1970
|
+
? (selected.ref?.collection ?? "")
|
|
1971
|
+
: "";
|
|
1972
|
+
if (!colName) {
|
|
1973
|
+
viewerLines = [pc.yellow("Select a collection first, then press [b] for batch operations.")];
|
|
1974
|
+
loadedItemId = "batch_info";
|
|
1975
|
+
draw();
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
openForm(`Batch Ops: ${colName}`, [
|
|
1979
|
+
{
|
|
1980
|
+
id: "action",
|
|
1981
|
+
label: "Action",
|
|
1982
|
+
value: "put",
|
|
1983
|
+
options: ["put", "delete"],
|
|
1984
|
+
},
|
|
1985
|
+
{
|
|
1986
|
+
id: "input",
|
|
1987
|
+
label: "JSON File Path or IDs (comma-sep for delete)",
|
|
1988
|
+
placeholder: "/path/to/file.json or id1,id2,id3",
|
|
1989
|
+
},
|
|
1990
|
+
], async (vals) => {
|
|
1991
|
+
const action = vals.action || "";
|
|
1992
|
+
const input = (vals.input || "").trim();
|
|
1993
|
+
if (!input) {
|
|
1994
|
+
throw new Error("Input is required.");
|
|
1995
|
+
}
|
|
1996
|
+
if (action === "put") {
|
|
1997
|
+
const data = JSON.parse(await fs.promises.readFile(input, "utf-8"));
|
|
1998
|
+
const objects = Array.isArray(data) ? data : (data.objects ?? [data]);
|
|
1999
|
+
const result = await db.putBatch(colName, objects);
|
|
2000
|
+
viewerLines = [
|
|
2001
|
+
` ${pc.bold("Batch Put Complete")}`,
|
|
2002
|
+
` ${pc.dim(`Collection: ${colName}`)}`,
|
|
2003
|
+
` ${pc.dim(`Objects: ${result.length}`)}`,
|
|
2004
|
+
];
|
|
2005
|
+
loadedItemId = "batch_result";
|
|
2006
|
+
draw();
|
|
2007
|
+
}
|
|
2008
|
+
else if (action === "delete") {
|
|
2009
|
+
const ids = input
|
|
2010
|
+
.split(",")
|
|
2011
|
+
.map((s) => s.trim())
|
|
2012
|
+
.filter(Boolean);
|
|
2013
|
+
const count = await db.deleteBatch(colName, ids);
|
|
2014
|
+
viewerLines = [
|
|
2015
|
+
` ${pc.bold("Batch Delete Complete")}`,
|
|
2016
|
+
` ${pc.dim(`Collection: ${colName}`)}`,
|
|
2017
|
+
` ${pc.dim(`Deleted: ${count} objects`)}`,
|
|
2018
|
+
];
|
|
2019
|
+
loadedItemId = "batch_result";
|
|
2020
|
+
draw();
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
1464
2024
|
async function handleMaintenance() {
|
|
1465
2025
|
// Cycle through maintenance operations with each press of 'm'
|
|
1466
2026
|
const operations = ["health", "checkpoint", "backup"];
|
|
@@ -1756,9 +2316,24 @@ function setupKeypress() {
|
|
|
1756
2316
|
else if (str === "i" || str === "I") {
|
|
1757
2317
|
await handleInfo();
|
|
1758
2318
|
}
|
|
1759
|
-
else if (str === "n"
|
|
2319
|
+
else if (str === "n") {
|
|
1760
2320
|
await handleNeighbors(tree[cursorIndex]);
|
|
1761
2321
|
}
|
|
2322
|
+
else if (str === "N") {
|
|
2323
|
+
await handleNlq(tree[cursorIndex]);
|
|
2324
|
+
}
|
|
2325
|
+
else if (str === "a" || str === "A") {
|
|
2326
|
+
await handleAggregate(tree[cursorIndex]);
|
|
2327
|
+
}
|
|
2328
|
+
else if (str === "t" || str === "T") {
|
|
2329
|
+
await handleTimeseries(tree[cursorIndex]);
|
|
2330
|
+
}
|
|
2331
|
+
else if (str === "o" || str === "O") {
|
|
2332
|
+
await handleCollectionOptions(tree[cursorIndex]);
|
|
2333
|
+
}
|
|
2334
|
+
else if (str === "b" || str === "B") {
|
|
2335
|
+
await handleBatchOps(tree[cursorIndex]);
|
|
2336
|
+
}
|
|
1762
2337
|
else if (str === "m" || str === "M") {
|
|
1763
2338
|
await handleMaintenance();
|
|
1764
2339
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thingd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.0",
|
|
4
4
|
"description": "CLI, Interactive TUI Dashboard, and MCP server for thingd — a fast object-first data engine for applications and AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://engine.thingd.cloud",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"cli-table3": "^0.6.5",
|
|
46
46
|
"picocolors": "^1.1.1",
|
|
47
47
|
"zod": "^4.4.3",
|
|
48
|
-
"@thingd/sdk": "0.
|
|
48
|
+
"@thingd/sdk": "0.56.0"
|
|
49
49
|
},
|
|
50
50
|
"engines": {
|
|
51
51
|
"node": ">=24.0.0"
|