khotan-data 0.2.0 → 0.3.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.
@@ -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
- async function stripeInvoiceCatchWorkflow(ctx: CatchContext) {
12
- "use workflow";
13
-
14
- async function persistEvent() {
15
- "use step";
16
- console.log("Handling webhook event", {
17
- eventType: ctx.eventType,
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
- // Khotan already records webhook deliveries. Add app-specific side effects
22
- // here, such as updating a local table or enqueueing downstream work.
23
- console.log("Webhook payload", {
24
- event: ctx.event,
25
- headers: ctx.headers,
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
- await persistEvent();
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({
@@ -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
- // async function pollinateCatchWorkflow(ctx: CatchContext) {
85
- // "use workflow";
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
- // async function notifyOps() {
88
- // "use step";
89
- // const cache = khotanCache(ctx, "pollinate-webhook-markers");
90
- // const eventId = String(ctx.event["id"] ?? "");
91
- // if (eventId && (await cache.get<boolean>(eventId))) return;
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
- // console.log("Handled webhook", {
94
- // eventType: ctx.eventType,
95
- // khotanRunId: ctx.khotanRunId,
96
- // });
95
+ // console.log("Handled webhook", {
96
+ // eventType: ctx.eventType,
97
+ // khotanRunId: ctx.khotanRunId,
98
+ // });
97
99
  //
98
- // if (eventId) {
99
- // await cache.set(eventId, true);
100
- // }
100
+ // if (eventId) {
101
+ // await cache.set(eventId, true);
101
102
  // }
103
+ // }
102
104
  //
103
- // await notifyOps();
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({
@@ -46,7 +46,14 @@ interface Flow {
46
46
  enabled: boolean;
47
47
  schedule: string | null;
48
48
  lastRunAt: string | null;
49
- lastRunStatus: "completed" | "partial" | "failed" | "cancelled" | null;
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 = "/logs",
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
- <Button
231
- variant="outline"
232
- size="sm"
233
- onClick={() => {
234
- window.location.href = logsHref;
235
- }}
236
- >
237
- Open Logs
238
- </Button>
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-colors hover:border-primary ${
246
- selectedPlugId === plug.id ? "border-primary" : ""
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
- 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
- });
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
- 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 : [];
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
- // 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
- });
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
- return {
46
- extracted: records.length,
47
- transformed: records.length,
48
- created: records.length,
49
- metadata: { source: ctx.flow.name },
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
- return extractAndLoad();
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({
@@ -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
- // 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 });
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
- // 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
- // }
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
- // return {
83
- // extracted: records.length,
84
- // transformed: records.length,
85
- // created: records.length,
86
- // metadata: { source: ctx.flow.name },
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 extractAndLoad();
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({
@@ -42,14 +42,13 @@ const khotanData = khotan({
42
42
  plugs: [
43
43
  // Example plug registration:
44
44
  //
45
+ // import { stripePlug } from "./plugs/stripe";
46
+ // import { stripeProductsInflow } from "./flows/stripe-products";
47
+ //
45
48
  // {
46
49
  // name: "stripe",
47
- // baseUrl: "https://api.stripe.com",
48
- // authType: "bearer",
49
- // flows: [
50
- // { name: "products-inflow", type: "inflow", schedule: "0 * * * *", resource: "products" },
51
- // { name: "invoices-inflow", type: "inflow", schedule: "0 0 * * *" },
52
- // ],
50
+ // plug: stripePlug,
51
+ // flows: [stripeProductsInflow],
53
52
  // },
54
53
  ],
55
54
 
@@ -64,3 +63,14 @@ const khotanData = khotan({
64
63
  });
65
64
 
66
65
  export default khotanData;
66
+
67
+ // Trigger a flow run from server code (route handler, action, cron):
68
+ //
69
+ // import khotanData from "@/lib/khotan/khotan";
70
+ // await khotanData.flow("products-inflow", { plugName: "stripe" }).start({
71
+ // runType: "delta", // or "full"
72
+ // });
73
+ //
74
+ // `flow(name).start(options)` is the single entry point — there is no
75
+ // `khotanData.api.*` surface. `plugName` only disambiguates when the same flow
76
+ // 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
- async function hubspotProductsWorkflow(ctx: OutflowContext) {
12
- "use workflow";
13
-
14
- async function loadAndPush() {
15
- "use step";
16
- console.log("Starting outflow", {
17
- flow: ctx.flow.name,
18
- khotanRunId: ctx.khotanRunId,
19
- runType: ctx.runType,
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 loadAndPush();
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
- // async function hubspotProductsWorkflow(ctx: OutflowContext) {
57
- // "use workflow";
58
- //
59
- // async function loadAndPush() {
60
- // "use step";
61
- // console.log("Starting outflow", {
62
- // flow: ctx.flow.name,
63
- // khotanRunId: ctx.khotanRunId,
64
- // runType: ctx.runType,
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
- // for (const record of records) {
71
- // await hubspot.post("/products", { body: record });
72
- // }
70
+ // const records = await db.select().from(products);
73
71
  //
74
- // return {
75
- // extracted: records.length,
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 loadAndPush();
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({