@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,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts DSL (Domain Specific Language) step-based format to graph node format
|
|
3
|
+
*/
|
|
4
|
+
import type { WorkflowStepMeta, WorkflowsMeta } from '@pikku/core/workflow'
|
|
5
|
+
import type {
|
|
6
|
+
SerializedGraphNode,
|
|
7
|
+
SerializedWorkflowGraph,
|
|
8
|
+
FunctionNode,
|
|
9
|
+
FlowNode,
|
|
10
|
+
DataRef,
|
|
11
|
+
} from './workflow-graph.types.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if a node is a terminal flow (no next step should follow)
|
|
15
|
+
*/
|
|
16
|
+
function isTerminalFlow(node: SerializedGraphNode): boolean {
|
|
17
|
+
if ('flow' in node) {
|
|
18
|
+
// Cancel and return are terminal flows - they end execution
|
|
19
|
+
return node.flow === 'cancel' || node.flow === 'return'
|
|
20
|
+
}
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Convert InputSource to DataRef
|
|
26
|
+
*/
|
|
27
|
+
function convertInputSource(source: {
|
|
28
|
+
from: string
|
|
29
|
+
path?: string
|
|
30
|
+
name?: string
|
|
31
|
+
value?: unknown
|
|
32
|
+
parts?: string[]
|
|
33
|
+
expressions?: unknown[]
|
|
34
|
+
}): unknown | DataRef {
|
|
35
|
+
if (source.from === 'literal') {
|
|
36
|
+
return source.value
|
|
37
|
+
}
|
|
38
|
+
if (source.from === 'input') {
|
|
39
|
+
return { $ref: 'trigger', path: source.path }
|
|
40
|
+
}
|
|
41
|
+
if (source.from === 'outputVar') {
|
|
42
|
+
return { $ref: source.name!, path: source.path }
|
|
43
|
+
}
|
|
44
|
+
if (source.from === 'item') {
|
|
45
|
+
return { $ref: '$item', path: source.path }
|
|
46
|
+
}
|
|
47
|
+
if (source.from === 'stateVar') {
|
|
48
|
+
return { $state: source.name!, path: source.path }
|
|
49
|
+
}
|
|
50
|
+
if (source.from === 'template') {
|
|
51
|
+
return {
|
|
52
|
+
$template: {
|
|
53
|
+
parts: source.parts,
|
|
54
|
+
expressions: source.expressions?.map((expr) =>
|
|
55
|
+
convertInputSource(expr as any)
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return source.value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Convert a single DSL step to graph node(s)
|
|
65
|
+
*/
|
|
66
|
+
function convertStepToNode(
|
|
67
|
+
step: WorkflowStepMeta,
|
|
68
|
+
index: number,
|
|
69
|
+
steps: WorkflowStepMeta[],
|
|
70
|
+
nodeIdPrefix: string = 'step'
|
|
71
|
+
): SerializedGraphNode[] {
|
|
72
|
+
const nodeId = `${nodeIdPrefix}_${index}`
|
|
73
|
+
const nextNodeId =
|
|
74
|
+
index < steps.length - 1 ? `${nodeIdPrefix}_${index + 1}` : undefined
|
|
75
|
+
|
|
76
|
+
switch (step.type) {
|
|
77
|
+
case 'rpc': {
|
|
78
|
+
const node: FunctionNode = {
|
|
79
|
+
nodeId,
|
|
80
|
+
rpcName: step.rpcName,
|
|
81
|
+
stepName: step.stepName,
|
|
82
|
+
next: nextNodeId,
|
|
83
|
+
}
|
|
84
|
+
if (step.inputs) {
|
|
85
|
+
if (step.inputs === 'passthrough') {
|
|
86
|
+
// Entire data is passed through - store as reference to trigger
|
|
87
|
+
node.input = { $passthrough: { $ref: 'trigger' } }
|
|
88
|
+
} else {
|
|
89
|
+
node.input = {}
|
|
90
|
+
for (const [key, source] of Object.entries(step.inputs)) {
|
|
91
|
+
node.input[key] = convertInputSource(source as any)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (step.outputVar) {
|
|
96
|
+
node.outputVar = step.outputVar
|
|
97
|
+
}
|
|
98
|
+
if (step.options) {
|
|
99
|
+
node.options = {
|
|
100
|
+
retries: step.options.retries,
|
|
101
|
+
retryDelay: step.options.retryDelay?.toString(),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return [node]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'sleep': {
|
|
108
|
+
const node: FlowNode = {
|
|
109
|
+
nodeId,
|
|
110
|
+
flow: 'sleep',
|
|
111
|
+
stepName: step.stepName,
|
|
112
|
+
duration: step.duration,
|
|
113
|
+
next: nextNodeId,
|
|
114
|
+
}
|
|
115
|
+
return [node]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case 'inline': {
|
|
119
|
+
const node: FlowNode = {
|
|
120
|
+
nodeId,
|
|
121
|
+
flow: 'inline',
|
|
122
|
+
stepName: step.stepName,
|
|
123
|
+
description: step.description,
|
|
124
|
+
next: nextNodeId,
|
|
125
|
+
}
|
|
126
|
+
return [node]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
case 'branch': {
|
|
130
|
+
// Convert all branch conditions (if/else-if chain)
|
|
131
|
+
const branchNodes: SerializedGraphNode[] = []
|
|
132
|
+
const branches: Array<{
|
|
133
|
+
condition: unknown
|
|
134
|
+
entry: string
|
|
135
|
+
}> = []
|
|
136
|
+
|
|
137
|
+
for (let i = 0; i < step.branches.length; i++) {
|
|
138
|
+
const branchSteps = convertStepsToNodes(
|
|
139
|
+
step.branches[i].steps,
|
|
140
|
+
`${nodeId}_branch${i}`
|
|
141
|
+
)
|
|
142
|
+
if (branchSteps.length > 0) {
|
|
143
|
+
branches.push({
|
|
144
|
+
condition: step.branches[i].condition,
|
|
145
|
+
entry: branchSteps[0].nodeId,
|
|
146
|
+
})
|
|
147
|
+
// Link last branch node back to next (unless terminal flow)
|
|
148
|
+
if (nextNodeId) {
|
|
149
|
+
const lastBranch = branchSteps[branchSteps.length - 1]
|
|
150
|
+
if (!lastBranch.next && !isTerminalFlow(lastBranch))
|
|
151
|
+
lastBranch.next = nextNodeId
|
|
152
|
+
}
|
|
153
|
+
branchNodes.push(...branchSteps)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Convert else branch
|
|
158
|
+
const elseNodes = step.elseSteps
|
|
159
|
+
? convertStepsToNodes(step.elseSteps, `${nodeId}_else`)
|
|
160
|
+
: []
|
|
161
|
+
if (elseNodes.length > 0 && nextNodeId) {
|
|
162
|
+
const lastElse = elseNodes[elseNodes.length - 1]
|
|
163
|
+
if (!lastElse.next && !isTerminalFlow(lastElse))
|
|
164
|
+
lastElse.next = nextNodeId
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const node: FlowNode = {
|
|
168
|
+
nodeId,
|
|
169
|
+
flow: 'branch',
|
|
170
|
+
branches,
|
|
171
|
+
elseEntry: elseNodes.length > 0 ? elseNodes[0].nodeId : undefined,
|
|
172
|
+
next: nextNodeId,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return [node, ...branchNodes, ...elseNodes]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case 'switch': {
|
|
179
|
+
const caseNodes: SerializedGraphNode[] = []
|
|
180
|
+
const cases: Array<{
|
|
181
|
+
value?: unknown
|
|
182
|
+
expression?: string
|
|
183
|
+
entry: string
|
|
184
|
+
}> = []
|
|
185
|
+
|
|
186
|
+
for (let i = 0; i < step.cases.length; i++) {
|
|
187
|
+
const caseSteps = convertStepsToNodes(
|
|
188
|
+
step.cases[i].steps,
|
|
189
|
+
`${nodeId}_case${i}`
|
|
190
|
+
)
|
|
191
|
+
if (caseSteps.length > 0) {
|
|
192
|
+
cases.push({
|
|
193
|
+
value: step.cases[i].value,
|
|
194
|
+
expression: step.cases[i].expression,
|
|
195
|
+
entry: caseSteps[0].nodeId,
|
|
196
|
+
})
|
|
197
|
+
// Link last case node to next (unless terminal flow)
|
|
198
|
+
if (nextNodeId) {
|
|
199
|
+
const lastCase = caseSteps[caseSteps.length - 1]
|
|
200
|
+
if (!lastCase.next && !isTerminalFlow(lastCase))
|
|
201
|
+
lastCase.next = nextNodeId
|
|
202
|
+
}
|
|
203
|
+
caseNodes.push(...caseSteps)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let defaultEntry: string | undefined
|
|
208
|
+
if (step.defaultSteps) {
|
|
209
|
+
const defaultNodes = convertStepsToNodes(
|
|
210
|
+
step.defaultSteps,
|
|
211
|
+
`${nodeId}_default`
|
|
212
|
+
)
|
|
213
|
+
if (defaultNodes.length > 0) {
|
|
214
|
+
defaultEntry = defaultNodes[0].nodeId
|
|
215
|
+
// Link last default node to next (unless terminal flow)
|
|
216
|
+
if (nextNodeId) {
|
|
217
|
+
const lastDefault = defaultNodes[defaultNodes.length - 1]
|
|
218
|
+
if (!lastDefault.next && !isTerminalFlow(lastDefault))
|
|
219
|
+
lastDefault.next = nextNodeId
|
|
220
|
+
}
|
|
221
|
+
caseNodes.push(...defaultNodes)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const node: FlowNode = {
|
|
226
|
+
nodeId,
|
|
227
|
+
flow: 'switch',
|
|
228
|
+
expression: step.expression,
|
|
229
|
+
cases,
|
|
230
|
+
defaultEntry,
|
|
231
|
+
next: nextNodeId,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return [node, ...caseNodes]
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
case 'parallel': {
|
|
238
|
+
// Convert children to nodes
|
|
239
|
+
const childNodes: SerializedGraphNode[] = []
|
|
240
|
+
const childEntries: string[] = []
|
|
241
|
+
|
|
242
|
+
for (let i = 0; i < step.children.length; i++) {
|
|
243
|
+
const childSteps = convertStepToNode(
|
|
244
|
+
step.children[i],
|
|
245
|
+
i,
|
|
246
|
+
step.children,
|
|
247
|
+
`${nodeId}_child`
|
|
248
|
+
)
|
|
249
|
+
if (childSteps.length > 0) {
|
|
250
|
+
childEntries.push(childSteps[0].nodeId)
|
|
251
|
+
childNodes.push(...childSteps)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const node: FlowNode = {
|
|
256
|
+
nodeId,
|
|
257
|
+
flow: 'parallel',
|
|
258
|
+
children: childEntries,
|
|
259
|
+
next: nextNodeId,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return [node, ...childNodes]
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
case 'fanout': {
|
|
266
|
+
// Convert child step
|
|
267
|
+
const childNodes = convertStepToNode(
|
|
268
|
+
step.child,
|
|
269
|
+
0,
|
|
270
|
+
[step.child],
|
|
271
|
+
`${nodeId}_item`
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
const node: FlowNode = {
|
|
275
|
+
nodeId,
|
|
276
|
+
flow: 'fanout',
|
|
277
|
+
stepName: step.stepName,
|
|
278
|
+
sourceVar: step.sourceVar,
|
|
279
|
+
itemVar: step.itemVar,
|
|
280
|
+
mode: step.mode,
|
|
281
|
+
childEntry: childNodes.length > 0 ? childNodes[0].nodeId : undefined,
|
|
282
|
+
timeBetween: step.timeBetween,
|
|
283
|
+
next: nextNodeId,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return [node, ...childNodes]
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
case 'filter': {
|
|
290
|
+
const node: FlowNode = {
|
|
291
|
+
nodeId,
|
|
292
|
+
flow: 'filter',
|
|
293
|
+
sourceVar: step.sourceVar,
|
|
294
|
+
itemVar: step.itemVar,
|
|
295
|
+
condition: step.condition,
|
|
296
|
+
outputVar: step.outputVar,
|
|
297
|
+
next: nextNodeId,
|
|
298
|
+
}
|
|
299
|
+
return [node]
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case 'arrayPredicate': {
|
|
303
|
+
const node: FlowNode = {
|
|
304
|
+
nodeId,
|
|
305
|
+
flow: 'arrayPredicate',
|
|
306
|
+
mode: step.mode,
|
|
307
|
+
sourceVar: step.sourceVar,
|
|
308
|
+
itemVar: step.itemVar,
|
|
309
|
+
condition: step.condition,
|
|
310
|
+
outputVar: step.outputVar,
|
|
311
|
+
next: nextNodeId,
|
|
312
|
+
}
|
|
313
|
+
return [node]
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case 'return': {
|
|
317
|
+
const node: FlowNode = {
|
|
318
|
+
nodeId,
|
|
319
|
+
flow: 'return',
|
|
320
|
+
outputs: step.outputs,
|
|
321
|
+
}
|
|
322
|
+
return [node]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
case 'cancel': {
|
|
326
|
+
const node: FlowNode = {
|
|
327
|
+
nodeId,
|
|
328
|
+
flow: 'cancel',
|
|
329
|
+
reason: step.reason,
|
|
330
|
+
}
|
|
331
|
+
return [node]
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
case 'set': {
|
|
335
|
+
const node: FlowNode = {
|
|
336
|
+
nodeId,
|
|
337
|
+
flow: 'set',
|
|
338
|
+
variable: step.variable,
|
|
339
|
+
value: step.value,
|
|
340
|
+
next: nextNodeId,
|
|
341
|
+
}
|
|
342
|
+
return [node]
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
default:
|
|
346
|
+
return []
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Convert array of steps to graph nodes
|
|
352
|
+
*/
|
|
353
|
+
function convertStepsToNodes(
|
|
354
|
+
steps: WorkflowStepMeta[],
|
|
355
|
+
nodeIdPrefix: string = 'step'
|
|
356
|
+
): SerializedGraphNode[] {
|
|
357
|
+
const allNodes: SerializedGraphNode[] = []
|
|
358
|
+
|
|
359
|
+
for (let i = 0; i < steps.length; i++) {
|
|
360
|
+
const nodes = convertStepToNode(steps[i], i, steps, nodeIdPrefix)
|
|
361
|
+
allNodes.push(...nodes)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return allNodes
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Convert a DSL workflow to graph format
|
|
369
|
+
*/
|
|
370
|
+
export function convertDslToGraph(
|
|
371
|
+
workflowName: string,
|
|
372
|
+
meta: WorkflowsMeta[string]
|
|
373
|
+
): SerializedWorkflowGraph {
|
|
374
|
+
const nodes = convertStepsToNodes(meta.steps)
|
|
375
|
+
const nodesRecord: Record<string, SerializedGraphNode> = {}
|
|
376
|
+
|
|
377
|
+
for (const node of nodes) {
|
|
378
|
+
nodesRecord[node.nodeId] = node
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Find entry nodes (step_0 is always entry for sequential workflows)
|
|
382
|
+
const entryNodeIds = nodes.length > 0 ? ['step_0'] : []
|
|
383
|
+
|
|
384
|
+
// Determine source type based on dsl flag:
|
|
385
|
+
// - dsl === true: pure DSL workflow, can be serialized
|
|
386
|
+
// - dsl === false: complex workflow with inline steps, not serializable
|
|
387
|
+
const source = meta.dsl === false ? 'complex' : 'dsl'
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
name: workflowName,
|
|
391
|
+
pikkuFuncName: meta.pikkuFuncName,
|
|
392
|
+
source,
|
|
393
|
+
description: meta.description,
|
|
394
|
+
tags: meta.tags,
|
|
395
|
+
context: meta.context,
|
|
396
|
+
wires: {}, // DSL workflows don't have explicit wires in meta
|
|
397
|
+
nodes: nodesRecord,
|
|
398
|
+
entryNodeIds,
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Convert all DSL workflows to graph format
|
|
404
|
+
*/
|
|
405
|
+
export function convertAllDslToGraphs(
|
|
406
|
+
workflowsMeta: WorkflowsMeta
|
|
407
|
+
): Record<string, SerializedWorkflowGraph> {
|
|
408
|
+
const result: Record<string, SerializedWorkflowGraph> = {}
|
|
409
|
+
|
|
410
|
+
for (const [name, meta] of Object.entries(workflowsMeta)) {
|
|
411
|
+
result[name] = convertDslToGraph(name, meta)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return result
|
|
415
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SerializedWorkflowGraph,
|
|
3
|
+
SerializedGraphNode,
|
|
4
|
+
FunctionNode,
|
|
5
|
+
DataRef,
|
|
6
|
+
SerializedNext,
|
|
7
|
+
} from './workflow-graph.types.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a RefValue (from runtime) to DataRef (serialized)
|
|
11
|
+
*/
|
|
12
|
+
function convertRef(ref: { nodeId: string; path?: string }): DataRef {
|
|
13
|
+
return {
|
|
14
|
+
$ref: ref.nodeId,
|
|
15
|
+
path: ref.path,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a value is a runtime RefValue
|
|
21
|
+
*/
|
|
22
|
+
function isRefValue(
|
|
23
|
+
value: unknown
|
|
24
|
+
): value is { __isRef: true; nodeId: string; path?: string } {
|
|
25
|
+
return (
|
|
26
|
+
typeof value === 'object' &&
|
|
27
|
+
value !== null &&
|
|
28
|
+
'__isRef' in value &&
|
|
29
|
+
(value as any).__isRef === true
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert input mapping from runtime format to serialized format
|
|
35
|
+
*/
|
|
36
|
+
function serializeInputMapping(
|
|
37
|
+
input: Record<string, unknown>
|
|
38
|
+
): Record<string, unknown | DataRef> {
|
|
39
|
+
const result: Record<string, unknown | DataRef> = {}
|
|
40
|
+
|
|
41
|
+
for (const [key, value] of Object.entries(input)) {
|
|
42
|
+
if (isRefValue(value)) {
|
|
43
|
+
result[key] = convertRef(value)
|
|
44
|
+
} else {
|
|
45
|
+
result[key] = value
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Convert next config from runtime format to serialized format
|
|
54
|
+
* Runtime uses Record<string, string | string[]> for branching with graph.branch()
|
|
55
|
+
* Serialized uses { conditions: [...], default: ... } for UI-friendly branching
|
|
56
|
+
*/
|
|
57
|
+
function serializeNext(
|
|
58
|
+
next: string | string[] | Record<string, string | string[]> | undefined
|
|
59
|
+
): SerializedNext | undefined {
|
|
60
|
+
if (!next) return undefined
|
|
61
|
+
|
|
62
|
+
if (typeof next === 'string') return next
|
|
63
|
+
if (Array.isArray(next)) return next
|
|
64
|
+
|
|
65
|
+
// Record format - convert to conditions format
|
|
66
|
+
// For now, treat keys as branch identifiers (from graph.branch())
|
|
67
|
+
// UI can display these as condition labels
|
|
68
|
+
const conditions = Object.entries(next).map(([key, target]) => ({
|
|
69
|
+
expression: key, // The branch key becomes the expression
|
|
70
|
+
target,
|
|
71
|
+
}))
|
|
72
|
+
|
|
73
|
+
return { conditions }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Serialize a workflow graph definition (from runtime) to JSON format
|
|
78
|
+
*
|
|
79
|
+
* @param definition - The runtime definition (with callbacks evaluated)
|
|
80
|
+
* @param rpcNameLookup - Function to get RPC name from a node's func
|
|
81
|
+
*/
|
|
82
|
+
export function serializeWorkflowGraph(
|
|
83
|
+
definition: {
|
|
84
|
+
name: string
|
|
85
|
+
wires: {
|
|
86
|
+
http?: { route: string; method: string }
|
|
87
|
+
queue?: string
|
|
88
|
+
}
|
|
89
|
+
graph: Record<
|
|
90
|
+
string,
|
|
91
|
+
{
|
|
92
|
+
func: { name?: string }
|
|
93
|
+
input?: (ref: any) => Record<string, unknown>
|
|
94
|
+
next?: string | string[] | Record<string, string | string[]>
|
|
95
|
+
onError?: string | string[]
|
|
96
|
+
}
|
|
97
|
+
>
|
|
98
|
+
},
|
|
99
|
+
options?: {
|
|
100
|
+
description?: string
|
|
101
|
+
tags?: string[]
|
|
102
|
+
}
|
|
103
|
+
): SerializedWorkflowGraph {
|
|
104
|
+
const nodes: Record<string, SerializedGraphNode> = {}
|
|
105
|
+
const entryNodeIds: string[] = []
|
|
106
|
+
|
|
107
|
+
// Create a ref function that captures refs
|
|
108
|
+
const createRef = (nodeId: string, path?: string) => ({
|
|
109
|
+
__isRef: true as const,
|
|
110
|
+
nodeId,
|
|
111
|
+
path,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Track which nodes have incoming edges
|
|
115
|
+
const hasIncomingEdge = new Set<string>()
|
|
116
|
+
|
|
117
|
+
// First pass: identify nodes with incoming edges
|
|
118
|
+
for (const [_nodeId, node] of Object.entries(definition.graph)) {
|
|
119
|
+
const next = node.next
|
|
120
|
+
if (!next) continue
|
|
121
|
+
|
|
122
|
+
if (typeof next === 'string') {
|
|
123
|
+
hasIncomingEdge.add(next)
|
|
124
|
+
} else if (Array.isArray(next)) {
|
|
125
|
+
next.forEach((n) => hasIncomingEdge.add(n))
|
|
126
|
+
} else {
|
|
127
|
+
for (const targets of Object.values(next)) {
|
|
128
|
+
if (typeof targets === 'string') {
|
|
129
|
+
hasIncomingEdge.add(targets)
|
|
130
|
+
} else {
|
|
131
|
+
targets.forEach((n) => hasIncomingEdge.add(n))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Second pass: serialize nodes
|
|
138
|
+
for (const [nodeId, node] of Object.entries(definition.graph)) {
|
|
139
|
+
// Evaluate input callback to get the mapping
|
|
140
|
+
let input: Record<string, unknown | DataRef> = {}
|
|
141
|
+
if (node.input) {
|
|
142
|
+
const rawInput = node.input(createRef)
|
|
143
|
+
input = serializeInputMapping(rawInput)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Get RPC name from func
|
|
147
|
+
const rpcName = node.func?.name || 'unknown'
|
|
148
|
+
|
|
149
|
+
const funcNode: FunctionNode = {
|
|
150
|
+
nodeId,
|
|
151
|
+
rpcName,
|
|
152
|
+
input,
|
|
153
|
+
next: serializeNext(node.next),
|
|
154
|
+
onError: node.onError,
|
|
155
|
+
}
|
|
156
|
+
nodes[nodeId] = funcNode
|
|
157
|
+
|
|
158
|
+
// Entry nodes have no incoming edges
|
|
159
|
+
if (!hasIncomingEdge.has(nodeId)) {
|
|
160
|
+
entryNodeIds.push(nodeId)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
name: definition.name,
|
|
166
|
+
pikkuFuncName: definition.name, // For graph workflows, pikkuFuncName is the workflow name
|
|
167
|
+
source: 'graph' as const,
|
|
168
|
+
description: options?.description,
|
|
169
|
+
tags: options?.tags,
|
|
170
|
+
wires: definition.wires as SerializedWorkflowGraph['wires'],
|
|
171
|
+
nodes,
|
|
172
|
+
entryNodeIds,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Deserialize a workflow graph from JSON to runtime format
|
|
178
|
+
* This re-hydrates the JSON so it can be executed
|
|
179
|
+
*/
|
|
180
|
+
export function deserializeWorkflowGraph(serialized: SerializedWorkflowGraph): {
|
|
181
|
+
name: string
|
|
182
|
+
wires: SerializedWorkflowGraph['wires']
|
|
183
|
+
graph: Record<
|
|
184
|
+
string,
|
|
185
|
+
{
|
|
186
|
+
rpcName: string
|
|
187
|
+
input: Record<string, unknown | DataRef>
|
|
188
|
+
next?: SerializedNext
|
|
189
|
+
onError?: string | string[]
|
|
190
|
+
}
|
|
191
|
+
>
|
|
192
|
+
entryNodeIds: string[]
|
|
193
|
+
} {
|
|
194
|
+
const graph: Record<
|
|
195
|
+
string,
|
|
196
|
+
{
|
|
197
|
+
rpcName: string
|
|
198
|
+
input: Record<string, unknown | DataRef>
|
|
199
|
+
next?: SerializedNext
|
|
200
|
+
onError?: string | string[]
|
|
201
|
+
}
|
|
202
|
+
> = {}
|
|
203
|
+
|
|
204
|
+
for (const [nodeId, node] of Object.entries(serialized.nodes)) {
|
|
205
|
+
// Only include FunctionNode properties (nodes with rpcName)
|
|
206
|
+
if ('rpcName' in node) {
|
|
207
|
+
const funcNode = node as FunctionNode
|
|
208
|
+
graph[nodeId] = {
|
|
209
|
+
rpcName: funcNode.rpcName,
|
|
210
|
+
input: funcNode.input ?? {},
|
|
211
|
+
next: funcNode.next,
|
|
212
|
+
onError: funcNode.onError,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
name: serialized.name,
|
|
219
|
+
wires: serialized.wires,
|
|
220
|
+
graph,
|
|
221
|
+
entryNodeIds: serialized.entryNodeIds,
|
|
222
|
+
}
|
|
223
|
+
}
|