khotan-data 0.2.0 → 0.3.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 +32 -20
- package/dist/cli.js +51 -0
- package/dist/factory.cjs +8 -1
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.cts +9 -1
- package/dist/factory.d.ts +9 -1
- package/dist/factory.js +8 -1
- package/dist/factory.js.map +1 -1
- package/dist/templates/catch.example.ts +25 -17
- package/dist/templates/catch.ts +20 -15
- package/dist/templates/hub.tsx +96 -13
- package/dist/templates/inflow.example.ts +46 -38
- package/dist/templates/inflow.ts +37 -31
- package/dist/templates/khotan-config.ts +11 -0
- package/dist/templates/outflow.example.ts +39 -31
- package/dist/templates/outflow.ts +28 -23
- package/dist/templates/pass.example.ts +38 -30
- package/dist/templates/pass.ts +29 -24
- package/dist/templates/relay.example.ts +52 -44
- package/dist/templates/relay.ts +38 -33
- package/dist/templates/skill-dashboard.md +2 -1
- package/dist/templates/skill-setup.md +77 -1
- package/dist/templates/skill-webhook.md +45 -23
- package/package.json +1 -1
|
@@ -4,29 +4,37 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Copy this file, rename it for your webhook source/event, and register the
|
|
6
6
|
// exported catch handler in {outputDir}/khotan.ts.
|
|
7
|
+
//
|
|
8
|
+
// IMPORTANT — Workflow step structure:
|
|
9
|
+
// Declare "use step" functions at module top level and pass them only
|
|
10
|
+
// serializable values (the `ctx` object is plain data and is safe to pass).
|
|
11
|
+
// Do NOT nest step functions inside the "use workflow" function — the Workflow
|
|
12
|
+
// compiler cannot hoist closures that capture workflow scope, and they fail at
|
|
13
|
+
// runtime in the sandbox. Keep the workflow body limited to orchestration.
|
|
7
14
|
// ============================================================================
|
|
8
15
|
|
|
9
16
|
import { catchEvent, type CatchContext } from "./catch";
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
khotanRunId: ctx.khotanRunId,
|
|
19
|
-
});
|
|
18
|
+
// Step: full Node.js access, retried independently. Receives serializable ctx.
|
|
19
|
+
async function persistEvent(ctx: CatchContext) {
|
|
20
|
+
"use step";
|
|
21
|
+
console.log("Handling webhook event", {
|
|
22
|
+
eventType: ctx.eventType,
|
|
23
|
+
khotanRunId: ctx.khotanRunId,
|
|
24
|
+
});
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// Khotan already records webhook deliveries. Add app-specific side effects
|
|
27
|
+
// here, such as updating a local table or enqueueing downstream work.
|
|
28
|
+
console.log("Webhook payload", {
|
|
29
|
+
event: ctx.event,
|
|
30
|
+
headers: ctx.headers,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
// Workflow: orchestration only. Calls top-level steps with serializable args.
|
|
35
|
+
async function stripeInvoiceCatchWorkflow(ctx: CatchContext) {
|
|
36
|
+
"use workflow";
|
|
37
|
+
await persistEvent(ctx);
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
export const stripeInvoiceCatch = catchEvent({
|
package/dist/templates/catch.ts
CHANGED
|
@@ -81,26 +81,31 @@ export function catchEvent(config: CatchConfig): CatchRegistration {
|
|
|
81
81
|
// them to khotan_runs. Use your catch workflow for app-specific side effects
|
|
82
82
|
// and optionally khotanCache(ctx, "name") for dedupe or cursor state.
|
|
83
83
|
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
84
|
+
// Declare "use step" functions at MODULE TOP LEVEL and pass them serializable
|
|
85
|
+
// values only (`ctx` is plain data). Do NOT nest steps inside the "use workflow"
|
|
86
|
+
// function — closures over workflow scope cannot be hoisted and fail at runtime.
|
|
86
87
|
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
88
|
+
// // Step: top-level, full Node.js access, retried independently.
|
|
89
|
+
// async function notifyOps(ctx: CatchContext) {
|
|
90
|
+
// "use step";
|
|
91
|
+
// const cache = khotanCache(ctx, "pollinate-webhook-markers");
|
|
92
|
+
// const eventId = String(ctx.event["id"] ?? "");
|
|
93
|
+
// if (eventId && (await cache.get<boolean>(eventId))) return;
|
|
92
94
|
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
95
|
+
// console.log("Handled webhook", {
|
|
96
|
+
// eventType: ctx.eventType,
|
|
97
|
+
// khotanRunId: ctx.khotanRunId,
|
|
98
|
+
// });
|
|
97
99
|
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
// }
|
|
100
|
+
// if (eventId) {
|
|
101
|
+
// await cache.set(eventId, true);
|
|
101
102
|
// }
|
|
103
|
+
// }
|
|
102
104
|
//
|
|
103
|
-
//
|
|
105
|
+
// // Workflow: orchestration only.
|
|
106
|
+
// async function pollinateCatchWorkflow(ctx: CatchContext) {
|
|
107
|
+
// "use workflow";
|
|
108
|
+
// await notifyOps(ctx);
|
|
104
109
|
// }
|
|
105
110
|
//
|
|
106
111
|
// export const pollinateCatch = catchEvent({
|
package/dist/templates/hub.tsx
CHANGED
|
@@ -46,7 +46,14 @@ interface Flow {
|
|
|
46
46
|
enabled: boolean;
|
|
47
47
|
schedule: string | null;
|
|
48
48
|
lastRunAt: string | null;
|
|
49
|
-
lastRunStatus:
|
|
49
|
+
lastRunStatus:
|
|
50
|
+
| "pending"
|
|
51
|
+
| "running"
|
|
52
|
+
| "completed"
|
|
53
|
+
| "partial"
|
|
54
|
+
| "failed"
|
|
55
|
+
| "cancelled"
|
|
56
|
+
| null;
|
|
50
57
|
plugName: string | null;
|
|
51
58
|
}
|
|
52
59
|
|
|
@@ -99,10 +106,11 @@ const runStatusVariant: Record<string, StatusVariant> = {
|
|
|
99
106
|
export function KhotanHub({
|
|
100
107
|
webhookUrl,
|
|
101
108
|
debugHref,
|
|
102
|
-
logsHref
|
|
109
|
+
logsHref,
|
|
103
110
|
}: {
|
|
104
111
|
webhookUrl?: string;
|
|
105
112
|
debugHref?: (plugName: string) => string;
|
|
113
|
+
/** When set, shows an "Open Logs" button linking here. Hidden if omitted. */
|
|
106
114
|
logsHref?: string;
|
|
107
115
|
} = {}) {
|
|
108
116
|
const [plugs, setPlugs] = useState<Plug[]>([]);
|
|
@@ -112,6 +120,11 @@ export function KhotanHub({
|
|
|
112
120
|
const [error, setError] = useState<unknown>(null);
|
|
113
121
|
const [selectedPlugId, setSelectedPlugId] = useState<string | null>(null);
|
|
114
122
|
const [debugEnabled, setDebugEnabled] = useState(false);
|
|
123
|
+
const [runningFlowId, setRunningFlowId] = useState<string | null>(null);
|
|
124
|
+
const [runNotice, setRunNotice] = useState<{
|
|
125
|
+
type: "ok" | "error";
|
|
126
|
+
text: string;
|
|
127
|
+
} | null>(null);
|
|
115
128
|
|
|
116
129
|
useEffect(() => {
|
|
117
130
|
fetch("/api/khotan/debug")
|
|
@@ -147,6 +160,35 @@ export function KhotanHub({
|
|
|
147
160
|
});
|
|
148
161
|
}
|
|
149
162
|
|
|
163
|
+
async function runFlow(flowId: string, flowName: string) {
|
|
164
|
+
setRunningFlowId(flowId);
|
|
165
|
+
setRunNotice(null);
|
|
166
|
+
try {
|
|
167
|
+
const res = await fetch(`/api/khotan/flows/${flowId}/runs`, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: { "Content-Type": "application/json" },
|
|
170
|
+
body: JSON.stringify({ runType: "full" }),
|
|
171
|
+
});
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
const data = (await res.json().catch(() => ({}))) as {
|
|
174
|
+
error?: string;
|
|
175
|
+
hint?: string;
|
|
176
|
+
};
|
|
177
|
+
setRunNotice({
|
|
178
|
+
type: "error",
|
|
179
|
+
text: data.hint ?? data.error ?? `Failed to trigger ${flowName}.`,
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
setRunNotice({ type: "ok", text: `Triggered ${flowName}.` });
|
|
184
|
+
await fetchData();
|
|
185
|
+
} catch {
|
|
186
|
+
setRunNotice({ type: "error", text: `Failed to trigger ${flowName}.` });
|
|
187
|
+
} finally {
|
|
188
|
+
setRunningFlowId(null);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
150
192
|
async function toggleWebhookHandler(handlerId: string, enabled: boolean) {
|
|
151
193
|
setWebhookHandlers((prev) =>
|
|
152
194
|
prev.map((h) => (h.id === handlerId ? { ...h, enabled } : h)),
|
|
@@ -227,23 +269,27 @@ export function KhotanHub({
|
|
|
227
269
|
<div className="space-y-6">
|
|
228
270
|
<div className="flex items-center justify-between gap-4">
|
|
229
271
|
<h2 className="text-2xl font-bold tracking-tight">Khotan Hub</h2>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
272
|
+
{logsHref && (
|
|
273
|
+
<Button
|
|
274
|
+
variant="outline"
|
|
275
|
+
size="sm"
|
|
276
|
+
onClick={() => {
|
|
277
|
+
window.location.href = logsHref;
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
Open Logs
|
|
281
|
+
</Button>
|
|
282
|
+
)}
|
|
239
283
|
</div>
|
|
240
284
|
|
|
241
285
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
242
286
|
{plugs.map((plug) => (
|
|
243
287
|
<Card
|
|
244
288
|
key={plug.id}
|
|
245
|
-
className={`cursor-pointer transition-
|
|
246
|
-
selectedPlugId === plug.id
|
|
289
|
+
className={`cursor-pointer transition-all hover:border-primary ${
|
|
290
|
+
selectedPlugId === plug.id
|
|
291
|
+
? "border-primary ring-2 ring-primary ring-offset-2 ring-offset-background shadow-md"
|
|
292
|
+
: "border-border"
|
|
247
293
|
}`}
|
|
248
294
|
onClick={() =>
|
|
249
295
|
setSelectedPlugId(selectedPlugId === plug.id ? null : plug.id)
|
|
@@ -287,6 +333,18 @@ export function KhotanHub({
|
|
|
287
333
|
))}
|
|
288
334
|
</div>
|
|
289
335
|
|
|
336
|
+
{!selectedPlug && (
|
|
337
|
+
<Card className="border-dashed">
|
|
338
|
+
<CardContent className="py-10 text-center">
|
|
339
|
+
<p className="text-muted-foreground mb-1">No plug selected yet.</p>
|
|
340
|
+
<p className="text-sm text-muted-foreground">
|
|
341
|
+
Select a plug above to view its flows, webhook handlers, and
|
|
342
|
+
credentials.
|
|
343
|
+
</p>
|
|
344
|
+
</CardContent>
|
|
345
|
+
</Card>
|
|
346
|
+
)}
|
|
347
|
+
|
|
290
348
|
{selectedPlug && (
|
|
291
349
|
<div className="space-y-4">
|
|
292
350
|
<Card>
|
|
@@ -294,6 +352,17 @@ export function KhotanHub({
|
|
|
294
352
|
<CardTitle>{selectedPlug.name} — Flows</CardTitle>
|
|
295
353
|
</CardHeader>
|
|
296
354
|
<CardContent>
|
|
355
|
+
{runNotice && (
|
|
356
|
+
<p
|
|
357
|
+
className={`mb-3 text-sm ${
|
|
358
|
+
runNotice.type === "error"
|
|
359
|
+
? "text-destructive"
|
|
360
|
+
: "text-muted-foreground"
|
|
361
|
+
}`}
|
|
362
|
+
>
|
|
363
|
+
{runNotice.text}
|
|
364
|
+
</p>
|
|
365
|
+
)}
|
|
297
366
|
{plugFlows.length === 0 ? (
|
|
298
367
|
<p className="text-sm text-muted-foreground">
|
|
299
368
|
No flows configured for this plug.
|
|
@@ -307,6 +376,7 @@ export function KhotanHub({
|
|
|
307
376
|
<TableHead>Schedule</TableHead>
|
|
308
377
|
<TableHead>Last Run</TableHead>
|
|
309
378
|
<TableHead>Enabled</TableHead>
|
|
379
|
+
<TableHead className="text-right">Actions</TableHead>
|
|
310
380
|
</TableRow>
|
|
311
381
|
</TableHeader>
|
|
312
382
|
<TableBody>
|
|
@@ -345,6 +415,19 @@ export function KhotanHub({
|
|
|
345
415
|
onCheckedChange={(v) => toggleFlow(flow.id, v)}
|
|
346
416
|
/>
|
|
347
417
|
</TableCell>
|
|
418
|
+
<TableCell className="text-right">
|
|
419
|
+
<Button
|
|
420
|
+
size="sm"
|
|
421
|
+
variant="outline"
|
|
422
|
+
className="h-7 px-2 text-xs"
|
|
423
|
+
disabled={runningFlowId !== null}
|
|
424
|
+
onClick={() => runFlow(flow.id, flow.name)}
|
|
425
|
+
>
|
|
426
|
+
{runningFlowId === flow.id
|
|
427
|
+
? "Running…"
|
|
428
|
+
: "Run now"}
|
|
429
|
+
</Button>
|
|
430
|
+
</TableCell>
|
|
348
431
|
</TableRow>
|
|
349
432
|
))}
|
|
350
433
|
</TableBody>
|
|
@@ -4,53 +4,61 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Copy this file, rename it for your source service/resource, and register the
|
|
6
6
|
// exported flow in {outputDir}/khotan.ts.
|
|
7
|
+
//
|
|
8
|
+
// IMPORTANT — Workflow step structure:
|
|
9
|
+
// Declare "use step" functions at module top level and pass them only
|
|
10
|
+
// serializable values (the `ctx` object is plain data and is safe to pass).
|
|
11
|
+
// Do NOT nest step functions inside the "use workflow" function — the Workflow
|
|
12
|
+
// compiler cannot hoist closures that capture workflow scope, and they fail at
|
|
13
|
+
// runtime in the sandbox. Keep the workflow body limited to orchestration.
|
|
7
14
|
// ============================================================================
|
|
8
15
|
|
|
9
16
|
import { inflow, type InflowContext } from "./inflow";
|
|
10
17
|
import { sendUpdate } from "khotan-data/factory";
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
metadata: { flow: ctx.flow.name, runType: ctx.runType },
|
|
25
|
-
});
|
|
19
|
+
// Step: full Node.js access, retried independently. Receives serializable ctx.
|
|
20
|
+
async function extractAndLoad(ctx: InflowContext) {
|
|
21
|
+
"use step";
|
|
22
|
+
console.log("Starting inflow", {
|
|
23
|
+
flow: ctx.flow.name,
|
|
24
|
+
khotanRunId: ctx.khotanRunId,
|
|
25
|
+
runType: ctx.runType,
|
|
26
|
+
});
|
|
27
|
+
await sendUpdate({
|
|
28
|
+
message: "Starting product inflow",
|
|
29
|
+
metadata: { flow: ctx.flow.name, runType: ctx.runType },
|
|
30
|
+
});
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
const response = await fetch("https://api.example.com/products", {
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: `Bearer ${ctx.vars["apiToken"] ?? ""}`,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const payload = (await response.json()) as {
|
|
38
|
+
data?: Array<Record<string, unknown>>;
|
|
39
|
+
};
|
|
40
|
+
const records = Array.isArray(payload.data) ? payload.data : [];
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// Replace this with your app-specific transform and DB upsert.
|
|
43
|
+
console.log("Fetched records", records.length);
|
|
44
|
+
await sendUpdate({
|
|
45
|
+
message: `Fetched ${String(records.length)} products`,
|
|
46
|
+
extracted: records.length,
|
|
47
|
+
progress: 50,
|
|
48
|
+
});
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
return {
|
|
51
|
+
extracted: records.length,
|
|
52
|
+
transformed: records.length,
|
|
53
|
+
created: records.length,
|
|
54
|
+
metadata: { source: ctx.flow.name },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
// Workflow: orchestration only. Calls top-level steps with serializable args.
|
|
59
|
+
async function shopifyProductsWorkflow(ctx: InflowContext) {
|
|
60
|
+
"use workflow";
|
|
61
|
+
return extractAndLoad(ctx);
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
export const shopifyProductsInflow = inflow({
|
package/dist/templates/inflow.ts
CHANGED
|
@@ -48,46 +48,52 @@ export function inflow(config: InflowConfig): FlowRegistration {
|
|
|
48
48
|
// Usage Example (create a file like flows/shopify-products.ts)
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
50
|
//
|
|
51
|
+
// Declare "use step" functions at MODULE TOP LEVEL and pass them serializable
|
|
52
|
+
// values only. The `ctx` object is plain data and is safe to pass. Do NOT nest
|
|
53
|
+
// step functions inside the "use workflow" function — the Workflow compiler
|
|
54
|
+
// cannot hoist closures that capture workflow scope, so they fail at runtime.
|
|
55
|
+
//
|
|
51
56
|
// import { bindWorkflowPlug, inflow, type InflowContext, sendUpdate } from "khotan-data/factory";
|
|
52
57
|
// import { db } from "@/db";
|
|
53
58
|
// import { products } from "@/db/schema";
|
|
54
59
|
// import { shopifyPlug } from "../plugs/shopify";
|
|
55
60
|
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
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 });
|
|
61
|
+
// // Step: top-level, full Node.js access, retried independently.
|
|
62
|
+
// async function extractAndLoad(ctx: InflowContext) {
|
|
63
|
+
// "use step";
|
|
64
|
+
// console.log("Starting inflow", {
|
|
65
|
+
// flow: ctx.flow.name,
|
|
66
|
+
// khotanRunId: ctx.khotanRunId,
|
|
67
|
+
// runType: ctx.runType,
|
|
68
|
+
// });
|
|
69
|
+
// await sendUpdate({ message: "Starting Shopify products inflow" });
|
|
70
|
+
// const shopify = bindWorkflowPlug(shopifyPlug, ctx);
|
|
72
71
|
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// externalId: record.id,
|
|
77
|
-
// sku: record.sku ?? record.id,
|
|
78
|
-
// })),
|
|
79
|
-
// );
|
|
80
|
-
// }
|
|
72
|
+
// const response = await shopify.get<{ data?: Array<{ id: string; sku?: string }> }>("/products");
|
|
73
|
+
// const records = Array.isArray(response.data) ? response.data : [];
|
|
74
|
+
// await sendUpdate({ message: `Fetched ${records.length} products`, extracted: records.length });
|
|
81
75
|
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
76
|
+
// if (records.length > 0) {
|
|
77
|
+
// await db.insert(products).values(
|
|
78
|
+
// records.map((record) => ({
|
|
79
|
+
// externalId: record.id,
|
|
80
|
+
// sku: record.sku ?? record.id,
|
|
81
|
+
// })),
|
|
82
|
+
// );
|
|
88
83
|
// }
|
|
89
84
|
//
|
|
90
|
-
// return
|
|
85
|
+
// return {
|
|
86
|
+
// extracted: records.length,
|
|
87
|
+
// transformed: records.length,
|
|
88
|
+
// created: records.length,
|
|
89
|
+
// metadata: { source: ctx.flow.name },
|
|
90
|
+
// };
|
|
91
|
+
// }
|
|
92
|
+
//
|
|
93
|
+
// // Workflow: orchestration only.
|
|
94
|
+
// async function shopifyProductsWorkflow(ctx: InflowContext) {
|
|
95
|
+
// "use workflow";
|
|
96
|
+
// return extractAndLoad(ctx);
|
|
91
97
|
// }
|
|
92
98
|
//
|
|
93
99
|
// export const shopifyProductsInflow = inflow({
|
|
@@ -64,3 +64,14 @@ const khotanData = khotan({
|
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
export default khotanData;
|
|
67
|
+
|
|
68
|
+
// Trigger a flow run from server code (route handler, action, cron):
|
|
69
|
+
//
|
|
70
|
+
// import khotanData from "@/lib/khotan/khotan";
|
|
71
|
+
// await khotanData.flow("products-inflow", { plugName: "stripe" }).start({
|
|
72
|
+
// runType: "delta", // or "full"
|
|
73
|
+
// });
|
|
74
|
+
//
|
|
75
|
+
// `flow(name).start(options)` is the single entry point — there is no
|
|
76
|
+
// `khotanData.api.*` surface. `plugName` only disambiguates when the same flow
|
|
77
|
+
// name is registered under multiple plugs.
|
|
@@ -4,44 +4,52 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Copy this file, rename it for your destination service/resource, and register
|
|
6
6
|
// the exported flow in {outputDir}/khotan.ts.
|
|
7
|
+
//
|
|
8
|
+
// IMPORTANT — Workflow step structure:
|
|
9
|
+
// Declare "use step" functions at module top level and pass them only
|
|
10
|
+
// serializable values (the `ctx` object is plain data and is safe to pass).
|
|
11
|
+
// Do NOT nest step functions inside the "use workflow" function — the Workflow
|
|
12
|
+
// compiler cannot hoist closures that capture workflow scope, and they fail at
|
|
13
|
+
// runtime in the sandbox. Keep the workflow body limited to orchestration.
|
|
7
14
|
// ============================================================================
|
|
8
15
|
|
|
9
16
|
import { outflow, type OutflowContext } from "./outflow";
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// Step: full Node.js access, retried independently. Receives serializable ctx.
|
|
19
|
+
async function loadAndPush(ctx: OutflowContext) {
|
|
20
|
+
"use step";
|
|
21
|
+
console.log("Starting outflow", {
|
|
22
|
+
flow: ctx.flow.name,
|
|
23
|
+
khotanRunId: ctx.khotanRunId,
|
|
24
|
+
runType: ctx.runType,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Replace this with your app-specific DB query.
|
|
28
|
+
const records: Array<Record<string, unknown>> = [];
|
|
29
|
+
|
|
30
|
+
for (const record of records) {
|
|
31
|
+
await fetch("https://api.example.com/products", {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: `Bearer ${ctx.vars["apiToken"] ?? ""}`,
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify(record),
|
|
20
38
|
});
|
|
21
|
-
|
|
22
|
-
// Replace this with your app-specific DB query.
|
|
23
|
-
const records: Array<Record<string, unknown>> = [];
|
|
24
|
-
|
|
25
|
-
for (const record of records) {
|
|
26
|
-
await fetch("https://api.example.com/products", {
|
|
27
|
-
method: "POST",
|
|
28
|
-
headers: {
|
|
29
|
-
Authorization: `Bearer ${ctx.vars["apiToken"] ?? ""}`,
|
|
30
|
-
"Content-Type": "application/json",
|
|
31
|
-
},
|
|
32
|
-
body: JSON.stringify(record),
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
extracted: records.length,
|
|
38
|
-
transformed: records.length,
|
|
39
|
-
created: records.length,
|
|
40
|
-
metadata: { destination: ctx.flow.name },
|
|
41
|
-
};
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
return
|
|
41
|
+
return {
|
|
42
|
+
extracted: records.length,
|
|
43
|
+
transformed: records.length,
|
|
44
|
+
created: records.length,
|
|
45
|
+
metadata: { destination: ctx.flow.name },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Workflow: orchestration only. Calls top-level steps with serializable args.
|
|
50
|
+
async function hubspotProductsWorkflow(ctx: OutflowContext) {
|
|
51
|
+
"use workflow";
|
|
52
|
+
return loadAndPush(ctx);
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
export const hubspotProductsOutflow = outflow({
|
|
@@ -48,38 +48,43 @@ export function outflow(config: OutflowConfig): FlowRegistration {
|
|
|
48
48
|
// Usage Example (create a file like flows/hubspot-products.ts)
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
50
|
//
|
|
51
|
+
// Declare "use step" functions at MODULE TOP LEVEL and pass them serializable
|
|
52
|
+
// values only (`ctx` is plain data). Do NOT nest steps inside the "use workflow"
|
|
53
|
+
// function — closures over workflow scope cannot be hoisted and fail at runtime.
|
|
54
|
+
//
|
|
51
55
|
// import { bindWorkflowPlug, outflow, type OutflowContext } from "khotan-data/factory";
|
|
52
56
|
// import { db } from "@/db";
|
|
53
57
|
// import { products } from "@/db/schema";
|
|
54
58
|
// import { hubspotPlug } from "../plugs/hubspot";
|
|
55
59
|
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// });
|
|
66
|
-
// const hubspot = bindWorkflowPlug(hubspotPlug, ctx);
|
|
67
|
-
//
|
|
68
|
-
// const records = await db.select().from(products);
|
|
60
|
+
// // Step: top-level, full Node.js access, retried independently.
|
|
61
|
+
// async function loadAndPush(ctx: OutflowContext) {
|
|
62
|
+
// "use step";
|
|
63
|
+
// console.log("Starting outflow", {
|
|
64
|
+
// flow: ctx.flow.name,
|
|
65
|
+
// khotanRunId: ctx.khotanRunId,
|
|
66
|
+
// runType: ctx.runType,
|
|
67
|
+
// });
|
|
68
|
+
// const hubspot = bindWorkflowPlug(hubspotPlug, ctx);
|
|
69
69
|
//
|
|
70
|
-
//
|
|
71
|
-
// await hubspot.post("/products", { body: record });
|
|
72
|
-
// }
|
|
70
|
+
// const records = await db.select().from(products);
|
|
73
71
|
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// transformed: records.length,
|
|
77
|
-
// created: records.length,
|
|
78
|
-
// metadata: { destination: ctx.flow.name },
|
|
79
|
-
// };
|
|
72
|
+
// for (const record of records) {
|
|
73
|
+
// await hubspot.post("/products", { body: record });
|
|
80
74
|
// }
|
|
81
75
|
//
|
|
82
|
-
// return
|
|
76
|
+
// return {
|
|
77
|
+
// extracted: records.length,
|
|
78
|
+
// transformed: records.length,
|
|
79
|
+
// created: records.length,
|
|
80
|
+
// metadata: { destination: ctx.flow.name },
|
|
81
|
+
// };
|
|
82
|
+
// }
|
|
83
|
+
//
|
|
84
|
+
// // Workflow: orchestration only.
|
|
85
|
+
// async function hubspotProductsWorkflow(ctx: OutflowContext) {
|
|
86
|
+
// "use workflow";
|
|
87
|
+
// return loadAndPush(ctx);
|
|
83
88
|
// }
|
|
84
89
|
//
|
|
85
90
|
// export const hubspotProductsOutflow = outflow({
|