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.
@@ -0,0 +1,9 @@
1
+ import { KhotanMappingBrowser } from "@/components/khotan/mapping-browser";
2
+
3
+ export default function KhotanMappingsPage() {
4
+ return (
5
+ <main className="container mx-auto max-w-7xl px-4 py-10">
6
+ <KhotanMappingBrowser />
7
+ </main>
8
+ );
9
+ }
@@ -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 "./outflow";
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 hubspotPlug.post("/products", {
69
- // vars: ctx.vars,
70
- // body: record,
71
- // });
71
+ // await hubspot.post("/products", { body: record });
72
72
  // }
73
73
  //
74
74
  // return {
@@ -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
- const res = await fetch(`${basePath}/debug/${plugName}`);
358
- if (!res.ok) {
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: { relay: ctx.flow.name, to: ctx.flow.to },
54
+ metadata: {
55
+ relay: ctx.flow.name,
56
+ to: ctx.flow.to,
57
+ previousCount: previousRecords.length,
58
+ },
49
59
  };
50
60
  }
51
61
 
@@ -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 "./relay";
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 response = await shopifyPlug.get<{ data?: Array<Record<string, unknown>> }>("/products", {
70
- // vars: ctx.vars,
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 hubspotPlug.post("/products", { body: record });
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: { relay: ctx.flow.name, to: ctx.flow.to },
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<string | null>(null);
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 res = await fetch(
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 instanceof Error ? err.message : "Unknown error");
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
- <div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
458
- {error}
459
- </div>
454
+ <ApiErrorState
455
+ error={error}
456
+ onRetry={() => setRefreshKey((v) => v + 1)}
457
+ compact
458
+ />
460
459
  ) : null}
461
460
 
462
- <Table>
463
- <TableHeader>
464
- <TableRow>
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
- <TableCell
479
- colSpan={8}
480
- className="text-sm text-muted-foreground"
481
- >
482
- Loading runs...
483
- </TableCell>
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
- ) : data?.items.length ? (
486
- data.items.map((item) => (
487
- <Fragment key={item.id}>
488
- <TableRow>
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>
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 colSpan={8}>
542
- <RunDetails
543
- run={item}
544
- streamingEnabled={streamingEnabled}
545
- onChanged={() => setRefreshKey((v) => v + 1)}
546
- onStreamInbound={pulseLiveIndicator}
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
- ) : 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>
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
- <div className="flex items-center justify-between gap-3">
567
- <p className="text-sm text-muted-foreground">
568
- Page {Math.floor(offset / pageSize) + 1}
569
- </p>
570
- <div className="flex items-center gap-2">
571
- <Button
572
- variant="outline"
573
- size="sm"
574
- disabled={offset === 0 || loading}
575
- onClick={() => setOffset(Math.max(offset - pageSize, 0))}
576
- >
577
- Previous
578
- </Button>
579
- <Button
580
- variant="outline"
581
- size="sm"
582
- disabled={!data?.page.hasMore || loading}
583
- onClick={() => setOffset(offset + pageSize)}
584
- >
585
- Next
586
- </Button>
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
- </div>
591
+ )}
589
592
  </CardContent>
590
593
  </Card>
591
594
  );
@@ -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;