khotan-data 0.1.1 → 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.
@@ -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({
@@ -4,43 +4,51 @@
4
4
  //
5
5
  // Copy this file, rename it for your source/destination pair, and register the
6
6
  // exported pass 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 { pass, type PassContext } from "./pass";
10
17
 
11
- async function stripeToSlackWorkflow(ctx: PassContext) {
12
- "use workflow";
13
-
14
- async function forwardEvent() {
15
- "use step";
16
- console.log("Forwarding 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 forwardEvent(ctx: PassContext) {
20
+ "use step";
21
+ console.log("Forwarding webhook event", {
22
+ eventType: ctx.eventType,
23
+ khotanRunId: ctx.khotanRunId,
24
+ });
20
25
 
21
- await fetch("https://slack.com/api/chat.postMessage", {
22
- method: "POST",
23
- headers: {
24
- Authorization: `Bearer ${ctx.destVars["botToken"] ?? ""}`,
25
- "Content-Type": "application/json",
26
- },
27
- body: JSON.stringify({
28
- channel: ctx.destVars["channelId"],
29
- text: `Received ${ctx.eventType}`,
30
- blocks: [
31
- {
32
- type: "section",
33
- text: {
34
- type: "mrkdwn",
35
- text: `Received ${ctx.eventType} from Stripe`,
36
- },
26
+ await fetch("https://slack.com/api/chat.postMessage", {
27
+ method: "POST",
28
+ headers: {
29
+ Authorization: `Bearer ${ctx.destVars["botToken"] ?? ""}`,
30
+ "Content-Type": "application/json",
31
+ },
32
+ body: JSON.stringify({
33
+ channel: ctx.destVars["channelId"],
34
+ text: `Received ${ctx.eventType}`,
35
+ blocks: [
36
+ {
37
+ type: "section",
38
+ text: {
39
+ type: "mrkdwn",
40
+ text: `Received ${ctx.eventType} from Stripe`,
37
41
  },
38
- ],
39
- }),
40
- });
41
- }
42
+ },
43
+ ],
44
+ }),
45
+ });
46
+ }
42
47
 
43
- await forwardEvent();
48
+ // Workflow: orchestration only. Calls top-level steps with serializable args.
49
+ async function stripeToSlackWorkflow(ctx: PassContext) {
50
+ "use workflow";
51
+ await forwardEvent(ctx);
44
52
  }
45
53
 
46
54
  export const stripeToSlack = pass({
@@ -85,36 +85,41 @@ export function pass(config: PassConfig): PassRegistration {
85
85
  // import { pass, type PassContext } from "./pass";
86
86
  // import { plug } from "../plugs/plug";
87
87
  //
88
- // async function pollinateToSlackWorkflow(ctx: PassContext) {
89
- // "use workflow";
88
+ // Declare "use step" functions at MODULE TOP LEVEL and pass them serializable
89
+ // values only (`ctx` is plain data). Do NOT nest steps inside the "use workflow"
90
+ // function — closures over workflow scope cannot be hoisted and fail at runtime.
90
91
  //
91
- // async function forwardEvent() {
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;
92
+ // // Step: top-level, full Node.js access, retried independently.
93
+ // async function forwardEvent(ctx: PassContext) {
94
+ // "use step";
95
+ // const cache = khotanCache(ctx, "pollinate-forwarded-events");
96
+ // const eventId = String(ctx.event["id"] ?? "");
97
+ // if (eventId && (await cache.get<boolean>(eventId))) return;
96
98
  //
97
- // // Construct destination plug from destVars
98
- // const slackPlug = plug({
99
- // name: "slack",
100
- // baseUrl: "https://hooks.slack.com",
101
- // authType: "bearer",
102
- // auth: { bearer: { token: ctx.destVars["token"] ?? "" } },
103
- // });
99
+ // // Construct destination plug from destVars
100
+ // const slackPlug = plug({
101
+ // name: "slack",
102
+ // baseUrl: "https://hooks.slack.com",
103
+ // authType: "bearer",
104
+ // auth: { bearer: { token: ctx.destVars["token"] ?? "" } },
105
+ // });
104
106
  //
105
- // await slackPlug.post("/services/webhook", {
106
- // body: {
107
- // text: `Received ${ctx.eventType} event from pollinate`,
108
- // event: ctx.event,
109
- // },
110
- // });
107
+ // await slackPlug.post("/services/webhook", {
108
+ // body: {
109
+ // text: `Received ${ctx.eventType} event from pollinate`,
110
+ // event: ctx.event,
111
+ // },
112
+ // });
111
113
  //
112
- // if (eventId) {
113
- // await cache.set(eventId, true);
114
- // }
114
+ // if (eventId) {
115
+ // await cache.set(eventId, true);
115
116
  // }
117
+ // }
116
118
  //
117
- // await forwardEvent();
119
+ // // Workflow: orchestration only.
120
+ // async function pollinateToSlackWorkflow(ctx: PassContext) {
121
+ // "use workflow";
122
+ // await forwardEvent(ctx);
118
123
  // }
119
124
  //
120
125
  // export const pollinateToSlack = pass({
@@ -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">
@@ -4,62 +4,70 @@
4
4
  //
5
5
  // Copy this file, rename it for your source/destination pair, 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 { khotanCache } from "khotan-data/factory";
10
17
  import { relay, type RelayContext } from "./relay";
11
18
 
12
- async function shopifyToHubspotWorkflow(ctx: RelayContext) {
13
- "use workflow";
19
+ // Step: full Node.js access, retried independently. Receives serializable ctx.
20
+ async function forwardProducts(ctx: RelayContext) {
21
+ "use step";
22
+ console.log("Starting relay", {
23
+ flow: ctx.flow.name,
24
+ to: ctx.flow.to,
25
+ khotanRunId: ctx.khotanRunId,
26
+ runType: ctx.runType,
27
+ });
14
28
 
15
- async function forwardProducts() {
16
- "use step";
17
- console.log("Starting relay", {
18
- flow: ctx.flow.name,
19
- to: ctx.flow.to,
20
- khotanRunId: ctx.khotanRunId,
21
- runType: ctx.runType,
22
- });
29
+ const sourceResponse = await fetch("https://source.example.com/products", {
30
+ headers: {
31
+ Authorization: `Bearer ${ctx.vars["sourceToken"] ?? ""}`,
32
+ },
33
+ });
34
+ const payload = (await sourceResponse.json()) as {
35
+ data?: Array<Record<string, unknown>>;
36
+ };
37
+ const records = Array.isArray(payload.data) ? payload.data : [];
38
+ const snapshotCache = khotanCache(ctx, "shopify-products-snapshot");
39
+ const previousRecords =
40
+ (await snapshotCache.get<Array<Record<string, unknown>>>("latest")) ?? [];
41
+
42
+ await snapshotCache.set("latest", records, { ttl: "6h" });
23
43
 
24
- const sourceResponse = await fetch("https://source.example.com/products", {
44
+ for (const record of records) {
45
+ await fetch("https://destination.example.com/products", {
46
+ method: "POST",
25
47
  headers: {
26
- Authorization: `Bearer ${ctx.vars["sourceToken"] ?? ""}`,
48
+ Authorization: `Bearer ${ctx.vars["destinationToken"] ?? ""}`,
49
+ "Content-Type": "application/json",
27
50
  },
51
+ body: JSON.stringify(record),
28
52
  });
29
- const payload = (await sourceResponse.json()) as {
30
- data?: Array<Record<string, unknown>>;
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" });
38
-
39
- for (const record of records) {
40
- await fetch("https://destination.example.com/products", {
41
- method: "POST",
42
- headers: {
43
- Authorization: `Bearer ${ctx.vars["destinationToken"] ?? ""}`,
44
- "Content-Type": "application/json",
45
- },
46
- body: JSON.stringify(record),
47
- });
48
- }
49
-
50
- return {
51
- extracted: records.length,
52
- transformed: records.length,
53
- created: records.length,
54
- metadata: {
55
- relay: ctx.flow.name,
56
- to: ctx.flow.to,
57
- previousCount: previousRecords.length,
58
- },
59
- };
60
53
  }
61
54
 
62
- return forwardProducts();
55
+ return {
56
+ extracted: records.length,
57
+ transformed: records.length,
58
+ created: records.length,
59
+ metadata: {
60
+ relay: ctx.flow.name,
61
+ to: ctx.flow.to,
62
+ previousCount: previousRecords.length,
63
+ },
64
+ };
65
+ }
66
+
67
+ // Workflow: orchestration only. Calls top-level steps with serializable args.
68
+ async function shopifyToHubspotWorkflow(ctx: RelayContext) {
69
+ "use workflow";
70
+ return forwardProducts(ctx);
63
71
  }
64
72
 
65
73
  export const shopifyToHubspotRelay = relay({
@@ -51,48 +51,53 @@ export function relay(config: RelayConfig): FlowRegistration {
51
51
  // Usage Example (create a file like flows/shopify-to-hubspot.ts)
52
52
  // ---------------------------------------------------------------------------
53
53
  //
54
+ // Declare "use step" functions at MODULE TOP LEVEL and pass them serializable
55
+ // values only (`ctx` is plain data). Do NOT nest steps inside the "use workflow"
56
+ // function — closures over workflow scope cannot be hoisted and fail at runtime.
57
+ //
54
58
  // import { bindWorkflowPlug, khotanCache, relay, type RelayContext } from "khotan-data/factory";
55
59
  // import { shopifyPlug } from "../plugs/shopify";
56
60
  // import { hubspotPlug } from "../plugs/hubspot";
57
61
  //
58
- // async function shopifyToHubspotWorkflow(ctx: RelayContext) {
59
- // "use workflow";
60
- //
61
- // async function forwardProducts() {
62
- // "use step";
63
- // console.log("Starting relay", {
64
- // flow: ctx.flow.name,
65
- // to: ctx.flow.to,
66
- // khotanRunId: ctx.khotanRunId,
67
- // runType: ctx.runType,
68
- // });
69
- // const shopify = bindWorkflowPlug(shopifyPlug, ctx);
70
- // const hubspot = bindWorkflowPlug(hubspotPlug, ctx, "hubspot");
71
- //
72
- // const snapshotCache = khotanCache(ctx, "shopify-products-snapshot");
73
- // const previous = await snapshotCache.get<Array<Record<string, unknown>>>("latest");
62
+ // // Step: top-level, full Node.js access, retried independently.
63
+ // async function forwardProducts(ctx: RelayContext) {
64
+ // "use step";
65
+ // console.log("Starting relay", {
66
+ // flow: ctx.flow.name,
67
+ // to: ctx.flow.to,
68
+ // khotanRunId: ctx.khotanRunId,
69
+ // runType: ctx.runType,
70
+ // });
71
+ // const shopify = bindWorkflowPlug(shopifyPlug, ctx);
72
+ // const hubspot = bindWorkflowPlug(hubspotPlug, ctx, "hubspot");
74
73
  //
75
- // const response = await shopify.get<{ data?: Array<Record<string, unknown>> }>("/products");
76
- // const records = Array.isArray(response.data) ? response.data : [];
77
- // await snapshotCache.set("latest", records);
74
+ // const snapshotCache = khotanCache(ctx, "shopify-products-snapshot");
75
+ // const previous = await snapshotCache.get<Array<Record<string, unknown>>>("latest");
78
76
  //
79
- // for (const record of records) {
80
- // await hubspot.post("/products", { body: record });
81
- // }
77
+ // const response = await shopify.get<{ data?: Array<Record<string, unknown>> }>("/products");
78
+ // const records = Array.isArray(response.data) ? response.data : [];
79
+ // await snapshotCache.set("latest", records);
82
80
  //
83
- // return {
84
- // extracted: records.length,
85
- // transformed: records.length,
86
- // created: records.length,
87
- // metadata: {
88
- // relay: ctx.flow.name,
89
- // to: ctx.flow.to,
90
- // previousCount: previous?.length ?? 0,
91
- // },
92
- // };
81
+ // for (const record of records) {
82
+ // await hubspot.post("/products", { body: record });
93
83
  // }
94
84
  //
95
- // return forwardProducts();
85
+ // return {
86
+ // extracted: records.length,
87
+ // transformed: records.length,
88
+ // created: records.length,
89
+ // metadata: {
90
+ // relay: ctx.flow.name,
91
+ // to: ctx.flow.to,
92
+ // previousCount: previous?.length ?? 0,
93
+ // },
94
+ // };
95
+ // }
96
+ //
97
+ // // Workflow: orchestration only.
98
+ // async function shopifyToHubspotWorkflow(ctx: RelayContext) {
99
+ // "use workflow";
100
+ // return forwardProducts(ctx);
96
101
  // }
97
102
  //
98
103
  // export const shopifyToHubspotRelay = relay({