@q1k-oss/btree-workflows 0.0.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.
- package/.claude/settings.local.json +31 -0
- package/CLAUDE.md +181 -0
- package/LICENSE +21 -0
- package/README.md +920 -0
- package/behaviour-tree-workflows-landing/index.html +16 -0
- package/behaviour-tree-workflows-landing/package-lock.json +2074 -0
- package/behaviour-tree-workflows-landing/package.json +31 -0
- package/behaviour-tree-workflows-landing/public/favicon.svg +17 -0
- package/behaviour-tree-workflows-landing/src/App.css +103 -0
- package/behaviour-tree-workflows-landing/src/App.tsx +176 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.css +89 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.tsx +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.css +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.tsx +34 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.css +107 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.tsx +85 -0
- package/behaviour-tree-workflows-landing/src/components/Header.css +50 -0
- package/behaviour-tree-workflows-landing/src/components/Header.tsx +26 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.css +45 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.tsx +15 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.css +74 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.tsx +53 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.css +67 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.tsx +192 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.css +18 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.tsx +96 -0
- package/behaviour-tree-workflows-landing/src/lib/count-nodes.ts +11 -0
- package/behaviour-tree-workflows-landing/src/lib/execution-engine.ts +96 -0
- package/behaviour-tree-workflows-landing/src/lib/tree-layout.ts +136 -0
- package/behaviour-tree-workflows-landing/src/lib/yaml-examples.ts +549 -0
- package/behaviour-tree-workflows-landing/src/main.tsx +9 -0
- package/behaviour-tree-workflows-landing/src/stubs/activepieces.ts +18 -0
- package/behaviour-tree-workflows-landing/src/stubs/fs.ts +24 -0
- package/behaviour-tree-workflows-landing/src/stubs/path.ts +16 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-activity.ts +6 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-workflow.ts +22 -0
- package/behaviour-tree-workflows-landing/tsconfig.json +25 -0
- package/behaviour-tree-workflows-landing/vite.config.ts +40 -0
- package/demo-google-sheets.ts +181 -0
- package/demo-runtime-variables.ts +174 -0
- package/demo-template.ts +208 -0
- package/docs/ARCHITECTURE_SUMMARY.md +613 -0
- package/docs/NODE_REFERENCE.md +504 -0
- package/docs/README.md +53 -0
- package/docs/custom-nodes-architecture.md +826 -0
- package/docs/observability.md +175 -0
- package/docs/yaml-specification.md +990 -0
- package/examples/temporal/README.md +117 -0
- package/examples/temporal/activities.ts +373 -0
- package/examples/temporal/client.ts +115 -0
- package/examples/temporal/python-worker/activities.py +339 -0
- package/examples/temporal/python-worker/requirements.txt +12 -0
- package/examples/temporal/python-worker/worker.py +106 -0
- package/examples/temporal/worker.ts +66 -0
- package/examples/temporal/workflows.ts +6 -0
- package/examples/temporal/yaml-workflow-loader.ts +105 -0
- package/examples/yaml-test.ts +97 -0
- package/examples/yaml-workflows/01-simple-sequence.yaml +25 -0
- package/examples/yaml-workflows/02-parallel-timeout.yaml +45 -0
- package/examples/yaml-workflows/03-ecommerce-checkout.yaml +94 -0
- package/examples/yaml-workflows/04-ai-agent-workflow.yaml +346 -0
- package/examples/yaml-workflows/05-order-processing.yaml +146 -0
- package/examples/yaml-workflows/06-activity-test.yaml +71 -0
- package/examples/yaml-workflows/07-activity-simple-test.yaml +43 -0
- package/examples/yaml-workflows/08-file-processing.yaml +141 -0
- package/examples/yaml-workflows/09-http-request.yaml +137 -0
- package/examples/yaml-workflows/README.md +211 -0
- package/package.json +38 -0
- package/src/actions/code-execution.schema.ts +27 -0
- package/src/actions/code-execution.ts +218 -0
- package/src/actions/generate-file.test.ts +516 -0
- package/src/actions/generate-file.ts +166 -0
- package/src/actions/http-request.test.ts +784 -0
- package/src/actions/http-request.ts +228 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/parse-file.test.ts +448 -0
- package/src/actions/parse-file.ts +139 -0
- package/src/actions/python-script.test.ts +439 -0
- package/src/actions/python-script.ts +154 -0
- package/src/base-node.test.ts +511 -0
- package/src/base-node.ts +605 -0
- package/src/behavior-tree.test.ts +431 -0
- package/src/behavior-tree.ts +283 -0
- package/src/blackboard.test.ts +222 -0
- package/src/blackboard.ts +192 -0
- package/src/composites/conditional.schema.ts +19 -0
- package/src/composites/conditional.test.ts +309 -0
- package/src/composites/conditional.ts +129 -0
- package/src/composites/for-each.schema.ts +23 -0
- package/src/composites/for-each.test.ts +254 -0
- package/src/composites/for-each.ts +132 -0
- package/src/composites/index.ts +15 -0
- package/src/composites/memory-sequence.schema.ts +19 -0
- package/src/composites/memory-sequence.test.ts +223 -0
- package/src/composites/memory-sequence.ts +98 -0
- package/src/composites/parallel.schema.ts +28 -0
- package/src/composites/parallel.test.ts +502 -0
- package/src/composites/parallel.ts +157 -0
- package/src/composites/reactive-sequence.schema.ts +19 -0
- package/src/composites/reactive-sequence.test.ts +170 -0
- package/src/composites/reactive-sequence.ts +85 -0
- package/src/composites/recovery.schema.ts +19 -0
- package/src/composites/recovery.test.ts +366 -0
- package/src/composites/recovery.ts +90 -0
- package/src/composites/selector.schema.ts +19 -0
- package/src/composites/selector.test.ts +387 -0
- package/src/composites/selector.ts +85 -0
- package/src/composites/sequence.schema.ts +19 -0
- package/src/composites/sequence.test.ts +337 -0
- package/src/composites/sequence.ts +72 -0
- package/src/composites/sub-tree.schema.ts +21 -0
- package/src/composites/sub-tree.test.ts +893 -0
- package/src/composites/sub-tree.ts +177 -0
- package/src/composites/while.schema.ts +24 -0
- package/src/composites/while.test.ts +381 -0
- package/src/composites/while.ts +149 -0
- package/src/data-store/index.ts +10 -0
- package/src/data-store/memory-store.ts +161 -0
- package/src/data-store/types.ts +94 -0
- package/src/debug/breakpoint.test.ts +47 -0
- package/src/debug/breakpoint.ts +30 -0
- package/src/debug/index.ts +17 -0
- package/src/debug/resume-point.test.ts +49 -0
- package/src/debug/resume-point.ts +29 -0
- package/src/decorators/delay.schema.ts +21 -0
- package/src/decorators/delay.test.ts +261 -0
- package/src/decorators/delay.ts +140 -0
- package/src/decorators/force-result.schema.ts +32 -0
- package/src/decorators/force-result.test.ts +133 -0
- package/src/decorators/force-result.ts +63 -0
- package/src/decorators/index.ts +13 -0
- package/src/decorators/invert.schema.ts +19 -0
- package/src/decorators/invert.test.ts +135 -0
- package/src/decorators/invert.ts +42 -0
- package/src/decorators/keep-running.schema.ts +20 -0
- package/src/decorators/keep-running.test.ts +105 -0
- package/src/decorators/keep-running.ts +49 -0
- package/src/decorators/precondition.schema.ts +19 -0
- package/src/decorators/precondition.test.ts +351 -0
- package/src/decorators/precondition.ts +139 -0
- package/src/decorators/repeat.schema.ts +21 -0
- package/src/decorators/repeat.test.ts +187 -0
- package/src/decorators/repeat.ts +94 -0
- package/src/decorators/run-once.schema.ts +19 -0
- package/src/decorators/run-once.test.ts +140 -0
- package/src/decorators/run-once.ts +61 -0
- package/src/decorators/soft-assert.schema.ts +19 -0
- package/src/decorators/soft-assert.test.ts +107 -0
- package/src/decorators/soft-assert.ts +68 -0
- package/src/decorators/timeout.schema.ts +21 -0
- package/src/decorators/timeout.test.ts +274 -0
- package/src/decorators/timeout.ts +159 -0
- package/src/errors.test.ts +63 -0
- package/src/errors.ts +34 -0
- package/src/events.test.ts +347 -0
- package/src/events.ts +183 -0
- package/src/index.ts +80 -0
- package/src/integrations/index.ts +30 -0
- package/src/integrations/integration-action.test.ts +571 -0
- package/src/integrations/integration-action.ts +233 -0
- package/src/integrations/piece-executor.ts +320 -0
- package/src/observability/execution-tracker.ts +320 -0
- package/src/observability/index.ts +23 -0
- package/src/observability/sinks.ts +138 -0
- package/src/observability/types.ts +130 -0
- package/src/registry-utils.ts +147 -0
- package/src/registry.test.ts +466 -0
- package/src/registry.ts +334 -0
- package/src/schemas/base.schema.ts +104 -0
- package/src/schemas/index.ts +223 -0
- package/src/schemas/integration.test.ts +238 -0
- package/src/schemas/tree-definition.schema.ts +170 -0
- package/src/schemas/validation.test.ts +146 -0
- package/src/schemas/validation.ts +122 -0
- package/src/scripting/index.ts +22 -0
- package/src/templates/template-loader.test.ts +281 -0
- package/src/templates/template-loader.ts +152 -0
- package/src/temporal-integration.test.ts +213 -0
- package/src/test-nodes.ts +259 -0
- package/src/types.ts +503 -0
- package/src/utilities/index.ts +17 -0
- package/src/utilities/log-message.test.ts +275 -0
- package/src/utilities/log-message.ts +134 -0
- package/src/utilities/regex-extract.test.ts +138 -0
- package/src/utilities/regex-extract.ts +108 -0
- package/src/utilities/variable-resolver.test.ts +416 -0
- package/src/utilities/variable-resolver.ts +318 -0
- package/src/utils/error-handler.test.ts +117 -0
- package/src/utils/error-handler.ts +48 -0
- package/src/utils/signal-check.test.ts +234 -0
- package/src/utils/signal-check.ts +140 -0
- package/src/yaml/errors.ts +143 -0
- package/src/yaml/index.ts +30 -0
- package/src/yaml/loader.ts +39 -0
- package/src/yaml/parser.ts +286 -0
- package/src/yaml/validation/semantic-validator.ts +196 -0
- package/templates/google-sheets/insert-row.yaml +76 -0
- package/templates/notification-sender.yaml +33 -0
- package/templates/order-validation.yaml +44 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +25 -0
- package/workflows/order-processor.yaml +59 -0
- package/workflows/process-order-workflow.yaml +142 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenerateFile Node Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { ScopedBlackboard } from "../blackboard.js";
|
|
7
|
+
import { Registry } from "../registry.js";
|
|
8
|
+
import { type TemporalContext, type BtreeActivities, NodeStatus } from "../types.js";
|
|
9
|
+
import { GenerateFile, type GenerateFileConfig } from "./generate-file.js";
|
|
10
|
+
|
|
11
|
+
describe("GenerateFile Node", () => {
|
|
12
|
+
let blackboard: ScopedBlackboard;
|
|
13
|
+
let registry: Registry;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
blackboard = new ScopedBlackboard();
|
|
17
|
+
registry = new Registry();
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("Construction and validation", () => {
|
|
22
|
+
it("should create node with valid config", () => {
|
|
23
|
+
const node = new GenerateFile({
|
|
24
|
+
id: "test",
|
|
25
|
+
format: "csv",
|
|
26
|
+
dataKey: "data",
|
|
27
|
+
filename: "export.csv",
|
|
28
|
+
storage: "temp",
|
|
29
|
+
outputKey: "fileResult",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(node).toBeDefined();
|
|
33
|
+
expect(node.id).toBe("test");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should require format", () => {
|
|
37
|
+
expect(() => {
|
|
38
|
+
new GenerateFile({
|
|
39
|
+
id: "test",
|
|
40
|
+
dataKey: "data",
|
|
41
|
+
filename: "export.csv",
|
|
42
|
+
storage: "temp",
|
|
43
|
+
outputKey: "fileResult",
|
|
44
|
+
} as GenerateFileConfig);
|
|
45
|
+
}).toThrow(/requires format/i);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should require dataKey", () => {
|
|
49
|
+
expect(() => {
|
|
50
|
+
new GenerateFile({
|
|
51
|
+
id: "test",
|
|
52
|
+
format: "csv",
|
|
53
|
+
filename: "export.csv",
|
|
54
|
+
storage: "temp",
|
|
55
|
+
outputKey: "fileResult",
|
|
56
|
+
} as GenerateFileConfig);
|
|
57
|
+
}).toThrow(/requires dataKey/i);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should require filename", () => {
|
|
61
|
+
expect(() => {
|
|
62
|
+
new GenerateFile({
|
|
63
|
+
id: "test",
|
|
64
|
+
format: "csv",
|
|
65
|
+
dataKey: "data",
|
|
66
|
+
storage: "temp",
|
|
67
|
+
outputKey: "fileResult",
|
|
68
|
+
} as GenerateFileConfig);
|
|
69
|
+
}).toThrow(/requires filename/i);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should require storage", () => {
|
|
73
|
+
expect(() => {
|
|
74
|
+
new GenerateFile({
|
|
75
|
+
id: "test",
|
|
76
|
+
format: "csv",
|
|
77
|
+
dataKey: "data",
|
|
78
|
+
filename: "export.csv",
|
|
79
|
+
outputKey: "fileResult",
|
|
80
|
+
} as GenerateFileConfig);
|
|
81
|
+
}).toThrow(/requires storage/i);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should require outputKey", () => {
|
|
85
|
+
expect(() => {
|
|
86
|
+
new GenerateFile({
|
|
87
|
+
id: "test",
|
|
88
|
+
format: "csv",
|
|
89
|
+
dataKey: "data",
|
|
90
|
+
filename: "export.csv",
|
|
91
|
+
storage: "temp",
|
|
92
|
+
} as GenerateFileConfig);
|
|
93
|
+
}).toThrow(/requires outputKey/i);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("Activity requirement", () => {
|
|
98
|
+
it("should fail without generateFile activity", async () => {
|
|
99
|
+
blackboard.set("data", [{ a: 1 }]);
|
|
100
|
+
|
|
101
|
+
const context: TemporalContext = {
|
|
102
|
+
blackboard,
|
|
103
|
+
treeRegistry: registry,
|
|
104
|
+
timestamp: Date.now(),
|
|
105
|
+
deltaTime: 0,
|
|
106
|
+
activities: undefined,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const node = new GenerateFile({
|
|
110
|
+
id: "test",
|
|
111
|
+
format: "csv",
|
|
112
|
+
dataKey: "data",
|
|
113
|
+
filename: "export.csv",
|
|
114
|
+
storage: "temp",
|
|
115
|
+
outputKey: "fileResult",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const status = await node.tick(context);
|
|
119
|
+
|
|
120
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
121
|
+
expect(node.lastError).toContain("requires activities.generateFile");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should fail when activities object exists but generateFile is missing", async () => {
|
|
125
|
+
blackboard.set("data", [{ a: 1 }]);
|
|
126
|
+
|
|
127
|
+
const context: TemporalContext = {
|
|
128
|
+
blackboard,
|
|
129
|
+
treeRegistry: registry,
|
|
130
|
+
timestamp: Date.now(),
|
|
131
|
+
deltaTime: 0,
|
|
132
|
+
activities: {
|
|
133
|
+
executePieceAction: vi.fn(),
|
|
134
|
+
// generateFile is not provided
|
|
135
|
+
} as BtreeActivities,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const node = new GenerateFile({
|
|
139
|
+
id: "test",
|
|
140
|
+
format: "csv",
|
|
141
|
+
dataKey: "data",
|
|
142
|
+
filename: "export.csv",
|
|
143
|
+
storage: "temp",
|
|
144
|
+
outputKey: "fileResult",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const status = await node.tick(context);
|
|
148
|
+
|
|
149
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
150
|
+
expect(node.lastError).toContain("requires activities.generateFile");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("Data validation", () => {
|
|
155
|
+
it("should fail if data is not an array", async () => {
|
|
156
|
+
blackboard.set("data", { notAnArray: true });
|
|
157
|
+
|
|
158
|
+
const mockGenerateActivity = vi.fn();
|
|
159
|
+
|
|
160
|
+
const context: TemporalContext = {
|
|
161
|
+
blackboard,
|
|
162
|
+
treeRegistry: registry,
|
|
163
|
+
timestamp: Date.now(),
|
|
164
|
+
deltaTime: 0,
|
|
165
|
+
activities: {
|
|
166
|
+
executePieceAction: vi.fn(),
|
|
167
|
+
generateFile: mockGenerateActivity,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const node = new GenerateFile({
|
|
172
|
+
id: "test",
|
|
173
|
+
format: "csv",
|
|
174
|
+
dataKey: "data",
|
|
175
|
+
filename: "export.csv",
|
|
176
|
+
storage: "temp",
|
|
177
|
+
outputKey: "fileResult",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const status = await node.tick(context);
|
|
181
|
+
|
|
182
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
183
|
+
expect(node.lastError).toContain("is not an array");
|
|
184
|
+
expect(mockGenerateActivity).not.toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should fail if data key does not exist", async () => {
|
|
188
|
+
// Don't set anything in blackboard
|
|
189
|
+
|
|
190
|
+
const mockGenerateActivity = vi.fn();
|
|
191
|
+
|
|
192
|
+
const context: TemporalContext = {
|
|
193
|
+
blackboard,
|
|
194
|
+
treeRegistry: registry,
|
|
195
|
+
timestamp: Date.now(),
|
|
196
|
+
deltaTime: 0,
|
|
197
|
+
activities: {
|
|
198
|
+
executePieceAction: vi.fn(),
|
|
199
|
+
generateFile: mockGenerateActivity,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const node = new GenerateFile({
|
|
204
|
+
id: "test",
|
|
205
|
+
format: "csv",
|
|
206
|
+
dataKey: "missingData",
|
|
207
|
+
filename: "export.csv",
|
|
208
|
+
storage: "temp",
|
|
209
|
+
outputKey: "fileResult",
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const status = await node.tick(context);
|
|
213
|
+
|
|
214
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
215
|
+
expect(node.lastError).toContain("is not an array");
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("Execution with activity", () => {
|
|
220
|
+
it("should generate CSV file via activity", async () => {
|
|
221
|
+
blackboard.set("orders", [
|
|
222
|
+
{ orderId: "1", amount: 100 },
|
|
223
|
+
{ orderId: "2", amount: 200 },
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
const mockGenerateActivity = vi.fn().mockResolvedValue({
|
|
227
|
+
filename: "export.csv",
|
|
228
|
+
contentType: "text/csv",
|
|
229
|
+
size: 1024,
|
|
230
|
+
path: "/tmp/export.csv",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const context: TemporalContext = {
|
|
234
|
+
blackboard,
|
|
235
|
+
treeRegistry: registry,
|
|
236
|
+
timestamp: Date.now(),
|
|
237
|
+
deltaTime: 0,
|
|
238
|
+
activities: {
|
|
239
|
+
executePieceAction: vi.fn(),
|
|
240
|
+
generateFile: mockGenerateActivity,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const node = new GenerateFile({
|
|
245
|
+
id: "test",
|
|
246
|
+
format: "csv",
|
|
247
|
+
dataKey: "orders",
|
|
248
|
+
filename: "export.csv",
|
|
249
|
+
storage: "temp",
|
|
250
|
+
outputKey: "fileResult",
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const status = await node.tick(context);
|
|
254
|
+
|
|
255
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
256
|
+
expect(mockGenerateActivity).toHaveBeenCalledWith(
|
|
257
|
+
expect.objectContaining({
|
|
258
|
+
format: "csv",
|
|
259
|
+
data: [
|
|
260
|
+
{ orderId: "1", amount: 100 },
|
|
261
|
+
{ orderId: "2", amount: 200 },
|
|
262
|
+
],
|
|
263
|
+
filename: "export.csv",
|
|
264
|
+
storage: "temp",
|
|
265
|
+
})
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should store file metadata in blackboard", async () => {
|
|
270
|
+
blackboard.set("data", [{ a: 1 }]);
|
|
271
|
+
|
|
272
|
+
const fileMetadata = {
|
|
273
|
+
filename: "export-123.csv",
|
|
274
|
+
contentType: "text/csv",
|
|
275
|
+
size: 2048,
|
|
276
|
+
path: "/storage/exports/export-123.csv",
|
|
277
|
+
url: "https://storage.example.com/exports/export-123.csv",
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const mockGenerateActivity = vi.fn().mockResolvedValue(fileMetadata);
|
|
281
|
+
|
|
282
|
+
const context: TemporalContext = {
|
|
283
|
+
blackboard,
|
|
284
|
+
treeRegistry: registry,
|
|
285
|
+
timestamp: Date.now(),
|
|
286
|
+
deltaTime: 0,
|
|
287
|
+
activities: {
|
|
288
|
+
executePieceAction: vi.fn(),
|
|
289
|
+
generateFile: mockGenerateActivity,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const node = new GenerateFile({
|
|
294
|
+
id: "test",
|
|
295
|
+
format: "csv",
|
|
296
|
+
dataKey: "data",
|
|
297
|
+
filename: "export.csv",
|
|
298
|
+
storage: "persistent",
|
|
299
|
+
outputKey: "exportedFile",
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await node.tick(context);
|
|
303
|
+
|
|
304
|
+
expect(blackboard.get("exportedFile")).toEqual(fileMetadata);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("should resolve filename from variables", async () => {
|
|
308
|
+
blackboard.set("data", [{ a: 1 }]);
|
|
309
|
+
blackboard.set("timestamp", "2024-01-15");
|
|
310
|
+
|
|
311
|
+
const mockGenerateActivity = vi.fn().mockResolvedValue({
|
|
312
|
+
filename: "report-2024-01-15.csv",
|
|
313
|
+
contentType: "text/csv",
|
|
314
|
+
size: 512,
|
|
315
|
+
path: "/tmp/report-2024-01-15.csv",
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const context: TemporalContext = {
|
|
319
|
+
blackboard,
|
|
320
|
+
treeRegistry: registry,
|
|
321
|
+
timestamp: Date.now(),
|
|
322
|
+
deltaTime: 0,
|
|
323
|
+
input: { reportType: "sales" },
|
|
324
|
+
activities: {
|
|
325
|
+
executePieceAction: vi.fn(),
|
|
326
|
+
generateFile: mockGenerateActivity,
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const node = new GenerateFile({
|
|
331
|
+
id: "test",
|
|
332
|
+
format: "csv",
|
|
333
|
+
dataKey: "data",
|
|
334
|
+
filename: "${input.reportType}-${bb.timestamp}.csv",
|
|
335
|
+
storage: "temp",
|
|
336
|
+
outputKey: "fileResult",
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
await node.tick(context);
|
|
340
|
+
|
|
341
|
+
expect(mockGenerateActivity).toHaveBeenCalledWith(
|
|
342
|
+
expect.objectContaining({
|
|
343
|
+
filename: "sales-2024-01-15.csv",
|
|
344
|
+
})
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("should pass column definitions to activity", async () => {
|
|
349
|
+
blackboard.set("orders", [
|
|
350
|
+
{ id: "1", customer: "Alice", total: 100 },
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
const mockGenerateActivity = vi.fn().mockResolvedValue({
|
|
354
|
+
filename: "orders.xlsx",
|
|
355
|
+
contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
356
|
+
size: 4096,
|
|
357
|
+
path: "/tmp/orders.xlsx",
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const context: TemporalContext = {
|
|
361
|
+
blackboard,
|
|
362
|
+
treeRegistry: registry,
|
|
363
|
+
timestamp: Date.now(),
|
|
364
|
+
deltaTime: 0,
|
|
365
|
+
activities: {
|
|
366
|
+
executePieceAction: vi.fn(),
|
|
367
|
+
generateFile: mockGenerateActivity,
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const node = new GenerateFile({
|
|
372
|
+
id: "test",
|
|
373
|
+
format: "xlsx",
|
|
374
|
+
dataKey: "orders",
|
|
375
|
+
columns: [
|
|
376
|
+
{ header: "Order ID", key: "id", width: 10 },
|
|
377
|
+
{ header: "Customer Name", key: "customer", width: 25 },
|
|
378
|
+
{ header: "Total Amount", key: "total", width: 15 },
|
|
379
|
+
],
|
|
380
|
+
filename: "orders.xlsx",
|
|
381
|
+
storage: "temp",
|
|
382
|
+
outputKey: "fileResult",
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await node.tick(context);
|
|
386
|
+
|
|
387
|
+
expect(mockGenerateActivity).toHaveBeenCalledWith(
|
|
388
|
+
expect.objectContaining({
|
|
389
|
+
format: "xlsx",
|
|
390
|
+
columns: [
|
|
391
|
+
{ header: "Order ID", key: "id", width: 10 },
|
|
392
|
+
{ header: "Customer Name", key: "customer", width: 25 },
|
|
393
|
+
{ header: "Total Amount", key: "total", width: 15 },
|
|
394
|
+
],
|
|
395
|
+
})
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("should generate JSON file", async () => {
|
|
400
|
+
blackboard.set("data", [{ key: "value" }]);
|
|
401
|
+
|
|
402
|
+
const mockGenerateActivity = vi.fn().mockResolvedValue({
|
|
403
|
+
filename: "data.json",
|
|
404
|
+
contentType: "application/json",
|
|
405
|
+
size: 256,
|
|
406
|
+
path: "/tmp/data.json",
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const context: TemporalContext = {
|
|
410
|
+
blackboard,
|
|
411
|
+
treeRegistry: registry,
|
|
412
|
+
timestamp: Date.now(),
|
|
413
|
+
deltaTime: 0,
|
|
414
|
+
activities: {
|
|
415
|
+
executePieceAction: vi.fn(),
|
|
416
|
+
generateFile: mockGenerateActivity,
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const node = new GenerateFile({
|
|
421
|
+
id: "test",
|
|
422
|
+
format: "json",
|
|
423
|
+
dataKey: "data",
|
|
424
|
+
filename: "data.json",
|
|
425
|
+
storage: "temp",
|
|
426
|
+
outputKey: "fileResult",
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
await node.tick(context);
|
|
430
|
+
|
|
431
|
+
expect(mockGenerateActivity).toHaveBeenCalledWith(
|
|
432
|
+
expect.objectContaining({
|
|
433
|
+
format: "json",
|
|
434
|
+
})
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("should handle activity errors", async () => {
|
|
439
|
+
blackboard.set("data", [{ a: 1 }]);
|
|
440
|
+
|
|
441
|
+
const mockGenerateActivity = vi.fn().mockRejectedValue(
|
|
442
|
+
new Error("Disk full: cannot write file")
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const context: TemporalContext = {
|
|
446
|
+
blackboard,
|
|
447
|
+
treeRegistry: registry,
|
|
448
|
+
timestamp: Date.now(),
|
|
449
|
+
deltaTime: 0,
|
|
450
|
+
activities: {
|
|
451
|
+
executePieceAction: vi.fn(),
|
|
452
|
+
generateFile: mockGenerateActivity,
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const node = new GenerateFile({
|
|
457
|
+
id: "test",
|
|
458
|
+
format: "csv",
|
|
459
|
+
dataKey: "data",
|
|
460
|
+
filename: "export.csv",
|
|
461
|
+
storage: "temp",
|
|
462
|
+
outputKey: "fileResult",
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const status = await node.tick(context);
|
|
466
|
+
|
|
467
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
468
|
+
expect(node.lastError).toContain("Disk full");
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe("Node lifecycle", () => {
|
|
473
|
+
it("should clone correctly", () => {
|
|
474
|
+
const node = new GenerateFile({
|
|
475
|
+
id: "original",
|
|
476
|
+
format: "csv",
|
|
477
|
+
dataKey: "data",
|
|
478
|
+
filename: "export.csv",
|
|
479
|
+
storage: "persistent",
|
|
480
|
+
outputKey: "fileResult",
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const cloned = node.clone() as GenerateFile;
|
|
484
|
+
|
|
485
|
+
expect(cloned.id).toBe("original");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("should reset status correctly", async () => {
|
|
489
|
+
const context: TemporalContext = {
|
|
490
|
+
blackboard,
|
|
491
|
+
treeRegistry: registry,
|
|
492
|
+
timestamp: Date.now(),
|
|
493
|
+
deltaTime: 0,
|
|
494
|
+
// No activities - will fail
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
blackboard.set("data", [{ a: 1 }]);
|
|
498
|
+
|
|
499
|
+
const node = new GenerateFile({
|
|
500
|
+
id: "test",
|
|
501
|
+
format: "csv",
|
|
502
|
+
dataKey: "data",
|
|
503
|
+
filename: "export.csv",
|
|
504
|
+
storage: "temp",
|
|
505
|
+
outputKey: "fileResult",
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
await node.tick(context);
|
|
509
|
+
expect(node.status()).toBe(NodeStatus.FAILURE);
|
|
510
|
+
|
|
511
|
+
node.reset();
|
|
512
|
+
expect(node.status()).toBe(NodeStatus.IDLE);
|
|
513
|
+
expect(node.lastError).toBeUndefined();
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenerateFile Node
|
|
3
|
+
*
|
|
4
|
+
* Generates CSV/Excel/JSON files from data via a Temporal activity.
|
|
5
|
+
* This node requires the `generateFile` activity to be configured in the context -
|
|
6
|
+
* it does not support standalone/inline execution because file I/O requires
|
|
7
|
+
* capabilities outside the workflow sandbox.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - CSV, Excel (xlsx), and JSON output formats
|
|
11
|
+
* - Column definitions with header names and widths
|
|
12
|
+
* - Temporary or persistent storage
|
|
13
|
+
* - Result metadata stored in blackboard (filename, path, URL)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { ActionNode } from "../base-node.js";
|
|
17
|
+
import { ConfigurationError } from "../errors.js";
|
|
18
|
+
import {
|
|
19
|
+
type TemporalContext,
|
|
20
|
+
type NodeConfiguration,
|
|
21
|
+
type GenerateFileRequest,
|
|
22
|
+
NodeStatus,
|
|
23
|
+
} from "../types.js";
|
|
24
|
+
import { resolveValue, type VariableContext } from "../utilities/variable-resolver.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration for GenerateFile node
|
|
28
|
+
*/
|
|
29
|
+
export interface GenerateFileConfig extends NodeConfiguration {
|
|
30
|
+
/** Output format */
|
|
31
|
+
format: "csv" | "xlsx" | "json";
|
|
32
|
+
/** Data source (blackboard key) */
|
|
33
|
+
dataKey: string;
|
|
34
|
+
/** Column definitions */
|
|
35
|
+
columns?: Array<{ header: string; key: string; width?: number }>;
|
|
36
|
+
/** Output filename template (supports ${input.x}, ${bb.x}) */
|
|
37
|
+
filename: string;
|
|
38
|
+
/** Storage type */
|
|
39
|
+
storage: "temp" | "persistent";
|
|
40
|
+
/** Output key for file metadata (path, url, size) */
|
|
41
|
+
outputKey: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* GenerateFile Node
|
|
46
|
+
*
|
|
47
|
+
* Generates a file from data in the blackboard and stores file metadata.
|
|
48
|
+
* Requires the `generateFile` activity to be configured.
|
|
49
|
+
*
|
|
50
|
+
* @example YAML
|
|
51
|
+
* ```yaml
|
|
52
|
+
* type: GenerateFile
|
|
53
|
+
* id: export-report
|
|
54
|
+
* props:
|
|
55
|
+
* format: csv
|
|
56
|
+
* dataKey: "processedOrders"
|
|
57
|
+
* columns:
|
|
58
|
+
* - header: "Order ID"
|
|
59
|
+
* key: "orderId"
|
|
60
|
+
* - header: "Customer"
|
|
61
|
+
* key: "customerName"
|
|
62
|
+
* - header: "Total"
|
|
63
|
+
* key: "amount"
|
|
64
|
+
* width: 15
|
|
65
|
+
* filename: "orders-${bb.timestamp}.csv"
|
|
66
|
+
* storage: persistent
|
|
67
|
+
* outputKey: "exportedFile"
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export class GenerateFile extends ActionNode {
|
|
71
|
+
private format: GenerateFileConfig["format"];
|
|
72
|
+
private dataKey: string;
|
|
73
|
+
private columns?: GenerateFileConfig["columns"];
|
|
74
|
+
private filename: string;
|
|
75
|
+
private storage: GenerateFileConfig["storage"];
|
|
76
|
+
private outputKey: string;
|
|
77
|
+
|
|
78
|
+
constructor(config: GenerateFileConfig) {
|
|
79
|
+
super(config);
|
|
80
|
+
|
|
81
|
+
if (!config.format) {
|
|
82
|
+
throw new ConfigurationError("GenerateFile requires format");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!config.dataKey) {
|
|
86
|
+
throw new ConfigurationError("GenerateFile requires dataKey");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!config.filename) {
|
|
90
|
+
throw new ConfigurationError("GenerateFile requires filename");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!config.storage) {
|
|
94
|
+
throw new ConfigurationError("GenerateFile requires storage");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!config.outputKey) {
|
|
98
|
+
throw new ConfigurationError("GenerateFile requires outputKey");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.format = config.format;
|
|
102
|
+
this.dataKey = config.dataKey;
|
|
103
|
+
this.columns = config.columns;
|
|
104
|
+
this.filename = config.filename;
|
|
105
|
+
this.storage = config.storage;
|
|
106
|
+
this.outputKey = config.outputKey;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
protected async executeTick(context: TemporalContext): Promise<NodeStatus> {
|
|
110
|
+
// Validate activity is available
|
|
111
|
+
if (!context.activities?.generateFile) {
|
|
112
|
+
this._lastError =
|
|
113
|
+
"GenerateFile requires activities.generateFile to be configured. " +
|
|
114
|
+
"This activity handles file I/O outside the workflow sandbox.";
|
|
115
|
+
this.log(`Error: ${this._lastError}`);
|
|
116
|
+
return NodeStatus.FAILURE;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const varCtx: VariableContext = {
|
|
121
|
+
blackboard: context.blackboard,
|
|
122
|
+
input: context.input,
|
|
123
|
+
testData: context.testData,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Get data from blackboard
|
|
127
|
+
const data = context.blackboard.get(this.dataKey);
|
|
128
|
+
if (!Array.isArray(data)) {
|
|
129
|
+
this._lastError = `Data at '${this.dataKey}' is not an array (got ${typeof data})`;
|
|
130
|
+
this.log(`Error: ${this._lastError}`);
|
|
131
|
+
return NodeStatus.FAILURE;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const resolvedFilename = resolveValue(this.filename, varCtx) as string;
|
|
135
|
+
|
|
136
|
+
const request: GenerateFileRequest = {
|
|
137
|
+
format: this.format,
|
|
138
|
+
data: data as Record<string, unknown>[],
|
|
139
|
+
columns: this.columns,
|
|
140
|
+
filename: resolvedFilename,
|
|
141
|
+
storage: this.storage,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
this.log(
|
|
145
|
+
`Generating ${this.format} file: ${resolvedFilename} ` +
|
|
146
|
+
`(${data.length} rows, storage: ${this.storage})`
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const result = await context.activities.generateFile(request);
|
|
150
|
+
|
|
151
|
+
// Store file metadata in blackboard
|
|
152
|
+
context.blackboard.set(this.outputKey, result);
|
|
153
|
+
|
|
154
|
+
this.log(
|
|
155
|
+
`Generated file: ${result.filename} ` +
|
|
156
|
+
`(${result.size} bytes, path: ${result.path})`
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return NodeStatus.SUCCESS;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
this._lastError = error instanceof Error ? error.message : String(error);
|
|
162
|
+
this.log(`Generate file failed: ${this._lastError}`);
|
|
163
|
+
return NodeStatus.FAILURE;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|