@pikku/inspector 0.11.1 → 0.11.2

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/add/add-forge-credential.d.ts +8 -0
  3. package/dist/add/add-forge-credential.js +77 -0
  4. package/dist/add/add-forge-node.d.ts +7 -0
  5. package/dist/add/add-forge-node.js +77 -0
  6. package/dist/add/add-functions.js +102 -9
  7. package/dist/add/add-http-route.js +24 -1
  8. package/dist/add/add-rpc-invocations.d.ts +3 -0
  9. package/dist/add/add-rpc-invocations.js +51 -25
  10. package/dist/add/add-workflow-graph.d.ts +6 -0
  11. package/dist/add/add-workflow-graph.js +659 -0
  12. package/dist/add/add-workflow.js +118 -22
  13. package/dist/error-codes.d.ts +3 -1
  14. package/dist/error-codes.js +3 -1
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.js +2 -0
  17. package/dist/inspector.js +19 -3
  18. package/dist/types.d.ts +26 -0
  19. package/dist/utils/extract-function-name.js +7 -7
  20. package/dist/utils/get-property-value.d.ts +2 -1
  21. package/dist/utils/get-property-value.js +6 -2
  22. package/dist/utils/serialize-inspector-state.d.ts +24 -1
  23. package/dist/utils/serialize-inspector-state.js +24 -0
  24. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
  25. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +898 -0
  26. package/dist/{workflow/extract-simple-workflow.d.ts → utils/workflow/dsl/extract-dsl-workflow.d.ts} +4 -2
  27. package/dist/{workflow/extract-simple-workflow.js → utils/workflow/dsl/extract-dsl-workflow.js} +549 -68
  28. package/dist/utils/workflow/dsl/index.d.ts +7 -0
  29. package/dist/utils/workflow/dsl/index.js +7 -0
  30. package/dist/{workflow → utils/workflow/dsl}/patterns.d.ts +21 -0
  31. package/dist/{workflow → utils/workflow/dsl}/patterns.js +90 -10
  32. package/dist/{workflow → utils/workflow/dsl}/validation.d.ts +2 -0
  33. package/dist/{workflow → utils/workflow/dsl}/validation.js +25 -7
  34. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
  35. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +316 -0
  36. package/dist/utils/workflow/graph/index.d.ts +6 -0
  37. package/dist/utils/workflow/graph/index.js +6 -0
  38. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +43 -0
  39. package/dist/utils/workflow/graph/serialize-workflow-graph.js +152 -0
  40. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +229 -0
  41. package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
  42. package/dist/visit.js +6 -0
  43. package/package.json +14 -2
  44. package/src/add/add-forge-credential.ts +119 -0
  45. package/src/add/add-forge-node.ts +132 -0
  46. package/src/add/add-functions.ts +129 -15
  47. package/src/add/add-http-route.ts +25 -1
  48. package/src/add/add-rpc-invocations.ts +61 -31
  49. package/src/add/add-workflow-graph.ts +864 -0
  50. package/src/add/add-workflow.ts +112 -26
  51. package/src/error-codes.ts +3 -1
  52. package/src/index.ts +10 -0
  53. package/src/inspector.ts +20 -4
  54. package/src/types.ts +25 -1
  55. package/src/utils/extract-function-name.ts +7 -7
  56. package/src/utils/get-property-value.ts +9 -2
  57. package/src/utils/serialize-inspector-state.ts +39 -1
  58. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1180 -0
  59. package/src/{workflow/extract-simple-workflow.ts → utils/workflow/dsl/extract-dsl-workflow.ts} +654 -81
  60. package/src/utils/workflow/dsl/index.ts +11 -0
  61. package/src/{workflow → utils/workflow/dsl}/patterns.ts +108 -11
  62. package/src/{workflow → utils/workflow/dsl}/validation.ts +34 -7
  63. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +415 -0
  64. package/src/utils/workflow/graph/index.ts +6 -0
  65. package/src/utils/workflow/graph/serialize-workflow-graph.ts +223 -0
  66. package/src/utils/workflow/graph/workflow-graph.types.ts +280 -0
  67. package/src/visit.ts +6 -0
  68. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,7 @@
