Skip to content

Commit 93d3e75

Browse files
authored
feat(api): add date manipulation nodes and enhance date handling (#149)
1 parent a3b7068 commit 93d3e75

16 files changed

Lines changed: 1298 additions & 0 deletions

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { CloudflareBrowserPdfNode } from "./browser/cloudflare-browser-pdf-node"
1212
import { CloudflareBrowserScrapeNode } from "./browser/cloudflare-browser-scrape-node";
1313
import { CloudflareBrowserScreenshotNode } from "./browser/cloudflare-browser-screenshot-node";
1414
import { CloudflareBrowserSnapshotNode } from "./browser/cloudflare-browser-snapshot-node";
15+
import { AddDateNode } from "./date/add-date-node";
16+
import { DiffDateNode } from "./date/diff-date-node";
17+
import { NowDateNode } from "./date/now-date-node";
18+
import { ParseDateNode } from "./date/parse-date-node";
1519
import { DocumentNode } from "./document/document-node";
1620
import { ToMarkdownNode } from "./document/to-markdown-node";
1721
import { ParseEmailNode } from "./email/parse-email-node";
@@ -335,6 +339,11 @@ export class CloudflareNodeRegistry extends BaseNodeRegistry {
335339
this.registerImplementation(JsonExecuteJavascriptNode);
336340
this.registerImplementation(JsonTemplateNode);
337341
this.registerImplementation(JsonEditorNode);
342+
// Date nodes
343+
this.registerImplementation(NowDateNode);
344+
this.registerImplementation(ParseDateNode);
345+
this.registerImplementation(AddDateNode);
346+
this.registerImplementation(DiffDateNode);
338347
this.registerImplementation(JsonArrayLengthNode);
339348
this.registerImplementation(JsonContainsNode);
340349
this.registerImplementation(JsonContainsPathNode);
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { Node } from "@dafthunk/types";
2+
import { describe, expect, it } from "vitest";
3+
4+
import { NodeContext } from "../types";
5+
import { AddDateNode } from "./add-date-node";
6+
7+
describe("AddDateNode", () => {
8+
const nodeId = "date-add";
9+
const node = new AddDateNode({
10+
nodeId,
11+
} as unknown as Node);
12+
13+
const createContext = (inputs: Record<string, any>): NodeContext =>
14+
({
15+
nodeId: "test",
16+
inputs,
17+
workflowId: "test",
18+
organizationId: "test-org",
19+
env: {},
20+
}) as unknown as NodeContext;
21+
22+
describe("execute", () => {
23+
it("should add days", async () => {
24+
const baseDate = "2023-12-25T10:30:00.000Z";
25+
const result = await node.execute(
26+
createContext({
27+
date: baseDate,
28+
amount: 5,
29+
unit: "days",
30+
})
31+
);
32+
33+
expect(result.status).toBe("completed");
34+
expect(result.outputs?.date).toBe("2023-12-30T10:30:00.000Z");
35+
});
36+
37+
it("should add hours", async () => {
38+
const baseDate = "2023-12-25T10:30:00.000Z";
39+
const result = await node.execute(
40+
createContext({
41+
date: baseDate,
42+
amount: 2,
43+
unit: "hours",
44+
})
45+
);
46+
47+
expect(result.status).toBe("completed");
48+
expect(result.outputs?.date).toBe("2023-12-25T12:30:00.000Z");
49+
});
50+
51+
it("should add minutes", async () => {
52+
const baseDate = "2023-12-25T10:30:00.000Z";
53+
const result = await node.execute(
54+
createContext({
55+
date: baseDate,
56+
amount: 45,
57+
unit: "minutes",
58+
})
59+
);
60+
61+
expect(result.status).toBe("completed");
62+
expect(result.outputs?.date).toBe("2023-12-25T11:15:00.000Z");
63+
});
64+
65+
it("should add seconds", async () => {
66+
const baseDate = "2023-12-25T10:30:00.000Z";
67+
const result = await node.execute(
68+
createContext({
69+
date: baseDate,
70+
amount: 30,
71+
unit: "seconds",
72+
})
73+
);
74+
75+
expect(result.status).toBe("completed");
76+
expect(result.outputs?.date).toBe("2023-12-25T10:30:30.000Z");
77+
});
78+
79+
it("should add milliseconds", async () => {
80+
const baseDate = "2023-12-25T10:30:00.000Z";
81+
const result = await node.execute(
82+
createContext({
83+
date: baseDate,
84+
amount: 500,
85+
unit: "milliseconds",
86+
})
87+
);
88+
89+
expect(result.status).toBe("completed");
90+
expect(result.outputs?.date).toBe("2023-12-25T10:30:00.500Z");
91+
});
92+
93+
it("should add weeks", async () => {
94+
const baseDate = "2023-12-25T10:30:00.000Z";
95+
const result = await node.execute(
96+
createContext({
97+
date: baseDate,
98+
amount: 2,
99+
unit: "weeks",
100+
})
101+
);
102+
103+
expect(result.status).toBe("completed");
104+
expect(result.outputs?.date).toBe("2024-01-08T10:30:00.000Z");
105+
});
106+
107+
it("should add months", async () => {
108+
const baseDate = "2023-12-25T10:30:00.000Z";
109+
const result = await node.execute(
110+
createContext({
111+
date: baseDate,
112+
amount: 1,
113+
unit: "months",
114+
})
115+
);
116+
117+
expect(result.status).toBe("completed");
118+
expect(result.outputs?.date).toBe("2024-01-25T10:30:00.000Z");
119+
});
120+
121+
it("should add years", async () => {
122+
const baseDate = "2023-12-25T10:30:00.000Z";
123+
const result = await node.execute(
124+
createContext({
125+
date: baseDate,
126+
amount: 1,
127+
unit: "years",
128+
})
129+
);
130+
131+
expect(result.status).toBe("completed");
132+
expect(result.outputs?.date).toBe("2024-12-25T10:30:00.000Z");
133+
});
134+
135+
it("should subtract time with negative amount", async () => {
136+
const baseDate = "2023-12-25T10:30:00.000Z";
137+
const result = await node.execute(
138+
createContext({
139+
date: baseDate,
140+
amount: -5,
141+
unit: "days",
142+
})
143+
);
144+
145+
expect(result.status).toBe("completed");
146+
expect(result.outputs?.date).toBe("2023-12-20T10:30:00.000Z");
147+
});
148+
149+
it("should handle invalid date input", async () => {
150+
const result = await node.execute(
151+
createContext({
152+
date: "invalid-date",
153+
amount: 5,
154+
unit: "days",
155+
})
156+
);
157+
158+
expect(result.status).toBe("completed");
159+
expect(result.outputs?.date).toBeUndefined();
160+
});
161+
162+
it("should handle invalid unit", async () => {
163+
const baseDate = "2023-12-25T10:30:00.000Z";
164+
const result = await node.execute(
165+
createContext({
166+
date: baseDate,
167+
amount: 5,
168+
unit: "invalid-unit",
169+
})
170+
);
171+
172+
expect(result.status).toBe("completed");
173+
expect(result.outputs?.date).toBeUndefined();
174+
});
175+
176+
it("should handle string amount", async () => {
177+
const baseDate = "2023-12-25T10:30:00.000Z";
178+
const result = await node.execute(
179+
createContext({
180+
date: baseDate,
181+
amount: "5",
182+
unit: "days",
183+
})
184+
);
185+
186+
expect(result.status).toBe("completed");
187+
expect(result.outputs?.date).toBe("2023-12-30T10:30:00.000Z");
188+
});
189+
190+
it("should handle leap year correctly", async () => {
191+
const baseDate = "2024-02-28T10:30:00.000Z";
192+
const result = await node.execute(
193+
createContext({
194+
date: baseDate,
195+
amount: 1,
196+
unit: "days",
197+
})
198+
);
199+
200+
expect(result.status).toBe("completed");
201+
expect(result.outputs?.date).toBe("2024-02-29T10:30:00.000Z");
202+
});
203+
});
204+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { NodeExecution, NodeType } from "@dafthunk/types";
2+
3+
import { ExecutableNode } from "../types";
4+
import { NodeContext } from "../types";
5+
6+
type Unit =
7+
| "milliseconds"
8+
| "seconds"
9+
| "minutes"
10+
| "hours"
11+
| "days"
12+
| "weeks"
13+
| "months"
14+
| "years";
15+
16+
function addToDate(
17+
iso: string,
18+
amount: number,
19+
unit: Unit
20+
): string | undefined {
21+
const d = new Date(iso);
22+
if (isNaN(d.getTime())) return undefined;
23+
switch (unit) {
24+
case "milliseconds":
25+
d.setTime(d.getTime() + amount);
26+
break;
27+
case "seconds":
28+
d.setSeconds(d.getSeconds() + amount);
29+
break;
30+
case "minutes":
31+
d.setMinutes(d.getMinutes() + amount);
32+
break;
33+
case "hours":
34+
d.setHours(d.getHours() + amount);
35+
break;
36+
case "days":
37+
d.setDate(d.getDate() + amount);
38+
break;
39+
case "weeks":
40+
d.setDate(d.getDate() + amount * 7);
41+
break;
42+
case "months":
43+
d.setMonth(d.getMonth() + amount);
44+
break;
45+
case "years":
46+
d.setFullYear(d.getFullYear() + amount);
47+
break;
48+
default:
49+
return undefined;
50+
}
51+
return d.toISOString();
52+
}
53+
54+
export class AddDateNode extends ExecutableNode {
55+
public static readonly nodeType: NodeType = {
56+
id: "date-add",
57+
name: "Add to Date",
58+
type: "date-add",
59+
description: "Add an amount of time to a date",
60+
documentation: `Adds a time offset to an ISO date.
61+
62+
### Inputs
63+
- date (date): Base date in ISO
64+
- amount (number): Amount to add (negative to subtract)
65+
- unit (string): One of milliseconds, seconds, minutes, hours, days, weeks, months, years
66+
67+
### Outputs
68+
- date (date): Resulting ISO date`,
69+
tags: ["Date"],
70+
icon: "calendar",
71+
inlinable: true,
72+
asTool: true,
73+
inputs: [
74+
{
75+
name: "date",
76+
type: "date",
77+
description: "Base date (ISO-8601)",
78+
required: true,
79+
},
80+
{
81+
name: "amount",
82+
type: "number",
83+
description: "Amount to add (can be negative)",
84+
required: true,
85+
},
86+
{
87+
name: "unit",
88+
type: "string",
89+
description:
90+
"Unit to add: milliseconds, seconds, minutes, hours, days, weeks, months, years",
91+
required: true,
92+
},
93+
],
94+
outputs: [
95+
{ name: "date", type: "date", description: "Resulting date (ISO-8601)" },
96+
],
97+
};
98+
99+
async execute(context: NodeContext): Promise<NodeExecution> {
100+
try {
101+
const base = context.inputs.date as string;
102+
const amount = Number(context.inputs.amount);
103+
const unit = String(context.inputs.unit) as Unit;
104+
const iso = addToDate(base, amount, unit);
105+
return this.createSuccessResult({ date: iso });
106+
} catch (error) {
107+
return this.createErrorResult(
108+
error instanceof Error ? error.message : "Unknown error"
109+
);
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)