@pikku/inspector 0.12.74 → 0.12.81
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 +347 -0
- package/dist/add/add-analytics.d.ts +9 -0
- package/dist/add/add-analytics.js +178 -0
- package/dist/add/add-auth.js +0 -54
- package/dist/add/add-feature-flag.d.ts +2 -0
- package/dist/add/add-feature-flag.js +162 -0
- package/dist/add/add-wire-addon.js +1 -0
- package/dist/add/add-wire-remote-addon.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/inspector.js +4 -0
- package/dist/surface/collect-surface.d.ts +58 -0
- package/dist/surface/collect-surface.js +613 -0
- package/dist/types.d.ts +20 -11
- package/dist/utils/filter-inspector-state.js +23 -13
- package/dist/utils/post-process.js +15 -0
- package/dist/utils/read-module-specifiers.d.ts +8 -0
- package/dist/utils/read-module-specifiers.js +23 -0
- package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
- package/dist/utils/read-tsconfig-out-dir.js +12 -0
- package/dist/utils/resolve-deploy-target.d.ts +7 -0
- package/dist/utils/resolve-deploy-target.js +15 -7
- package/dist/utils/serialize-inspector-state.d.ts +5 -0
- package/dist/utils/serialize-inspector-state.js +8 -0
- package/dist/visit.js +4 -0
- package/package.json +16 -6
- package/run-tests.sh +21 -6
- package/src/add/add-addon-bans.ts +0 -85
- package/src/add/add-agent-scorer.test.ts +0 -197
- package/src/add/add-agent-scorer.ts +0 -83
- package/src/add/add-agent.test.ts +0 -87
- package/src/add/add-agent.ts +0 -638
- package/src/add/add-approval-description.ts +0 -76
- package/src/add/add-auth.test.ts +0 -791
- package/src/add/add-auth.ts +0 -424
- package/src/add/add-channel.ts +0 -778
- package/src/add/add-cli-renderers.test.ts +0 -74
- package/src/add/add-cli.ts +0 -1006
- package/src/add/add-credential.ts +0 -186
- package/src/add/add-feature.test.ts +0 -306
- package/src/add/add-feature.ts +0 -253
- package/src/add/add-file-extends-core-type.ts +0 -66
- package/src/add/add-file-with-config.ts +0 -45
- package/src/add/add-file-with-factory.test.ts +0 -134
- package/src/add/add-file-with-factory.ts +0 -288
- package/src/add/add-functions.test.ts +0 -743
- package/src/add/add-functions.ts +0 -1548
- package/src/add/add-gateway.ts +0 -143
- package/src/add/add-http-route-ref.test.ts +0 -149
- package/src/add/add-http-route.ts +0 -530
- package/src/add/add-http-routes.ts +0 -471
- package/src/add/add-keyed-wiring.ts +0 -180
- package/src/add/add-mcp-prompt.ts +0 -157
- package/src/add/add-mcp-resource.ts +0 -175
- package/src/add/add-middleware.test.ts +0 -196
- package/src/add/add-middleware.ts +0 -879
- package/src/add/add-permission-auth.test.ts +0 -59
- package/src/add/add-permission.ts +0 -311
- package/src/add/add-personas.test.ts +0 -406
- package/src/add/add-personas.ts +0 -287
- package/src/add/add-queue-worker.ts +0 -117
- package/src/add/add-rpc-invocations.test.ts +0 -287
- package/src/add/add-rpc-invocations.ts +0 -221
- package/src/add/add-scenario-step.test.ts +0 -509
- package/src/add/add-schedule.ts +0 -120
- package/src/add/add-scope.test.ts +0 -419
- package/src/add/add-scope.ts +0 -221
- package/src/add/add-secret.ts +0 -10
- package/src/add/add-subject-scenario-steps.test.ts +0 -161
- package/src/add/add-system-role.test.ts +0 -297
- package/src/add/add-system-role.ts +0 -162
- package/src/add/add-trigger.ts +0 -165
- package/src/add/add-variable.ts +0 -10
- package/src/add/add-wire-addon.test.ts +0 -185
- package/src/add/add-wire-addon.ts +0 -146
- package/src/add/add-wire-remote-addon.ts +0 -80
- package/src/add/add-workflow-edge-cases.test.ts +0 -201
- package/src/add/add-workflow-fanout.test.ts +0 -166
- package/src/add/add-workflow-graph-foreach.test.ts +0 -174
- package/src/add/add-workflow-graph-input.test.ts +0 -94
- package/src/add/add-workflow-graph-notes.test.ts +0 -74
- package/src/add/add-workflow-graph.ts +0 -644
- package/src/add/add-workflow-on-error.test.ts +0 -118
- package/src/add/add-workflow-return-switch.test.ts +0 -121
- package/src/add/add-workflow-silent-drops.test.ts +0 -261
- package/src/add/add-workflow-sleep-expression.test.ts +0 -96
- package/src/add/add-workflow.test.ts +0 -155
- package/src/add/add-workflow.ts +0 -482
- package/src/add/addon-bans.test.ts +0 -121
- package/src/add/addon-contracts.test.ts +0 -221
- package/src/add/allow-escape-hatches.test.ts +0 -144
- package/src/add/audit-meta.test.ts +0 -75
- package/src/add/classified-sink-check.test.ts +0 -347
- package/src/add/config-value-metadata.test.ts +0 -263
- package/src/add/dynamic-import-check.test.ts +0 -119
- package/src/add/expect-eventually-scenario-only.test.ts +0 -108
- package/src/add/function-body-span.test.ts +0 -96
- package/src/add/inline-wiring-function.test.ts +0 -104
- package/src/add/pii-check.test.ts +0 -258
- package/src/add/rpc-type-cast.test.ts +0 -123
- package/src/add/scenario-instrumentation.test.ts +0 -89
- package/src/add/scenario-instrumentation.ts +0 -42
- package/src/add/scenario-marker.test.ts +0 -89
- package/src/add/scenario-not-extractable.test.ts +0 -159
- package/src/add/scenario-services.test.ts +0 -103
- package/src/add/scenario-skip.test.ts +0 -83
- package/src/add/validate-workflow-graph-addons.test.ts +0 -102
- package/src/add/wire-name-literal.test.ts +0 -117
- package/src/error-codes-emitted.test.ts +0 -76
- package/src/error-codes.ts +0 -145
- package/src/index.ts +0 -66
- package/src/inspector.ts +0 -575
- package/src/schema-generator.ts +0 -1
- package/src/source-file-cache.ts +0 -73
- package/src/tsconfig-paths.test.ts +0 -90
- package/src/types-map.ts +0 -141
- package/src/types.ts +0 -775
- package/src/utils/annotate-http-route-auth.test.ts +0 -161
- package/src/utils/annotate-http-route-auth.ts +0 -53
- package/src/utils/check-classified-sinks.ts +0 -215
- package/src/utils/check-pii-output.ts +0 -120
- package/src/utils/compute-diagnostics.test.ts +0 -106
- package/src/utils/compute-required-schemas.ts +0 -48
- package/src/utils/contract-hashes.test.ts +0 -553
- package/src/utils/contract-hashes.ts +0 -387
- package/src/utils/custom-types-generator.test.ts +0 -124
- package/src/utils/custom-types-generator.ts +0 -150
- package/src/utils/detect-schema-vendor.test.ts +0 -123
- package/src/utils/detect-schema-vendor.ts +0 -120
- package/src/utils/does-type-extend-core-type.ts +0 -53
- package/src/utils/ensure-function-metadata.ts +0 -385
- package/src/utils/extract-function-name.ts +0 -612
- package/src/utils/extract-function-node.ts +0 -70
- package/src/utils/extract-node-value.test.ts +0 -117
- package/src/utils/extract-node-value.ts +0 -172
- package/src/utils/extract-secret-usage.test.ts +0 -81
- package/src/utils/extract-secret-usage.ts +0 -73
- package/src/utils/extract-services.ts +0 -70
- package/src/utils/extract-surface-usage.test.ts +0 -147
- package/src/utils/extract-surface-usage.ts +0 -121
- package/src/utils/filter-inspector-state.test.ts +0 -2207
- package/src/utils/filter-inspector-state.ts +0 -1302
- package/src/utils/filter-utils.test.ts +0 -843
- package/src/utils/filter-utils.ts +0 -161
- package/src/utils/find-root-dir.ts +0 -68
- package/src/utils/find-zod-transform.test.ts +0 -70
- package/src/utils/find-zod-transform.ts +0 -103
- package/src/utils/get-exported-variable-name.ts +0 -48
- package/src/utils/get-files-and-methods.ts +0 -157
- package/src/utils/get-property-value.test.ts +0 -141
- package/src/utils/get-property-value.ts +0 -331
- package/src/utils/hash.ts +0 -26
- package/src/utils/literal-properties.ts +0 -97
- package/src/utils/load-addon-functions-meta.test.ts +0 -157
- package/src/utils/load-addon-functions-meta.ts +0 -508
- package/src/utils/load-addon-scopes.test.ts +0 -138
- package/src/utils/middleware.test.ts +0 -204
- package/src/utils/middleware.ts +0 -303
- package/src/utils/nested-project-filter.test.ts +0 -53
- package/src/utils/nested-project-filter.ts +0 -28
- package/src/utils/permissions-shorthand.test.ts +0 -79
- package/src/utils/permissions.test.ts +0 -125
- package/src/utils/permissions.ts +0 -129
- package/src/utils/post-process.test.ts +0 -1094
- package/src/utils/post-process.ts +0 -1604
- package/src/utils/resolve-addon-package.ts +0 -106
- package/src/utils/resolve-core-type.test.ts +0 -142
- package/src/utils/resolve-core-type.ts +0 -35
- package/src/utils/resolve-deploy-target.test.ts +0 -135
- package/src/utils/resolve-deploy-target.ts +0 -65
- package/src/utils/resolve-function-meta.ts +0 -25
- package/src/utils/resolve-function-types.ts +0 -42
- package/src/utils/resolve-identifier.ts +0 -46
- package/src/utils/resolve-ref-contract.ts +0 -71
- package/src/utils/resolve-versions.test.ts +0 -499
- package/src/utils/resolve-versions.ts +0 -228
- package/src/utils/schema-cache.test.ts +0 -124
- package/src/utils/schema-generator.test.ts +0 -272
- package/src/utils/schema-generator.ts +0 -780
- package/src/utils/secret-alias-services.test.ts +0 -146
- package/src/utils/secret-alias-services.ts +0 -58
- package/src/utils/serialize-inspector-state.ts +0 -742
- package/src/utils/serialize-mcp-json.ts +0 -150
- package/src/utils/serialize-middleware-groups-meta.ts +0 -36
- package/src/utils/serialize-openapi-json.ts +0 -277
- package/src/utils/serialize-permissions-groups-meta.ts +0 -7
- package/src/utils/single-declaration.ts +0 -57
- package/src/utils/type-utils.ts +0 -145
- package/src/utils/validate-auth-sessionless.ts +0 -30
- package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
- package/src/utils/validate-exposed-functions-gated.ts +0 -103
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
- package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
- package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
- package/src/utils/workflow/derive-workflow-plan.ts +0 -117
- package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
- package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
- package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
- package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
- package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
- package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
- package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
- package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
- package/src/utils/workflow/dsl/index.ts +0 -11
- package/src/utils/workflow/dsl/patterns.ts +0 -440
- package/src/utils/workflow/dsl/validation.ts +0 -203
- package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
- package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
- package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
- package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
- package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
- package/src/utils/workflow/graph/index.ts +0 -28
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
- package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
- package/src/visit.ts +0 -181
- package/tsconfig.json +0 -19
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,543 +0,0 @@
|
|
|
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
|
-
function makeNodeId(
|
|
14
|
-
step: WorkflowStepMeta,
|
|
15
|
-
index: number,
|
|
16
|
-
prefix: string
|
|
17
|
-
): string {
|
|
18
|
-
if ('stepName' in step && step.stepName) {
|
|
19
|
-
return step.stepName
|
|
20
|
-
}
|
|
21
|
-
return `${prefix}_${index}`
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Check if a node is a terminal flow (no next step should follow)
|
|
26
|
-
*/
|
|
27
|
-
function isTerminalFlow(node: SerializedGraphNode): boolean {
|
|
28
|
-
if ('flow' in node) {
|
|
29
|
-
// Cancel and return are terminal flows - they end execution
|
|
30
|
-
return node.flow === 'cancel' || node.flow === 'return'
|
|
31
|
-
}
|
|
32
|
-
return false
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Convert InputSource to DataRef
|
|
37
|
-
*/
|
|
38
|
-
function convertInputSource(source: {
|
|
39
|
-
from: string
|
|
40
|
-
path?: string
|
|
41
|
-
name?: string
|
|
42
|
-
value?: unknown
|
|
43
|
-
parts?: string[]
|
|
44
|
-
expressions?: unknown[]
|
|
45
|
-
}): unknown | DataRef {
|
|
46
|
-
if (source.from === 'literal') {
|
|
47
|
-
return source.value
|
|
48
|
-
}
|
|
49
|
-
if (source.from === 'input') {
|
|
50
|
-
return { $ref: 'trigger', path: source.path }
|
|
51
|
-
}
|
|
52
|
-
if (source.from === 'outputVar') {
|
|
53
|
-
return { $ref: source.name!, path: source.path }
|
|
54
|
-
}
|
|
55
|
-
if (source.from === 'item') {
|
|
56
|
-
return { $ref: '$item', path: source.path }
|
|
57
|
-
}
|
|
58
|
-
if (source.from === 'stateVar') {
|
|
59
|
-
return { $state: source.name!, path: source.path }
|
|
60
|
-
}
|
|
61
|
-
if (source.from === 'template') {
|
|
62
|
-
return {
|
|
63
|
-
$template: {
|
|
64
|
-
parts: source.parts,
|
|
65
|
-
expressions: source.expressions?.map((expr) =>
|
|
66
|
-
convertInputSource(expr as any)
|
|
67
|
-
),
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return source.value
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Convert a single DSL step to graph node(s)
|
|
76
|
-
*/
|
|
77
|
-
function convertStepToNode(
|
|
78
|
-
step: WorkflowStepMeta,
|
|
79
|
-
index: number,
|
|
80
|
-
steps: WorkflowStepMeta[],
|
|
81
|
-
nodeIdPrefix: string = 'step'
|
|
82
|
-
): SerializedGraphNode[] {
|
|
83
|
-
const nodeId = makeNodeId(step, index, nodeIdPrefix)
|
|
84
|
-
const nextNodeId =
|
|
85
|
-
index < steps.length - 1
|
|
86
|
-
? makeNodeId(steps[index + 1], index + 1, nodeIdPrefix)
|
|
87
|
-
: undefined
|
|
88
|
-
|
|
89
|
-
switch (step.type) {
|
|
90
|
-
case 'rpc': {
|
|
91
|
-
const node: FunctionNode = {
|
|
92
|
-
nodeId,
|
|
93
|
-
rpcName: step.rpcName,
|
|
94
|
-
stepName: step.stepName,
|
|
95
|
-
next: nextNodeId,
|
|
96
|
-
}
|
|
97
|
-
if (step.actor) {
|
|
98
|
-
node.actor = step.actor
|
|
99
|
-
}
|
|
100
|
-
if (step.expectEventually) {
|
|
101
|
-
node.expectEventually = true
|
|
102
|
-
}
|
|
103
|
-
if (step.inputs) {
|
|
104
|
-
if (step.inputs === 'passthrough') {
|
|
105
|
-
// Entire data is passed through - store as reference to trigger
|
|
106
|
-
node.input = { $passthrough: { $ref: 'trigger' } }
|
|
107
|
-
} else {
|
|
108
|
-
node.input = {}
|
|
109
|
-
for (const [key, source] of Object.entries(step.inputs)) {
|
|
110
|
-
node.input[key] = convertInputSource(source as any)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
if (step.outputVar) {
|
|
115
|
-
node.outputVar = step.outputVar
|
|
116
|
-
}
|
|
117
|
-
if (step.options) {
|
|
118
|
-
node.options = {
|
|
119
|
-
retries: step.options.retries,
|
|
120
|
-
retryDelay: step.options.retryDelay,
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
// An onError handler is written as an rpc name, but the graph routes to
|
|
124
|
-
// node ids — materialise a node for it so the console has something to
|
|
125
|
-
// draw the error edge to, exactly as in a hand-authored graph.
|
|
126
|
-
if (step.options?.onError) {
|
|
127
|
-
const handlerId = `${nodeId}_onError`
|
|
128
|
-
node.onError = handlerId
|
|
129
|
-
return [
|
|
130
|
-
node,
|
|
131
|
-
{
|
|
132
|
-
nodeId: handlerId,
|
|
133
|
-
rpcName: step.options.onError,
|
|
134
|
-
stepName: `${step.stepName} (on error)`,
|
|
135
|
-
} satisfies FunctionNode,
|
|
136
|
-
]
|
|
137
|
-
}
|
|
138
|
-
return [node]
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// A declared step is a function node like any other — the console draws it
|
|
142
|
-
// the same way, with the phase available for the "Given/When/Then" prose.
|
|
143
|
-
case 'scenarioStep': {
|
|
144
|
-
const node: FunctionNode = {
|
|
145
|
-
nodeId,
|
|
146
|
-
rpcName: step.stepFunc,
|
|
147
|
-
stepName: step.stepName,
|
|
148
|
-
scenarioStepPhase: step.phase,
|
|
149
|
-
next: nextNodeId,
|
|
150
|
-
}
|
|
151
|
-
if (step.actor) {
|
|
152
|
-
node.actor = step.actor
|
|
153
|
-
}
|
|
154
|
-
if (step.inputs) {
|
|
155
|
-
if (step.inputs === 'passthrough') {
|
|
156
|
-
node.input = { $passthrough: { $ref: 'trigger' } }
|
|
157
|
-
} else {
|
|
158
|
-
node.input = {}
|
|
159
|
-
for (const [key, source] of Object.entries(step.inputs)) {
|
|
160
|
-
node.input[key] = convertInputSource(source as any)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (step.outputVar) {
|
|
165
|
-
node.outputVar = step.outputVar
|
|
166
|
-
}
|
|
167
|
-
if (step.options) {
|
|
168
|
-
node.options = {
|
|
169
|
-
retries: step.options.retries,
|
|
170
|
-
retryDelay: step.options.retryDelay,
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return [node]
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
case 'sleep': {
|
|
177
|
-
const node: FlowNode = {
|
|
178
|
-
nodeId,
|
|
179
|
-
flow: 'sleep',
|
|
180
|
-
stepName: step.stepName,
|
|
181
|
-
duration: step.duration,
|
|
182
|
-
expression: step.expression,
|
|
183
|
-
next: nextNodeId,
|
|
184
|
-
}
|
|
185
|
-
return [node]
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
case 'suspend': {
|
|
189
|
-
// A suspend yields no value, but it must still be a node: the previous
|
|
190
|
-
// step's `next` points at it, so omitting it dead-ends traversal and
|
|
191
|
-
// silently deletes every step that follows.
|
|
192
|
-
const node: FlowNode = {
|
|
193
|
-
nodeId,
|
|
194
|
-
flow: 'suspend',
|
|
195
|
-
reason: step.reason,
|
|
196
|
-
next: nextNodeId,
|
|
197
|
-
}
|
|
198
|
-
return [node]
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
case 'approval': {
|
|
202
|
-
// Unlike `suspend` — which has no output and so is deliberately absent
|
|
203
|
-
// from the graph — an approval yields a value a later step can reference,
|
|
204
|
-
// so it must be a real node for $ref resolution and graphHash to see it.
|
|
205
|
-
const node: FlowNode = {
|
|
206
|
-
nodeId,
|
|
207
|
-
flow: 'approval',
|
|
208
|
-
reason: step.reason,
|
|
209
|
-
outputVar: step.outputVar,
|
|
210
|
-
expiry: step.expiry,
|
|
211
|
-
next: nextNodeId,
|
|
212
|
-
}
|
|
213
|
-
return [node]
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
case 'inline': {
|
|
217
|
-
const node: FlowNode = {
|
|
218
|
-
nodeId,
|
|
219
|
-
flow: 'inline',
|
|
220
|
-
description: step.description,
|
|
221
|
-
next: nextNodeId,
|
|
222
|
-
}
|
|
223
|
-
return [node]
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
case 'branch': {
|
|
227
|
-
// Convert all branch conditions (if/else-if chain)
|
|
228
|
-
const branchNodes: SerializedGraphNode[] = []
|
|
229
|
-
const branches: Array<{
|
|
230
|
-
condition: unknown
|
|
231
|
-
entry: string
|
|
232
|
-
}> = []
|
|
233
|
-
|
|
234
|
-
for (let i = 0; i < step.branches.length; i++) {
|
|
235
|
-
const branchSteps = convertStepsToNodes(
|
|
236
|
-
step.branches[i].steps,
|
|
237
|
-
`${nodeId}_branch${i}`
|
|
238
|
-
)
|
|
239
|
-
if (branchSteps.length > 0) {
|
|
240
|
-
branches.push({
|
|
241
|
-
condition: step.branches[i].condition,
|
|
242
|
-
entry: branchSteps[0].nodeId,
|
|
243
|
-
})
|
|
244
|
-
// Link last branch node back to next (unless terminal flow)
|
|
245
|
-
if (nextNodeId) {
|
|
246
|
-
const lastBranch = branchSteps[branchSteps.length - 1]
|
|
247
|
-
if (!lastBranch.next && !isTerminalFlow(lastBranch))
|
|
248
|
-
lastBranch.next = nextNodeId
|
|
249
|
-
}
|
|
250
|
-
branchNodes.push(...branchSteps)
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Convert else branch
|
|
255
|
-
const elseNodes = step.elseSteps
|
|
256
|
-
? convertStepsToNodes(step.elseSteps, `${nodeId}_else`)
|
|
257
|
-
: []
|
|
258
|
-
if (elseNodes.length > 0 && nextNodeId) {
|
|
259
|
-
const lastElse = elseNodes[elseNodes.length - 1]
|
|
260
|
-
if (!lastElse.next && !isTerminalFlow(lastElse))
|
|
261
|
-
lastElse.next = nextNodeId
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const node: FlowNode = {
|
|
265
|
-
nodeId,
|
|
266
|
-
flow: 'branch',
|
|
267
|
-
branches,
|
|
268
|
-
elseEntry: elseNodes.length > 0 ? elseNodes[0].nodeId : undefined,
|
|
269
|
-
next: nextNodeId,
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return [node, ...branchNodes, ...elseNodes]
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
case 'switch': {
|
|
276
|
-
const caseNodes: SerializedGraphNode[] = []
|
|
277
|
-
const cases: Array<{
|
|
278
|
-
value?: unknown
|
|
279
|
-
expression?: string
|
|
280
|
-
entry: string
|
|
281
|
-
}> = []
|
|
282
|
-
|
|
283
|
-
// A case with no statements falls through to the next one. Dropping it
|
|
284
|
-
// would lose the value entirely, so it is held until an entry exists.
|
|
285
|
-
let fallingThrough: Array<{ value?: unknown; expression?: string }> = []
|
|
286
|
-
|
|
287
|
-
for (let i = 0; i < step.cases.length; i++) {
|
|
288
|
-
const caseSteps = convertStepsToNodes(
|
|
289
|
-
step.cases[i].steps,
|
|
290
|
-
`${nodeId}_case${i}`
|
|
291
|
-
)
|
|
292
|
-
if (caseSteps.length === 0) {
|
|
293
|
-
fallingThrough.push({
|
|
294
|
-
value: step.cases[i].value,
|
|
295
|
-
expression: step.cases[i].expression,
|
|
296
|
-
})
|
|
297
|
-
continue
|
|
298
|
-
}
|
|
299
|
-
{
|
|
300
|
-
const entry = caseSteps[0].nodeId
|
|
301
|
-
for (const pending of fallingThrough) {
|
|
302
|
-
cases.push({ ...pending, entry })
|
|
303
|
-
}
|
|
304
|
-
fallingThrough = []
|
|
305
|
-
cases.push({
|
|
306
|
-
value: step.cases[i].value,
|
|
307
|
-
expression: step.cases[i].expression,
|
|
308
|
-
entry,
|
|
309
|
-
})
|
|
310
|
-
// Link last case node to next (unless terminal flow)
|
|
311
|
-
if (nextNodeId) {
|
|
312
|
-
const lastCase = caseSteps[caseSteps.length - 1]
|
|
313
|
-
if (!lastCase.next && !isTerminalFlow(lastCase))
|
|
314
|
-
lastCase.next = nextNodeId
|
|
315
|
-
}
|
|
316
|
-
caseNodes.push(...caseSteps)
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
let defaultEntry: string | undefined
|
|
321
|
-
if (step.defaultSteps) {
|
|
322
|
-
const defaultNodes = convertStepsToNodes(
|
|
323
|
-
step.defaultSteps,
|
|
324
|
-
`${nodeId}_default`
|
|
325
|
-
)
|
|
326
|
-
if (defaultNodes.length > 0) {
|
|
327
|
-
defaultEntry = defaultNodes[0].nodeId
|
|
328
|
-
// Link last default node to next (unless terminal flow)
|
|
329
|
-
if (nextNodeId) {
|
|
330
|
-
const lastDefault = defaultNodes[defaultNodes.length - 1]
|
|
331
|
-
if (!lastDefault.next && !isTerminalFlow(lastDefault))
|
|
332
|
-
lastDefault.next = nextNodeId
|
|
333
|
-
}
|
|
334
|
-
caseNodes.push(...defaultNodes)
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Cases falling through past the last one land in default, or exit.
|
|
339
|
-
const trailingEntry = defaultEntry ?? nextNodeId
|
|
340
|
-
if (trailingEntry) {
|
|
341
|
-
for (const pending of fallingThrough) {
|
|
342
|
-
cases.push({ ...pending, entry: trailingEntry })
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const node: FlowNode = {
|
|
347
|
-
nodeId,
|
|
348
|
-
flow: 'switch',
|
|
349
|
-
expression: step.expression,
|
|
350
|
-
cases,
|
|
351
|
-
defaultEntry,
|
|
352
|
-
next: nextNodeId,
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return [node, ...caseNodes]
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
case 'parallel': {
|
|
359
|
-
// Convert children to nodes
|
|
360
|
-
const childNodes: SerializedGraphNode[] = []
|
|
361
|
-
const childEntries: string[] = []
|
|
362
|
-
|
|
363
|
-
for (let i = 0; i < step.children.length; i++) {
|
|
364
|
-
const childSteps = convertStepToNode(
|
|
365
|
-
step.children[i],
|
|
366
|
-
i,
|
|
367
|
-
step.children,
|
|
368
|
-
`${nodeId}_child`
|
|
369
|
-
)
|
|
370
|
-
if (childSteps.length > 0) {
|
|
371
|
-
childEntries.push(childSteps[0].nodeId)
|
|
372
|
-
childNodes.push(...childSteps)
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const node: FlowNode = {
|
|
377
|
-
nodeId,
|
|
378
|
-
flow: 'parallel',
|
|
379
|
-
children: childEntries,
|
|
380
|
-
next: nextNodeId,
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return [node, ...childNodes]
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
case 'fanout': {
|
|
387
|
-
// Convert the per-iteration body into a `next`-chained sequence
|
|
388
|
-
const childNodes: SerializedGraphNode[] = []
|
|
389
|
-
for (let i = 0; i < step.body.length; i++) {
|
|
390
|
-
childNodes.push(
|
|
391
|
-
...convertStepToNode(step.body[i], i, step.body, `${nodeId}_item`)
|
|
392
|
-
)
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const node: FlowNode = {
|
|
396
|
-
nodeId,
|
|
397
|
-
flow: 'fanout',
|
|
398
|
-
sourceVar: step.sourceVar,
|
|
399
|
-
itemVar: step.itemVar,
|
|
400
|
-
mode: step.mode,
|
|
401
|
-
childEntry: childNodes.length > 0 ? childNodes[0].nodeId : undefined,
|
|
402
|
-
timeBetween: step.timeBetween,
|
|
403
|
-
next: nextNodeId,
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return [node, ...childNodes]
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
case 'filter': {
|
|
410
|
-
const node: FlowNode = {
|
|
411
|
-
nodeId,
|
|
412
|
-
flow: 'filter',
|
|
413
|
-
sourceVar: step.sourceVar,
|
|
414
|
-
itemVar: step.itemVar,
|
|
415
|
-
condition: step.condition,
|
|
416
|
-
outputVar: step.outputVar,
|
|
417
|
-
next: nextNodeId,
|
|
418
|
-
}
|
|
419
|
-
return [node]
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
case 'arrayPredicate': {
|
|
423
|
-
const node: FlowNode = {
|
|
424
|
-
nodeId,
|
|
425
|
-
flow: 'arrayPredicate',
|
|
426
|
-
mode: step.mode,
|
|
427
|
-
sourceVar: step.sourceVar,
|
|
428
|
-
itemVar: step.itemVar,
|
|
429
|
-
condition: step.condition,
|
|
430
|
-
outputVar: step.outputVar,
|
|
431
|
-
next: nextNodeId,
|
|
432
|
-
}
|
|
433
|
-
return [node]
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
case 'return': {
|
|
437
|
-
const node: FlowNode = {
|
|
438
|
-
nodeId,
|
|
439
|
-
flow: 'return',
|
|
440
|
-
outputs: step.outputs,
|
|
441
|
-
spread: step.spread,
|
|
442
|
-
}
|
|
443
|
-
return [node]
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
case 'cancel': {
|
|
447
|
-
const node: FlowNode = {
|
|
448
|
-
nodeId,
|
|
449
|
-
flow: 'cancel',
|
|
450
|
-
reason: step.reason,
|
|
451
|
-
}
|
|
452
|
-
return [node]
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
case 'set': {
|
|
456
|
-
const node: FlowNode = {
|
|
457
|
-
nodeId,
|
|
458
|
-
flow: 'set',
|
|
459
|
-
variable: step.variable,
|
|
460
|
-
value: step.value,
|
|
461
|
-
expression: step.expression,
|
|
462
|
-
next: nextNodeId,
|
|
463
|
-
}
|
|
464
|
-
return [node]
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
default:
|
|
468
|
-
return []
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Convert array of steps to graph nodes
|
|
474
|
-
*/
|
|
475
|
-
function convertStepsToNodes(
|
|
476
|
-
steps: WorkflowStepMeta[],
|
|
477
|
-
nodeIdPrefix: string = 'step'
|
|
478
|
-
): SerializedGraphNode[] {
|
|
479
|
-
const allNodes: SerializedGraphNode[] = []
|
|
480
|
-
|
|
481
|
-
for (let i = 0; i < steps.length; i++) {
|
|
482
|
-
const nodes = convertStepToNode(steps[i], i, steps, nodeIdPrefix)
|
|
483
|
-
allNodes.push(...nodes)
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
return allNodes
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Convert a DSL workflow to graph format
|
|
491
|
-
*/
|
|
492
|
-
export function convertDslToGraph(
|
|
493
|
-
workflowName: string,
|
|
494
|
-
meta: WorkflowsMeta[string]
|
|
495
|
-
): SerializedWorkflowGraph {
|
|
496
|
-
const nodes = convertStepsToNodes(meta.steps)
|
|
497
|
-
const nodesRecord: Record<string, SerializedGraphNode> = {}
|
|
498
|
-
|
|
499
|
-
for (const node of nodes) {
|
|
500
|
-
nodesRecord[node.nodeId] = node
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const entryNodeIds = nodes.length > 0 ? [nodes[0].nodeId] : []
|
|
504
|
-
|
|
505
|
-
// Determine source type:
|
|
506
|
-
// - scenario: complex workflow whose steps run as actors (scenario)
|
|
507
|
-
// - dsl === true: pure DSL workflow, can be serialized
|
|
508
|
-
// - dsl === false: complex workflow with inline steps, not serializable
|
|
509
|
-
const source = meta.scenario
|
|
510
|
-
? 'scenario'
|
|
511
|
-
: meta.dsl === false
|
|
512
|
-
? 'complex'
|
|
513
|
-
: 'dsl'
|
|
514
|
-
|
|
515
|
-
return {
|
|
516
|
-
name: workflowName,
|
|
517
|
-
pikkuFuncId: meta.pikkuFuncId,
|
|
518
|
-
source,
|
|
519
|
-
title: meta.title,
|
|
520
|
-
description: meta.description,
|
|
521
|
-
tags: meta.tags,
|
|
522
|
-
skip: meta.skip,
|
|
523
|
-
actors: meta.actors,
|
|
524
|
-
context: meta.context,
|
|
525
|
-
nodes: nodesRecord,
|
|
526
|
-
entryNodeIds,
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Convert all DSL workflows to graph format
|
|
532
|
-
*/
|
|
533
|
-
export function convertAllDslToGraphs(
|
|
534
|
-
workflowsMeta: WorkflowsMeta
|
|
535
|
-
): Record<string, SerializedWorkflowGraph> {
|
|
536
|
-
const result: Record<string, SerializedWorkflowGraph> = {}
|
|
537
|
-
|
|
538
|
-
for (const [name, meta] of Object.entries(workflowsMeta)) {
|
|
539
|
-
result[name] = convertDslToGraph(name, meta)
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
return result
|
|
543
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { strict as assert } from 'assert'
|
|
2
|
-
import { describe, test } from 'node:test'
|
|
3
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
4
|
-
import { tmpdir } from 'node:os'
|
|
5
|
-
import { join } from 'node:path'
|
|
6
|
-
import { inspect } from '../../../inspector.js'
|
|
7
|
-
import type { InspectorLogger } from '../../../types.js'
|
|
8
|
-
|
|
9
|
-
const STEP_FILE = [
|
|
10
|
-
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
11
|
-
'export const notify = pikkuSessionlessFunc({',
|
|
12
|
-
" func: async () => ({ id: 'n' }),",
|
|
13
|
-
'})',
|
|
14
|
-
].join('\n')
|
|
15
|
-
|
|
16
|
-
async function nodesFor(body: string): Promise<Record<string, any>> {
|
|
17
|
-
const logger = {
|
|
18
|
-
debug: () => {},
|
|
19
|
-
info: () => {},
|
|
20
|
-
warn: () => {},
|
|
21
|
-
error: () => {},
|
|
22
|
-
diagnostic: () => {},
|
|
23
|
-
critical: () => {},
|
|
24
|
-
hasCriticalErrors: () => false,
|
|
25
|
-
} as unknown as InspectorLogger
|
|
26
|
-
|
|
27
|
-
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-fid-'))
|
|
28
|
-
const stepFile = join(rootDir, 'w.steps.ts')
|
|
29
|
-
const wfFile = join(rootDir, 'w.workflow.ts')
|
|
30
|
-
await writeFile(stepFile, STEP_FILE)
|
|
31
|
-
await writeFile(
|
|
32
|
-
wfFile,
|
|
33
|
-
[
|
|
34
|
-
"import { pikkuWorkflowFunc } from '@pikku/core/workflow'",
|
|
35
|
-
'export const wf = pikkuWorkflowFunc(async (_, data: any, { workflow }) => {',
|
|
36
|
-
body,
|
|
37
|
-
'})',
|
|
38
|
-
].join('\n')
|
|
39
|
-
)
|
|
40
|
-
try {
|
|
41
|
-
const state: any = await inspect(logger, [stepFile, wfFile], { rootDir })
|
|
42
|
-
const gm = state.workflows.graphMeta
|
|
43
|
-
const graph = (
|
|
44
|
-
gm instanceof Map ? [...gm.values()] : Object.values(gm)
|
|
45
|
-
)[0] as any
|
|
46
|
-
return graph?.nodes ?? {}
|
|
47
|
-
} finally {
|
|
48
|
-
await rm(rootDir, { recursive: true, force: true })
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
describe('fanout node identity — a body step must not overwrite its own loop', () => {
|
|
53
|
-
test('the fanout node survives alongside its named body step', async () => {
|
|
54
|
-
const nodes = await nodesFor(
|
|
55
|
-
[
|
|
56
|
-
' await Promise.all(',
|
|
57
|
-
' data.users.map(async (u: any) => {',
|
|
58
|
-
" await workflow.do('Notify', 'notify', { id: u.id })",
|
|
59
|
-
' })',
|
|
60
|
-
' )',
|
|
61
|
-
' return { ok: true }',
|
|
62
|
-
].join('\n')
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
const fanout = Object.values(nodes).find(
|
|
66
|
-
(n: any) => n.flow === 'fanout'
|
|
67
|
-
) as any
|
|
68
|
-
assert.ok(
|
|
69
|
-
fanout,
|
|
70
|
-
`the loop must be in the graph — without it the workflow renders as a single call, got: ${JSON.stringify(nodes)}`
|
|
71
|
-
)
|
|
72
|
-
assert.ok(
|
|
73
|
-
fanout.childEntry && nodes[fanout.childEntry],
|
|
74
|
-
'the fanout must point at a body entry node that exists'
|
|
75
|
-
)
|
|
76
|
-
assert.notEqual(
|
|
77
|
-
fanout.nodeId,
|
|
78
|
-
fanout.childEntry,
|
|
79
|
-
'a fanout and its body entry must not share a node id'
|
|
80
|
-
)
|
|
81
|
-
assert.equal((nodes[fanout.childEntry] as any).rpcName, 'notify')
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
test('the step following a fanout is still reachable', async () => {
|
|
85
|
-
const nodes = await nodesFor(
|
|
86
|
-
[
|
|
87
|
-
' await Promise.all(',
|
|
88
|
-
' data.users.map(async (u: any) => {',
|
|
89
|
-
" await workflow.do('Notify', 'notify', { id: u.id })",
|
|
90
|
-
' })',
|
|
91
|
-
' )',
|
|
92
|
-
' return { ok: true }',
|
|
93
|
-
].join('\n')
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
const fanout = Object.values(nodes).find(
|
|
97
|
-
(n: any) => n.flow === 'fanout'
|
|
98
|
-
) as any
|
|
99
|
-
assert.ok(fanout, 'a fanout node should exist')
|
|
100
|
-
assert.ok(
|
|
101
|
-
fanout.next && nodes[fanout.next],
|
|
102
|
-
`the return after the loop must stay reachable, got: ${JSON.stringify(nodes)}`
|
|
103
|
-
)
|
|
104
|
-
})
|
|
105
|
-
})
|