khotan-data 0.0.1 → 0.1.1

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.
Files changed (55) hide show
  1. package/AGENTS.md +54 -0
  2. package/README.md +117 -1
  3. package/dist/cli.js +2869 -0
  4. package/dist/factory.cjs +3303 -0
  5. package/dist/factory.cjs.map +1 -0
  6. package/dist/factory.d.cts +662 -0
  7. package/dist/factory.d.ts +662 -0
  8. package/dist/factory.js +3292 -0
  9. package/dist/factory.js.map +1 -0
  10. package/dist/plug-client.cjs +99 -0
  11. package/dist/plug-client.cjs.map +1 -0
  12. package/dist/plug-client.d.cts +71 -0
  13. package/dist/plug-client.d.ts +71 -0
  14. package/dist/plug-client.js +96 -0
  15. package/dist/plug-client.js.map +1 -0
  16. package/dist/templates/agent-skill.md +73 -0
  17. package/dist/templates/agents.md +41 -0
  18. package/dist/templates/cache.example.ts +11 -0
  19. package/dist/templates/cache.ts +58 -0
  20. package/dist/templates/catch.example.ts +36 -0
  21. package/dist/templates/catch.ts +119 -0
  22. package/dist/templates/config-page.tsx +20 -0
  23. package/dist/templates/debug-index-page.tsx +101 -0
  24. package/dist/templates/debug-page.tsx +48 -0
  25. package/dist/templates/graph-page.tsx +11 -0
  26. package/dist/templates/hub.tsx +450 -0
  27. package/dist/templates/inflow.example.ts +61 -0
  28. package/dist/templates/inflow.ts +98 -0
  29. package/dist/templates/khotan-config.ts +49 -0
  30. package/dist/templates/khotan-route.ts +13 -0
  31. package/dist/templates/logs-page.tsx +9 -0
  32. package/dist/templates/logs.tsx +20 -0
  33. package/dist/templates/mapping-browser.tsx +761 -0
  34. package/dist/templates/mappings-page.tsx +9 -0
  35. package/dist/templates/outflow.example.ts +52 -0
  36. package/dist/templates/outflow.ts +90 -0
  37. package/dist/templates/pass.example.ts +51 -0
  38. package/dist/templates/pass.ts +134 -0
  39. package/dist/templates/plug-debugger.tsx +1185 -0
  40. package/dist/templates/plug.example.ts +93 -0
  41. package/dist/templates/plug.ts +806 -0
  42. package/dist/templates/relay.example.ts +71 -0
  43. package/dist/templates/relay.ts +104 -0
  44. package/dist/templates/runs-table.tsx +592 -0
  45. package/dist/templates/schema.ts +505 -0
  46. package/dist/templates/skill-dashboard.md +144 -0
  47. package/dist/templates/skill-plug.md +216 -0
  48. package/dist/templates/skill-setup.md +161 -0
  49. package/dist/templates/skill-webhook.md +196 -0
  50. package/dist/templates/topology-canvas.tsx +1406 -0
  51. package/dist/templates/var-panel.tsx +276 -0
  52. package/dist/templates/webhook-events-table.tsx +241 -0
  53. package/dist/templates/wire-panel.tsx +216 -0
  54. package/dist/templates/wire.ts +155 -0
  55. package/package.json +46 -5
