Skip to content

Commit 2cd16d3

Browse files
committed
feat(api): add @cf-wasm/quickjs dependency and register JsonScriptNode in node registry; enhance JSON parsing in parameter mapper
1 parent 1d84b6b commit 2cd16d3

5 files changed

Lines changed: 175 additions & 11 deletions

File tree

apps/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"dependencies": {
4141
"@aws-sdk/client-ses": "^3.812.0",
4242
"@cf-wasm/photon": "^0.1.30",
43+
"@cf-wasm/quickjs": "^0.0.6",
4344
"@dafthunk/types": "workspace:*",
4445
"@hono/oauth-providers": "^0.7.1",
4546
"@hono/zod-validator": "^0.5.0",
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { NodeExecution, NodeType } from "@dafthunk/types";
2+
import {
3+
getQuickJSWASMModule,
4+
QuickJSContext,
5+
QuickJSHandle,
6+
} from "@cf-wasm/quickjs";
7+
8+
import { ExecutableNode, NodeContext } from "../types";
9+
10+
export class JsonScriptNode extends ExecutableNode {
11+
public static readonly nodeType: NodeType = {
12+
id: "json-script",
13+
name: "JSON Script",
14+
type: "json-script",
15+
description:
16+
"Executes a JavaScript script with a JSON object as input. The input JSON is available as a global variable 'inputJson'. The result of the last expression is returned.",
17+
category: "JSON",
18+
icon: "code", // Using 'code' icon, assuming it exists or is appropriate
19+
inputs: [
20+
{
21+
name: "json",
22+
type: "json",
23+
description: "The JSON object to process",
24+
required: true,
25+
},
26+
{
27+
name: "script",
28+
type: "string",
29+
description:
30+
"JavaScript code to execute. 'inputJson' is available globally. The last expression's result is returned.",
31+
required: true,
32+
},
33+
],
34+
outputs: [
35+
{
36+
name: "result",
37+
type: "json",
38+
description: "The result of the script execution, expected to be a JSON object or array",
39+
},
40+
{
41+
name: "error",
42+
type: "string",
43+
description: "Error message if script execution failed",
44+
hidden: true,
45+
},
46+
],
47+
};
48+
49+
public async execute(context: NodeContext): Promise<NodeExecution> {
50+
const { json, script } = context.inputs;
51+
52+
if (json === undefined || json === null) {
53+
return this.createErrorResult("Missing JSON input object.");
54+
}
55+
56+
if (!script || typeof script !== "string" || script.trim() === "") {
57+
return this.createErrorResult("Missing or empty script.");
58+
}
59+
60+
let vm: QuickJSContext | undefined;
61+
try {
62+
const QuickJSModule = await getQuickJSWASMModule();
63+
vm = QuickJSModule.newContext();
64+
65+
// 1. Convert JSON to string and create a string handle in the VM
66+
const jsonStringified = JSON.stringify(json);
67+
const jsonStringHandle = vm.newString(jsonStringified);
68+
69+
// 2. Set this string handle to a temporary global variable in the VM
70+
vm.setProp(vm.global, "__tempInputJsonString__", jsonStringHandle);
71+
jsonStringHandle.dispose(); // Dispose handle, vm's global has it now
72+
73+
// 3. Bootstrap script to parse the string and set globalThis.inputJson
74+
const bootstrapScript = `
75+
try {
76+
globalThis.inputJson = JSON.parse(globalThis.__tempInputJsonString__);
77+
} finally {
78+
delete globalThis.__tempInputJsonString__;
79+
}
80+
`;
81+
const bootstrapResult = vm.evalCode(bootstrapScript);
82+
83+
if (bootstrapResult.error) {
84+
const errorDump = vm.dump(bootstrapResult.error);
85+
bootstrapResult.error.dispose();
86+
return this.createErrorResult(
87+
`Failed to initialize inputJson in VM: ${JSON.stringify(errorDump)}`
88+
);
89+
}
90+
// Result of delete is boolean, or undefined if try/finally completes normally without returning a value from try.
91+
// In either case, dispose the handle for the result of the bootstrap script.
92+
bootstrapResult.value.dispose();
93+
94+
// 4. Now, 'inputJson' global variable is ready for the user's script
95+
const evalResult = vm.evalCode(script);
96+
97+
if (evalResult.error) {
98+
const errorDump = vm.dump(evalResult.error);
99+
evalResult.error.dispose();
100+
return this.createErrorResult(
101+
`Script execution error: ${JSON.stringify(errorDump)}`
102+
);
103+
} else {
104+
const resultOutput = vm.dump(evalResult.value);
105+
evalResult.value.dispose();
106+
return this.createSuccessResult({
107+
result: resultOutput,
108+
error: null, // Explicitly set error to null on success
109+
});
110+
}
111+
} catch (err) {
112+
const error = err as Error;
113+
return this.createErrorResult(
114+
`Error during script execution: ${error.message}`
115+
);
116+
} finally {
117+
if (vm) {
118+
vm.dispose();
119+
}
120+
}
121+
}
122+
}

apps/api/src/nodes/node-registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { WebcamNode } from "./image/webcam-node";
5656
import { JsonBooleanExtractorNode } from "./json/json-boolean-extractor-node";
5757
import { JsonNumberExtractorNode } from "./json/json-number-extractor-node";
5858
import { JsonObjectArrayExtractorNode } from "./json/json-object-array-extractor-node";
59+
import { JsonScriptNode } from "./json/json-script-node";
5960
import { JsonStringExtractorNode } from "./json/json-string-extractor-node";
6061
import { JsonTemplateNode } from "./json/json-template-node";
6162
import { MonacoEditorNode } from "./json/monaco-editor-node";
@@ -177,6 +178,7 @@ export class NodeRegistry {
177178
this.registerImplementation(JsonBooleanExtractorNode);
178179
this.registerImplementation(JsonNumberExtractorNode);
179180
this.registerImplementation(JsonObjectArrayExtractorNode);
181+
this.registerImplementation(JsonScriptNode);
180182
this.registerImplementation(JsonTemplateNode);
181183
this.registerImplementation(MultiVariableStringTemplateNode);
182184
this.registerImplementation(SingleVariableStringTemplateNode);

apps/api/src/nodes/parameter-mapper.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,18 @@ const converters = {
205205
json: {
206206
nodeToApi: (value: NodeParameterValue) =>
207207
(isPlainJsonObject(value) ? value : undefined) as ApiParameterValue,
208-
apiToNode: (value: ApiParameterValue) =>
209-
(isPlainJsonObject(value) ? value : undefined) as NodeParameterValue,
208+
apiToNode: (value: ApiParameterValue) => {
209+
if (typeof value === "string") {
210+
try {
211+
const parsed = JSON.parse(value);
212+
return (isPlainJsonObject(parsed) ? parsed : undefined) as NodeParameterValue;
213+
} catch (e) {
214+
// If parsing fails, it's not valid JSON in string form
215+
return undefined;
216+
}
217+
}
218+
return (isPlainJsonObject(value) ? value : undefined) as NodeParameterValue;
219+
},
210220
},
211221
any: {
212222
nodeToApi: (value: NodeParameterValue) => value as ApiParameterValue,

pnpm-lock.yaml

Lines changed: 38 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)