@pikku/inspector 0.11.0 → 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.
- package/CHANGELOG.md +32 -2
- package/dist/add/add-channel.js +11 -10
- package/dist/add/add-file-with-factory.js +10 -10
- package/dist/add/add-forge-credential.d.ts +8 -0
- package/dist/add/add-forge-credential.js +77 -0
- package/dist/add/add-forge-node.d.ts +7 -0
- package/dist/add/add-forge-node.js +77 -0
- package/dist/add/add-functions.js +158 -51
- package/dist/add/add-http-route.js +28 -4
- package/dist/add/add-mcp-prompt.js +6 -5
- package/dist/add/add-mcp-resource.js +6 -5
- package/dist/add/add-mcp-tool.js +6 -5
- package/dist/add/add-middleware.js +1 -1
- package/dist/add/add-permission.js +1 -1
- package/dist/add/add-queue-worker.js +6 -5
- package/dist/add/add-rpc-invocations.d.ts +3 -0
- package/dist/add/add-rpc-invocations.js +51 -25
- package/dist/add/add-schedule.js +5 -4
- package/dist/add/add-workflow-graph.d.ts +6 -0
- package/dist/add/add-workflow-graph.js +659 -0
- package/dist/add/add-workflow.d.ts +1 -1
- package/dist/add/add-workflow.js +191 -69
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.js +3 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/inspector.js +29 -9
- package/dist/types.d.ts +47 -8
- package/dist/utils/extract-function-name.js +7 -7
- package/dist/utils/extract-function-node.d.ts +10 -0
- package/dist/utils/extract-function-node.js +38 -0
- package/dist/utils/extract-node-value.d.ts +8 -0
- package/dist/utils/extract-node-value.js +24 -0
- package/dist/utils/extract-service-metadata.d.ts +19 -0
- package/dist/utils/extract-service-metadata.js +244 -0
- package/dist/utils/get-files-and-methods.d.ts +3 -3
- package/dist/utils/get-files-and-methods.js +3 -3
- package/dist/utils/get-property-value.d.ts +14 -6
- package/dist/utils/get-property-value.js +55 -43
- package/dist/utils/post-process.d.ts +9 -0
- package/dist/utils/post-process.js +30 -3
- package/dist/utils/serialize-inspector-state.d.ts +42 -6
- package/dist/utils/serialize-inspector-state.js +36 -10
- package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
- package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +898 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +17 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +1284 -0
- package/dist/utils/workflow/dsl/index.d.ts +7 -0
- package/dist/utils/workflow/dsl/index.js +7 -0
- package/dist/utils/workflow/dsl/patterns.d.ts +60 -0
- package/dist/utils/workflow/dsl/patterns.js +218 -0
- package/dist/utils/workflow/dsl/validation.d.ts +30 -0
- package/dist/utils/workflow/dsl/validation.js +142 -0
- package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +316 -0
- package/dist/utils/workflow/graph/index.d.ts +6 -0
- package/dist/utils/workflow/graph/index.js +6 -0
- package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +43 -0
- package/dist/utils/workflow/graph/serialize-workflow-graph.js +152 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +229 -0
- package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
- package/dist/utils/write-service-metadata.d.ts +13 -0
- package/dist/utils/write-service-metadata.js +37 -0
- package/dist/visit.js +8 -2
- package/package.json +16 -4
- package/src/add/add-channel.ts +37 -17
- package/src/add/add-file-with-factory.ts +10 -10
- package/src/add/add-forge-credential.ts +119 -0
- package/src/add/add-forge-node.ts +132 -0
- package/src/add/add-functions.ts +199 -69
- package/src/add/add-http-route.ts +34 -5
- package/src/add/add-mcp-prompt.ts +11 -7
- package/src/add/add-mcp-resource.ts +11 -7
- package/src/add/add-mcp-tool.ts +11 -7
- package/src/add/add-middleware.ts +1 -1
- package/src/add/add-permission.ts +1 -1
- package/src/add/add-queue-worker.ts +11 -12
- package/src/add/add-rpc-invocations.ts +61 -31
- package/src/add/add-schedule.ts +10 -5
- package/src/add/add-workflow-graph.ts +864 -0
- package/src/add/add-workflow.ts +212 -116
- package/src/error-codes.ts +3 -0
- package/src/index.ts +12 -0
- package/src/inspector.ts +36 -10
- package/src/types.ts +43 -9
- package/src/utils/extract-function-name.ts +7 -7
- package/src/utils/extract-function-node.ts +58 -0
- package/src/utils/extract-node-value.ts +31 -0
- package/src/utils/extract-service-metadata.ts +353 -0
- package/src/utils/filter-inspector-state.test.ts +3 -3
- package/src/utils/filter-utils.test.ts +45 -51
- package/src/utils/get-files-and-methods.ts +11 -11
- package/src/utils/get-property-value.ts +67 -53
- package/src/utils/permissions.test.ts +3 -3
- package/src/utils/post-process.ts +56 -3
- package/src/utils/serialize-inspector-state.ts +67 -19
- package/src/utils/test-data/inspector-state.json +9 -9
- package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1180 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +1608 -0
- package/src/utils/workflow/dsl/index.ts +11 -0
- package/src/utils/workflow/dsl/patterns.ts +279 -0
- package/src/utils/workflow/dsl/validation.ts +180 -0
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +415 -0
- package/src/utils/workflow/graph/index.ts +6 -0
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +223 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +280 -0
- package/src/utils/write-service-metadata.ts +51 -0
- package/src/visit.ts +9 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { isWorkflowDoCall, isWorkflowSleepCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, } from './patterns.js';
|
|
3
|
+
import { validateNoDisallowedPatterns, validateAwaitedCalls, formatValidationErrors, } from './validation.js';
|
|
4
|
+
import { extractStringLiteral, extractNumberLiteral, } from '../../extract-node-value.js';
|
|
5
|
+
/**
|
|
6
|
+
* Extract full source path from an expression (e.g., data.memberEmails)
|
|
7
|
+
*/
|
|
8
|
+
function extractSourcePath(expr) {
|
|
9
|
+
if (ts.isIdentifier(expr)) {
|
|
10
|
+
return expr.text;
|
|
11
|
+
}
|
|
12
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
13
|
+
const base = extractSourcePath(expr.expression);
|
|
14
|
+
if (base) {
|
|
15
|
+
return `${base}.${expr.name.text}`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extract simple workflow metadata from a function declaration
|
|
22
|
+
*/
|
|
23
|
+
export function extractDSLWorkflow(funcNode, checker) {
|
|
24
|
+
try {
|
|
25
|
+
// Find the async arrow function
|
|
26
|
+
const arrowFunc = findWorkflowFunction(funcNode);
|
|
27
|
+
if (!arrowFunc) {
|
|
28
|
+
return {
|
|
29
|
+
status: 'error',
|
|
30
|
+
reason: 'Could not find async arrow function in workflow definition',
|
|
31
|
+
simple: false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Extract input parameter name (second parameter)
|
|
35
|
+
const inputParamName = extractInputParamName(arrowFunc);
|
|
36
|
+
if (!inputParamName) {
|
|
37
|
+
return {
|
|
38
|
+
status: 'error',
|
|
39
|
+
reason: 'Could not determine input parameter name',
|
|
40
|
+
simple: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Initialize extraction context
|
|
44
|
+
const context = {
|
|
45
|
+
checker,
|
|
46
|
+
outputVars: new Map(),
|
|
47
|
+
arrayVars: new Set(),
|
|
48
|
+
conditionalVars: new Set(),
|
|
49
|
+
inputParamName,
|
|
50
|
+
errors: [],
|
|
51
|
+
loopVars: new Set(),
|
|
52
|
+
contextVars: new Map(),
|
|
53
|
+
depth: 0,
|
|
54
|
+
};
|
|
55
|
+
// Validate no disallowed patterns
|
|
56
|
+
const patternErrors = validateNoDisallowedPatterns(arrowFunc.body);
|
|
57
|
+
if (patternErrors.length > 0) {
|
|
58
|
+
return {
|
|
59
|
+
status: 'error',
|
|
60
|
+
reason: formatValidationErrors(patternErrors),
|
|
61
|
+
simple: false,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Validate all workflow calls are awaited
|
|
65
|
+
const awaitErrors = validateAwaitedCalls(arrowFunc.body);
|
|
66
|
+
if (awaitErrors.length > 0) {
|
|
67
|
+
return {
|
|
68
|
+
status: 'error',
|
|
69
|
+
reason: formatValidationErrors(awaitErrors),
|
|
70
|
+
simple: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Extract steps from function body
|
|
74
|
+
const steps = extractSteps(arrowFunc.body, context);
|
|
75
|
+
// Check for any accumulated errors
|
|
76
|
+
if (context.errors.length > 0) {
|
|
77
|
+
return {
|
|
78
|
+
status: 'error',
|
|
79
|
+
reason: formatValidationErrors(context.errors),
|
|
80
|
+
simple: false,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Build workflow context from extracted context variables
|
|
84
|
+
const workflowContext = {};
|
|
85
|
+
for (const [name, info] of context.contextVars) {
|
|
86
|
+
workflowContext[name] = {
|
|
87
|
+
type: info.type,
|
|
88
|
+
default: info.default,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
status: 'ok',
|
|
93
|
+
steps,
|
|
94
|
+
context: Object.keys(workflowContext).length > 0 ? workflowContext : undefined,
|
|
95
|
+
simple: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return {
|
|
100
|
+
status: 'error',
|
|
101
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
102
|
+
simple: false,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Find the workflow function (async arrow function)
|
|
108
|
+
*/
|
|
109
|
+
function findWorkflowFunction(node) {
|
|
110
|
+
// Handle pikkuWorkflowFunc(async () => {}) or pikkuWorkflowComplexFunc(async () => {})
|
|
111
|
+
if (ts.isCallExpression(node)) {
|
|
112
|
+
const arg = node.arguments[0];
|
|
113
|
+
if (arg && ts.isArrowFunction(arg)) {
|
|
114
|
+
return arg;
|
|
115
|
+
}
|
|
116
|
+
// Also check if first argument is an object with func property
|
|
117
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
118
|
+
for (const prop of arg.properties) {
|
|
119
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
120
|
+
ts.isIdentifier(prop.name) &&
|
|
121
|
+
prop.name.text === 'func') {
|
|
122
|
+
if (ts.isArrowFunction(prop.initializer)) {
|
|
123
|
+
return prop.initializer;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Handle pikkuWorkflowFunc({ func: async () => {} })
|
|
130
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
131
|
+
for (const prop of node.properties) {
|
|
132
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
133
|
+
ts.isIdentifier(prop.name) &&
|
|
134
|
+
prop.name.text === 'func') {
|
|
135
|
+
if (ts.isArrowFunction(prop.initializer)) {
|
|
136
|
+
return prop.initializer;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Extract the input parameter name from the arrow function
|
|
145
|
+
*/
|
|
146
|
+
function extractInputParamName(arrowFunc) {
|
|
147
|
+
if (arrowFunc.parameters.length < 2) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const secondParam = arrowFunc.parameters[1];
|
|
151
|
+
if (ts.isIdentifier(secondParam.name)) {
|
|
152
|
+
return secondParam.name.text;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Extract steps from the function body
|
|
158
|
+
*/
|
|
159
|
+
function extractSteps(body, context, incrementDepth = false) {
|
|
160
|
+
const steps = [];
|
|
161
|
+
if (!ts.isBlock(body)) {
|
|
162
|
+
return steps;
|
|
163
|
+
}
|
|
164
|
+
// Increment depth when entering a nested block
|
|
165
|
+
if (incrementDepth) {
|
|
166
|
+
context.depth++;
|
|
167
|
+
}
|
|
168
|
+
for (const statement of body.statements) {
|
|
169
|
+
const extracted = extractStep(statement, context);
|
|
170
|
+
if (extracted) {
|
|
171
|
+
steps.push(extracted);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Restore depth
|
|
175
|
+
if (incrementDepth) {
|
|
176
|
+
context.depth--;
|
|
177
|
+
}
|
|
178
|
+
return steps;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Extract a single step from a statement
|
|
182
|
+
*/
|
|
183
|
+
function extractStep(statement, context) {
|
|
184
|
+
// Variable declaration with workflow.do assignment
|
|
185
|
+
if (ts.isVariableStatement(statement)) {
|
|
186
|
+
return extractVariableDeclaration(statement, context);
|
|
187
|
+
}
|
|
188
|
+
// Expression statement (await workflow.do without assignment)
|
|
189
|
+
if (ts.isExpressionStatement(statement)) {
|
|
190
|
+
return extractExpressionStatement(statement, context);
|
|
191
|
+
}
|
|
192
|
+
// If statement (branch)
|
|
193
|
+
if (ts.isIfStatement(statement)) {
|
|
194
|
+
return extractBranch(statement, context);
|
|
195
|
+
}
|
|
196
|
+
// Switch statement
|
|
197
|
+
if (ts.isSwitchStatement(statement)) {
|
|
198
|
+
return extractSwitch(statement, context);
|
|
199
|
+
}
|
|
200
|
+
// For-of statement (sequential fanout)
|
|
201
|
+
if (ts.isForOfStatement(statement)) {
|
|
202
|
+
return extractSequentialFanout(statement, context);
|
|
203
|
+
}
|
|
204
|
+
// Return statement
|
|
205
|
+
if (ts.isReturnStatement(statement)) {
|
|
206
|
+
return extractReturn(statement, context);
|
|
207
|
+
}
|
|
208
|
+
// Throw statement (for WorkflowCancelledException)
|
|
209
|
+
if (ts.isThrowStatement(statement)) {
|
|
210
|
+
return extractThrowCancel(statement, context);
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Extract variable declaration (const x = await workflow.do(...))
|
|
216
|
+
*/
|
|
217
|
+
function extractVariableDeclaration(statement, context) {
|
|
218
|
+
const declList = statement.declarationList;
|
|
219
|
+
if (declList.declarations.length !== 1) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
const decl = declList.declarations[0];
|
|
223
|
+
if (!ts.isIdentifier(decl.name)) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const varName = decl.name.text;
|
|
227
|
+
const init = decl.initializer;
|
|
228
|
+
// Check for block-scoped variable declarations (not allowed)
|
|
229
|
+
if (context.depth > 0) {
|
|
230
|
+
context.errors.push({
|
|
231
|
+
message: `Variable declaration '${varName}' inside block is not supported in DSL workflows. Move all let/const declarations to the top level.`,
|
|
232
|
+
node: statement,
|
|
233
|
+
});
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
if (!init) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
// Check for simple literal/expression context variable (let x = 'value')
|
|
240
|
+
const literalValue = extractLiteralValue(init);
|
|
241
|
+
if (literalValue !== undefined) {
|
|
242
|
+
const tsType = context.checker.getTypeAtLocation(decl);
|
|
243
|
+
const typeStr = inferSimpleType(tsType, context.checker);
|
|
244
|
+
context.contextVars.set(varName, { type: typeStr, default: literalValue });
|
|
245
|
+
return null; // No step emitted, just register the context var
|
|
246
|
+
}
|
|
247
|
+
// Check for await workflow.do(...)
|
|
248
|
+
if (ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)) {
|
|
249
|
+
const call = init.expression;
|
|
250
|
+
if (isWorkflowDoCall(call, context.checker)) {
|
|
251
|
+
const step = extractRpcStep(call, context, varName);
|
|
252
|
+
if (step) {
|
|
253
|
+
// Track output variable
|
|
254
|
+
const type = context.checker.getTypeAtLocation(decl);
|
|
255
|
+
context.outputVars.set(varName, { type, node: decl });
|
|
256
|
+
// Check if it's an array type
|
|
257
|
+
if (isArrayType(type, context.checker)) {
|
|
258
|
+
context.arrayVars.add(varName);
|
|
259
|
+
}
|
|
260
|
+
// Check if it's a conditional variable (let x: T | undefined)
|
|
261
|
+
if (declList.flags & ts.NodeFlags.Let) {
|
|
262
|
+
const typeNode = decl.type;
|
|
263
|
+
if (typeNode && ts.isUnionTypeNode(typeNode)) {
|
|
264
|
+
// Check if union includes undefined
|
|
265
|
+
const hasUndefined = typeNode.types.some((t) => (ts.isLiteralTypeNode(t) &&
|
|
266
|
+
t.literal.kind === ts.SyntaxKind.UndefinedKeyword) ||
|
|
267
|
+
t.kind === ts.SyntaxKind.UndefinedKeyword);
|
|
268
|
+
if (hasUndefined) {
|
|
269
|
+
context.conditionalVars.add(varName);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return step;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Check for array.filter(...)
|
|
278
|
+
if (ts.isCallExpression(init)) {
|
|
279
|
+
if (isArrayFilter(init)) {
|
|
280
|
+
const filterStep = extractArrayFilter(init, context, varName);
|
|
281
|
+
if (filterStep) {
|
|
282
|
+
const type = context.checker.getTypeAtLocation(decl);
|
|
283
|
+
context.outputVars.set(varName, { type, node: decl });
|
|
284
|
+
if (isArrayType(type, context.checker)) {
|
|
285
|
+
context.arrayVars.add(varName);
|
|
286
|
+
}
|
|
287
|
+
return filterStep;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (isArraySome(init) || isArrayEvery(init)) {
|
|
291
|
+
const predicateStep = extractArrayPredicate(init, context, varName);
|
|
292
|
+
if (predicateStep) {
|
|
293
|
+
const type = context.checker.getTypeAtLocation(decl);
|
|
294
|
+
context.outputVars.set(varName, { type, node: decl });
|
|
295
|
+
return predicateStep;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Extract expression statement (await workflow.do(...) without assignment)
|
|
303
|
+
*/
|
|
304
|
+
function extractExpressionStatement(statement, context) {
|
|
305
|
+
let expr = statement.expression;
|
|
306
|
+
// Handle assignment: x = value or x = await workflow.do(...)
|
|
307
|
+
let outputVar;
|
|
308
|
+
if (ts.isBinaryExpression(expr) &&
|
|
309
|
+
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
310
|
+
// Extract variable name from left side
|
|
311
|
+
if (ts.isIdentifier(expr.left)) {
|
|
312
|
+
outputVar = expr.left.text;
|
|
313
|
+
// Check if this is an assignment to a context variable (set step)
|
|
314
|
+
if (context.contextVars.has(outputVar)) {
|
|
315
|
+
const literalValue = extractLiteralValue(expr.right);
|
|
316
|
+
if (literalValue !== undefined) {
|
|
317
|
+
return {
|
|
318
|
+
type: 'set',
|
|
319
|
+
variable: outputVar,
|
|
320
|
+
value: literalValue,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
// Non-literal assignment to context var - use expression as string
|
|
324
|
+
return {
|
|
325
|
+
type: 'set',
|
|
326
|
+
variable: outputVar,
|
|
327
|
+
value: getSourceText(expr.right),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Use right side as the expression to extract from
|
|
332
|
+
expr = expr.right;
|
|
333
|
+
}
|
|
334
|
+
// await workflow.do(...)
|
|
335
|
+
if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
|
|
336
|
+
const call = expr.expression;
|
|
337
|
+
if (isWorkflowDoCall(call, context.checker)) {
|
|
338
|
+
const step = extractRpcStep(call, context, outputVar);
|
|
339
|
+
// Track output variable if this is an assignment
|
|
340
|
+
if (outputVar && step) {
|
|
341
|
+
const type = context.checker.getTypeAtLocation(expr);
|
|
342
|
+
context.outputVars.set(outputVar, { type, node: expr });
|
|
343
|
+
// Check if it's an array type
|
|
344
|
+
if (isArrayType(type, context.checker)) {
|
|
345
|
+
context.arrayVars.add(outputVar);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return step;
|
|
349
|
+
}
|
|
350
|
+
if (isWorkflowSleepCall(call, context.checker)) {
|
|
351
|
+
return extractSleepStep(call, context);
|
|
352
|
+
}
|
|
353
|
+
// Check for parallel group or fanout
|
|
354
|
+
if (isParallelFanout(call)) {
|
|
355
|
+
return extractParallelFanout(call, context);
|
|
356
|
+
}
|
|
357
|
+
if (isParallelGroup(call)) {
|
|
358
|
+
return extractParallelGroup(call, context);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Extract RPC step from workflow.do() call
|
|
365
|
+
*/
|
|
366
|
+
function extractRpcStep(call, context, outputVar) {
|
|
367
|
+
const args = call.arguments;
|
|
368
|
+
if (args.length < 2) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const stepName = extractStringLiteral(args[0], context.checker);
|
|
373
|
+
const rpcName = extractStringLiteral(args[1], context.checker);
|
|
374
|
+
// Extract inputs from third argument
|
|
375
|
+
const inputs = args.length >= 3 ? extractInputSources(args[2], context) : undefined;
|
|
376
|
+
// Extract options from fourth argument
|
|
377
|
+
const options = args.length >= 4 && ts.isObjectLiteralExpression(args[3])
|
|
378
|
+
? extractStepOptions(args[3], context)
|
|
379
|
+
: undefined;
|
|
380
|
+
return {
|
|
381
|
+
type: 'rpc',
|
|
382
|
+
stepName,
|
|
383
|
+
rpcName,
|
|
384
|
+
outputVar,
|
|
385
|
+
inputs,
|
|
386
|
+
options,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
context.errors.push({
|
|
391
|
+
message: `Failed to extract RPC step: ${error instanceof Error ? error.message : String(error)}`,
|
|
392
|
+
node: call,
|
|
393
|
+
});
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Extract step options from options object
|
|
399
|
+
*/
|
|
400
|
+
function extractStepOptions(optionsNode, context) {
|
|
401
|
+
const options = {};
|
|
402
|
+
for (const prop of optionsNode.properties) {
|
|
403
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
404
|
+
const propName = prop.name.text;
|
|
405
|
+
if (propName === 'retries') {
|
|
406
|
+
const retries = extractNumberLiteral(prop.initializer);
|
|
407
|
+
if (retries !== null) {
|
|
408
|
+
options.retries = retries;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else if (propName === 'retryDelay') {
|
|
412
|
+
try {
|
|
413
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
414
|
+
options.retryDelay = prop.initializer.text;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
const delay = extractNumberLiteral(prop.initializer);
|
|
418
|
+
if (delay !== null) {
|
|
419
|
+
options.retryDelay = delay;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
// Ignore extraction errors for retryDelay
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else if (propName === 'description') {
|
|
428
|
+
try {
|
|
429
|
+
options.description = extractStringLiteral(prop.initializer, context.checker);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// Ignore extraction errors for description
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Extract sleep step from workflow.sleep() call
|
|
441
|
+
*/
|
|
442
|
+
function extractSleepStep(call, context) {
|
|
443
|
+
const args = call.arguments;
|
|
444
|
+
if (args.length < 2) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const stepName = extractStringLiteral(args[0], context.checker);
|
|
449
|
+
let duration;
|
|
450
|
+
const numValue = extractNumberLiteral(args[1]);
|
|
451
|
+
if (numValue !== null) {
|
|
452
|
+
duration = numValue;
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
duration = extractStringLiteral(args[1], context.checker);
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
type: 'sleep',
|
|
459
|
+
stepName,
|
|
460
|
+
duration,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
context.errors.push({
|
|
465
|
+
message: `Failed to extract sleep step: ${error instanceof Error ? error.message : String(error)}`,
|
|
466
|
+
node: call,
|
|
467
|
+
});
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Extract cancel step from throw WorkflowCancelledException statement
|
|
473
|
+
*/
|
|
474
|
+
function extractThrowCancel(statement, context) {
|
|
475
|
+
if (!isThrowCancelException(statement)) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
const reason = extractCancelReason(statement, context.checker);
|
|
479
|
+
return {
|
|
480
|
+
type: 'cancel',
|
|
481
|
+
reason,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Parse a condition expression into a Condition structure
|
|
486
|
+
*/
|
|
487
|
+
function parseCondition(expr) {
|
|
488
|
+
// Handle binary expressions (&&, ||)
|
|
489
|
+
if (ts.isBinaryExpression(expr)) {
|
|
490
|
+
const operator = expr.operatorToken.kind;
|
|
491
|
+
// AND operator (&&)
|
|
492
|
+
if (operator === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
493
|
+
return {
|
|
494
|
+
type: 'and',
|
|
495
|
+
conditions: [parseCondition(expr.left), parseCondition(expr.right)],
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
// OR operator (||)
|
|
499
|
+
if (operator === ts.SyntaxKind.BarBarToken) {
|
|
500
|
+
return {
|
|
501
|
+
type: 'or',
|
|
502
|
+
conditions: [parseCondition(expr.left), parseCondition(expr.right)],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Handle parenthesized expressions - unwrap and parse inner
|
|
507
|
+
if (ts.isParenthesizedExpression(expr)) {
|
|
508
|
+
return parseCondition(expr.expression);
|
|
509
|
+
}
|
|
510
|
+
// Simple condition (comparison, function call, variable, etc.)
|
|
511
|
+
return {
|
|
512
|
+
type: 'simple',
|
|
513
|
+
expression: getSourceText(expr),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Extract branch step from if statement (supports if/else-if/else chains)
|
|
518
|
+
*/
|
|
519
|
+
function extractBranch(statement, context) {
|
|
520
|
+
const branches = [];
|
|
521
|
+
let elseSteps;
|
|
522
|
+
// Walk the if/else-if chain
|
|
523
|
+
let current = statement;
|
|
524
|
+
while (current) {
|
|
525
|
+
const condition = parseCondition(current.expression);
|
|
526
|
+
const steps = ts.isBlock(current.thenStatement)
|
|
527
|
+
? extractSteps(current.thenStatement, context, true)
|
|
528
|
+
: extractStepsFromStatement(current.thenStatement, context);
|
|
529
|
+
branches.push({ condition, steps });
|
|
530
|
+
// Check for else-if or else
|
|
531
|
+
if (current.elseStatement) {
|
|
532
|
+
if (ts.isIfStatement(current.elseStatement)) {
|
|
533
|
+
// else-if: continue the chain
|
|
534
|
+
current = current.elseStatement;
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
// else: extract the final else block and stop
|
|
538
|
+
elseSteps = ts.isBlock(current.elseStatement)
|
|
539
|
+
? extractSteps(current.elseStatement, context, true)
|
|
540
|
+
: extractStepsFromStatement(current.elseStatement, context);
|
|
541
|
+
current = undefined;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
// No else clause
|
|
546
|
+
current = undefined;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
type: 'branch',
|
|
551
|
+
branches,
|
|
552
|
+
elseSteps,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Extract steps from a single statement (non-block)
|
|
557
|
+
*/
|
|
558
|
+
function extractStepsFromStatement(statement, context) {
|
|
559
|
+
// Increment depth for single-statement blocks (if without braces)
|
|
560
|
+
context.depth++;
|
|
561
|
+
const step = extractStep(statement, context);
|
|
562
|
+
context.depth--;
|
|
563
|
+
return step ? [step] : [];
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Extract switch statement
|
|
567
|
+
*/
|
|
568
|
+
function extractSwitch(statement, context) {
|
|
569
|
+
const expression = getSourceText(statement.expression);
|
|
570
|
+
const cases = [];
|
|
571
|
+
let defaultSteps;
|
|
572
|
+
for (const clause of statement.caseBlock.clauses) {
|
|
573
|
+
if (ts.isCaseClause(clause)) {
|
|
574
|
+
const caseValue = extractCaseValue(clause.expression);
|
|
575
|
+
const steps = extractCaseSteps(clause.statements, context);
|
|
576
|
+
cases.push({
|
|
577
|
+
value: caseValue.value,
|
|
578
|
+
expression: caseValue.expression,
|
|
579
|
+
steps,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
else if (ts.isDefaultClause(clause)) {
|
|
583
|
+
defaultSteps = extractCaseSteps(clause.statements, context);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
type: 'switch',
|
|
588
|
+
expression,
|
|
589
|
+
cases,
|
|
590
|
+
defaultSteps,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Extract case value from expression
|
|
595
|
+
*/
|
|
596
|
+
function extractCaseValue(expr) {
|
|
597
|
+
if (ts.isStringLiteral(expr)) {
|
|
598
|
+
return { value: expr.text };
|
|
599
|
+
}
|
|
600
|
+
if (ts.isNumericLiteral(expr)) {
|
|
601
|
+
return { value: Number(expr.text) };
|
|
602
|
+
}
|
|
603
|
+
if (expr.kind === ts.SyntaxKind.TrueKeyword) {
|
|
604
|
+
return { value: true };
|
|
605
|
+
}
|
|
606
|
+
if (expr.kind === ts.SyntaxKind.FalseKeyword) {
|
|
607
|
+
return { value: false };
|
|
608
|
+
}
|
|
609
|
+
if (expr.kind === ts.SyntaxKind.NullKeyword) {
|
|
610
|
+
return { value: null };
|
|
611
|
+
}
|
|
612
|
+
return { expression: getSourceText(expr) };
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Extract steps from case statements, stopping at break
|
|
616
|
+
*/
|
|
617
|
+
function extractCaseSteps(statements, context) {
|
|
618
|
+
const steps = [];
|
|
619
|
+
// Increment depth for case blocks
|
|
620
|
+
context.depth++;
|
|
621
|
+
for (const statement of statements) {
|
|
622
|
+
if (ts.isBreakStatement(statement)) {
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
const step = extractStep(statement, context);
|
|
626
|
+
if (step) {
|
|
627
|
+
steps.push(step);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Restore depth
|
|
631
|
+
context.depth--;
|
|
632
|
+
return steps;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Extract array filter operation
|
|
636
|
+
*/
|
|
637
|
+
function extractArrayFilter(call, context, outputVar) {
|
|
638
|
+
if (!ts.isPropertyAccessExpression(call.expression)) {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
const sourceExpr = call.expression.expression;
|
|
642
|
+
const sourceVar = extractSourcePath(sourceExpr);
|
|
643
|
+
if (!sourceVar) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
const filterFn = call.arguments[0];
|
|
647
|
+
if (!filterFn || !ts.isArrowFunction(filterFn)) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
const itemParam = filterFn.parameters[0];
|
|
651
|
+
if (!itemParam || !ts.isIdentifier(itemParam.name)) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
const itemVar = itemParam.name.text;
|
|
655
|
+
let condition;
|
|
656
|
+
if (ts.isBlock(filterFn.body)) {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
condition = parseCondition(filterFn.body);
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
type: 'filter',
|
|
664
|
+
sourceVar,
|
|
665
|
+
itemVar,
|
|
666
|
+
condition,
|
|
667
|
+
outputVar,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Extract array predicate operation (some/every)
|
|
672
|
+
*/
|
|
673
|
+
function extractArrayPredicate(call, context, outputVar) {
|
|
674
|
+
if (!ts.isPropertyAccessExpression(call.expression)) {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
const mode = call.expression.name.text;
|
|
678
|
+
const sourceExpr = call.expression.expression;
|
|
679
|
+
const sourceVar = extractSourcePath(sourceExpr);
|
|
680
|
+
if (!sourceVar) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
const predicateFn = call.arguments[0];
|
|
684
|
+
if (!predicateFn || !ts.isArrowFunction(predicateFn)) {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
const itemParam = predicateFn.parameters[0];
|
|
688
|
+
if (!itemParam || !ts.isIdentifier(itemParam.name)) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
const itemVar = itemParam.name.text;
|
|
692
|
+
let condition;
|
|
693
|
+
if (ts.isBlock(predicateFn.body)) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
condition = parseCondition(predicateFn.body);
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
type: 'arrayPredicate',
|
|
701
|
+
mode,
|
|
702
|
+
sourceVar,
|
|
703
|
+
itemVar,
|
|
704
|
+
condition,
|
|
705
|
+
outputVar,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Extract parallel fanout from Promise.all(array.map(...))
|
|
710
|
+
*/
|
|
711
|
+
function extractParallelFanout(call, context) {
|
|
712
|
+
const mapCall = call.arguments[0];
|
|
713
|
+
if (!ts.isCallExpression(mapCall)) {
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
if (!ts.isPropertyAccessExpression(mapCall.expression)) {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
// Extract source array
|
|
720
|
+
const sourceExpr = mapCall.expression.expression;
|
|
721
|
+
const sourceVar = extractSourcePath(sourceExpr);
|
|
722
|
+
if (!sourceVar) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
// Extract map function
|
|
726
|
+
const mapFn = mapCall.arguments[0];
|
|
727
|
+
if (!ts.isArrowFunction(mapFn)) {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
// Extract item variable
|
|
731
|
+
const itemParam = mapFn.parameters[0];
|
|
732
|
+
if (!itemParam || !ts.isIdentifier(itemParam.name)) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const itemVar = itemParam.name.text;
|
|
736
|
+
// Extract workflow.do call from map body
|
|
737
|
+
let doCall = null;
|
|
738
|
+
if (ts.isCallExpression(mapFn.body)) {
|
|
739
|
+
doCall = mapFn.body;
|
|
740
|
+
}
|
|
741
|
+
else if (ts.isAwaitExpression(mapFn.body)) {
|
|
742
|
+
// Handle: async (email) => await workflow.do(...)
|
|
743
|
+
if (ts.isCallExpression(mapFn.body.expression)) {
|
|
744
|
+
doCall = mapFn.body.expression;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
else if (ts.isBlock(mapFn.body)) {
|
|
748
|
+
// Look for workflow.do in block
|
|
749
|
+
for (const stmt of mapFn.body.statements) {
|
|
750
|
+
if (ts.isExpressionStatement(stmt)) {
|
|
751
|
+
// Handle: await workflow.do(...)
|
|
752
|
+
if (ts.isAwaitExpression(stmt.expression) &&
|
|
753
|
+
ts.isCallExpression(stmt.expression.expression)) {
|
|
754
|
+
doCall = stmt.expression.expression;
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
else if (ts.isReturnStatement(stmt) && stmt.expression) {
|
|
759
|
+
if (ts.isCallExpression(stmt.expression)) {
|
|
760
|
+
doCall = stmt.expression;
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
else if (ts.isAwaitExpression(stmt.expression)) {
|
|
764
|
+
// Handle: return await workflow.do(...)
|
|
765
|
+
if (ts.isCallExpression(stmt.expression.expression)) {
|
|
766
|
+
doCall = stmt.expression.expression;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (!doCall || !isWorkflowDoCall(doCall, context.checker)) {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
// Create a temporary context for the child step with the loop variable
|
|
777
|
+
const childContext = {
|
|
778
|
+
...context,
|
|
779
|
+
outputVars: new Map(context.outputVars),
|
|
780
|
+
loopVars: new Set([...context.loopVars, itemVar]),
|
|
781
|
+
};
|
|
782
|
+
const childStep = extractRpcStep(doCall, childContext);
|
|
783
|
+
if (!childStep) {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
type: 'fanout',
|
|
788
|
+
stepName: childStep.stepName,
|
|
789
|
+
sourceVar,
|
|
790
|
+
itemVar,
|
|
791
|
+
mode: 'parallel',
|
|
792
|
+
child: childStep,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Extract parallel group from Promise.all([...])
|
|
797
|
+
*/
|
|
798
|
+
function extractParallelGroup(call, context) {
|
|
799
|
+
const arrayArg = call.arguments[0];
|
|
800
|
+
if (!ts.isArrayLiteralExpression(arrayArg)) {
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
const children = [];
|
|
804
|
+
for (const elem of arrayArg.elements) {
|
|
805
|
+
if (ts.isCallExpression(elem) && isWorkflowDoCall(elem, context.checker)) {
|
|
806
|
+
const step = extractRpcStep(elem, context);
|
|
807
|
+
if (step) {
|
|
808
|
+
children.push(step);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (children.length === 0) {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
type: 'parallel',
|
|
817
|
+
children,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Extract sequential fanout from for-of loop
|
|
822
|
+
*/
|
|
823
|
+
function extractSequentialFanout(statement, context) {
|
|
824
|
+
if (!isSequentialFanout(statement)) {
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
const vars = extractForOfVariable(statement);
|
|
828
|
+
if (!vars) {
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
const { itemVar, sourceVar } = vars;
|
|
832
|
+
// Extract child step and optional sleep from loop body
|
|
833
|
+
if (!ts.isBlock(statement.statement)) {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
let childStep = null;
|
|
837
|
+
let timeBetween = undefined;
|
|
838
|
+
// Create a child context with the loop variable added
|
|
839
|
+
const childContext = {
|
|
840
|
+
...context,
|
|
841
|
+
outputVars: new Map(context.outputVars),
|
|
842
|
+
loopVars: new Set([...context.loopVars, itemVar]),
|
|
843
|
+
};
|
|
844
|
+
let workflowDoCount = 0;
|
|
845
|
+
for (const stmt of statement.statement.statements) {
|
|
846
|
+
// Look for workflow.do in VariableStatement (const x = await workflow.do(...))
|
|
847
|
+
if (ts.isVariableStatement(stmt)) {
|
|
848
|
+
const declList = stmt.declarationList;
|
|
849
|
+
if (declList.declarations.length === 1) {
|
|
850
|
+
const decl = declList.declarations[0];
|
|
851
|
+
const init = decl.initializer;
|
|
852
|
+
if (init &&
|
|
853
|
+
ts.isAwaitExpression(init) &&
|
|
854
|
+
ts.isCallExpression(init.expression)) {
|
|
855
|
+
const call = init.expression;
|
|
856
|
+
if (isWorkflowDoCall(call, context.checker)) {
|
|
857
|
+
workflowDoCount++;
|
|
858
|
+
const varName = ts.isIdentifier(decl.name)
|
|
859
|
+
? decl.name.text
|
|
860
|
+
: undefined;
|
|
861
|
+
const step = extractRpcStep(call, childContext, varName);
|
|
862
|
+
if (step) {
|
|
863
|
+
childStep = step;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// Look for workflow.do in ExpressionStatement (await workflow.do(...))
|
|
870
|
+
if (ts.isExpressionStatement(stmt)) {
|
|
871
|
+
const expr = stmt.expression;
|
|
872
|
+
if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
|
|
873
|
+
const call = expr.expression;
|
|
874
|
+
if (isWorkflowDoCall(call, context.checker)) {
|
|
875
|
+
workflowDoCount++;
|
|
876
|
+
const step = extractRpcStep(call, childContext);
|
|
877
|
+
if (step) {
|
|
878
|
+
childStep = step;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (isWorkflowSleepCall(call, context.checker)) {
|
|
882
|
+
// Extract duration for timeBetween
|
|
883
|
+
const args = call.arguments;
|
|
884
|
+
if (args.length >= 2) {
|
|
885
|
+
try {
|
|
886
|
+
const numValue = extractNumberLiteral(args[1]);
|
|
887
|
+
if (numValue !== null) {
|
|
888
|
+
timeBetween = `${numValue}ms`;
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
timeBetween = extractStringLiteral(args[1], context.checker);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
// Ignore extraction errors
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// Look for if statement with sleep
|
|
902
|
+
if (ts.isIfStatement(stmt)) {
|
|
903
|
+
if (ts.isBlock(stmt.thenStatement)) {
|
|
904
|
+
for (const thenStmt of stmt.thenStatement.statements) {
|
|
905
|
+
if (ts.isExpressionStatement(thenStmt)) {
|
|
906
|
+
const expr = thenStmt.expression;
|
|
907
|
+
if (ts.isAwaitExpression(expr) &&
|
|
908
|
+
ts.isCallExpression(expr.expression)) {
|
|
909
|
+
const call = expr.expression;
|
|
910
|
+
if (isWorkflowSleepCall(call, context.checker)) {
|
|
911
|
+
const args = call.arguments;
|
|
912
|
+
if (args.length >= 2) {
|
|
913
|
+
try {
|
|
914
|
+
const numValue = extractNumberLiteral(args[1]);
|
|
915
|
+
if (numValue !== null) {
|
|
916
|
+
timeBetween = `${numValue}ms`;
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
timeBetween = extractStringLiteral(args[1], context.checker);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
catch {
|
|
923
|
+
// Ignore extraction errors
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (!childStep) {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
// If there are multiple workflow.do calls, the loop is too complex for DSL
|
|
937
|
+
if (workflowDoCount > 1) {
|
|
938
|
+
context.errors.push({
|
|
939
|
+
message: `For-of loop has ${workflowDoCount} workflow.do calls but DSL only supports 1. Use pikkuWorkflowComplexFunc for complex loops.`,
|
|
940
|
+
node: statement,
|
|
941
|
+
});
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
type: 'fanout',
|
|
946
|
+
stepName: childStep.stepName,
|
|
947
|
+
sourceVar,
|
|
948
|
+
itemVar,
|
|
949
|
+
mode: 'sequential',
|
|
950
|
+
child: childStep,
|
|
951
|
+
timeBetween,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Extract a single output binding from an expression
|
|
956
|
+
*/
|
|
957
|
+
function extractOutputBinding(expr, context) {
|
|
958
|
+
// Check for property access (e.g., org.id, payment.status)
|
|
959
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
960
|
+
const objName = ts.isIdentifier(expr.expression)
|
|
961
|
+
? expr.expression.text
|
|
962
|
+
: null;
|
|
963
|
+
const propPath = expr.name.text;
|
|
964
|
+
if (objName && context.outputVars.has(objName)) {
|
|
965
|
+
return { from: 'outputVar', name: objName, path: propPath };
|
|
966
|
+
}
|
|
967
|
+
if (objName && context.contextVars.has(objName)) {
|
|
968
|
+
return { from: 'stateVar', name: objName, path: propPath };
|
|
969
|
+
}
|
|
970
|
+
if (objName === context.inputParamName) {
|
|
971
|
+
return { from: 'input', path: propPath };
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
// Check for identifier (simple variable reference)
|
|
975
|
+
if (ts.isIdentifier(expr)) {
|
|
976
|
+
const varName = expr.text;
|
|
977
|
+
if (context.outputVars.has(varName)) {
|
|
978
|
+
return { from: 'outputVar', name: varName };
|
|
979
|
+
}
|
|
980
|
+
if (context.contextVars.has(varName)) {
|
|
981
|
+
return { from: 'stateVar', name: varName };
|
|
982
|
+
}
|
|
983
|
+
if (varName === context.inputParamName) {
|
|
984
|
+
return { from: 'input', path: varName };
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
// Check for literals
|
|
988
|
+
if (ts.isStringLiteral(expr)) {
|
|
989
|
+
return { from: 'literal', value: expr.text };
|
|
990
|
+
}
|
|
991
|
+
if (ts.isNumericLiteral(expr)) {
|
|
992
|
+
return { from: 'literal', value: Number(expr.text) };
|
|
993
|
+
}
|
|
994
|
+
if (expr.kind === ts.SyntaxKind.TrueKeyword) {
|
|
995
|
+
return { from: 'literal', value: true };
|
|
996
|
+
}
|
|
997
|
+
if (expr.kind === ts.SyntaxKind.FalseKeyword) {
|
|
998
|
+
return { from: 'literal', value: false };
|
|
999
|
+
}
|
|
1000
|
+
if (expr.kind === ts.SyntaxKind.NullKeyword) {
|
|
1001
|
+
return { from: 'literal', value: null };
|
|
1002
|
+
}
|
|
1003
|
+
// For any other expression (comparisons, method calls, etc.), capture as expression
|
|
1004
|
+
return { from: 'expression', expression: getSourceText(expr) };
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Extract return step
|
|
1008
|
+
*/
|
|
1009
|
+
function extractReturn(statement, context) {
|
|
1010
|
+
if (!statement.expression) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
if (!ts.isObjectLiteralExpression(statement.expression)) {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
const outputs = {};
|
|
1017
|
+
for (const prop of statement.expression.properties) {
|
|
1018
|
+
if (ts.isPropertyAssignment(prop) ||
|
|
1019
|
+
ts.isShorthandPropertyAssignment(prop)) {
|
|
1020
|
+
const propName = ts.isIdentifier(prop.name) ? prop.name.text : null;
|
|
1021
|
+
if (!propName) {
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
let binding = null;
|
|
1025
|
+
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
1026
|
+
// { orgId } - must be an output variable, context variable, or input
|
|
1027
|
+
const varName = prop.name.text;
|
|
1028
|
+
if (context.outputVars.has(varName)) {
|
|
1029
|
+
binding = { from: 'outputVar', name: varName };
|
|
1030
|
+
}
|
|
1031
|
+
else if (context.contextVars.has(varName)) {
|
|
1032
|
+
binding = { from: 'stateVar', name: varName };
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
binding = { from: 'input', path: varName };
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
else if (ts.isPropertyAssignment(prop)) {
|
|
1039
|
+
binding = extractOutputBinding(prop.initializer, context);
|
|
1040
|
+
}
|
|
1041
|
+
if (binding) {
|
|
1042
|
+
outputs[propName] = binding;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (Object.keys(outputs).length === 0) {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
type: 'return',
|
|
1051
|
+
outputs,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Extract input sources from an argument node
|
|
1056
|
+
*/
|
|
1057
|
+
function extractInputSources(node, context) {
|
|
1058
|
+
// Handle when data is passed directly (e.g., workflow.do('step', 'rpc', data))
|
|
1059
|
+
if (ts.isIdentifier(node)) {
|
|
1060
|
+
if (node.text === context.inputParamName) {
|
|
1061
|
+
// The entire input data is being passed through
|
|
1062
|
+
return 'passthrough';
|
|
1063
|
+
}
|
|
1064
|
+
// Check if it's an output variable being passed directly
|
|
1065
|
+
if (context.outputVars.has(node.text)) {
|
|
1066
|
+
return 'passthrough';
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (!ts.isObjectLiteralExpression(node)) {
|
|
1070
|
+
return undefined;
|
|
1071
|
+
}
|
|
1072
|
+
const inputs = {};
|
|
1073
|
+
for (const prop of node.properties) {
|
|
1074
|
+
if (ts.isPropertyAssignment(prop) ||
|
|
1075
|
+
ts.isShorthandPropertyAssignment(prop)) {
|
|
1076
|
+
const propName = ts.isIdentifier(prop.name) ? prop.name.text : null;
|
|
1077
|
+
if (!propName) {
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
let source = null;
|
|
1081
|
+
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
1082
|
+
// { email } - could be from loop var, output var, or input
|
|
1083
|
+
const varName = prop.name.text;
|
|
1084
|
+
if (context.loopVars.has(varName)) {
|
|
1085
|
+
source = { from: 'item', path: varName };
|
|
1086
|
+
}
|
|
1087
|
+
else if (context.outputVars.has(varName)) {
|
|
1088
|
+
source = { from: 'outputVar', name: varName };
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
source = { from: 'input', path: varName };
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
else if (ts.isPropertyAssignment(prop)) {
|
|
1095
|
+
source = extractInputSource(prop.initializer, context);
|
|
1096
|
+
}
|
|
1097
|
+
if (source) {
|
|
1098
|
+
inputs[propName] = source;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (ts.isSpreadAssignment(prop)) {
|
|
1102
|
+
// Handle spread: { ...data }
|
|
1103
|
+
if (ts.isIdentifier(prop.expression)) {
|
|
1104
|
+
const varName = prop.expression.text;
|
|
1105
|
+
if (varName === context.inputParamName) {
|
|
1106
|
+
// This is spreading the input data
|
|
1107
|
+
// We can't fully model this in v1, so we'll skip it
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return Object.keys(inputs).length > 0 ? inputs : undefined;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Extract a single input source
|
|
1117
|
+
*/
|
|
1118
|
+
function extractInputSource(node, context) {
|
|
1119
|
+
// Property access: data.email, org.id
|
|
1120
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
1121
|
+
const objExpr = node.expression;
|
|
1122
|
+
const propName = node.name.text;
|
|
1123
|
+
if (ts.isIdentifier(objExpr)) {
|
|
1124
|
+
const objName = objExpr.text;
|
|
1125
|
+
if (objName === context.inputParamName) {
|
|
1126
|
+
return { from: 'input', path: propName };
|
|
1127
|
+
}
|
|
1128
|
+
if (context.outputVars.has(objName)) {
|
|
1129
|
+
return { from: 'outputVar', name: objName, path: propName };
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
// Identifier: email, orgId
|
|
1134
|
+
if (ts.isIdentifier(node)) {
|
|
1135
|
+
const varName = node.text;
|
|
1136
|
+
// Check if it's a loop variable (from fanout)
|
|
1137
|
+
if (context.loopVars.has(varName)) {
|
|
1138
|
+
return { from: 'item', path: varName };
|
|
1139
|
+
}
|
|
1140
|
+
if (context.outputVars.has(varName)) {
|
|
1141
|
+
return { from: 'outputVar', name: varName };
|
|
1142
|
+
}
|
|
1143
|
+
// Assume it's from input
|
|
1144
|
+
return { from: 'input', path: varName };
|
|
1145
|
+
}
|
|
1146
|
+
// Literal: "string", 123, true, false, null
|
|
1147
|
+
if (ts.isStringLiteral(node) ||
|
|
1148
|
+
ts.isNumericLiteral(node) ||
|
|
1149
|
+
node.kind === ts.SyntaxKind.TrueKeyword ||
|
|
1150
|
+
node.kind === ts.SyntaxKind.FalseKeyword ||
|
|
1151
|
+
node.kind === ts.SyntaxKind.NullKeyword) {
|
|
1152
|
+
let value;
|
|
1153
|
+
if (ts.isStringLiteral(node)) {
|
|
1154
|
+
value = node.text;
|
|
1155
|
+
}
|
|
1156
|
+
else if (ts.isNumericLiteral(node)) {
|
|
1157
|
+
value = Number(node.text);
|
|
1158
|
+
}
|
|
1159
|
+
else if (node.kind === ts.SyntaxKind.TrueKeyword) {
|
|
1160
|
+
value = true;
|
|
1161
|
+
}
|
|
1162
|
+
else if (node.kind === ts.SyntaxKind.FalseKeyword) {
|
|
1163
|
+
value = false;
|
|
1164
|
+
}
|
|
1165
|
+
else if (node.kind === ts.SyntaxKind.NullKeyword) {
|
|
1166
|
+
value = null;
|
|
1167
|
+
}
|
|
1168
|
+
return { from: 'literal', value };
|
|
1169
|
+
}
|
|
1170
|
+
// Object literal
|
|
1171
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1172
|
+
const obj = {};
|
|
1173
|
+
for (const prop of node.properties) {
|
|
1174
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
1175
|
+
const propName = prop.name.text;
|
|
1176
|
+
const propSource = extractInputSource(prop.initializer, context);
|
|
1177
|
+
if (propSource && propSource.from === 'literal') {
|
|
1178
|
+
obj[propName] = propSource.value;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return { from: 'literal', value: obj };
|
|
1183
|
+
}
|
|
1184
|
+
// Array literal
|
|
1185
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
1186
|
+
const arr = [];
|
|
1187
|
+
for (const elem of node.elements) {
|
|
1188
|
+
const elemSource = extractInputSource(elem, context);
|
|
1189
|
+
if (elemSource && elemSource.from === 'literal') {
|
|
1190
|
+
arr.push(elemSource.value);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return { from: 'literal', value: arr };
|
|
1194
|
+
}
|
|
1195
|
+
// No substitution template literal: `hello`
|
|
1196
|
+
if (ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
1197
|
+
return { from: 'literal', value: node.text };
|
|
1198
|
+
}
|
|
1199
|
+
// Template expression with substitutions: `hello ${name}`
|
|
1200
|
+
if (ts.isTemplateExpression(node)) {
|
|
1201
|
+
const parts = [node.head.text];
|
|
1202
|
+
const expressions = [];
|
|
1203
|
+
for (const span of node.templateSpans) {
|
|
1204
|
+
// Extract each expression
|
|
1205
|
+
const exprSource = extractInputSource(span.expression, context);
|
|
1206
|
+
if (exprSource) {
|
|
1207
|
+
expressions.push(exprSource);
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
// Fallback: use source text as literal
|
|
1211
|
+
expressions.push({
|
|
1212
|
+
from: 'literal',
|
|
1213
|
+
value: getSourceText(span.expression),
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
parts.push(span.literal.text);
|
|
1217
|
+
}
|
|
1218
|
+
return { from: 'template', parts, expressions };
|
|
1219
|
+
}
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Extract a literal value from an expression
|
|
1224
|
+
*/
|
|
1225
|
+
function extractLiteralValue(expr) {
|
|
1226
|
+
if (ts.isStringLiteral(expr)) {
|
|
1227
|
+
return expr.text;
|
|
1228
|
+
}
|
|
1229
|
+
if (ts.isNumericLiteral(expr)) {
|
|
1230
|
+
return Number(expr.text);
|
|
1231
|
+
}
|
|
1232
|
+
if (expr.kind === ts.SyntaxKind.TrueKeyword) {
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
if (expr.kind === ts.SyntaxKind.FalseKeyword) {
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
if (expr.kind === ts.SyntaxKind.NullKeyword) {
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
// Array literal
|
|
1242
|
+
if (ts.isArrayLiteralExpression(expr)) {
|
|
1243
|
+
const values = [];
|
|
1244
|
+
for (const el of expr.elements) {
|
|
1245
|
+
const v = extractLiteralValue(el);
|
|
1246
|
+
if (v === undefined)
|
|
1247
|
+
return undefined;
|
|
1248
|
+
values.push(v);
|
|
1249
|
+
}
|
|
1250
|
+
return values;
|
|
1251
|
+
}
|
|
1252
|
+
// Object literal (simple keys with literal values)
|
|
1253
|
+
if (ts.isObjectLiteralExpression(expr)) {
|
|
1254
|
+
const obj = {};
|
|
1255
|
+
for (const prop of expr.properties) {
|
|
1256
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
1257
|
+
const v = extractLiteralValue(prop.initializer);
|
|
1258
|
+
if (v === undefined)
|
|
1259
|
+
return undefined;
|
|
1260
|
+
obj[prop.name.text] = v;
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
return undefined;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return obj;
|
|
1267
|
+
}
|
|
1268
|
+
return undefined;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Infer a simple type string from a TypeScript type
|
|
1272
|
+
*/
|
|
1273
|
+
function inferSimpleType(type, checker) {
|
|
1274
|
+
const typeStr = checker.typeToString(type);
|
|
1275
|
+
if (typeStr === 'string')
|
|
1276
|
+
return 'string';
|
|
1277
|
+
if (typeStr === 'number')
|
|
1278
|
+
return 'number';
|
|
1279
|
+
if (typeStr === 'boolean')
|
|
1280
|
+
return 'boolean';
|
|
1281
|
+
if (typeStr.endsWith('[]') || typeStr.startsWith('Array<'))
|
|
1282
|
+
return 'array';
|
|
1283
|
+
return 'object';
|
|
1284
|
+
}
|