khotan-data 0.1.0 → 0.2.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/README.md +89 -6
- package/dist/cli.js +405 -35
- package/dist/factory.cjs +1160 -106
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.cts +262 -38
- package/dist/factory.d.ts +262 -38
- package/dist/factory.js +1158 -108
- package/dist/factory.js.map +1 -1
- package/dist/templates/api-state.tsx +249 -0
- package/dist/templates/cache.example.ts +11 -0
- package/dist/templates/cache.ts +58 -0
- package/dist/templates/catch.ts +13 -1
- package/dist/templates/debug-index-page.tsx +56 -36
- package/dist/templates/hub.tsx +9 -23
- package/dist/templates/inflow.ts +5 -6
- package/dist/templates/khotan-config.ts +30 -4
- package/dist/templates/mapping-browser.tsx +773 -0
- package/dist/templates/mappings-page.tsx +9 -0
- package/dist/templates/outflow.ts +5 -5
- package/dist/templates/pass.ts +10 -0
- package/dist/templates/plug-debugger.tsx +15 -7
- package/dist/templates/relay.example.ts +11 -1
- package/dist/templates/relay.ts +16 -7
- package/dist/templates/runs-table.tsx +133 -130
- package/dist/templates/schema.ts +81 -0
- package/dist/templates/skill-plug.md +38 -15
- package/dist/templates/skill-setup.md +80 -3
- package/dist/templates/topology-canvas.tsx +19 -30
- package/dist/templates/var-panel.tsx +33 -10
- package/dist/templates/webhook-events-table.tsx +105 -102
- package/dist/templates/wire-panel.tsx +30 -8
- package/package.json +1 -1
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// This file defines the outflow() builder and types. Create per-service flow
|
|
6
6
|
// files (e.g. crm-audiences.ts) using this builder to read app data and push it
|
|
7
7
|
// to an external service with durable, retryable Vercel Workflow steps.
|
|
8
|
+
// Outflow workflows can also use khotanCache(ctx, "name") for checkpoints, cursor
|
|
9
|
+
// state, or dedupe markers between runs.
|
|
8
10
|
// ============================================================================
|
|
9
11
|
|
|
10
12
|
import type {
|
|
@@ -46,7 +48,7 @@ export function outflow(config: OutflowConfig): FlowRegistration {
|
|
|
46
48
|
// Usage Example (create a file like flows/hubspot-products.ts)
|
|
47
49
|
// ---------------------------------------------------------------------------
|
|
48
50
|
//
|
|
49
|
-
// import { outflow, type OutflowContext } from "
|
|
51
|
+
// import { bindWorkflowPlug, outflow, type OutflowContext } from "khotan-data/factory";
|
|
50
52
|
// import { db } from "@/db";
|
|
51
53
|
// import { products } from "@/db/schema";
|
|
52
54
|
// import { hubspotPlug } from "../plugs/hubspot";
|
|
@@ -61,14 +63,12 @@ export function outflow(config: OutflowConfig): FlowRegistration {
|
|
|
61
63
|
// khotanRunId: ctx.khotanRunId,
|
|
62
64
|
// runType: ctx.runType,
|
|
63
65
|
// });
|
|
66
|
+
// const hubspot = bindWorkflowPlug(hubspotPlug, ctx);
|
|
64
67
|
//
|
|
65
68
|
// const records = await db.select().from(products);
|
|
66
69
|
//
|
|
67
70
|
// for (const record of records) {
|
|
68
|
-
// await
|
|
69
|
-
// vars: ctx.vars,
|
|
70
|
-
// body: record,
|
|
71
|
-
// });
|
|
71
|
+
// await hubspot.post("/products", { body: record });
|
|
72
72
|
// }
|
|
73
73
|
//
|
|
74
74
|
// return {
|
package/dist/templates/pass.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface PassContext {
|
|
|
26
26
|
destVars: Record<string, string>;
|
|
27
27
|
/** Khotan run ID created for this webhook handler execution */
|
|
28
28
|
khotanRunId: string;
|
|
29
|
+
/** Internal Khotan instance identifier for helper APIs */
|
|
30
|
+
khotanInstanceId: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
// ---------------------------------------------------------------------------
|
|
@@ -79,6 +81,7 @@ export function pass(config: PassConfig): PassRegistration {
|
|
|
79
81
|
// Usage Example (create a file like webhooks/pollinate-to-slack.ts)
|
|
80
82
|
// ---------------------------------------------------------------------------
|
|
81
83
|
//
|
|
84
|
+
// import { khotanCache } from "khotan-data/factory";
|
|
82
85
|
// import { pass, type PassContext } from "./pass";
|
|
83
86
|
// import { plug } from "../plugs/plug";
|
|
84
87
|
//
|
|
@@ -87,6 +90,9 @@ export function pass(config: PassConfig): PassRegistration {
|
|
|
87
90
|
//
|
|
88
91
|
// async function forwardEvent() {
|
|
89
92
|
// "use step";
|
|
93
|
+
// const cache = khotanCache(ctx, "pollinate-forwarded-events");
|
|
94
|
+
// const eventId = String(ctx.event["id"] ?? "");
|
|
95
|
+
// if (eventId && (await cache.get<boolean>(eventId))) return;
|
|
90
96
|
//
|
|
91
97
|
// // Construct destination plug from destVars
|
|
92
98
|
// const slackPlug = plug({
|
|
@@ -102,6 +108,10 @@ export function pass(config: PassConfig): PassRegistration {
|
|
|
102
108
|
// event: ctx.event,
|
|
103
109
|
// },
|
|
104
110
|
// });
|
|
111
|
+
//
|
|
112
|
+
// if (eventId) {
|
|
113
|
+
// await cache.set(eventId, true);
|
|
114
|
+
// }
|
|
105
115
|
// }
|
|
106
116
|
//
|
|
107
117
|
// await forwardEvent();
|
|
@@ -5,6 +5,7 @@ import { Badge } from "@/components/ui/badge";
|
|
|
5
5
|
import { Button } from "@/components/ui/button";
|
|
6
6
|
import { Input } from "@/components/ui/input";
|
|
7
7
|
import { Label } from "@/components/ui/label";
|
|
8
|
+
import { khotanFetch, isKhotanApiError, ApiErrorState } from "./api-state";
|
|
8
9
|
|
|
9
10
|
// ============================================================================
|
|
10
11
|
// Plug Debugger — Lightweight Postman for your plugs
|
|
@@ -331,6 +332,7 @@ export function PlugDebugger({
|
|
|
331
332
|
}: PlugDebuggerProps) {
|
|
332
333
|
const [meta, setMeta] = useState<PlugMeta | null>(null);
|
|
333
334
|
const [loading, setLoading] = useState(true);
|
|
335
|
+
const [metaError, setMetaError] = useState<unknown>(null);
|
|
334
336
|
|
|
335
337
|
const [method, setMethod] = useState<string>("GET");
|
|
336
338
|
const [path, setPath] = useState("");
|
|
@@ -353,15 +355,17 @@ export function PlugDebugger({
|
|
|
353
355
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
354
356
|
|
|
355
357
|
const fetchMeta = useCallback(async () => {
|
|
358
|
+
setLoading(true);
|
|
359
|
+
setMetaError(null);
|
|
356
360
|
try {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
setMeta(null);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
setMeta(await res.json());
|
|
363
|
-
} catch {
|
|
361
|
+
setMeta(await khotanFetch<PlugMeta>(`${basePath}/debug/${plugName}`));
|
|
362
|
+
} catch (err) {
|
|
364
363
|
setMeta(null);
|
|
364
|
+
// A 404 means debug is off or the plug isn't registered — handled by the
|
|
365
|
+
// dedicated "not available" message below. Surface everything else.
|
|
366
|
+
if (!(isKhotanApiError(err) && err.status === 404)) {
|
|
367
|
+
setMetaError(err);
|
|
368
|
+
}
|
|
365
369
|
} finally {
|
|
366
370
|
setLoading(false);
|
|
367
371
|
}
|
|
@@ -401,6 +405,10 @@ export function PlugDebugger({
|
|
|
401
405
|
);
|
|
402
406
|
}
|
|
403
407
|
|
|
408
|
+
if (metaError) {
|
|
409
|
+
return <ApiErrorState error={metaError} onRetry={() => void fetchMeta()} />;
|
|
410
|
+
}
|
|
411
|
+
|
|
404
412
|
if (!meta) {
|
|
405
413
|
return (
|
|
406
414
|
<div className="rounded-lg border border-border p-6 text-center">
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// exported flow in {outputDir}/khotan.ts.
|
|
7
7
|
// ============================================================================
|
|
8
8
|
|
|
9
|
+
import { khotanCache } from "khotan-data/factory";
|
|
9
10
|
import { relay, type RelayContext } from "./relay";
|
|
10
11
|
|
|
11
12
|
async function shopifyToHubspotWorkflow(ctx: RelayContext) {
|
|
@@ -29,6 +30,11 @@ async function shopifyToHubspotWorkflow(ctx: RelayContext) {
|
|
|
29
30
|
data?: Array<Record<string, unknown>>;
|
|
30
31
|
};
|
|
31
32
|
const records = Array.isArray(payload.data) ? payload.data : [];
|
|
33
|
+
const snapshotCache = khotanCache(ctx, "shopify-products-snapshot");
|
|
34
|
+
const previousRecords =
|
|
35
|
+
(await snapshotCache.get<Array<Record<string, unknown>>>("latest")) ?? [];
|
|
36
|
+
|
|
37
|
+
await snapshotCache.set("latest", records, { ttl: "6h" });
|
|
32
38
|
|
|
33
39
|
for (const record of records) {
|
|
34
40
|
await fetch("https://destination.example.com/products", {
|
|
@@ -45,7 +51,11 @@ async function shopifyToHubspotWorkflow(ctx: RelayContext) {
|
|
|
45
51
|
extracted: records.length,
|
|
46
52
|
transformed: records.length,
|
|
47
53
|
created: records.length,
|
|
48
|
-
metadata: {
|
|
54
|
+
metadata: {
|
|
55
|
+
relay: ctx.flow.name,
|
|
56
|
+
to: ctx.flow.to,
|
|
57
|
+
previousCount: previousRecords.length,
|
|
58
|
+
},
|
|
49
59
|
};
|
|
50
60
|
}
|
|
51
61
|
|
package/dist/templates/relay.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// This file defines the relay() builder and types. Create per-service flow
|
|
6
6
|
// files (e.g. shopify-to-hubspot.ts) using this builder to read from the source
|
|
7
7
|
// plug and forward to a destination system with durable, retryable Vercel
|
|
8
|
-
// Workflow steps.
|
|
8
|
+
// Workflow steps. Relay workflows can also use khotanCache(ctx, "name") for durable
|
|
9
|
+
// snapshots, checkpoints, and dedupe state between runs.
|
|
9
10
|
// ============================================================================
|
|
10
11
|
|
|
11
12
|
import type {
|
|
@@ -50,7 +51,7 @@ export function relay(config: RelayConfig): FlowRegistration {
|
|
|
50
51
|
// Usage Example (create a file like flows/shopify-to-hubspot.ts)
|
|
51
52
|
// ---------------------------------------------------------------------------
|
|
52
53
|
//
|
|
53
|
-
// import { relay, type RelayContext } from "
|
|
54
|
+
// import { bindWorkflowPlug, khotanCache, relay, type RelayContext } from "khotan-data/factory";
|
|
54
55
|
// import { shopifyPlug } from "../plugs/shopify";
|
|
55
56
|
// import { hubspotPlug } from "../plugs/hubspot";
|
|
56
57
|
//
|
|
@@ -65,21 +66,29 @@ export function relay(config: RelayConfig): FlowRegistration {
|
|
|
65
66
|
// khotanRunId: ctx.khotanRunId,
|
|
66
67
|
// runType: ctx.runType,
|
|
67
68
|
// });
|
|
69
|
+
// const shopify = bindWorkflowPlug(shopifyPlug, ctx);
|
|
70
|
+
// const hubspot = bindWorkflowPlug(hubspotPlug, ctx, "hubspot");
|
|
68
71
|
//
|
|
69
|
-
// const
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
+
// const snapshotCache = khotanCache(ctx, "shopify-products-snapshot");
|
|
73
|
+
// const previous = await snapshotCache.get<Array<Record<string, unknown>>>("latest");
|
|
74
|
+
//
|
|
75
|
+
// const response = await shopify.get<{ data?: Array<Record<string, unknown>> }>("/products");
|
|
72
76
|
// const records = Array.isArray(response.data) ? response.data : [];
|
|
77
|
+
// await snapshotCache.set("latest", records);
|
|
73
78
|
//
|
|
74
79
|
// for (const record of records) {
|
|
75
|
-
// await
|
|
80
|
+
// await hubspot.post("/products", { body: record });
|
|
76
81
|
// }
|
|
77
82
|
//
|
|
78
83
|
// return {
|
|
79
84
|
// extracted: records.length,
|
|
80
85
|
// transformed: records.length,
|
|
81
86
|
// created: records.length,
|
|
82
|
-
// metadata: {
|
|
87
|
+
// metadata: {
|
|
88
|
+
// relay: ctx.flow.name,
|
|
89
|
+
// to: ctx.flow.to,
|
|
90
|
+
// previousCount: previous?.length ?? 0,
|
|
91
|
+
// },
|
|
83
92
|
// };
|
|
84
93
|
// }
|
|
85
94
|
//
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Fragment, useCallback, useEffect, useState } from "react";
|
|
4
4
|
import { RefreshCw } from "lucide-react";
|
|
5
|
+
import { khotanFetch, ApiErrorState } from "./api-state";
|
|
5
6
|
import { Badge } from "@/components/ui/badge";
|
|
6
7
|
import { Button } from "@/components/ui/button";
|
|
7
8
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
@@ -359,7 +360,7 @@ export function KhotanRunsTable({ pageSize = 10 }: { pageSize?: number } = {}) {
|
|
|
359
360
|
const [data, setData] = useState<PageResponse<RunLogItem> | null>(null);
|
|
360
361
|
const [offset, setOffset] = useState(0);
|
|
361
362
|
const [loading, setLoading] = useState(true);
|
|
362
|
-
const [error, setError] = useState<
|
|
363
|
+
const [error, setError] = useState<unknown>(null);
|
|
363
364
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
364
365
|
const [expandedRunId, setExpandedRunId] = useState<string | null>(null);
|
|
365
366
|
const [streamingEnabled, setStreamingEnabled] = useState(false);
|
|
@@ -378,20 +379,16 @@ export function KhotanRunsTable({ pageSize = 10 }: { pageSize?: number } = {}) {
|
|
|
378
379
|
setLoading(true);
|
|
379
380
|
setError(null);
|
|
380
381
|
try {
|
|
381
|
-
const
|
|
382
|
+
const json = await khotanFetch<PageResponse<RunLogItem>>(
|
|
382
383
|
`/api/khotan/runs?limit=${String(pageSize)}&offset=${String(offset)}`,
|
|
383
384
|
);
|
|
384
|
-
if (!res.ok) {
|
|
385
|
-
throw new Error("Failed to load runs");
|
|
386
|
-
}
|
|
387
|
-
const json = (await res.json()) as PageResponse<RunLogItem>;
|
|
388
385
|
if (!cancelled) {
|
|
389
386
|
setData(json);
|
|
390
387
|
setLastUpdatedAt(new Date().toISOString());
|
|
391
388
|
}
|
|
392
389
|
} catch (err) {
|
|
393
390
|
if (!cancelled) {
|
|
394
|
-
setError(err
|
|
391
|
+
setError(err);
|
|
395
392
|
}
|
|
396
393
|
} finally {
|
|
397
394
|
if (!cancelled) {
|
|
@@ -454,138 +451,144 @@ export function KhotanRunsTable({ pageSize = 10 }: { pageSize?: number } = {}) {
|
|
|
454
451
|
</CardHeader>
|
|
455
452
|
<CardContent className="space-y-4">
|
|
456
453
|
{error ? (
|
|
457
|
-
<
|
|
458
|
-
{error}
|
|
459
|
-
|
|
454
|
+
<ApiErrorState
|
|
455
|
+
error={error}
|
|
456
|
+
onRetry={() => setRefreshKey((v) => v + 1)}
|
|
457
|
+
compact
|
|
458
|
+
/>
|
|
460
459
|
) : null}
|
|
461
460
|
|
|
462
|
-
|
|
463
|
-
<
|
|
464
|
-
<
|
|
465
|
-
<TableHead>Started</TableHead>
|
|
466
|
-
<TableHead>Status</TableHead>
|
|
467
|
-
<TableHead>Source</TableHead>
|
|
468
|
-
<TableHead>Plug</TableHead>
|
|
469
|
-
<TableHead>Run Type</TableHead>
|
|
470
|
-
<TableHead>Counts</TableHead>
|
|
471
|
-
<TableHead>Workflow</TableHead>
|
|
472
|
-
<TableHead />
|
|
473
|
-
</TableRow>
|
|
474
|
-
</TableHeader>
|
|
475
|
-
<TableBody>
|
|
476
|
-
{loading ? (
|
|
461
|
+
{error ? null : (
|
|
462
|
+
<Table>
|
|
463
|
+
<TableHeader>
|
|
477
464
|
<TableRow>
|
|
478
|
-
<
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
>
|
|
482
|
-
|
|
483
|
-
</
|
|
465
|
+
<TableHead>Started</TableHead>
|
|
466
|
+
<TableHead>Status</TableHead>
|
|
467
|
+
<TableHead>Source</TableHead>
|
|
468
|
+
<TableHead>Plug</TableHead>
|
|
469
|
+
<TableHead>Run Type</TableHead>
|
|
470
|
+
<TableHead>Counts</TableHead>
|
|
471
|
+
<TableHead>Workflow</TableHead>
|
|
472
|
+
<TableHead />
|
|
484
473
|
</TableRow>
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
{statusLabel[item.status]}
|
|
500
|
-
</Badge>
|
|
501
|
-
{item.error ? (
|
|
502
|
-
<div
|
|
503
|
-
className="mt-1 max-w-56 truncate text-xs text-destructive"
|
|
504
|
-
title={item.error}
|
|
505
|
-
>
|
|
506
|
-
{item.error}
|
|
507
|
-
</div>
|
|
508
|
-
) : null}
|
|
509
|
-
</TableCell>
|
|
510
|
-
<TableCell className="font-medium">
|
|
511
|
-
{formatSource(item)}
|
|
512
|
-
</TableCell>
|
|
513
|
-
<TableCell className="text-muted-foreground">
|
|
514
|
-
{item.plugName ?? "-"}
|
|
515
|
-
</TableCell>
|
|
516
|
-
<TableCell className="font-mono text-xs">
|
|
517
|
-
{item.runType}
|
|
518
|
-
</TableCell>
|
|
519
|
-
<TableCell className="max-w-64 text-xs text-muted-foreground">
|
|
520
|
-
{formatCounts(item)}
|
|
521
|
-
</TableCell>
|
|
522
|
-
<TableCell className="font-mono text-xs text-muted-foreground">
|
|
523
|
-
{item.workflowRunId ?? "-"}
|
|
524
|
-
</TableCell>
|
|
525
|
-
<TableCell className="text-right">
|
|
526
|
-
<Button
|
|
527
|
-
variant="outline"
|
|
528
|
-
size="sm"
|
|
529
|
-
onClick={() =>
|
|
530
|
-
setExpandedRunId((current) =>
|
|
531
|
-
current === item.id ? null : item.id,
|
|
532
|
-
)
|
|
533
|
-
}
|
|
534
|
-
>
|
|
535
|
-
{expandedRunId === item.id ? "Hide" : "Details"}
|
|
536
|
-
</Button>
|
|
537
|
-
</TableCell>
|
|
538
|
-
</TableRow>
|
|
539
|
-
{expandedRunId === item.id ? (
|
|
474
|
+
</TableHeader>
|
|
475
|
+
<TableBody>
|
|
476
|
+
{loading ? (
|
|
477
|
+
<TableRow>
|
|
478
|
+
<TableCell
|
|
479
|
+
colSpan={8}
|
|
480
|
+
className="text-sm text-muted-foreground"
|
|
481
|
+
>
|
|
482
|
+
Loading runs...
|
|
483
|
+
</TableCell>
|
|
484
|
+
</TableRow>
|
|
485
|
+
) : data?.items.length ? (
|
|
486
|
+
data.items.map((item) => (
|
|
487
|
+
<Fragment key={item.id}>
|
|
540
488
|
<TableRow>
|
|
541
|
-
<TableCell
|
|
542
|
-
<
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
489
|
+
<TableCell className="text-sm text-muted-foreground">
|
|
490
|
+
<div>{formatDateTime(item.startedAt)}</div>
|
|
491
|
+
<div className="text-xs">
|
|
492
|
+
{item.completedAt
|
|
493
|
+
? `completed ${formatDateTime(item.completedAt)}`
|
|
494
|
+
: "in progress"}
|
|
495
|
+
</div>
|
|
496
|
+
</TableCell>
|
|
497
|
+
<TableCell>
|
|
498
|
+
<Badge variant={statusVariant[item.status]}>
|
|
499
|
+
{statusLabel[item.status]}
|
|
500
|
+
</Badge>
|
|
501
|
+
{item.error ? (
|
|
502
|
+
<div
|
|
503
|
+
className="mt-1 max-w-56 truncate text-xs text-destructive"
|
|
504
|
+
title={item.error}
|
|
505
|
+
>
|
|
506
|
+
{item.error}
|
|
507
|
+
</div>
|
|
508
|
+
) : null}
|
|
509
|
+
</TableCell>
|
|
510
|
+
<TableCell className="font-medium">
|
|
511
|
+
{formatSource(item)}
|
|
512
|
+
</TableCell>
|
|
513
|
+
<TableCell className="text-muted-foreground">
|
|
514
|
+
{item.plugName ?? "-"}
|
|
515
|
+
</TableCell>
|
|
516
|
+
<TableCell className="font-mono text-xs">
|
|
517
|
+
{item.runType}
|
|
518
|
+
</TableCell>
|
|
519
|
+
<TableCell className="max-w-64 text-xs text-muted-foreground">
|
|
520
|
+
{formatCounts(item)}
|
|
521
|
+
</TableCell>
|
|
522
|
+
<TableCell className="font-mono text-xs text-muted-foreground">
|
|
523
|
+
{item.workflowRunId ?? "-"}
|
|
524
|
+
</TableCell>
|
|
525
|
+
<TableCell className="text-right">
|
|
526
|
+
<Button
|
|
527
|
+
variant="outline"
|
|
528
|
+
size="sm"
|
|
529
|
+
onClick={() =>
|
|
530
|
+
setExpandedRunId((current) =>
|
|
531
|
+
current === item.id ? null : item.id,
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
>
|
|
535
|
+
{expandedRunId === item.id ? "Hide" : "Details"}
|
|
536
|
+
</Button>
|
|
548
537
|
</TableCell>
|
|
549
538
|
</TableRow>
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
539
|
+
{expandedRunId === item.id ? (
|
|
540
|
+
<TableRow>
|
|
541
|
+
<TableCell colSpan={8}>
|
|
542
|
+
<RunDetails
|
|
543
|
+
run={item}
|
|
544
|
+
streamingEnabled={streamingEnabled}
|
|
545
|
+
onChanged={() => setRefreshKey((v) => v + 1)}
|
|
546
|
+
onStreamInbound={pulseLiveIndicator}
|
|
547
|
+
/>
|
|
548
|
+
</TableCell>
|
|
549
|
+
</TableRow>
|
|
550
|
+
) : null}
|
|
551
|
+
</Fragment>
|
|
552
|
+
))
|
|
553
|
+
) : (
|
|
554
|
+
<TableRow>
|
|
555
|
+
<TableCell
|
|
556
|
+
colSpan={8}
|
|
557
|
+
className="text-sm text-muted-foreground"
|
|
558
|
+
>
|
|
559
|
+
No runs recorded yet.
|
|
560
|
+
</TableCell>
|
|
561
|
+
</TableRow>
|
|
562
|
+
)}
|
|
563
|
+
</TableBody>
|
|
564
|
+
</Table>
|
|
565
|
+
)}
|
|
565
566
|
|
|
566
|
-
|
|
567
|
-
<
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
<
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
567
|
+
{error ? null : (
|
|
568
|
+
<div className="flex items-center justify-between gap-3">
|
|
569
|
+
<p className="text-sm text-muted-foreground">
|
|
570
|
+
Page {Math.floor(offset / pageSize) + 1}
|
|
571
|
+
</p>
|
|
572
|
+
<div className="flex items-center gap-2">
|
|
573
|
+
<Button
|
|
574
|
+
variant="outline"
|
|
575
|
+
size="sm"
|
|
576
|
+
disabled={offset === 0 || loading}
|
|
577
|
+
onClick={() => setOffset(Math.max(offset - pageSize, 0))}
|
|
578
|
+
>
|
|
579
|
+
Previous
|
|
580
|
+
</Button>
|
|
581
|
+
<Button
|
|
582
|
+
variant="outline"
|
|
583
|
+
size="sm"
|
|
584
|
+
disabled={!data?.page.hasMore || loading}
|
|
585
|
+
onClick={() => setOffset(offset + pageSize)}
|
|
586
|
+
>
|
|
587
|
+
Next
|
|
588
|
+
</Button>
|
|
589
|
+
</div>
|
|
587
590
|
</div>
|
|
588
|
-
|
|
591
|
+
)}
|
|
589
592
|
</CardContent>
|
|
590
593
|
</Card>
|
|
591
594
|
);
|
package/dist/templates/schema.ts
CHANGED
|
@@ -303,6 +303,67 @@ export const khotanMappings = pgTable(
|
|
|
303
303
|
],
|
|
304
304
|
);
|
|
305
305
|
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// khotan_caches — one row per registered durable cache namespace
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
export const khotanCaches = pgTable(
|
|
311
|
+
"khotan_caches",
|
|
312
|
+
{
|
|
313
|
+
id: text("id")
|
|
314
|
+
.primaryKey()
|
|
315
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
316
|
+
name: text("name").notNull().unique(),
|
|
317
|
+
scope: jsonb("scope").$type<{
|
|
318
|
+
plug?: string;
|
|
319
|
+
resource?: string;
|
|
320
|
+
flow?: string;
|
|
321
|
+
}>(),
|
|
322
|
+
ttlSeconds: integer("ttl_seconds"),
|
|
323
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
324
|
+
.defaultNow()
|
|
325
|
+
.notNull(),
|
|
326
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
327
|
+
.defaultNow()
|
|
328
|
+
.notNull(),
|
|
329
|
+
},
|
|
330
|
+
(table) => [index("khotan_caches_name_idx").on(table.name)],
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// khotan_cache_entries — latest-value durable cache rows keyed by cache + key
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
export const khotanCacheEntries = pgTable(
|
|
338
|
+
"khotan_cache_entries",
|
|
339
|
+
{
|
|
340
|
+
id: text("id")
|
|
341
|
+
.primaryKey()
|
|
342
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
343
|
+
cacheId: text("cache_id")
|
|
344
|
+
.notNull()
|
|
345
|
+
.references(() => khotanCaches.id),
|
|
346
|
+
key: text("key").notNull(),
|
|
347
|
+
value: jsonb("value").notNull(),
|
|
348
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
349
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
350
|
+
.defaultNow()
|
|
351
|
+
.notNull(),
|
|
352
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
353
|
+
.defaultNow()
|
|
354
|
+
.notNull(),
|
|
355
|
+
},
|
|
356
|
+
(table) => [
|
|
357
|
+
unique("khotan_cache_entries_cache_id_key_unique").on(
|
|
358
|
+
table.cacheId,
|
|
359
|
+
table.key,
|
|
360
|
+
),
|
|
361
|
+
index("khotan_cache_entries_cache_id_idx").on(table.cacheId),
|
|
362
|
+
index("khotan_cache_entries_cache_id_key_idx").on(table.cacheId, table.key),
|
|
363
|
+
index("khotan_cache_entries_expires_at_idx").on(table.expiresAt),
|
|
364
|
+
],
|
|
365
|
+
);
|
|
366
|
+
|
|
306
367
|
// ---------------------------------------------------------------------------
|
|
307
368
|
// Relations
|
|
308
369
|
// ---------------------------------------------------------------------------
|
|
@@ -395,6 +456,20 @@ export const khotanMappingsRelations = relations(khotanMappings, ({ one }) => ({
|
|
|
395
456
|
}),
|
|
396
457
|
}));
|
|
397
458
|
|
|
459
|
+
export const khotanCachesRelations = relations(khotanCaches, ({ many }) => ({
|
|
460
|
+
entries: many(khotanCacheEntries),
|
|
461
|
+
}));
|
|
462
|
+
|
|
463
|
+
export const khotanCacheEntriesRelations = relations(
|
|
464
|
+
khotanCacheEntries,
|
|
465
|
+
({ one }) => ({
|
|
466
|
+
cache: one(khotanCaches, {
|
|
467
|
+
fields: [khotanCacheEntries.cacheId],
|
|
468
|
+
references: [khotanCaches.id],
|
|
469
|
+
}),
|
|
470
|
+
}),
|
|
471
|
+
);
|
|
472
|
+
|
|
398
473
|
// ---------------------------------------------------------------------------
|
|
399
474
|
// Type helpers
|
|
400
475
|
// ---------------------------------------------------------------------------
|
|
@@ -422,3 +497,9 @@ export type NewKhotanResource = typeof khotanResources.$inferInsert;
|
|
|
422
497
|
|
|
423
498
|
export type KhotanMapping = typeof khotanMappings.$inferSelect;
|
|
424
499
|
export type NewKhotanMapping = typeof khotanMappings.$inferInsert;
|
|
500
|
+
|
|
501
|
+
export type KhotanCache = typeof khotanCaches.$inferSelect;
|
|
502
|
+
export type NewKhotanCache = typeof khotanCaches.$inferInsert;
|
|
503
|
+
|
|
504
|
+
export type KhotanCacheEntry = typeof khotanCacheEntries.$inferSelect;
|
|
505
|
+
export type NewKhotanCacheEntry = typeof khotanCacheEntries.$inferInsert;
|