@@ -0,0 +1,450 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from "@/components/ui/table";
14
+ import { Switch } from "@/components/ui/switch";
15
+ import { Button } from "@/components/ui/button";
16
+ import { WirePanel } from "./wire";
17
+ import { VarPanel } from "./var-panel";
18
+
19
+ // ============================================================================
20
+ // Khotan Hub — Dashboard for configured plugs and flows
21
+ // Generated by khotan CLI · https://github.com/khotan-data
22
+ //
23
+ // This file is yours. Restyle, extend, or embed it however you like.
24
+ // It uses shadcn/ui primitives and fetches from /api/khotan.
25
+ // ============================================================================
26
+
27
+ interface Plug {
28
+ id: string;
29
+ name: string;
30
+ baseUrl: string;
31
+ authType: string;
32
+ enabled: boolean;
33
+ status: "connected" | "error" | "idle";
34
+ statusMessage: string | null;
35
+ flowCount: number;
36
+ createdAt: string;
37
+ updatedAt: string;
38
+ }
39
+
40
+ interface Flow {
41
+ id: string;
42
+ plugId: string;
43
+ name: string;
44
+ type: "inflow" | "outflow" | "relay" | "webhook";
45
+ enabled: boolean;
46
+ schedule: string | null;
47
+ lastRunAt: string | null;
48
+ lastRunStatus: "completed" | "partial" | "failed" | "cancelled" | null;
49
+ plugName: string | null;
50
+ }
51
+
52
+ interface WebhookHandler {
53
+ id: string;
54
+ wireId: string;
55
+ name: string;
56
+ type: "catch" | "pass";
57
+ destinationPlugId: string | null;
58
+ events: string[] | null;
59
+ enabled: boolean;
60
+ lastRunAt: string | null;
61
+ lastRunStatus:
62
+ | "pending"
63
+ | "running"
64
+ | "completed"
65
+ | "partial"
66
+ | "failed"
67
+ | "cancelled"
68
+ | null;
69
+ createdAt: string;
70
+ updatedAt: string;
71
+ }
72
+
73
+ type StatusVariant = "default" | "secondary" | "destructive" | "outline";
74
+ type FlowTypeVariant = "default" | "secondary" | "outline";
75
+
76
+ const statusVariant: Record<string, StatusVariant> = {
77
+ connected: "default",
78
+ idle: "secondary",
79
+ error: "destructive",
80
+ };
81
+
82
+ const flowTypeVariant: Record<string, FlowTypeVariant> = {
83
+ inflow: "default",
84
+ outflow: "secondary",
85
+ relay: "outline",
86
+ webhook: "outline",
87
+ };
88
+
89
+ const runStatusVariant: Record<string, StatusVariant> = {
90
+ pending: "outline",
91
+ running: "secondary",
92
+ completed: "default",
93
+ partial: "secondary",
94
+ failed: "destructive",
95
+ cancelled: "outline",
96
+ };
97
+
98
+ export function KhotanHub({
99
+ webhookUrl,
100
+ debugHref,
101
+ logsHref = "/logs",
102
+ }: {
103
+ webhookUrl?: string;
104
+ debugHref?: (plugName: string) => string;
105
+ logsHref?: string;
106
+ } = {}) {
107
+ const [plugs, setPlugs] = useState<Plug[]>([]);
108
+ const [flows, setFlows] = useState<Flow[]>([]);
109
+ const [webhookHandlers, setWebhookHandlers] = useState<WebhookHandler[]>([]);
110
+ const [loading, setLoading] = useState(true);
111
+ const [error, setError] = useState<string | null>(null);
112
+ const [selectedPlugId, setSelectedPlugId] = useState<string | null>(null);
113
+ const [debugEnabled, setDebugEnabled] = useState(false);
114
+
115
+ useEffect(() => {
116
+ fetch("/api/khotan/debug")
117
+ .then((res) => setDebugEnabled(res.ok))
118
+ .catch(() => setDebugEnabled(false));
119
+ }, []);
120
+
121
+ async function fetchData() {
122
+ setLoading(true);
123
+ setError(null);
124
+ try {
125
+ const [plugsRes, flowsRes] = await Promise.all([
126
+ fetch("/api/khotan/plugs"),
127
+ fetch("/api/khotan/flows"),
128
+ ]);
129
+ if (!plugsRes.ok || !flowsRes.ok) {
130
+ throw new Error("Failed to fetch khotan data");
131
+ }
132
+ setPlugs(await plugsRes.json());
133
+ setFlows(await flowsRes.json());
134
+ } catch (err) {
135
+ setError(err instanceof Error ? err.message : "Unknown error");
136
+ } finally {
137
+ setLoading(false);
138
+ }
139
+ }
140
+
141
+ async function toggleFlow(flowId: string, enabled: boolean) {
142
+ setFlows((prev) =>
143
+ prev.map((flow) => (flow.id === flowId ? { ...flow, enabled } : flow)),
144
+ );
145
+ await fetch(`/api/khotan/flows/${flowId}`, {
146
+ method: "PATCH",
147
+ headers: { "Content-Type": "application/json" },
148
+ body: JSON.stringify({ enabled }),
149
+ });
150
+ }
151
+
152
+ async function toggleWebhookHandler(handlerId: string, enabled: boolean) {
153
+ setWebhookHandlers((prev) =>
154
+ prev.map((h) => (h.id === handlerId ? { ...h, enabled } : h)),
155
+ );
156
+ await fetch(`/api/khotan/webhook-handlers/${handlerId}`, {
157
+ method: "PATCH",
158
+ headers: { "Content-Type": "application/json" },
159
+ body: JSON.stringify({ enabled }),
160
+ });
161
+ }
162
+
163
+ useEffect(() => {
164
+ fetchData();
165
+ }, []);
166
+
167
+ useEffect(() => {
168
+ if (!selectedPlugId) {
169
+ setWebhookHandlers([]);
170
+ return;
171
+ }
172
+ const plug = plugs.find((p) => p.id === selectedPlugId);
173
+ if (!plug) return;
174
+ fetch(`/api/khotan/webhook-handlers/${plug.name}`)
175
+ .then((res) => (res.ok ? res.json() : []))
176
+ .then((data) => setWebhookHandlers(data))
177
+ .catch(() => setWebhookHandlers([]));
178
+ }, [selectedPlugId, plugs]);
179
+
180
+ if (loading) {
181
+ return (
182
+ <div className="space-y-4">
183
+ <div className="h-8 w-48 animate-pulse rounded bg-muted" />
184
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
185
+ {[1, 2, 3].map((i) => (
186
+ <Card key={i}>
187
+ <CardHeader>
188
+ <div className="h-5 w-32 animate-pulse rounded bg-muted" />
189
+ </CardHeader>
190
+ <CardContent>
191
+ <div className="space-y-2">
192
+ <div className="h-4 w-full animate-pulse rounded bg-muted" />
193
+ <div className="h-4 w-2/3 animate-pulse rounded bg-muted" />
194
+ </div>
195
+ </CardContent>
196
+ </Card>
197
+ ))}
198
+ </div>
199
+ </div>
200
+ );
201
+ }
202
+
203
+ if (error) {
204
+ return (
205
+ <Card>
206
+ <CardContent className="py-8 text-center">
207
+ <p className="text-destructive mb-4">{error}</p>
208
+ <button
209
+ onClick={fetchData}
210
+ className="text-sm underline hover:no-underline"
211
+ >
212
+ Retry
213
+ </button>
214
+ </CardContent>
215
+ </Card>
216
+ );
217
+ }
218
+
219
+ if (plugs.length === 0) {
220
+ return (
221
+ <Card>
222
+ <CardContent className="py-8 text-center">
223
+ <p className="text-muted-foreground mb-2">No plugs configured yet.</p>
224
+ <p className="text-sm text-muted-foreground">
225
+ Register plugs in your <code>khotan.ts</code> config file, then
226
+ restart the dev server.
227
+ </p>
228
+ </CardContent>
229
+ </Card>
230
+ );
231
+ }
232
+
233
+ const selectedPlug = selectedPlugId
234
+ ? plugs.find((p) => p.id === selectedPlugId)
235
+ : null;
236
+ const plugFlows = selectedPlugId
237
+ ? flows.filter((flow) => flow.plugId === selectedPlugId)
238
+ : [];
239
+
240
+ return (
241
+ <div className="space-y-6">
242
+ <div className="flex items-center justify-between gap-4">
243
+ <h2 className="text-2xl font-bold tracking-tight">Khotan Hub</h2>
244
+ <Button
245
+ variant="outline"
246
+ size="sm"
247
+ onClick={() => {
248
+ window.location.href = logsHref;
249
+ }}
250
+ >
251
+ Open Logs
252
+ </Button>
253
+ </div>
254
+
255
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
256
+ {plugs.map((plug) => (
257
+ <Card
258
+ key={plug.id}
259
+ className={`cursor-pointer transition-colors hover:border-primary ${
260
+ selectedPlugId === plug.id ? "border-primary" : ""
261
+ }`}
262
+ onClick={() =>
263
+ setSelectedPlugId(selectedPlugId === plug.id ? null : plug.id)
264
+ }
265
+ >
266
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
267
+ <CardTitle className="text-sm font-medium">{plug.name}</CardTitle>
268
+ <Badge variant={statusVariant[plug.status] ?? "secondary"}>
269
+ {plug.status}
270
+ </Badge>
271
+ </CardHeader>
272
+ <CardContent>
273
+ <p className="text-xs text-muted-foreground truncate">
274
+ {plug.baseUrl}
275
+ </p>
276
+ <div className="mt-2 flex items-center justify-between">
277
+ <p className="text-xs text-muted-foreground">
278
+ {plug.authType} · {plug.flowCount} flow
279
+ {plug.flowCount !== 1 ? "s" : ""}
280
+ </p>
281
+ {debugEnabled && (
282
+ <Button
283
+ size="sm"
284
+ variant="ghost"
285
+ className="h-6 px-2 text-[11px]"
286
+ onClick={(e) => {
287
+ e.stopPropagation();
288
+ if (debugHref) {
289
+ window.location.href = debugHref(plug.name);
290
+ } else {
291
+ window.location.href = `/debug/${plug.name}`;
292
+ }
293
+ }}
294
+ >
295
+ Debug
296
+ </Button>
297
+ )}
298
+ </div>
299
+ </CardContent>
300
+ </Card>
301
+ ))}
302
+ </div>
303
+
304
+ {selectedPlug && (
305
+ <div className="space-y-4">
306
+ <Card>
307
+ <CardHeader>
308
+ <CardTitle>{selectedPlug.name} — Flows</CardTitle>
309
+ </CardHeader>
310
+ <CardContent>
311
+ {plugFlows.length === 0 ? (
312
+ <p className="text-sm text-muted-foreground">
313
+ No flows configured for this plug.
314
+ </p>
315
+ ) : (
316
+ <Table>
317
+ <TableHeader>
318
+ <TableRow>
319
+ <TableHead>Name</TableHead>
320
+ <TableHead>Type</TableHead>
321
+ <TableHead>Schedule</TableHead>
322
+ <TableHead>Last Run</TableHead>
323
+ <TableHead>Enabled</TableHead>
324
+ </TableRow>
325
+ </TableHeader>
326
+ <TableBody>
327
+ {plugFlows.map((flow) => (
328
+ <TableRow key={flow.id}>
329
+ <TableCell className="font-medium">
330
+ {flow.name}
331
+ </TableCell>
332
+ <TableCell>
333
+ <Badge
334
+ variant={flowTypeVariant[flow.type] ?? "outline"}
335
+ >
336
+ {flow.type}
337
+ </Badge>
338
+ </TableCell>
339
+ <TableCell className="text-muted-foreground">
340
+ {flow.schedule ?? "—"}
341
+ </TableCell>
342
+ <TableCell>
343
+ {flow.lastRunStatus ? (
344
+ <Badge
345
+ variant={
346
+ runStatusVariant[flow.lastRunStatus] ??
347
+ "secondary"
348
+ }
349
+ >
350
+ {flow.lastRunStatus}
351
+ </Badge>
352
+ ) : (
353
+ <span className="text-muted-foreground">never</span>
354
+ )}
355
+ </TableCell>
356
+ <TableCell>
357
+ <Switch
358
+ checked={flow.enabled}
359
+ onCheckedChange={(v) => toggleFlow(flow.id, v)}
360
+ />
361
+ </TableCell>
362
+ </TableRow>
363
+ ))}
364
+ </TableBody>
365
+ </Table>
366
+ )}
367
+ </CardContent>
368
+ </Card>
369
+
370
+ {webhookHandlers.length > 0 && (
371
+ <Card>
372
+ <CardHeader>
373
+ <CardTitle>{selectedPlug.name} — Webhook Handlers</CardTitle>
374
+ </CardHeader>
375
+ <CardContent>
376
+ <Table>
377
+ <TableHeader>
378
+ <TableRow>
379
+ <TableHead>Name</TableHead>
380
+ <TableHead>Type</TableHead>
381
+ <TableHead>Events</TableHead>
382
+ <TableHead>Destination</TableHead>
383
+ <TableHead>Last Run</TableHead>
384
+ <TableHead>Enabled</TableHead>
385
+ </TableRow>
386
+ </TableHeader>
387
+ <TableBody>
388
+ {webhookHandlers.map((handler) => (
389
+ <TableRow key={handler.id}>
390
+ <TableCell className="font-medium">
391
+ {handler.name}
392
+ </TableCell>
393
+ <TableCell>
394
+ <Badge
395
+ variant={
396
+ handler.type === "catch" ? "default" : "secondary"
397
+ }
398
+ >
399
+ {handler.type}
400
+ </Badge>
401
+ </TableCell>
402
+ <TableCell className="text-muted-foreground">
403
+ {handler.events?.length
404
+ ? handler.events.join(", ")
405
+ : "from wire config"}
406
+ </TableCell>
407
+ <TableCell className="text-muted-foreground">
408
+ {handler.destinationPlugId
409
+ ? (plugs.find(
410
+ (p) => p.id === handler.destinationPlugId,
411
+ )?.name ?? handler.destinationPlugId)
412
+ : "—"}
413
+ </TableCell>
414
+ <TableCell>
415
+ {handler.lastRunStatus ? (
416
+ <Badge
417
+ variant={
418
+ runStatusVariant[handler.lastRunStatus] ??
419
+ "secondary"
420
+ }
421
+ >
422
+ {handler.lastRunStatus}
423
+ </Badge>
424
+ ) : (
425
+ <span className="text-muted-foreground">never</span>
426
+ )}
427
+ </TableCell>
428
+ <TableCell>
429
+ <Switch
430
+ checked={handler.enabled}
431
+ onCheckedChange={(v) =>
432
+ toggleWebhookHandler(handler.id, v)
433
+ }
434
+ />
435
+ </TableCell>
436
+ </TableRow>
437
+ ))}
438
+ </TableBody>
439
+ </Table>
440
+ </CardContent>
441
+ </Card>
442
+ )}
443
+
444
+ <VarPanel plugName={selectedPlug.name} />
445
+ <WirePanel plugName={selectedPlug.name} webhookUrl={webhookUrl} />
446
+ </div>
447
+ )}
448
+ </div>
449
+ );
450
+ }
@@ -0,0 +1,61 @@
1
+ // ============================================================================
2
+ // Example: Inflow
3
+ // Generated by khotan CLI · https://github.com/khotan-data
4
+ //
5
+ // Copy this file, rename it for your source service/resource, and register the
6
+ // exported flow in {outputDir}/khotan.ts.
7
+ // ============================================================================
8
+
9
+ import { inflow, type InflowContext } from "./inflow";
10
+ import { sendUpdate } from "khotan-data/factory";
11
+
12
+ async function shopifyProductsWorkflow(ctx: InflowContext) {
13
+ "use workflow";
14
+
15
+ async function extractAndLoad() {
16
+ "use step";
17
+ console.log("Starting inflow", {
18
+ flow: ctx.flow.name,
19
+ khotanRunId: ctx.khotanRunId,
20
+ runType: ctx.runType,
21
+ });
22
+ await sendUpdate({
23
+ message: "Starting product inflow",
24
+ metadata: { flow: ctx.flow.name, runType: ctx.runType },
25
+ });
26
+
27
+ const response = await fetch("https://api.example.com/products", {
28
+ headers: {
29
+ Authorization: `Bearer ${ctx.vars["apiToken"] ?? ""}`,
30
+ },
31
+ });
32
+ const payload = (await response.json()) as {
33
+ data?: Array<Record<string, unknown>>;
34
+ };
35
+ const records = Array.isArray(payload.data) ? payload.data : [];
36
+
37
+ // Replace this with your app-specific transform and DB upsert.
38
+ console.log("Fetched records", records.length);
39
+ await sendUpdate({
40
+ message: `Fetched ${String(records.length)} products`,
41
+ extracted: records.length,
42
+ progress: 50,
43
+ });
44
+
45
+ return {
46
+ extracted: records.length,
47
+ transformed: records.length,
48
+ created: records.length,
49
+ metadata: { source: ctx.flow.name },
50
+ };
51
+ }
52
+
53
+ return extractAndLoad();
54
+ }
55
+
56
+ export const shopifyProductsInflow = inflow({
57
+ name: "shopify-products-inflow",
58
+ resource: "products",
59
+ schedule: "0 * * * *",
60
+ workflow: shopifyProductsWorkflow,
61
+ });
@@ -0,0 +1,98 @@
1
+ // ============================================================================
2
+ // Inflow — durable external service → app data movement
3
+ // Generated by khotan CLI · https://github.com/khotan-data
4
+ //
5
+ // This file defines the inflow() builder and types. Create per-service flow
6
+ // files (e.g. shopify-products.ts) using this builder to pull records from an
7
+ // external service, transform them, and load them into your app with durable,
8
+ // retryable Vercel Workflow steps. Inflow workflows can also use
9
+ // khotanCache(ctx, "name") for checkpoints and last-seen markers across runs.
10
+ // ============================================================================
11
+
12
+ import type {
13
+ FlowRegistration,
14
+ FlowRunResult,
15
+ FlowWorkflowContext,
16
+ } from "khotan-data/factory";
17
+
18
+ export type InflowContext = FlowWorkflowContext & {
19
+ flow: FlowWorkflowContext["flow"] & { type: "inflow" };
20
+ };
21
+
22
+ export type InflowWorkflow = (
23
+ ctx: InflowContext,
24
+ ) => Promise<FlowRunResult | void>;
25
+
26
+ export interface InflowConfig {
27
+ /** Unique name for this flow (used for DB tracking and Hub display) */
28
+ name: string;
29
+ /** Logical resource this flow feeds, e.g. "products" */
30
+ resource?: string;
31
+ /** Optional cron schedule used by scheduler integrations */
32
+ schedule?: string;
33
+ /** Durable workflow that extracts, transforms, and loads records */
34
+ workflow: InflowWorkflow;
35
+ }
36
+
37
+ export function inflow(config: InflowConfig): FlowRegistration {
38
+ return {
39
+ name: config.name,
40
+ type: "inflow",
41
+ resource: config.resource,
42
+ schedule: config.schedule,
43
+ workflow: config.workflow as FlowRegistration["workflow"],
44
+ };
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Usage Example (create a file like flows/shopify-products.ts)
49
+ // ---------------------------------------------------------------------------
50
+ //
51
+ // import { bindWorkflowPlug, inflow, type InflowContext, sendUpdate } from "khotan-data/factory";
52
+ // import { db } from "@/db";
53
+ // import { products } from "@/db/schema";
54
+ // import { shopifyPlug } from "../plugs/shopify";
55
+ //
56
+ // async function shopifyProductsWorkflow(ctx: InflowContext) {
57
+ // "use workflow";
58
+ //
59
+ // async function extractAndLoad() {
60
+ // "use step";
61
+ // console.log("Starting inflow", {
62
+ // flow: ctx.flow.name,
63
+ // khotanRunId: ctx.khotanRunId,
64
+ // runType: ctx.runType,
65
+ // });
66
+ // await sendUpdate({ message: "Starting Shopify products inflow" });
67
+ // const shopify = bindWorkflowPlug(shopifyPlug, ctx);
68
+ //
69
+ // const response = await shopify.get<{ data?: Array<{ id: string; sku?: string }> }>("/products");
70
+ // const records = Array.isArray(response.data) ? response.data : [];
71
+ // await sendUpdate({ message: `Fetched ${records.length} products`, extracted: records.length });
72
+ //
73
+ // if (records.length > 0) {
74
+ // await db.insert(products).values(
75
+ // records.map((record) => ({
76
+ // externalId: record.id,
77
+ // sku: record.sku ?? record.id,
78
+ // })),
79
+ // );
80
+ // }
81
+ //
82
+ // return {
83
+ // extracted: records.length,
84
+ // transformed: records.length,
85
+ // created: records.length,
86
+ // metadata: { source: ctx.flow.name },
87
+ // };
88
+ // }
89
+ //
90
+ // return extractAndLoad();
91
+ // }
92
+ //
93
+ // export const shopifyProductsInflow = inflow({
94
+ // name: "shopify-products-inflow",
95
+ // resource: "products",
96
+ // schedule: "0 * * * *",
97
+ // workflow: shopifyProductsWorkflow,
98
+ // });
@@ -0,0 +1,49 @@
1
+ // ============================================================================
2
+ // Khotan Config — register your plugs, caches, and flows here
3
+ // Generated by khotan CLI · https://github.com/khotan-data
4
+ //
5
+ // Import your Drizzle database instance and register plugs below.
6
+ // Restart the dev server after making changes.
7
+ // ============================================================================
8
+
9
+ import { khotan, drizzleAdapter } from "khotan-data/factory";
10
+ // TODO: Update this import to your Drizzle database instance
11
+ import { db } from "@/db";
12
+
13
+ const khotanData = khotan({
14
+ adapter: drizzleAdapter(db),
15
+
16
+ // Resources define logical entity types for cross-referencing across plugs.
17
+ // The mapping block declares the shared identity contract for that resource.
18
+ resources: [
19
+ // Example resource registration:
20
+ //
21
+ // { name: "products", mapping: { connectField: "sku" }, description: "Product catalog" },
22
+ // { name: "orders", mapping: { connectField: "order_number" } },
23
+ ],
24
+
25
+ plugs: [
26
+ // Example plug registration:
27
+ //
28
+ // {
29
+ // name: "stripe",
30
+ // baseUrl: "https://api.stripe.com",
31
+ // authType: "bearer",
32
+ // flows: [
33
+ // { name: "products-inflow", type: "inflow", schedule: "0 * * * *", resource: "products" },
34
+ // { name: "invoices-inflow", type: "inflow", schedule: "0 0 * * *" },
35
+ // ],
36
+ // },
37
+ ],
38
+
39
+ // Caches store durable key/value state for relays, inflows, outflows, and
40
+ // webhook workflows. Register named caches you want to access via
41
+ // khotanCache(ctx, "cache-name") or khotanData.cache("cache-name").
42
+ caches: [
43
+ // Example cache registration:
44
+ //
45
+ // cin7ProductsSnapshotCache,
46
+ ],
47
+ });
48
+
49
+ export default khotanData;
@@ -0,0 +1,13 @@
1
+ // ============================================================================
2
+ // Khotan API Route — catch-all handler for /api/khotan/*
3
+ // Generated by khotan CLI · https://github.com/khotan-data
4
+ //
5
+ // This file wires your khotan config into a Next.js App Router route.
6
+ // ============================================================================
7
+
8
+ import { toNextJsHandler } from "khotan-data/factory";
9
+ import khotanData from "@/khotan/khotan";
10
+
11
+ export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(
12
+ khotanData.handler,
13
+ );
@@ -0,0 +1,9 @@
1
+ import { KhotanLogs } from "@/components/khotan/logs";
2
+
3
+ export default function KhotanLogsPage() {
4
+ return (
5
+ <main className="container mx-auto max-w-7xl px-4 py-10">
6
+ <KhotanLogs />
7
+ </main>
8
+ );
9
+ }