1
+ /**
2
+ * DSL (Domain Specific Language) workflow extraction exports
3
+ */
4
+ export { extractDSLWorkflow } from './extract-dsl-workflow.js';
5
+ export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './deserialize-dsl-workflow.js';
6
+ export * from './patterns.js';
7
+ export * from './validation.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * DSL (Domain Specific Language) workflow extraction exports
3
+ */
4
+ export { extractDSLWorkflow } from './extract-dsl-workflow.js';
5
+ export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './deserialize-dsl-workflow.js';
6
+ export * from './patterns.js';
7
+ export * from './validation.js';
@@ -10,6 +10,27 @@ export declare function isWorkflowDoCall(node: ts.CallExpression, checker: ts.Ty
10
10
  * Check if a call expression is workflow.sleep()
11
11
  */
12
12
  export declare function isWorkflowSleepCall(node: ts.CallExpression, checker: ts.TypeChecker): boolean;
13
+ /**
14
+ * Check if a throw statement throws WorkflowCancelledException
15
+ * Matches: throw new WorkflowCancelledException(...) or throw WorkflowCancelledException(...)
16
+ */
17
+ export declare function isThrowCancelException(node: ts.ThrowStatement): boolean;
18
+ /**
19
+ * Extract the reason string from a throw WorkflowCancelledException statement
20
+ */
21
+ export declare function extractCancelReason(node: ts.ThrowStatement, checker: ts.TypeChecker): string | undefined;
22
+ /**
23
+ * Check if a call expression is array.filter()
24
+ */
25
+ export declare function isArrayFilter(node: ts.CallExpression): boolean;
26
+ /**
27
+ * Check if a call expression is array.some()
28
+ */
29
+ export declare function isArraySome(node: ts.CallExpression): boolean;
30
+ /**
31
+ * Check if a call expression is array.every()
32
+ */
33
+ export declare function isArrayEvery(node: ts.CallExpression): boolean;
13
34
  /**
14
35
  * Check if an expression is Promise.all(array.map(...))
15
36
  */
@@ -26,6 +26,79 @@ export function isWorkflowSleepCall(node, checker) {
26
26
  ts.isIdentifier(propAccess.expression) &&
27
27
  propAccess.expression.text === 'workflow');
28
28
  }
