@pikku/inspector 0.11.1 → 0.12.0
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 +26 -1
- package/OPTIMIZATION-PLAN.md +195 -0
- package/dist/add/add-ai-agent.d.ts +2 -0
- package/dist/add/add-ai-agent.js +314 -0
- package/dist/add/add-channel.js +69 -61
- package/dist/add/add-cli.js +36 -18
- package/dist/add/add-file-with-factory.js +2 -0
- package/dist/add/add-functions.js +327 -59
- package/dist/add/add-http-route.d.ts +19 -10
- package/dist/add/add-http-route.js +153 -44
- package/dist/add/add-http-routes.d.ts +5 -0
- package/dist/add/add-http-routes.js +159 -0
- package/dist/add/add-keyed-wiring.d.ts +12 -0
- package/dist/add/add-keyed-wiring.js +97 -0
- package/dist/add/add-mcp-prompt.js +14 -9
- package/dist/add/add-mcp-resource.js +14 -9
- package/dist/add/add-middleware.d.ts +1 -4
- package/dist/add/add-middleware.js +364 -79
- package/dist/add/add-permission.d.ts +1 -1
- package/dist/add/add-permission.js +152 -40
- package/dist/add/add-queue-worker.js +18 -12
- package/dist/add/add-rpc-invocations.d.ts +3 -0
- package/dist/add/add-rpc-invocations.js +65 -25
- package/dist/add/add-schedule.js +11 -5
- package/dist/add/add-secret.d.ts +3 -0
- package/dist/add/add-secret.js +82 -0
- package/dist/add/add-trigger.d.ts +2 -0
- package/dist/add/add-trigger.js +87 -0
- package/dist/add/add-variable.d.ts +1 -0
- package/dist/add/add-variable.js +8 -0
- package/dist/add/add-workflow-graph.d.ts +7 -0
- package/dist/add/add-workflow-graph.js +396 -0
- package/dist/add/add-workflow.js +124 -26
- package/dist/error-codes.d.ts +16 -1
- package/dist/error-codes.js +21 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.js +5 -2
- package/dist/inspector.d.ts +1 -1
- package/dist/inspector.js +106 -13
- package/dist/schema-generator.d.ts +1 -0
- package/dist/schema-generator.js +1 -0
- package/dist/types-map.js +10 -1
- package/dist/types.d.ts +180 -30
- package/dist/utils/compute-required-schemas.d.ts +4 -0
- package/dist/utils/compute-required-schemas.js +41 -0
- package/dist/utils/contract-hashes.d.ts +35 -0
- package/dist/utils/contract-hashes.js +202 -0
- package/dist/utils/custom-types-generator.d.ts +9 -0
- package/dist/utils/custom-types-generator.js +71 -0
- package/dist/utils/detect-schema-vendor.d.ts +22 -0
- package/dist/utils/detect-schema-vendor.js +76 -0
- package/dist/utils/ensure-function-metadata.d.ts +5 -2
- package/dist/utils/ensure-function-metadata.js +220 -6
- package/dist/utils/extract-function-name.d.ts +5 -16
- package/dist/utils/extract-function-name.js +93 -298
- package/dist/utils/extract-services.d.ts +2 -1
- package/dist/utils/extract-services.js +25 -1
- package/dist/utils/filter-inspector-state.js +107 -23
- package/dist/utils/get-property-value.d.ts +8 -2
- package/dist/utils/get-property-value.js +33 -4
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.js +23 -0
- package/dist/utils/middleware.d.ts +7 -30
- package/dist/utils/middleware.js +80 -66
- package/dist/utils/permissions.d.ts +2 -2
- package/dist/utils/permissions.js +10 -10
- package/dist/utils/post-process.d.ts +9 -10
- package/dist/utils/post-process.js +231 -24
- package/dist/utils/resolve-external-package.d.ts +12 -0
- package/dist/utils/resolve-external-package.js +34 -0
- package/dist/utils/resolve-function-types.d.ts +6 -0
- package/dist/utils/resolve-function-types.js +29 -0
- package/dist/utils/resolve-identifier.d.ts +10 -0
- package/dist/utils/resolve-identifier.js +36 -0
- package/dist/utils/resolve-versions.d.ts +2 -0
- package/dist/utils/resolve-versions.js +78 -0
- package/dist/utils/schema-generator.d.ts +9 -0
- package/dist/utils/schema-generator.js +209 -0
- package/dist/utils/serialize-inspector-state.d.ts +73 -13
- package/dist/utils/serialize-inspector-state.js +102 -6
- package/dist/utils/serialize-mcp-json.d.ts +2 -0
- package/dist/utils/serialize-mcp-json.js +99 -0
- package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
- package/dist/utils/serialize-middleware-groups-meta.js +28 -0
- package/dist/utils/serialize-openapi-json.d.ts +85 -0
- package/dist/utils/serialize-openapi-json.js +151 -0
- package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
- package/dist/utils/serialize-permissions-groups-meta.js +31 -0
- package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
- package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +830 -0
- package/dist/{workflow/extract-simple-workflow.d.ts → utils/workflow/dsl/extract-dsl-workflow.d.ts} +4 -2
- package/dist/{workflow/extract-simple-workflow.js → utils/workflow/dsl/extract-dsl-workflow.js} +572 -72
- package/dist/utils/workflow/dsl/index.d.ts +7 -0
- package/dist/utils/workflow/dsl/index.js +7 -0
- package/dist/{workflow → utils/workflow/dsl}/patterns.d.ts +21 -0
- package/dist/{workflow → utils/workflow/dsl}/patterns.js +90 -10
- package/dist/{workflow → utils/workflow/dsl}/validation.d.ts +2 -0
- package/dist/{workflow → utils/workflow/dsl}/validation.js +25 -7
- package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +318 -0
- package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
- package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
- package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
- package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
- package/dist/utils/workflow/graph/index.d.ts +8 -0
- package/dist/utils/workflow/graph/index.js +8 -0
- package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +35 -0
- package/dist/utils/workflow/graph/serialize-workflow-graph.js +150 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +203 -0
- package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
- package/dist/visit.js +13 -2
- package/package.json +26 -4
- package/src/add/add-ai-agent.ts +468 -0
- package/src/add/add-channel.ts +82 -79
- package/src/add/add-cli.ts +49 -20
- package/src/add/add-file-with-factory.ts +2 -0
- package/src/add/add-functions.ts +429 -71
- package/src/add/add-http-route.ts +246 -65
- package/src/add/add-http-routes.ts +228 -0
- package/src/add/add-keyed-wiring.ts +151 -0
- package/src/add/add-mcp-prompt.ts +26 -15
- package/src/add/add-mcp-resource.ts +27 -15
- package/src/add/add-middleware.ts +482 -80
- package/src/add/add-permission.ts +199 -40
- package/src/add/add-queue-worker.ts +24 -19
- package/src/add/add-rpc-invocations.ts +78 -31
- package/src/add/add-schedule.ts +16 -11
- package/src/add/add-secret.ts +140 -0
- package/src/add/add-trigger.ts +154 -0
- package/src/add/add-variable.ts +9 -0
- package/src/add/add-workflow-graph.ts +522 -0
- package/src/add/add-workflow.ts +117 -30
- package/src/error-codes.ts +26 -1
- package/src/index.ts +27 -8
- package/src/inspector.ts +145 -17
- package/src/schema-generator.ts +1 -0
- package/src/types-map.ts +12 -1
- package/src/types.ts +192 -51
- package/src/utils/compute-required-schemas.ts +49 -0
- package/src/utils/contract-hashes.test.ts +528 -0
- package/src/utils/contract-hashes.ts +290 -0
- package/src/utils/custom-types-generator.ts +88 -0
- package/src/utils/detect-schema-vendor.ts +90 -0
- package/src/utils/ensure-function-metadata.ts +324 -7
- package/src/utils/extract-function-name.ts +108 -358
- package/src/utils/extract-services.ts +35 -2
- package/src/utils/filter-inspector-state.test.ts +34 -20
- package/src/utils/filter-inspector-state.ts +140 -31
- package/src/utils/get-property-value.ts +50 -5
- package/src/utils/hash.ts +26 -0
- package/src/utils/middleware.test.ts +204 -0
- package/src/utils/middleware.ts +129 -67
- package/src/utils/permissions.test.ts +35 -12
- package/src/utils/permissions.ts +10 -10
- package/src/utils/post-process.ts +283 -43
- package/src/utils/resolve-external-package.ts +42 -0
- package/src/utils/resolve-function-types.ts +42 -0
- package/src/utils/resolve-identifier.ts +46 -0
- package/src/utils/resolve-versions.test.ts +249 -0
- package/src/utils/resolve-versions.ts +105 -0
- package/src/utils/schema-generator.ts +329 -0
- package/src/utils/serialize-inspector-state.ts +181 -20
- package/src/utils/serialize-mcp-json.ts +145 -0
- package/src/utils/serialize-middleware-groups-meta.ts +33 -0
- package/src/utils/serialize-openapi-json.ts +277 -0
- package/src/utils/serialize-permissions-groups-meta.ts +35 -0
- package/src/utils/test-data/inspector-state.json +69 -66
- package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1104 -0
- package/src/{workflow/extract-simple-workflow.ts → utils/workflow/dsl/extract-dsl-workflow.ts} +678 -85
- package/src/utils/workflow/dsl/index.ts +11 -0
- package/src/{workflow → utils/workflow/dsl}/patterns.ts +108 -11
- package/src/{workflow → utils/workflow/dsl}/validation.ts +34 -7
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +422 -0
- package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
- package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
- package/src/utils/workflow/graph/index.ts +11 -0
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +216 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +231 -0
- package/src/visit.ts +14 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/add/add-mcp-tool.d.ts +0 -2
- package/dist/add/add-mcp-tool.js +0 -81
- package/dist/utils/extract-service-metadata.d.ts +0 -19
- package/dist/utils/extract-service-metadata.js +0 -244
- package/dist/utils/write-service-metadata.d.ts +0 -13
- package/dist/utils/write-service-metadata.js +0 -37
- package/src/add/add-mcp-tool.ts +0 -141
- package/src/utils/extract-service-metadata.ts +0 -353
- package/src/utils/write-service-metadata.ts +0 -51
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deserialize workflow JSON back to DSL code
|
|
3
|
+
* Converts the serialized workflow graph format back to TypeScript DSL code
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
SerializedWorkflowGraph,
|
|
8
|
+
SerializedGraphNode,
|
|
9
|
+
DataRef,
|
|
10
|
+
ContextVariable,
|
|
11
|
+
} from '../graph/workflow-graph.types.js'
|
|
12
|
+
|
|
13
|
+
interface DeserializeOptions {
|
|
14
|
+
/** Import path for pikkuWorkflowFunc */
|
|
15
|
+
pikkuImportPath?: string
|
|
16
|
+
/** Whether to include type annotations */
|
|
17
|
+
includeTypes?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a value is a DataRef
|
|
22
|
+
*/
|
|
23
|
+
function isDataRef(value: unknown): value is DataRef {
|
|
24
|
+
return (
|
|
25
|
+
typeof value === 'object' &&
|
|
26
|
+
value !== null &&
|
|
27
|
+
'$ref' in value &&
|
|
28
|
+
typeof (value as DataRef).$ref === 'string'
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* State reference (context variable)
|
|
34
|
+
*/
|
|
35
|
+
interface StateRef {
|
|
36
|
+
$state: string
|
|
37
|
+
path?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a value is a StateRef
|
|
42
|
+
*/
|
|
43
|
+
function isStateRef(value: unknown): value is StateRef {
|
|
44
|
+
return (
|
|
45
|
+
typeof value === 'object' &&
|
|
46
|
+
value !== null &&
|
|
47
|
+
'$state' in value &&
|
|
48
|
+
typeof (value as StateRef).$state === 'string'
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if value is a template literal reference
|
|
54
|
+
*/
|
|
55
|
+
interface TemplateRef {
|
|
56
|
+
$template: {
|
|
57
|
+
parts: string[]
|
|
58
|
+
expressions: unknown[]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isTemplateRef(value: unknown): value is TemplateRef {
|
|
63
|
+
return (
|
|
64
|
+
typeof value === 'object' &&
|
|
65
|
+
value !== null &&
|
|
66
|
+
'$template' in value &&
|
|
67
|
+
typeof (value as TemplateRef).$template === 'object'
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convert a DataRef to code expression
|
|
73
|
+
*/
|
|
74
|
+
function dataRefToCode(ref: DataRef, itemVar?: string): string {
|
|
75
|
+
if (ref.$ref === 'trigger') {
|
|
76
|
+
// Reference to trigger input (data)
|
|
77
|
+
return ref.path ? `data.${ref.path}` : 'data'
|
|
78
|
+
}
|
|
79
|
+
if (ref.$ref === '$item') {
|
|
80
|
+
// Reference to the current loop item
|
|
81
|
+
// The path contains the variable name in this case
|
|
82
|
+
return ref.path || itemVar || 'item'
|
|
83
|
+
}
|
|
84
|
+
// Reference to a step output variable
|
|
85
|
+
return ref.path ? `${ref.$ref}.${ref.path}` : ref.$ref
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Convert a template ref to template literal code
|
|
90
|
+
*/
|
|
91
|
+
function templateRefToCode(template: TemplateRef, itemVar?: string): string {
|
|
92
|
+
const { parts, expressions } = template.$template
|
|
93
|
+
let result = '`'
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < parts.length; i++) {
|
|
96
|
+
result += parts[i]
|
|
97
|
+
if (i < expressions.length) {
|
|
98
|
+
const expr = expressions[i]
|
|
99
|
+
let exprCode: string
|
|
100
|
+
if (isDataRef(expr)) {
|
|
101
|
+
exprCode = dataRefToCode(expr, itemVar)
|
|
102
|
+
} else if (isTemplateRef(expr)) {
|
|
103
|
+
// Nested template (unlikely but handle it)
|
|
104
|
+
exprCode = templateRefToCode(expr, itemVar)
|
|
105
|
+
} else {
|
|
106
|
+
// Literal value
|
|
107
|
+
exprCode = String(expr)
|
|
108
|
+
}
|
|
109
|
+
result += '${' + exprCode + '}'
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
result += '`'
|
|
114
|
+
return result
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Convert a StateRef to code expression
|
|
119
|
+
*/
|
|
120
|
+
function stateRefToCode(ref: StateRef): string {
|
|
121
|
+
return ref.path ? `${ref.$state}.${ref.path}` : ref.$state
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Convert a single value to code (handles refs, templates, state refs, and literals)
|
|
126
|
+
*/
|
|
127
|
+
function valueToCode(value: unknown, itemVar?: string): string {
|
|
128
|
+
if (isDataRef(value)) {
|
|
129
|
+
return dataRefToCode(value, itemVar)
|
|
130
|
+
}
|
|
131
|
+
if (isStateRef(value)) {
|
|
132
|
+
return stateRefToCode(value)
|
|
133
|
+
}
|
|
134
|
+
if (isTemplateRef(value)) {
|
|
135
|
+
return templateRefToCode(value, itemVar)
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(value)) {
|
|
138
|
+
const elements = value.map((v) => valueToCode(v, itemVar))
|
|
139
|
+
return `[${elements.join(', ')}]`
|
|
140
|
+
}
|
|
141
|
+
if (typeof value === 'object' && value !== null) {
|
|
142
|
+
const entries = Object.entries(value)
|
|
143
|
+
const props = entries.map(([k, v]) => `${k}: ${valueToCode(v, itemVar)}`)
|
|
144
|
+
return `{ ${props.join(', ')} }`
|
|
145
|
+
}
|
|
146
|
+
return JSON.stringify(value)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if input represents passthrough (entire data object)
|
|
151
|
+
*/
|
|
152
|
+
function isPassthrough(input: Record<string, unknown>): boolean {
|
|
153
|
+
if (Object.keys(input).length === 1 && '$passthrough' in input) {
|
|
154
|
+
const passthrough = input.$passthrough
|
|
155
|
+
return isDataRef(passthrough) && passthrough.$ref === 'trigger'
|
|
156
|
+
}
|
|
157
|
+
return false
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Convert input object to code
|
|
162
|
+
*/
|
|
163
|
+
function inputToCode(
|
|
164
|
+
input: Record<string, unknown>,
|
|
165
|
+
indent: string,
|
|
166
|
+
itemVar?: string
|
|
167
|
+
): string {
|
|
168
|
+
// Check if this is a passthrough (entire data object)
|
|
169
|
+
if (isPassthrough(input)) {
|
|
170
|
+
return 'data'
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const entries = Object.entries(input)
|
|
174
|
+
if (entries.length === 0) return '{}'
|
|
175
|
+
|
|
176
|
+
const lines = entries.map(([key, value]) => {
|
|
177
|
+
return `${indent} ${key}: ${valueToCode(value, itemVar)},`
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return `{\n${lines.join('\n')}\n${indent}}`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Convert options to code
|
|
185
|
+
*/
|
|
186
|
+
function optionsToCode(options: Record<string, unknown>): string {
|
|
187
|
+
const parts: string[] = []
|
|
188
|
+
if (options.retries !== undefined) {
|
|
189
|
+
parts.push(`retries: ${options.retries}`)
|
|
190
|
+
}
|
|
191
|
+
if (options.retryDelay !== undefined) {
|
|
192
|
+
parts.push(`retryDelay: '${options.retryDelay}'`)
|
|
193
|
+
}
|
|
194
|
+
return parts.length > 0 ? `{ ${parts.join(', ')} }` : ''
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Convert a simple condition to code expression
|
|
199
|
+
*/
|
|
200
|
+
function conditionToCode(condition: any): string {
|
|
201
|
+
if (!condition) return 'true'
|
|
202
|
+
|
|
203
|
+
if (condition.type === 'simple') {
|
|
204
|
+
return condition.expression
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (condition.type === 'and') {
|
|
208
|
+
const parts = condition.conditions.map(conditionToCode)
|
|
209
|
+
return parts.length > 1 ? `(${parts.join(' && ')})` : parts[0]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (condition.type === 'or') {
|
|
213
|
+
const parts = condition.conditions.map(conditionToCode)
|
|
214
|
+
return parts.length > 1 ? `(${parts.join(' || ')})` : parts[0]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return 'true'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Traverse nodes in execution order starting from entry
|
|
222
|
+
*/
|
|
223
|
+
function traverseNodes(
|
|
224
|
+
nodes: Record<string, SerializedGraphNode>,
|
|
225
|
+
entryNodeIds: string[]
|
|
226
|
+
): SerializedGraphNode[] {
|
|
227
|
+
const result: SerializedGraphNode[] = []
|
|
228
|
+
const visited = new Set<string>()
|
|
229
|
+
|
|
230
|
+
function visit(nodeId: string) {
|
|
231
|
+
if (visited.has(nodeId)) return
|
|
232
|
+
visited.add(nodeId)
|
|
233
|
+
|
|
234
|
+
const node = nodes[nodeId]
|
|
235
|
+
if (!node) return
|
|
236
|
+
|
|
237
|
+
result.push(node)
|
|
238
|
+
|
|
239
|
+
// Follow next pointer
|
|
240
|
+
if ('next' in node && node.next) {
|
|
241
|
+
if (typeof node.next === 'string') {
|
|
242
|
+
visit(node.next)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const entryId of entryNodeIds) {
|
|
248
|
+
visit(entryId)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return result
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Collect conditional variables that need to be declared before a branch
|
|
256
|
+
*/
|
|
257
|
+
function collectBranchConditionalVars(
|
|
258
|
+
branchNode: any,
|
|
259
|
+
nodes: Record<string, SerializedGraphNode>,
|
|
260
|
+
conditionalVars: Set<string>
|
|
261
|
+
): string[] {
|
|
262
|
+
const vars: string[] = []
|
|
263
|
+
|
|
264
|
+
// Check all branches (if/else-if chain)
|
|
265
|
+
if (branchNode.branches) {
|
|
266
|
+
for (const branch of branchNode.branches) {
|
|
267
|
+
if (branch.entry) {
|
|
268
|
+
collectVarsFromBranch(branch.entry, nodes, conditionalVars, vars)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check else branch
|
|
274
|
+
if (branchNode.elseEntry) {
|
|
275
|
+
collectVarsFromBranch(branchNode.elseEntry, nodes, conditionalVars, vars)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return vars
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Recursively collect output variables from a branch that are in conditionalVars
|
|
283
|
+
*/
|
|
284
|
+
function collectVarsFromBranch(
|
|
285
|
+
nodeId: string,
|
|
286
|
+
nodes: Record<string, SerializedGraphNode>,
|
|
287
|
+
conditionalVars: Set<string>,
|
|
288
|
+
result: string[]
|
|
289
|
+
): void {
|
|
290
|
+
const node = nodes[nodeId]
|
|
291
|
+
if (!node) return
|
|
292
|
+
|
|
293
|
+
// Check if this node has an outputVar that's conditional
|
|
294
|
+
if ('outputVar' in node && node.outputVar) {
|
|
295
|
+
const varName = node.outputVar as string
|
|
296
|
+
if (conditionalVars.has(varName) && !result.includes(varName)) {
|
|
297
|
+
result.push(varName)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Follow the chain of nodes within the branch
|
|
302
|
+
if ('next' in node && node.next) {
|
|
303
|
+
const nextId = node.next as string
|
|
304
|
+
// Only follow if it's still within the branch
|
|
305
|
+
if (isWithinBranch(nextId)) {
|
|
306
|
+
collectVarsFromBranch(nextId, nodes, conditionalVars, result)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check if a node ID is still within a branch (not the main flow)
|
|
313
|
+
*/
|
|
314
|
+
function isWithinBranch(nodeId: string): boolean {
|
|
315
|
+
return (
|
|
316
|
+
nodeId.includes('_then_') ||
|
|
317
|
+
nodeId.includes('_else_') ||
|
|
318
|
+
nodeId.includes('_branch')
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Generate code for branch content (then/else blocks)
|
|
324
|
+
*/
|
|
325
|
+
function generateBranchContent(
|
|
326
|
+
entryNodeId: string,
|
|
327
|
+
nodes: Record<string, SerializedGraphNode>,
|
|
328
|
+
indent: string,
|
|
329
|
+
conditionalVars: Set<string>
|
|
330
|
+
): string[] {
|
|
331
|
+
const lines: string[] = []
|
|
332
|
+
let currentId: string | undefined = entryNodeId
|
|
333
|
+
|
|
334
|
+
while (currentId) {
|
|
335
|
+
const node = nodes[currentId]
|
|
336
|
+
if (!node) break
|
|
337
|
+
|
|
338
|
+
const nodeLines = nodeToCode(node, nodes, indent, conditionalVars, true)
|
|
339
|
+
lines.push(...nodeLines)
|
|
340
|
+
|
|
341
|
+
// Follow to next node within the branch
|
|
342
|
+
if ('next' in node && node.next) {
|
|
343
|
+
const nextId = node.next as string
|
|
344
|
+
// Only continue if it's still within the branch
|
|
345
|
+
if (isWithinBranch(nextId)) {
|
|
346
|
+
currentId = nextId
|
|
347
|
+
} else {
|
|
348
|
+
break
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
break
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return lines
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Generate DSL code for a single node
|
|
360
|
+
*/
|
|
361
|
+
function nodeToCode(
|
|
362
|
+
node: SerializedGraphNode,
|
|
363
|
+
nodes: Record<string, SerializedGraphNode>,
|
|
364
|
+
indent: string,
|
|
365
|
+
conditionalVars: Set<string> = new Set(),
|
|
366
|
+
isInsideBranch: boolean = false
|
|
367
|
+
): string[] {
|
|
368
|
+
const lines: string[] = []
|
|
369
|
+
|
|
370
|
+
// Handle RPC nodes (function calls)
|
|
371
|
+
if ('rpcName' in node && node.rpcName && node.rpcName !== 'unknown') {
|
|
372
|
+
const stepName = node.stepName || `Call ${node.rpcName}`
|
|
373
|
+
const input = (node.input || {}) as Record<string, unknown>
|
|
374
|
+
const inputCode = inputToCode(input, indent)
|
|
375
|
+
const outputVar = (node as any).outputVar
|
|
376
|
+
|
|
377
|
+
let doCall = `await workflow.do('${stepName}', '${node.rpcName}', ${inputCode}`
|
|
378
|
+
|
|
379
|
+
// Add options if present
|
|
380
|
+
if ((node as any).options) {
|
|
381
|
+
const optCode = optionsToCode((node as any).options)
|
|
382
|
+
if (optCode) {
|
|
383
|
+
doCall += `, ${optCode}`
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
doCall += ')'
|
|
387
|
+
|
|
388
|
+
if (outputVar) {
|
|
389
|
+
// If this is a conditional var inside a branch, use assignment (let was declared above)
|
|
390
|
+
if (isInsideBranch && conditionalVars.has(outputVar)) {
|
|
391
|
+
lines.push(`${indent}${outputVar} = ${doCall}`)
|
|
392
|
+
} else {
|
|
393
|
+
lines.push(`${indent}const ${outputVar} = ${doCall}`)
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
lines.push(`${indent}${doCall}`)
|
|
397
|
+
}
|
|
398
|
+
lines.push('')
|
|
399
|
+
return lines
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Handle flow nodes
|
|
403
|
+
if ('flow' in node) {
|
|
404
|
+
const flowNode = node as any
|
|
405
|
+
|
|
406
|
+
switch (flowNode.flow) {
|
|
407
|
+
case 'sleep':
|
|
408
|
+
lines.push(
|
|
409
|
+
`${indent}await workflow.sleep('${flowNode.stepName || 'Sleep'}', '${flowNode.duration}')`
|
|
410
|
+
)
|
|
411
|
+
lines.push('')
|
|
412
|
+
break
|
|
413
|
+
|
|
414
|
+
case 'cancel':
|
|
415
|
+
const cancelReason =
|
|
416
|
+
flowNode.reason || flowNode.stepName || 'Workflow cancelled'
|
|
417
|
+
lines.push(
|
|
418
|
+
`${indent}throw new WorkflowCancelledException('${cancelReason}')`
|
|
419
|
+
)
|
|
420
|
+
lines.push('')
|
|
421
|
+
break
|
|
422
|
+
|
|
423
|
+
case 'branch':
|
|
424
|
+
// Declare conditional variables before the if statement
|
|
425
|
+
const branchConditionalVars = collectBranchConditionalVars(
|
|
426
|
+
flowNode,
|
|
427
|
+
nodes,
|
|
428
|
+
conditionalVars
|
|
429
|
+
)
|
|
430
|
+
for (const varName of branchConditionalVars) {
|
|
431
|
+
lines.push(`${indent}let ${varName}`)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Generate if/else-if/else chain
|
|
435
|
+
const branches = flowNode.branches || []
|
|
436
|
+
for (let i = 0; i < branches.length; i++) {
|
|
437
|
+
const branch = branches[i]
|
|
438
|
+
const condition = conditionToCode(branch.condition)
|
|
439
|
+
const keyword = i === 0 ? 'if' : 'else if'
|
|
440
|
+
lines.push(`${indent}${keyword} (${condition}) {`)
|
|
441
|
+
|
|
442
|
+
if (branch.entry && nodes[branch.entry]) {
|
|
443
|
+
const branchLines = generateBranchContent(
|
|
444
|
+
branch.entry,
|
|
445
|
+
nodes,
|
|
446
|
+
indent + ' ',
|
|
447
|
+
conditionalVars
|
|
448
|
+
)
|
|
449
|
+
lines.push(...branchLines)
|
|
450
|
+
}
|
|
451
|
+
lines.push(`${indent}}`)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Generate else block if present
|
|
455
|
+
if (flowNode.elseEntry && nodes[flowNode.elseEntry]) {
|
|
456
|
+
lines.push(`${indent}else {`)
|
|
457
|
+
const elseLines = generateBranchContent(
|
|
458
|
+
flowNode.elseEntry,
|
|
459
|
+
nodes,
|
|
460
|
+
indent + ' ',
|
|
461
|
+
conditionalVars
|
|
462
|
+
)
|
|
463
|
+
lines.push(...elseLines)
|
|
464
|
+
lines.push(`${indent}}`)
|
|
465
|
+
}
|
|
466
|
+
lines.push('')
|
|
467
|
+
break
|
|
468
|
+
|
|
469
|
+
case 'switch':
|
|
470
|
+
lines.push(`${indent}switch (${flowNode.expression}) {`)
|
|
471
|
+
for (const caseItem of flowNode.cases || []) {
|
|
472
|
+
lines.push(`${indent} case '${caseItem.value}':`)
|
|
473
|
+
if (caseItem.entry && nodes[caseItem.entry]) {
|
|
474
|
+
const caseLines = nodeToCode(
|
|
475
|
+
nodes[caseItem.entry],
|
|
476
|
+
nodes,
|
|
477
|
+
indent + ' '
|
|
478
|
+
)
|
|
479
|
+
lines.push(...caseLines)
|
|
480
|
+
}
|
|
481
|
+
lines.push(`${indent} break`)
|
|
482
|
+
}
|
|
483
|
+
if (flowNode.defaultEntry && nodes[flowNode.defaultEntry]) {
|
|
484
|
+
lines.push(`${indent} default:`)
|
|
485
|
+
const defaultLines = nodeToCode(
|
|
486
|
+
nodes[flowNode.defaultEntry],
|
|
487
|
+
nodes,
|
|
488
|
+
indent + ' '
|
|
489
|
+
)
|
|
490
|
+
lines.push(...defaultLines)
|
|
491
|
+
lines.push(`${indent} break`)
|
|
492
|
+
}
|
|
493
|
+
lines.push(`${indent}}`)
|
|
494
|
+
lines.push('')
|
|
495
|
+
break
|
|
496
|
+
|
|
497
|
+
case 'parallel':
|
|
498
|
+
lines.push(`${indent}await Promise.all([`)
|
|
499
|
+
for (const childId of flowNode.children || []) {
|
|
500
|
+
if (nodes[childId]) {
|
|
501
|
+
const childNode = nodes[childId]
|
|
502
|
+
if ('rpcName' in childNode && childNode.rpcName) {
|
|
503
|
+
const stepName = childNode.stepName || `Call ${childNode.rpcName}`
|
|
504
|
+
const input = (childNode.input || {}) as Record<string, unknown>
|
|
505
|
+
const inputCode = inputToCode(input, indent + ' ')
|
|
506
|
+
lines.push(
|
|
507
|
+
`${indent} workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode}),`
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
lines.push(`${indent}])`)
|
|
513
|
+
lines.push('')
|
|
514
|
+
break
|
|
515
|
+
|
|
516
|
+
case 'fanout':
|
|
517
|
+
if (flowNode.mode === 'parallel') {
|
|
518
|
+
lines.push(`${indent}await Promise.all(`)
|
|
519
|
+
lines.push(
|
|
520
|
+
`${indent} ${flowNode.sourceVar}.map(async (${flowNode.itemVar}) =>`
|
|
521
|
+
)
|
|
522
|
+
if (flowNode.childEntry && nodes[flowNode.childEntry]) {
|
|
523
|
+
const childNode = nodes[flowNode.childEntry]
|
|
524
|
+
if ('rpcName' in childNode && childNode.rpcName) {
|
|
525
|
+
const stepName = childNode.stepName || `Call ${childNode.rpcName}`
|
|
526
|
+
const input = (childNode.input || {}) as Record<string, unknown>
|
|
527
|
+
const inputCode = inputToCode(
|
|
528
|
+
input,
|
|
529
|
+
indent + ' ',
|
|
530
|
+
flowNode.itemVar
|
|
531
|
+
)
|
|
532
|
+
lines.push(
|
|
533
|
+
`${indent} await workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode})`
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
lines.push(`${indent} )`)
|
|
538
|
+
lines.push(`${indent})`)
|
|
539
|
+
} else {
|
|
540
|
+
// Sequential fanout
|
|
541
|
+
lines.push(
|
|
542
|
+
`${indent}for (const ${flowNode.itemVar} of ${flowNode.sourceVar}) {`
|
|
543
|
+
)
|
|
544
|
+
if (flowNode.childEntry && nodes[flowNode.childEntry]) {
|
|
545
|
+
const childNode = nodes[flowNode.childEntry]
|
|
546
|
+
if ('rpcName' in childNode && childNode.rpcName) {
|
|
547
|
+
const stepName = childNode.stepName || `Call ${childNode.rpcName}`
|
|
548
|
+
const input = (childNode.input || {}) as Record<string, unknown>
|
|
549
|
+
const inputCode = inputToCode(
|
|
550
|
+
input,
|
|
551
|
+
indent + ' ',
|
|
552
|
+
flowNode.itemVar
|
|
553
|
+
)
|
|
554
|
+
lines.push(
|
|
555
|
+
`${indent} await workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode})`
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (flowNode.timeBetween) {
|
|
560
|
+
lines.push(
|
|
561
|
+
`${indent} await workflow.sleep('Wait between iterations', '${flowNode.timeBetween}')`
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
lines.push(`${indent}}`)
|
|
565
|
+
}
|
|
566
|
+
lines.push('')
|
|
567
|
+
break
|
|
568
|
+
|
|
569
|
+
case 'filter':
|
|
570
|
+
lines.push(
|
|
571
|
+
`${indent}const ${flowNode.outputVar} = ${flowNode.sourceVar}.filter((${flowNode.itemVar}) => ${conditionToCode(flowNode.condition)})`
|
|
572
|
+
)
|
|
573
|
+
lines.push('')
|
|
574
|
+
break
|
|
575
|
+
|
|
576
|
+
case 'arrayPredicate':
|
|
577
|
+
const method = flowNode.mode === 'some' ? 'some' : 'every'
|
|
578
|
+
lines.push(
|
|
579
|
+
`${indent}const ${flowNode.outputVar} = ${flowNode.sourceVar}.${method}((${flowNode.itemVar}) => ${conditionToCode(flowNode.condition)})`
|
|
580
|
+
)
|
|
581
|
+
lines.push('')
|
|
582
|
+
break
|
|
583
|
+
|
|
584
|
+
case 'return':
|
|
585
|
+
if (flowNode.outputs) {
|
|
586
|
+
const returnObj: string[] = []
|
|
587
|
+
for (const [key, output] of Object.entries(
|
|
588
|
+
flowNode.outputs as Record<string, any>
|
|
589
|
+
)) {
|
|
590
|
+
let value: string
|
|
591
|
+
if (output.from === 'outputVar') {
|
|
592
|
+
value = output.path
|
|
593
|
+
? `${output.name}?.${output.path}`
|
|
594
|
+
: output.name
|
|
595
|
+
} else if (output.from === 'stateVar') {
|
|
596
|
+
value = output.path
|
|
597
|
+
? `${output.name}.${output.path}`
|
|
598
|
+
: output.name
|
|
599
|
+
} else if (output.from === 'input') {
|
|
600
|
+
value = `data.${output.path}`
|
|
601
|
+
} else if (output.from === 'literal') {
|
|
602
|
+
value = JSON.stringify(output.value)
|
|
603
|
+
} else if (output.from === 'expression') {
|
|
604
|
+
value = output.expression
|
|
605
|
+
} else {
|
|
606
|
+
continue
|
|
607
|
+
}
|
|
608
|
+
returnObj.push(`${indent} ${key}: ${value},`)
|
|
609
|
+
}
|
|
610
|
+
if (returnObj.length > 0) {
|
|
611
|
+
lines.push(`${indent}return {`)
|
|
612
|
+
lines.push(...returnObj)
|
|
613
|
+
lines.push(`${indent}}`)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
break
|
|
617
|
+
|
|
618
|
+
case 'inline':
|
|
619
|
+
lines.push(
|
|
620
|
+
`${indent}// Inline step: ${flowNode.stepName || 'Dynamic code'}`
|
|
621
|
+
)
|
|
622
|
+
lines.push(`${indent}// ${flowNode.description || '<dynamic code>'}`)
|
|
623
|
+
lines.push('')
|
|
624
|
+
break
|
|
625
|
+
|
|
626
|
+
case 'set':
|
|
627
|
+
// Generate variable assignment: varName = value
|
|
628
|
+
const setVar = flowNode.variable
|
|
629
|
+
const setValue =
|
|
630
|
+
typeof flowNode.value === 'string'
|
|
631
|
+
? `'${flowNode.value}'`
|
|
632
|
+
: JSON.stringify(flowNode.value)
|
|
633
|
+
lines.push(`${indent}${setVar} = ${setValue}`)
|
|
634
|
+
lines.push('')
|
|
635
|
+
break
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return lines
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Find variables that are defined inside branches but used in return statements
|
|
644
|
+
* These need to be hoisted with `let` declarations
|
|
645
|
+
*/
|
|
646
|
+
function findConditionalVars(
|
|
647
|
+
nodes: Record<string, SerializedGraphNode>
|
|
648
|
+
): Set<string> {
|
|
649
|
+
const conditionalVars = new Set<string>()
|
|
650
|
+
const varsInBranches = new Set<string>()
|
|
651
|
+
const varsUsedInReturn = new Set<string>()
|
|
652
|
+
|
|
653
|
+
// Collect variables defined in branches (then/else/case/default nodes)
|
|
654
|
+
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
655
|
+
if (
|
|
656
|
+
nodeId.includes('_then_') ||
|
|
657
|
+
nodeId.includes('_else_') ||
|
|
658
|
+
nodeId.includes('_case') ||
|
|
659
|
+
nodeId.includes('_default_')
|
|
660
|
+
) {
|
|
661
|
+
if ('outputVar' in node && node.outputVar) {
|
|
662
|
+
varsInBranches.add(node.outputVar as string)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Collect variables used in return statements
|
|
668
|
+
for (const node of Object.values(nodes)) {
|
|
669
|
+
if ('flow' in node && node.flow === 'return' && node.outputs) {
|
|
670
|
+
for (const output of Object.values(
|
|
671
|
+
node.outputs as Record<string, { from: string; name?: string }>
|
|
672
|
+
)) {
|
|
673
|
+
if (output.from === 'outputVar' && output.name) {
|
|
674
|
+
varsUsedInReturn.add(output.name)
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Variables that are both in branches and used in return need hoisting
|
|
681
|
+
for (const varName of varsInBranches) {
|
|
682
|
+
if (varsUsedInReturn.has(varName)) {
|
|
683
|
+
conditionalVars.add(varName)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return conditionalVars
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Get default value for a context variable type
|
|
692
|
+
*/
|
|
693
|
+
function getDefaultForType(type: string): string {
|
|
694
|
+
switch (type) {
|
|
695
|
+
case 'string':
|
|
696
|
+
return "''"
|
|
697
|
+
case 'number':
|
|
698
|
+
return '0'
|
|
699
|
+
case 'boolean':
|
|
700
|
+
return 'false'
|
|
701
|
+
case 'array':
|
|
702
|
+
return '[]'
|
|
703
|
+
case 'object':
|
|
704
|
+
return '{}'
|
|
705
|
+
default:
|
|
706
|
+
return 'undefined'
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Deserialize a workflow graph to DSL code
|
|
712
|
+
*/
|
|
713
|
+
export function deserializeDslWorkflow(
|
|
714
|
+
workflow: SerializedWorkflowGraph,
|
|
715
|
+
options: DeserializeOptions = {}
|
|
716
|
+
): string {
|
|
717
|
+
const { pikkuImportPath = '../.pikku/workflow/pikku-workflow-types.gen.js' } =
|
|
718
|
+
options
|
|
719
|
+
|
|
720
|
+
const lines: string[] = []
|
|
721
|
+
|
|
722
|
+
// Check if workflow has any cancel nodes
|
|
723
|
+
const hasCancelNode = Object.values(workflow.nodes).some(
|
|
724
|
+
(node) => 'flow' in node && (node as any).flow === 'cancel'
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
// Find variables defined in branches that need hoisting
|
|
728
|
+
const conditionalVars = findConditionalVars(workflow.nodes)
|
|
729
|
+
|
|
730
|
+
// Import statement
|
|
731
|
+
if (hasCancelNode) {
|
|
732
|
+
lines.push(
|
|
733
|
+
`import { pikkuWorkflowFunc, WorkflowCancelledException } from '${pikkuImportPath}'`
|
|
734
|
+
)
|
|
735
|
+
} else {
|
|
736
|
+
lines.push(`import { pikkuWorkflowFunc } from '${pikkuImportPath}'`)
|
|
737
|
+
}
|
|
738
|
+
lines.push('')
|
|
739
|
+
|
|
740
|
+
// Add description as comment if present
|
|
741
|
+
if (workflow.description) {
|
|
742
|
+
lines.push(`/**`)
|
|
743
|
+
lines.push(` * ${workflow.description}`)
|
|
744
|
+
lines.push(` */`)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Function signature
|
|
748
|
+
const tagsComment = workflow.tags?.length
|
|
749
|
+
? ` // tags: ${workflow.tags.join(', ')}`
|
|
750
|
+
: ''
|
|
751
|
+
|
|
752
|
+
lines.push(
|
|
753
|
+
`export const ${workflow.name} = pikkuWorkflowFunc(async ({}, data, { workflow }) => {${tagsComment}`
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
// Generate context variable declarations at the top
|
|
757
|
+
if (workflow.context && Object.keys(workflow.context).length > 0) {
|
|
758
|
+
for (const [varName, varDef] of Object.entries(workflow.context) as [
|
|
759
|
+
string,
|
|
760
|
+
ContextVariable,
|
|
761
|
+
][]) {
|
|
762
|
+
const defaultValue =
|
|
763
|
+
varDef.default !== undefined
|
|
764
|
+
? typeof varDef.default === 'string'
|
|
765
|
+
? `'${varDef.default}'`
|
|
766
|
+
: JSON.stringify(varDef.default)
|
|
767
|
+
: getDefaultForType(varDef.type)
|
|
768
|
+
lines.push(` let ${varName} = ${defaultValue}`)
|
|
769
|
+
}
|
|
770
|
+
lines.push('')
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Process nodes in order
|
|
774
|
+
const orderedNodes = traverseNodes(workflow.nodes, workflow.entryNodeIds)
|
|
775
|
+
|
|
776
|
+
for (const node of orderedNodes) {
|
|
777
|
+
// Skip child nodes that are processed as part of their parent
|
|
778
|
+
if (
|
|
779
|
+
node.nodeId.includes('_then_') ||
|
|
780
|
+
node.nodeId.includes('_else_') ||
|
|
781
|
+
node.nodeId.includes('_case') ||
|
|
782
|
+
node.nodeId.includes('_default_') ||
|
|
783
|
+
node.nodeId.includes('_child_') ||
|
|
784
|
+
node.nodeId.includes('_item_')
|
|
785
|
+
) {
|
|
786
|
+
continue
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const nodeLines = nodeToCode(node, workflow.nodes, ' ', conditionalVars)
|
|
790
|
+
lines.push(...nodeLines)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
lines.push('})')
|
|
794
|
+
lines.push('')
|
|
795
|
+
|
|
796
|
+
return lines.join('\n')
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Convert a DataRef to graph ref() call
|
|
801
|
+
* @param ref - The data reference
|
|
802
|
+
* @param outputVarToNodeId - Map from outputVar names to node IDs
|
|
803
|
+
*/
|
|
804
|
+
function dataRefToGraphRef(
|
|
805
|
+
ref: DataRef,
|
|
806
|
+
outputVarToNodeId: Map<string, string>
|
|
807
|
+
): string {
|
|
808
|
+
// Convert outputVar reference to nodeId reference
|
|
809
|
+
const nodeId = outputVarToNodeId.get(ref.$ref) || ref.$ref
|
|
810
|
+
if (ref.path) {
|
|
811
|
+
return `ref('${nodeId}', '${ref.path}')`
|
|
812
|
+
}
|
|
813
|
+
return `ref('${nodeId}')`
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Convert a template ref to template() function call for graph code
|
|
818
|
+
* e.g. {$template: {parts: ["Hello ", ""], expressions: [{$ref: "trigger", path: "name"}]}}
|
|
819
|
+
* becomes: template('Hello $0', [ref('trigger', 'name')])
|
|
820
|
+
*/
|
|
821
|
+
function templateRefToGraphCode(
|
|
822
|
+
tmpl: TemplateRef,
|
|
823
|
+
outputVarToNodeId: Map<string, string>
|
|
824
|
+
): string {
|
|
825
|
+
const { parts, expressions } = tmpl.$template
|
|
826
|
+
|
|
827
|
+
// Build the template string with $0, $1, etc. placeholders
|
|
828
|
+
let templateStr = ''
|
|
829
|
+
for (let i = 0; i < parts.length; i++) {
|
|
830
|
+
templateStr += parts[i]
|
|
831
|
+
if (i < expressions.length) {
|
|
832
|
+
templateStr += `$${i}`
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Build the refs array
|
|
837
|
+
const refs: string[] = []
|
|
838
|
+
for (const expr of expressions) {
|
|
839
|
+
if (isDataRef(expr)) {
|
|
840
|
+
refs.push(dataRefToGraphRef(expr, outputVarToNodeId))
|
|
841
|
+
} else {
|
|
842
|
+
// Literal JS expression - can't be represented as a typed ref
|
|
843
|
+
refs.push(`{ $ref: '${String(expr).replace(/'/g, "\\'")}' } as any`)
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Escape single quotes and newlines in the template string
|
|
848
|
+
templateStr = templateStr
|
|
849
|
+
.replace(/\\/g, '\\\\')
|
|
850
|
+
.replace(/'/g, "\\'")
|
|
851
|
+
.replace(/\n/g, '\\n')
|
|
852
|
+
.replace(/\r/g, '\\r')
|
|
853
|
+
|
|
854
|
+
return `template('${templateStr}', [${refs.join(', ')}])`
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function valueToGraphCode(
|
|
858
|
+
value: unknown,
|
|
859
|
+
outputVarToNodeId: Map<string, string>,
|
|
860
|
+
refTracker: { hasRefs: boolean }
|
|
861
|
+
): string {
|
|
862
|
+
if (isDataRef(value)) {
|
|
863
|
+
refTracker.hasRefs = true
|
|
864
|
+
return dataRefToGraphRef(value, outputVarToNodeId)
|
|
865
|
+
}
|
|
866
|
+
if (isTemplateRef(value)) {
|
|
867
|
+
refTracker.hasRefs = true
|
|
868
|
+
return templateRefToGraphCode(value, outputVarToNodeId)
|
|
869
|
+
}
|
|
870
|
+
if (Array.isArray(value)) {
|
|
871
|
+
const elements = value.map((v) =>
|
|
872
|
+
valueToGraphCode(v, outputVarToNodeId, refTracker)
|
|
873
|
+
)
|
|
874
|
+
return `[${elements.join(', ')}]`
|
|
875
|
+
}
|
|
876
|
+
if (typeof value === 'object' && value !== null) {
|
|
877
|
+
const entries = Object.entries(value)
|
|
878
|
+
const props = entries.map(
|
|
879
|
+
([k, v]) => `${k}: ${valueToGraphCode(v, outputVarToNodeId, refTracker)}`
|
|
880
|
+
)
|
|
881
|
+
return `{ ${props.join(', ')} }`
|
|
882
|
+
}
|
|
883
|
+
return JSON.stringify(value)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Convert input object to graph input code using ref()
|
|
888
|
+
* @param input - The input mapping
|
|
889
|
+
* @param outputVarToNodeId - Map from outputVar names to node IDs
|
|
890
|
+
*/
|
|
891
|
+
function inputToGraphCode(
|
|
892
|
+
input: Record<string, unknown>,
|
|
893
|
+
outputVarToNodeId: Map<string, string>
|
|
894
|
+
): {
|
|
895
|
+
hasRefs: boolean
|
|
896
|
+
code: string
|
|
897
|
+
} {
|
|
898
|
+
const entries = Object.entries(input)
|
|
899
|
+
if (entries.length === 0) return { hasRefs: false, code: '{}' }
|
|
900
|
+
|
|
901
|
+
const refTracker = { hasRefs: false }
|
|
902
|
+
const lines = entries.map(([key, value]) => {
|
|
903
|
+
return ` ${key}: ${valueToGraphCode(value, outputVarToNodeId, refTracker)},`
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
return {
|
|
907
|
+
hasRefs: refTracker.hasRefs,
|
|
908
|
+
code: `{\n${lines.join('\n')}\n }`,
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Check if a node is a flow node (non-RPC control flow)
|
|
914
|
+
*/
|
|
915
|
+
function isFlowNode(node: any): boolean {
|
|
916
|
+
return 'flow' in node
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Follow through flow nodes to find the next RPC node
|
|
921
|
+
* This traverses the 'next' chain, skipping flow nodes until finding an RPC node
|
|
922
|
+
*/
|
|
923
|
+
function findNextRpcNode(
|
|
924
|
+
startNextId: string,
|
|
925
|
+
nodes: Record<string, SerializedGraphNode>,
|
|
926
|
+
flowNodeIds: Set<string>,
|
|
927
|
+
visited: Set<string> = new Set()
|
|
928
|
+
): string | null {
|
|
929
|
+
if (visited.has(startNextId)) {
|
|
930
|
+
return null // Cycle detected, stop
|
|
931
|
+
}
|
|
932
|
+
visited.add(startNextId)
|
|
933
|
+
|
|
934
|
+
// If it's not a flow node, we found our target
|
|
935
|
+
if (!flowNodeIds.has(startNextId)) {
|
|
936
|
+
// Make sure the node exists and has an rpcName (is an RPC node)
|
|
937
|
+
const node = nodes[startNextId]
|
|
938
|
+
if (node && 'rpcName' in node) {
|
|
939
|
+
return startNextId
|
|
940
|
+
}
|
|
941
|
+
return null
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// It's a flow node - follow its 'next' if it has one
|
|
945
|
+
const flowNode = nodes[startNextId]
|
|
946
|
+
if (flowNode && 'next' in flowNode && flowNode.next) {
|
|
947
|
+
return findNextRpcNode(flowNode.next as string, nodes, flowNodeIds, visited)
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return null
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Deserialize a graph workflow to pikkuWorkflowGraph code
|
|
955
|
+
*/
|
|
956
|
+
export function deserializeGraphWorkflow(
|
|
957
|
+
workflow: SerializedWorkflowGraph,
|
|
958
|
+
options: DeserializeOptions = {}
|
|
959
|
+
): string {
|
|
960
|
+
const { pikkuImportPath = '../.pikku/workflow/pikku-workflow-types.gen.js' } =
|
|
961
|
+
options
|
|
962
|
+
|
|
963
|
+
const lines: string[] = []
|
|
964
|
+
|
|
965
|
+
// Import statement
|
|
966
|
+
lines.push(`import { pikkuWorkflowGraph } from '${pikkuImportPath}'`)
|
|
967
|
+
lines.push('')
|
|
968
|
+
|
|
969
|
+
// Add description as comment if present
|
|
970
|
+
if (workflow.description) {
|
|
971
|
+
lines.push(`/**`)
|
|
972
|
+
lines.push(` * ${workflow.description}`)
|
|
973
|
+
lines.push(` */`)
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Identify flow nodes (non-RPC nodes like return, sleep, branch)
|
|
977
|
+
const flowNodeIds = new Set<string>()
|
|
978
|
+
for (const [nodeId, node] of Object.entries(workflow.nodes)) {
|
|
979
|
+
if (isFlowNode(node)) {
|
|
980
|
+
flowNodeIds.add(nodeId)
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Build outputVar to nodeId mapping (for resolving variable references to node IDs)
|
|
985
|
+
const outputVarToNodeId = new Map<string, string>()
|
|
986
|
+
for (const [nodeId, node] of Object.entries(workflow.nodes)) {
|
|
987
|
+
if ('outputVar' in node && typeof node.outputVar === 'string') {
|
|
988
|
+
outputVarToNodeId.set(node.outputVar, nodeId)
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Build node to RPC mapping (only RPC nodes, not flow nodes)
|
|
993
|
+
const nodeRpcMap: Record<string, string> = {}
|
|
994
|
+
for (const [nodeId, node] of Object.entries(workflow.nodes)) {
|
|
995
|
+
if (
|
|
996
|
+
'rpcName' in node &&
|
|
997
|
+
typeof node.rpcName === 'string' &&
|
|
998
|
+
node.rpcName !== 'unknown'
|
|
999
|
+
) {
|
|
1000
|
+
nodeRpcMap[nodeId] = node.rpcName
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Build node configurations (only for RPC nodes)
|
|
1005
|
+
const nodeConfigs: string[] = []
|
|
1006
|
+
|
|
1007
|
+
for (const [nodeId, node] of Object.entries(workflow.nodes)) {
|
|
1008
|
+
// Skip flow nodes - they can't be represented in pikkuWorkflowGraph
|
|
1009
|
+
if (flowNodeIds.has(nodeId)) {
|
|
1010
|
+
continue
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const configParts: string[] = []
|
|
1014
|
+
|
|
1015
|
+
// Add next if present - follow through flow nodes to find the actual next RPC node
|
|
1016
|
+
if ('next' in node && node.next) {
|
|
1017
|
+
const nextId = node.next as string
|
|
1018
|
+
// If next points to a flow node, follow through to find the next RPC node
|
|
1019
|
+
const actualNextId = flowNodeIds.has(nextId)
|
|
1020
|
+
? findNextRpcNode(nextId, workflow.nodes, flowNodeIds)
|
|
1021
|
+
: nextId
|
|
1022
|
+
// Only add if we found a valid next RPC node
|
|
1023
|
+
if (actualNextId && !flowNodeIds.has(actualNextId)) {
|
|
1024
|
+
configParts.push(`next: '${actualNextId}'`)
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Add input if present
|
|
1029
|
+
// Always use callback form to avoid excess property checking in TypeScript
|
|
1030
|
+
if ('input' in node && node.input) {
|
|
1031
|
+
const input = node.input as Record<string, unknown>
|
|
1032
|
+
if (Object.keys(input).length > 0) {
|
|
1033
|
+
const { hasRefs, code } = inputToGraphCode(input, outputVarToNodeId)
|
|
1034
|
+
if (hasRefs) {
|
|
1035
|
+
// Always pass both ref and template for consistent type signature
|
|
1036
|
+
configParts.push(`input: (ref, template) => (${code})`)
|
|
1037
|
+
} else {
|
|
1038
|
+
// Wrap in callback to avoid TypeScript excess property checking
|
|
1039
|
+
configParts.push(`input: () => (${code})`)
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (configParts.length > 0) {
|
|
1045
|
+
nodeConfigs.push(
|
|
1046
|
+
` ${nodeId}: {\n ${configParts.join(',\n ')},\n }`
|
|
1047
|
+
)
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Generate the pikkuWorkflowGraph call (builds graph and registers with core)
|
|
1052
|
+
lines.push(`export const ${workflow.name} = pikkuWorkflowGraph({`)
|
|
1053
|
+
lines.push(` name: '${workflow.name}',`)
|
|
1054
|
+
if (workflow.description) {
|
|
1055
|
+
lines.push(` description: '${workflow.description}',`)
|
|
1056
|
+
}
|
|
1057
|
+
if (workflow.tags && workflow.tags.length > 0) {
|
|
1058
|
+
lines.push(` tags: [${workflow.tags.map((t) => `'${t}'`).join(', ')}],`)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Generate nodes (RPC mapping)
|
|
1062
|
+
const rpcMapEntries = Object.entries(nodeRpcMap)
|
|
1063
|
+
if (rpcMapEntries.length > 0) {
|
|
1064
|
+
lines.push(` nodes: {`)
|
|
1065
|
+
for (const [nodeId, rpcName] of rpcMapEntries) {
|
|
1066
|
+
lines.push(` ${nodeId}: '${rpcName}',`)
|
|
1067
|
+
}
|
|
1068
|
+
lines.push(` },`)
|
|
1069
|
+
} else {
|
|
1070
|
+
lines.push(` nodes: {},`)
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Generate config (node configurations)
|
|
1074
|
+
if (nodeConfigs.length > 0) {
|
|
1075
|
+
lines.push(` config: {`)
|
|
1076
|
+
lines.push(nodeConfigs.join(',\n'))
|
|
1077
|
+
lines.push(` },`)
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
lines.push(`})`)
|
|
1081
|
+
lines.push('')
|
|
1082
|
+
|
|
1083
|
+
return lines.join('\n')
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Deserialize all workflows from JSON to DSL code
|
|
1088
|
+
*/
|
|
1089
|
+
export function deserializeAllDslWorkflows(
|
|
1090
|
+
workflows: Record<string, SerializedWorkflowGraph>,
|
|
1091
|
+
options: DeserializeOptions = {}
|
|
1092
|
+
): Record<string, string> {
|
|
1093
|
+
const result: Record<string, string> = {}
|
|
1094
|
+
|
|
1095
|
+
for (const [name, workflow] of Object.entries(workflows)) {
|
|
1096
|
+
if (workflow.source === 'dsl') {
|
|
1097
|
+
result[name] = deserializeDslWorkflow(workflow, options)
|
|
1098
|
+
} else if (workflow.source === 'graph') {
|
|
1099
|
+
result[name] = deserializeGraphWorkflow(workflow, options)
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return result
|
|
1104
|
+
}
|