Skip to content

Commit c3abc14

Browse files
committed
feat(api): add CalculatorNode to CloudflareNodeRegistry and improve formatting in JavascriptScriptNode
1 parent e3c64ef commit c3abc14

7 files changed

Lines changed: 454 additions & 20 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ import { ConditionalJoinNode } from "./logic/conditional-join-node";
185185
import { AbsoluteValueNode } from "./math/absolute-value-node";
186186
import { AdditionNode } from "./math/addition-node";
187187
import { AvgNode } from "./math/avg-node";
188+
import { CalculatorNode } from "./math/calculator-node";
188189
import { DivisionNode } from "./math/division-node";
189190
import { ExponentiationNode } from "./math/exponentiation-node";
190191
import { MaxNode } from "./math/max-node";
@@ -276,6 +277,7 @@ export class CloudflareNodeRegistry extends BaseNodeRegistry {
276277
this.registerImplementation(ExponentiationNode);
277278
this.registerImplementation(SquareRootNode);
278279
this.registerImplementation(AbsoluteValueNode);
280+
this.registerImplementation(CalculatorNode);
279281
this.registerImplementation(SumNode);
280282
this.registerImplementation(MaxNode);
281283
this.registerImplementation(MinNode);

apps/api/src/nodes/javascript/javascript-script-node.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ describe("JavascriptScriptNode", () => {
9090
const result = await node.execute(context);
9191
expect(result.status).toBe("completed");
9292
expect(result.outputs?.result).toEqual(["HELLO", "WORLD", "JAVASCRIPT"]);
93-
expect(result.outputs?.stdout).toBe("Processing values: hello, world, javascript");
93+
expect(result.outputs?.stdout).toBe(
94+
"Processing values: hello, world, javascript"
95+
);
9496
expect(result.outputs?.error).toBeNull();
9597
});
9698

apps/api/src/nodes/javascript/javascript-script-node.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ export class JavascriptScriptNode extends ExecutableNode {
2424
{
2525
name: "args",
2626
type: "json",
27-
description: "Command line arguments (array of strings) to pass to the script",
27+
description:
28+
"Command line arguments (array of strings) to pass to the script",
2829
required: false,
2930
},
3031
{
@@ -38,7 +39,8 @@ export class JavascriptScriptNode extends ExecutableNode {
3839
{
3940
name: "result",
4041
type: "json",
41-
description: "The result of the script execution (return value or last expression)",
42+
description:
43+
"The result of the script execution (return value or last expression)",
4244
},
4345
{
4446
name: "stdout",
@@ -81,8 +83,8 @@ export class JavascriptScriptNode extends ExecutableNode {
8183
const stdout: string[] = [];
8284
const stderr: string[] = [];
8385

84-
// Create a bootstrap script that sets up the environment
85-
const bootstrapScript = `
86+
// Create a bootstrap script that sets up the environment
87+
const bootstrapScript = `
8688
// Set up console object
8789
globalThis.console = {
8890
log: function(...args) {
@@ -146,13 +148,15 @@ export class JavascriptScriptNode extends ExecutableNode {
146148

147149
// Execute the original script (either it didn't start with '{' or wrapping failed)
148150
const evalResult = vm!.evalCode(scriptToExecute);
149-
151+
150152
if (evalResult.error) {
151153
const errorDump = vm!.dump(evalResult.error);
152154
evalResult.error.dispose();
153-
throw new Error(`Script execution error: ${JSON.stringify(errorDump)}`);
155+
throw new Error(
156+
`Script execution error: ${JSON.stringify(errorDump)}`
157+
);
154158
}
155-
159+
156160
const resultOutput = vm!.dump(evalResult.value);
157161
evalResult.value.dispose();
158162
return resultOutput;
@@ -164,10 +168,14 @@ export class JavascriptScriptNode extends ExecutableNode {
164168
// Get captured output
165169
const stdoutResult = vm.evalCode("globalThis.__stdout__.join('\\n')");
166170
const stderrResult = vm.evalCode("globalThis.__stderr__.join('\\n')");
167-
168-
const stdoutOutput = stdoutResult.error ? "" : vm.dump(stdoutResult.value);
169-
const stderrOutput = stderrResult.error ? "" : vm.dump(stderrResult.value);
170-
171+
172+
const stdoutOutput = stdoutResult.error
173+
? ""
174+
: vm.dump(stdoutResult.value);
175+
const stderrOutput = stderrResult.error
176+
? ""
177+
: vm.dump(stderrResult.value);
178+
171179
if (!stdoutResult.error) stdoutResult.value.dispose();
172180
if (!stderrResult.error) stderrResult.value.dispose();
173181

@@ -177,7 +185,6 @@ export class JavascriptScriptNode extends ExecutableNode {
177185
stderr: stderrOutput,
178186
error: null,
179187
});
180-
181188
} catch (err) {
182189
const error = err as Error;
183190
return this.createErrorResult(
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { describe, it, expect } from "vitest";
2+
import { CalculatorNode } from "./calculator-node";
3+
import { Node } from "@dafthunk/types";
4+
import { NodeContext } from "../types";
5+
6+
describe("CalculatorNode", () => {
7+
const createNode = (): Node => ({
8+
id: "test-calculator",
9+
type: "calculator",
10+
position: { x: 0, y: 0 },
11+
} as unknown as Node);
12+
13+
const createContext = (inputs: Record<string, any>) => ({
14+
nodeId: "test-calculator",
15+
inputs,
16+
workflowId: "test-workflow",
17+
executionId: "test-execution",
18+
organizationId: "test-org",
19+
env: {},
20+
nodeRegistry: null,
21+
toolRegistry: null,
22+
} as unknown as NodeContext);
23+
24+
describe("nodeType", () => {
25+
it("should have correct node type definition", () => {
26+
expect(CalculatorNode.nodeType).toEqual({
27+
id: "calculator",
28+
name: "Calculator",
29+
type: "calculator",
30+
description:
31+
"Evaluates mathematical expressions with comprehensive support for arithmetic operations, mathematical functions, trigonometric functions, constants, and complex formulas. Supports: basic arithmetic (+, -, *, /, ^, %), bitwise operators (&, |, <, >, ~), mathematical functions (sqrt, cbrt, pow, exp, log, log10, abs, floor, ceil, round, min, max, sign, trunc, hypot), trigonometric functions (sin, cos, tan, asin, acos, atan, atan2, sinh, cosh, tanh, asinh, acosh, atanh), mathematical constants (PI, E), random numbers, and complex nested expressions with proper order of operations and parentheses. All inputs are validated for security and only mathematical operations are allowed.",
32+
tags: ["Math"],
33+
icon: "calculator",
34+
inlinable: true,
35+
asTool: true,
36+
inputs: [
37+
{
38+
name: "expression",
39+
type: "string",
40+
description:
41+
"The mathematical expression to evaluate as a string. Examples: '2 + 3 * 4', 'sqrt(16)', 'sin(PI/2)', 'pow(2, 3)', 'abs(-5)', 'floor(3.7)', 'PI * 2^2', '(10 + 5) * 2 / 4', 'log(100)', 'random * 10', '17 % 5', '15 & 7', '8 | 4', '~10'. Supports all standard mathematical operations, functions, and constants. Use ^ for exponentiation (e.g., 2^3 = 8), % for modulo (e.g., 17 % 5 = 2), & for bitwise AND, | for bitwise OR, ~ for bitwise NOT. All expressions are validated for security.",
42+
required: true,
43+
},
44+
],
45+
outputs: [
46+
{
47+
name: "result",
48+
type: "number",
49+
description:
50+
"The calculated result of the mathematical expression as a number. Returns the final computed value after evaluating the expression with proper order of operations. Will be NaN or throw an error for invalid expressions.",
51+
},
52+
],
53+
});
54+
});
55+
});
56+
57+
describe("execute", () => {
58+
it("should evaluate simple arithmetic expressions", async () => {
59+
const node = new CalculatorNode(createNode());
60+
const context = createContext({ expression: "2 + 3" });
61+
62+
const result = await node.execute(context);
63+
64+
expect(result.status).toBe("completed");
65+
expect(result.outputs?.result).toBe(5);
66+
});
67+
68+
it("should handle multiplication and division", async () => {
69+
const node = new CalculatorNode(createNode());
70+
const context = createContext({ expression: "10 * 5 / 2" });
71+
72+
const result = await node.execute(context);
73+
74+
expect(result.status).toBe("completed");
75+
expect(result.outputs?.result).toBe(25);
76+
});
77+
78+
it("should handle parentheses and order of operations", async () => {
79+
const node = new CalculatorNode(createNode());
80+
const context = createContext({ expression: "(2 + 3) * 4" });
81+
82+
const result = await node.execute(context);
83+
84+
expect(result.status).toBe("completed");
85+
expect(result.outputs?.result).toBe(20);
86+
});
87+
88+
it("should handle exponentiation with ^ symbol", async () => {
89+
const node = new CalculatorNode(createNode());
90+
const context = createContext({ expression: "2^3" });
91+
92+
const result = await node.execute(context);
93+
94+
expect(result.status).toBe("completed");
95+
expect(result.outputs?.result).toBe(8);
96+
});
97+
98+
it("should handle mathematical functions", async () => {
99+
const node = new CalculatorNode(createNode());
100+
const context = createContext({ expression: "sqrt(16)" });
101+
102+
const result = await node.execute(context);
103+
104+
expect(result.status).toBe("completed");
105+
expect(result.outputs?.result).toBe(4);
106+
});
107+
108+
it("should handle trigonometric functions", async () => {
109+
const node = new CalculatorNode(createNode());
110+
const context = createContext({ expression: "sin(0)" });
111+
112+
const result = await node.execute(context);
113+
114+
expect(result.status).toBe("completed");
115+
expect(result.outputs?.result).toBe(0);
116+
});
117+
118+
it("should handle constants", async () => {
119+
const node = new CalculatorNode(createNode());
120+
const context = createContext({ expression: "PI" });
121+
122+
const result = await node.execute(context);
123+
124+
expect(result.status).toBe("completed");
125+
expect(result.outputs?.result).toBe(Math.PI);
126+
});
127+
128+
it("should handle complex expressions", async () => {
129+
const node = new CalculatorNode(createNode());
130+
const context = createContext({ expression: "sqrt(16) + sin(0) * 5" });
131+
132+
const result = await node.execute(context);
133+
134+
expect(result.status).toBe("completed");
135+
expect(result.outputs?.result).toBe(4);
136+
});
137+
138+
it("should return error for empty expression", async () => {
139+
const node = new CalculatorNode(createNode());
140+
const context = createContext({ expression: "" });
141+
142+
const result = await node.execute(context);
143+
144+
expect(result.status).toBe("error");
145+
expect(result.error).toBe("Missing or empty expression.");
146+
});
147+
148+
it("should return error for invalid characters", async () => {
149+
const node = new CalculatorNode(createNode());
150+
const context = createContext({ expression: "alert('hello')" });
151+
152+
const result = await node.execute(context);
153+
154+
expect(result.status).toBe("error");
155+
expect(result.error).toContain("Expression contains invalid characters");
156+
});
157+
158+
it("should return error for invalid mathematical expressions", async () => {
159+
const node = new CalculatorNode(createNode());
160+
const context = createContext({ expression: "2 +" });
161+
162+
const result = await node.execute(context);
163+
164+
expect(result.status).toBe("error");
165+
});
166+
167+
it("should handle decimal numbers", async () => {
168+
const node = new CalculatorNode(createNode());
169+
const context = createContext({ expression: "3.14 * 2" });
170+
171+
const result = await node.execute(context);
172+
173+
expect(result.status).toBe("completed");
174+
expect(result.outputs?.result).toBe(6.28);
175+
});
176+
177+
it("should handle modulo operator", async () => {
178+
const node = new CalculatorNode(createNode());
179+
const context = createContext({ expression: "17 % 5" });
180+
181+
const result = await node.execute(context);
182+
183+
expect(result.status).toBe("completed");
184+
expect(result.outputs?.result).toBe(2);
185+
});
186+
187+
it("should handle bitwise AND operator", async () => {
188+
const node = new CalculatorNode(createNode());
189+
const context = createContext({ expression: "15 & 7" });
190+
191+
const result = await node.execute(context);
192+
193+
expect(result.status).toBe("completed");
194+
expect(result.outputs?.result).toBe(7);
195+
});
196+
197+
it("should handle bitwise OR operator", async () => {
198+
const node = new CalculatorNode(createNode());
199+
const context = createContext({ expression: "8 | 4" });
200+
201+
const result = await node.execute(context);
202+
203+
expect(result.status).toBe("completed");
204+
expect(result.outputs?.result).toBe(12);
205+
});
206+
207+
it("should handle bitwise NOT operator", async () => {
208+
const node = new CalculatorNode(createNode());
209+
const context = createContext({ expression: "~10" });
210+
211+
const result = await node.execute(context);
212+
213+
expect(result.status).toBe("completed");
214+
expect(result.outputs?.result).toBe(-11);
215+
});
216+
217+
it("should handle additional math functions", async () => {
218+
const node = new CalculatorNode(createNode());
219+
const context = createContext({ expression: "sign(-5) + trunc(3.7) + hypot(3, 4)" });
220+
221+
const result = await node.execute(context);
222+
223+
expect(result.status).toBe("completed");
224+
expect(result.outputs?.result).toBe(-1 + 3 + 5); // -1 + 3 + 5 = 7
225+
});
226+
227+
it("should handle atan2 function", async () => {
228+
const node = new CalculatorNode(createNode());
229+
const context = createContext({ expression: "atan2(1, 1)" });
230+
231+
const result = await node.execute(context);
232+
233+
expect(result.status).toBe("completed");
234+
expect(result.outputs?.result).toBe(Math.PI / 4);
235+
});
236+
});
237+
});

0 commit comments

Comments
 (0)