29
+ /**
30
+ * Check if a throw statement throws WorkflowCancelledException
31
+ * Matches: throw new WorkflowCancelledException(...) or throw WorkflowCancelledException(...)
32
+ */
33
+ export function isThrowCancelException(node) {
34
+ const expr = node.expression;
35
+ if (!expr)
36
+ return false;
37
+ // Check for: throw new WorkflowCancelledException(...)
38
+ if (ts.isNewExpression(expr)) {
39
+ if (ts.isIdentifier(expr.expression)) {
40
+ return expr.expression.text === 'WorkflowCancelledException';
41
+ }
42
+ }
43
+ // Check for: throw WorkflowCancelledException(...) - function call style
44
+ if (ts.isCallExpression(expr)) {
45
+ if (ts.isIdentifier(expr.expression)) {
46
+ return expr.expression.text === 'WorkflowCancelledException';
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+ /**
52
+ * Extract the reason string from a throw WorkflowCancelledException statement
53
+ */
54
+ export function extractCancelReason(node, checker) {
55
+ const expr = node.expression;
56
+ if (!expr)
57
+ return undefined;
58
+ let args;
59
+ if (ts.isNewExpression(expr) && expr.arguments) {
60
+ args = expr.arguments;
61
+ }
62
+ else if (ts.isCallExpression(expr)) {
63
+ args = expr.arguments;
64
+ }
65
+ if (args && args.length > 0) {
66
+ const firstArg = args[0];
67
+ if (ts.isStringLiteral(firstArg)) {
68
+ return firstArg.text;
69
+ }
70
+ // For template literals or other expressions, return the source text
71
+ return firstArg.getText();
72
+ }
73
+ return undefined;
74
+ }
75
+ /**
76
+ * Check if a call expression is array.filter()
77
+ */
78
+ export function isArrayFilter(node) {
79
+ if (!ts.isPropertyAccessExpression(node.expression)) {
80
+ return false;
81
+ }
82
+ return node.expression.name.text === 'filter';
83
+ }
84
+ /**
85
+ * Check if a call expression is array.some()
86
+ */
87
+ export function isArraySome(node) {
88
+ if (!ts.isPropertyAccessExpression(node.expression)) {
89
+ return false;
90
+ }
91
+ return node.expression.name.text === 'some';
92
+ }
93
+ /**
94
+ * Check if a call expression is array.every()
95
+ */
96
+ export function isArrayEvery(node) {
97
+ if (!ts.isPropertyAccessExpression(node.expression)) {
98
+ return false;
99
+ }
100
+ return node.expression.name.text === 'every';
101
+ }
29
102
  /**
30
103
  * Check if an expression is Promise.all(array.map(...))
31
104
  */
@@ -86,6 +159,21 @@ export function isSequentialFanout(node) {
86
159
  }
87
160
  return true;
88
161
  }
162
+ /**
163
+ * Extract full source path from an expression (e.g., data.memberEmails)
164
+ */
165
+ function extractSourcePath(expr) {
166
+ if (ts.isIdentifier(expr)) {
167
+ return expr.text;
168
+ }
169
+ if (ts.isPropertyAccessExpression(expr)) {
170
+ const base = extractSourcePath(expr.expression);
171
+ if (base) {
172
+ return `${base}.${expr.name.text}`;
173
+ }
174
+ }
175
+ return null;
176
+ }
89
177
  /**
90
178
  * Extract the variable name from a for..of statement
91
179
  */
@@ -98,16 +186,8 @@ export function extractForOfVariable(node) {
98
186
  return null;
99
187
  }
100
188
  const itemVar = decl.name.text;
101
- // Extract source variable
102
- let sourceVar = null;
103
- if (ts.isIdentifier(node.expression)) {
104
- sourceVar = node.expression.text;
105
- }
106
- else if (ts.isPropertyAccessExpression(node.expression) &&
107
- ts.isIdentifier(node.expression.expression)) {
108
- // Handle data.memberEmails
109
- sourceVar = node.expression.expression.text;
110
- }
189
+ // Extract source variable with full path (e.g., data.memberEmails)
190
+ const sourceVar = extractSourcePath(node.expression);
111
191
  if (!sourceVar) {
112
192
  return null;
113
193
  }
@@ -13,8 +13,10 @@ export interface ValidationError {
13
13
  * - VariableStatement (const/let declarations)
14
14
  * - ExpressionStatement (await workflow.do, await workflow.sleep, await Promise.all)
15
15
  * - IfStatement (branches)
16
+ * - SwitchStatement (switch/case)
16
17
  * - ForOfStatement (sequential fanout)
17
18
  * - ReturnStatement
19
+ * - ThrowStatement (for WorkflowCancelledException)
18
20
  * - Block (containers)
19
21
  */
20
22
  export declare function validateNoDisallowedPatterns(node: ts.Node): ValidationError[];
@@ -6,8 +6,10 @@ import * as ts from 'typescript';
6
6
  * - VariableStatement (const/let declarations)
7
7
  * - ExpressionStatement (await workflow.do, await workflow.sleep, await Promise.all)
8
8
  * - IfStatement (branches)
9
+ * - SwitchStatement (switch/case)
9
10
  * - ForOfStatement (sequential fanout)
10
11
  * - ReturnStatement
12
+ * - ThrowStatement (for WorkflowCancelledException)
11
13
  * - Block (containers)
12
14
  */
13
15
  export function validateNoDisallowedPatterns(node) {
@@ -17,8 +19,10 @@ export function validateNoDisallowedPatterns(node) {
17
19
  if (ts.isVariableStatement(statement) ||
18
20
  ts.isExpressionStatement(statement) ||
19
21
  ts.isIfStatement(statement) ||
22
+ ts.isSwitchStatement(statement) ||
20
23
  ts.isForOfStatement(statement) ||
21
- ts.isReturnStatement(statement)) {
24
+ ts.isReturnStatement(statement) ||
25
+ ts.isThrowStatement(statement)) {
22
26
  // Allowed statement type - recurse into it
23
27
  visitNode(statement);
24
28
  }
@@ -26,7 +30,7 @@ export function validateNoDisallowedPatterns(node) {
26
30
  // Unknown/disallowed statement type
27
31
  const nodeType = ts.SyntaxKind[statement.kind];
28
32
  errors.push({
29
- message: `Statement type '${nodeType}' is not allowed in simple workflows. Allowed: const/let, if/else, for..of, return, and workflow calls. If this should be supported, please report the node type: ${nodeType}`,
33
+ message: `Statement type '${nodeType}' is not allowed in simple workflows. Allowed: const/let, if/else, switch/case, for..of, return, throw, and workflow calls. If this should be supported, please report the node type: ${nodeType}`,
30
34
  node: statement,
31
35
  });
32
36
  }
@@ -85,14 +89,28 @@ export function validateNoDisallowedPatterns(node) {
85
89
  */
86
90
  export function validateAwaitedCalls(node) {
87
91
  const errors = [];
88
- function visit(node, parentIsAwait = false) {
92
+ function visit(node, parentIsAwait = false, insidePromiseAll = false) {
93
+ // Check if this is Promise.all(...) first, before checking for workflow calls
94
+ if (ts.isCallExpression(node) &&
95
+ ts.isPropertyAccessExpression(node.expression)) {
96
+ const propAccess = node.expression;
97
+ if (propAccess.name.text === 'all' &&
98
+ ts.isIdentifier(propAccess.expression) &&
99
+ propAccess.expression.text === 'Promise') {
100
+ // console.log('[DEBUG] Found Promise.all, setting insidePromiseAll=true')
101
+ // Visit children with insidePromiseAll = true
102
+ ts.forEachChild(node, (child) => visit(child, parentIsAwait, true));
103
+ return;
104
+ }
105
+ }
106
+ // Now check for workflow calls
89
107
  if (ts.isCallExpression(node)) {
90
108
  if (ts.isPropertyAccessExpression(node.expression)) {
91
109
  const propAccess = node.expression;
92
110
  if ((propAccess.name.text === 'do' || propAccess.name.text === 'sleep') &&
93
111
  ts.isIdentifier(propAccess.expression) &&
94
112
  propAccess.expression.text === 'workflow') {
95
- if (!parentIsAwait) {
113
+ if (!parentIsAwait && !insidePromiseAll) {
96
114
  errors.push({
97
115
  message: `workflow.${propAccess.name.text}() must be awaited`,
98
116
  node,
@@ -103,11 +121,11 @@ export function validateAwaitedCalls(node) {
103
121
  }
104
122
  }
105
123
  if (ts.isAwaitExpression(node)) {
106
- // Mark child as awaited
107
- ts.forEachChild(node.expression, (child) => visit(child, true));
124
+ // Visit the expression itself with parentIsAwait=true
125
+ visit(node.expression, true, insidePromiseAll);
108
126
  }
109
127
  else {
110
- ts.forEachChild(node, (child) => visit(child, false));
128
+ ts.forEachChild(node, (child) => visit(child, false, insidePromiseAll));
111
129
  }
112
130
  }
113
131
  visit(node);
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Converts DSL (Domain Specific Language) step-based format to graph node format
3
+ */
4
+ import type { WorkflowsMeta } from '@pikku/core/workflow';
5
+ import type { SerializedWorkflowGraph } from './workflow-graph.types.js';
6
+ /**
7
+ * Convert a DSL workflow to graph format
8
+ */
9
+ export declare function convertDslToGraph(workflowName: string, meta: WorkflowsMeta[string]): SerializedWorkflowGraph;
10
+ /**
11
+ * Convert all DSL workflows to graph format
12
+ */
13
+ export declare function convertAllDslToGraphs(workflowsMeta: WorkflowsMeta): Record<string, SerializedWorkflowGraph>;
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Check if a node is a terminal flow (no next step should follow)
3
+ */
4
+ function isTerminalFlow(node) {
5
+ if ('flow' in node) {
6
+ // Cancel and return are terminal flows - they end execution
7
+ return node.flow === 'cancel' || node.flow === 'return';
8
+ }
9
+ return false;
10
+ }
11
+ /**
12
+ * Convert InputSource to DataRef
13
+ */
14
+ function convertInputSource(source) {
15
+ if (source.from === 'literal') {
16
+ return source.value;
17
+ }
18
+ if (source.from === 'input') {
19
+ return { $ref: 'trigger', path: source.path };
20
+ }
21
+ if (source.from === 'outputVar') {
22
+ return { $ref: source.name, path: source.path };
23
+ }
24
+ if (source.from === 'item') {
25
+ return { $ref: '$item', path: source.path };
26
+ }
27
+ if (source.from === 'stateVar') {
28
+ return { $state: source.name, path: source.path };
29
+ }
30
+ if (source.from === 'template') {
31
+ return {
32
+ $template: {
33
+ parts: source.parts,
34
+ expressions: source.expressions?.map((expr) => convertInputSource(expr)),
35
+ },
36
+ };
37
+ }
38
+ return source.value;
39
+ }
40
+ /**
41
+ * Convert a single DSL step to graph node(s)
42
+ */
43
+ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
44
+ const nodeId = `${nodeIdPrefix}_${index}`;
45
+ const nextNodeId = index < steps.length - 1 ? `${nodeIdPrefix}_${index + 1}` : undefined;
46
+ switch (step.type) {
47
+ case 'rpc': {
48
+ const node = {
49
+ nodeId,
50
+ rpcName: step.rpcName,
51
+ stepName: step.stepName,
52
+ next: nextNodeId,
53
+ };
54
+ if (step.inputs) {
55
+ if (step.inputs === 'passthrough') {
56
+ // Entire data is passed through - store as reference to trigger
57
+ node.input = { $passthrough: { $ref: 'trigger' } };
58
+ }
59
+ else {
60
+ node.input = {};
61
+ for (const [key, source] of Object.entries(step.inputs)) {
62
+ node.input[key] = convertInputSource(source);
63
+ }
64
+ }
65
+ }
66
+ if (step.outputVar) {
67
+ node.outputVar = step.outputVar;
68
+ }
69
+ if (step.options) {
70
+ node.options = {
71
+ retries: step.options.retries,
72
+ retryDelay: step.options.retryDelay?.toString(),
73
+ };
74
+ }
75
+ return [node];
76
+ }
77
+ case 'sleep': {
78
+ const node = {
79
+ nodeId,
80
+ flow: 'sleep',
81
+ stepName: step.stepName,
82
+ duration: step.duration,
83
+ next: nextNodeId,
84
+ };
85
+ return [node];
86
+ }
87
+ case 'inline': {
88
+ const node = {
89
+ nodeId,
90
+ flow: 'inline',
91
+ stepName: step.stepName,
92
+ description: step.description,
93
+ next: nextNodeId,
94
+ };
95
+ return [node];
96
+ }
97
+ case 'branch': {
98
+ // Convert all branch conditions (if/else-if chain)
99
+ const branchNodes = [];
100
+ const branches = [];
101
+ for (let i = 0; i < step.branches.length; i++) {
102
+ const branchSteps = convertStepsToNodes(step.branches[i].steps, `${nodeId}_branch${i}`);
103
+ if (branchSteps.length > 0) {
104
+ branches.push({
105
+ condition: step.branches[i].condition,
106
+ entry: branchSteps[0].nodeId,
107
+ });
108
+ // Link last branch node back to next (unless terminal flow)
109
+ if (nextNodeId) {
110
+ const lastBranch = branchSteps[branchSteps.length - 1];
111
+ if (!lastBranch.next && !isTerminalFlow(lastBranch))
112
+ lastBranch.next = nextNodeId;
113
+ }
114
+ branchNodes.push(...branchSteps);
115
+ }
116
+ }
117
+ // Convert else branch
118
+ const elseNodes = step.elseSteps
119
+ ? convertStepsToNodes(step.elseSteps, `${nodeId}_else`)
120
+ : [];
121
+ if (elseNodes.length > 0 && nextNodeId) {
122
+ const lastElse = elseNodes[elseNodes.length - 1];
123
+ if (!lastElse.next && !isTerminalFlow(lastElse))
124
+ lastElse.next = nextNodeId;
125
+ }
126
+ const node = {
127
+ nodeId,
128
+ flow: 'branch',
129
+ branches,
130
+ elseEntry: elseNodes.length > 0 ? elseNodes[0].nodeId : undefined,
131
+ next: nextNodeId,
132
+ };
133
+ return [node, ...branchNodes, ...elseNodes];
134
+ }
135
+ case 'switch': {
136
+ const caseNodes = [];
137
+ const cases = [];
138
+ for (let i = 0; i < step.cases.length; i++) {
139
+ const caseSteps = convertStepsToNodes(step.cases[i].steps, `${nodeId}_case${i}`);
140
+ if (caseSteps.length > 0) {
141
+ cases.push({
142
+ value: step.cases[i].value,
143
+ expression: step.cases[i].expression,
144
+ entry: caseSteps[0].nodeId,
145
+ });
146
+ // Link last case node to next (unless terminal flow)
147
+ if (nextNodeId) {
148
+ const lastCase = caseSteps[caseSteps.length - 1];
149
+ if (!lastCase.next && !isTerminalFlow(lastCase))
150
+ lastCase.next = nextNodeId;
151
+ }
152
+ caseNodes.push(...caseSteps);
153
+ }
154
+ }
155
+ let defaultEntry;
156
+ if (step.defaultSteps) {
157
+ const defaultNodes = convertStepsToNodes(step.defaultSteps, `${nodeId}_default`);
158
+ if (defaultNodes.length > 0) {
159
+ defaultEntry = defaultNodes[0].nodeId;
160
+ // Link last default node to next (unless terminal flow)
161
+ if (nextNodeId) {
162
+ const lastDefault = defaultNodes[defaultNodes.length - 1];
163
+ if (!lastDefault.next && !isTerminalFlow(lastDefault))
164
+ lastDefault.next = nextNodeId;
165
+ }
166
+ caseNodes.push(...defaultNodes);
167
+ }
168
+ }
169
+ const node = {
170
+ nodeId,
171
+ flow: 'switch',
172
+ expression: step.expression,
173
+ cases,
174
+ defaultEntry,
175
+ next: nextNodeId,
176
+ };
177
+ return [node, ...caseNodes];
178
+ }
179
+ case 'parallel': {
180
+ // Convert children to nodes
181
+ const childNodes = [];
182
+ const childEntries = [];
183
+ for (let i = 0; i < step.children.length; i++) {
184
+ const childSteps = convertStepToNode(step.children[i], i, step.children, `${nodeId}_child`);
185
+ if (childSteps.length > 0) {
186
+ childEntries.push(childSteps[0].nodeId);
187
+ childNodes.push(...childSteps);
188
+ }
189
+ }
190
+ const node = {
191
+ nodeId,
192
+ flow: 'parallel',
193
+ children: childEntries,
194
+ next: nextNodeId,
195
+ };
196
+ return [node, ...childNodes];
197
+ }
198
+ case 'fanout': {
199
+ // Convert child step
200
+ const childNodes = convertStepToNode(step.child, 0, [step.child], `${nodeId}_item`);
201
+ const node = {
202
+ nodeId,
203
+ flow: 'fanout',
204
+ stepName: step.stepName,
205
+ sourceVar: step.sourceVar,
206
+ itemVar: step.itemVar,
207
+ mode: step.mode,
208
+ childEntry: childNodes.length > 0 ? childNodes[0].nodeId : undefined,
209
+ timeBetween: step.timeBetween,
210
+ next: nextNodeId,
211
+ };
212
+ return [node, ...childNodes];
213
+ }
214
+ case 'filter': {
215
+ const node = {
216
+ nodeId,
217
+ flow: 'filter',
218
+ sourceVar: step.sourceVar,
219
+ itemVar: step.itemVar,
220
+ condition: step.condition,
221
+ outputVar: step.outputVar,
222
+ next: nextNodeId,
223
+ };
224
+ return [node];
225
+ }
226
+ case 'arrayPredicate': {
227
+ const node = {
228
+ nodeId,
229
+ flow: 'arrayPredicate',
230
+ mode: step.mode,
231
+ sourceVar: step.sourceVar,
232
+ itemVar: step.itemVar,
233
+ condition: step.condition,
234
+ outputVar: step.outputVar,
235
+ next: nextNodeId,
236
+ };
237
+ return [node];
238
+ }
239
+ case 'return': {
240
+ const node = {
241
+ nodeId,
242
+ flow: 'return',
243
+ outputs: step.outputs,
244
+ };
245
+ return [node];
246
+ }
247
+ case 'cancel': {
248
+ const node = {
249
+ nodeId,
250
+ flow: 'cancel',
251
+ reason: step.reason,
252
+ };
253
+ return [node];
254
+ }
255
+ case 'set': {
256
+ const node = {
257
+ nodeId,
258
+ flow: 'set',
259
+ variable: step.variable,
260
+ value: step.value,
261
+ next: nextNodeId,
262
+ };
263
+ return [node];
264
+ }
265
+ default:
266
+ return [];
267
+ }
268
+ }
269
+ /**
270
+ * Convert array of steps to graph nodes
271
+ */
272
+ function convertStepsToNodes(steps, nodeIdPrefix = 'step') {
273
+ const allNodes = [];
274
+ for (let i = 0; i < steps.length; i++) {
275
+ const nodes = convertStepToNode(steps[i], i, steps, nodeIdPrefix);
276
+ allNodes.push(...nodes);
277
+ }
278
+ return allNodes;
279
+ }
280
+ /**
281
+ * Convert a DSL workflow to graph format
282
+ */
283
+ export function convertDslToGraph(workflowName, meta) {
284
+ const nodes = convertStepsToNodes(meta.steps);
285
+ const nodesRecord = {};
286
+ for (const node of nodes) {
287
+ nodesRecord[node.nodeId] = node;
288
+ }
289
+ // Find entry nodes (step_0 is always entry for sequential workflows)
290
+ const entryNodeIds = nodes.length > 0 ? ['step_0'] : [];
291
+ // Determine source type based on dsl flag:
292
+ // - dsl === true: pure DSL workflow, can be serialized
293
+ // - dsl === false: complex workflow with inline steps, not serializable
294
+ const source = meta.dsl === false ? 'complex' : 'dsl';
295
+ return {
296
+ name: workflowName,
297
+ pikkuFuncName: meta.pikkuFuncName,
298
+ source,
299
+ description: meta.description,
300
+ tags: meta.tags,
301
+ context: meta.context,
302
+ wires: {}, // DSL workflows don't have explicit wires in meta
303
+ nodes: nodesRecord,
304
+ entryNodeIds,
305
+ };
306
+ }
307
+ /**
308
+ * Convert all DSL workflows to graph format
309
+ */
310
+ export function convertAllDslToGraphs(workflowsMeta) {
311
+ const result = {};
312
+ for (const [name, meta] of Object.entries(workflowsMeta)) {
313
+ result[name] = convertDslToGraph(name, meta);
314
+ }
315
+ return result;
316
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Workflow graph serialization exports
3
+ */
4
+ export * from './workflow-graph.types.js';
5
+ export { serializeWorkflowGraph } from './serialize-workflow-graph.js';
6
+ export { convertDslToGraph } from './convert-dsl-to-graph.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Workflow graph serialization exports
3
+ */
4
+ export * from './workflow-graph.types.js';
5
+ export { serializeWorkflowGraph } from './serialize-workflow-graph.js';
6
+ export { convertDslToGraph } from './convert-dsl-to-graph.js';
@@ -0,0 +1,43 @@
1
+ import type { SerializedWorkflowGraph, DataRef, SerializedNext } from './workflow-graph.types.js';
2
+ /**
3
+ * Serialize a workflow graph definition (from runtime) to JSON format
4
+ *
5
+ * @param definition - The runtime definition (with callbacks evaluated)
6
+ * @param rpcNameLookup - Function to get RPC name from a node's func
7
+ */
8
+ export declare function serializeWorkflowGraph(definition: {
9
+ name: string;
10
+ wires: {
11
+ http?: {
12
+ route: string;
13
+ method: string;
14
+ };
15
+ queue?: string;
16
+ };
17
+ graph: Record<string, {
18
+ func: {
19
+ name?: string;
20
+ };
21
+ input?: (ref: any) => Record<string, unknown>;
22
+ next?: string | string[] | Record<string, string | string[]>;
23
+ onError?: string | string[];
24
+ }>;
25
+ }, options?: {
26
+ description?: string;
27
+ tags?: string[];
28
+ }): SerializedWorkflowGraph;
29
+ /**
30
+ * Deserialize a workflow graph from JSON to runtime format
31
+ * This re-hydrates the JSON so it can be executed
32
+ */
33
+ export declare function deserializeWorkflowGraph(serialized: SerializedWorkflowGraph): {
34
+ name: string;
35
+ wires: SerializedWorkflowGraph['wires'];
36
+ graph: Record<string, {
37
+ rpcName: string;
38
+ input: Record<string, unknown | DataRef>;
39
+ next?: SerializedNext;
40
+ onError?: string | string[];
41
+ }>;
42
+ entryNodeIds: string[];
43
+ };