@pikku/inspector 0.12.33 → 0.12.34
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 +16 -0
- package/dist/add/add-workflow.js +35 -4
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +9 -4
- package/dist/utils/workflow/dsl/patterns.d.ts +12 -1
- package/dist/utils/workflow/dsl/patterns.js +37 -2
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +8 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +8 -0
- package/package.json +2 -2
- package/src/add/add-workflow.ts +42 -4
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +12 -3
- package/src/utils/workflow/dsl/patterns.ts +48 -2
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +8 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +8 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 0.12.34
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 92bd643: User flows in the console: workflow graph extraction now captures
|
|
6
|
+
`workflow.expectEventually` steps and per-step actor names (`{ actor:
|
|
7
|
+
actors.x }`), workflow meta carries `actors`/`title` into the serialized
|
|
8
|
+
graph, the CLI emits `user-flow-actors.gen.json` for the new
|
|
9
|
+
`MetaService.getUserFlowActorsMeta()`, and the console Workflows page gains a
|
|
10
|
+
Workflows / User Flows / Personas toggle. Also fixes complex-workflow graphs
|
|
11
|
+
being clobbered by a duplicate basic-extraction pass after successful DSL
|
|
12
|
+
extraction.
|
|
13
|
+
- Updated dependencies [35a9bab]
|
|
14
|
+
- Updated dependencies [92bd643]
|
|
15
|
+
- @pikku/core@0.12.50
|
|
16
|
+
|
|
1
17
|
## 0.12.33
|
|
2
18
|
|
|
3
19
|
### Patch Changes
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ErrorCode } from '../error-codes.js';
|
|
|
5
5
|
import { extractStringLiteral, isStringLike, isFunctionLike, extractDescription, extractDuration, } from '../utils/extract-node-value.js';
|
|
6
6
|
import { getCommonWireMetaData, getPropertyValue, } from '../utils/get-property-value.js';
|
|
7
7
|
import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js';
|
|
8
|
-
import { getSourceText } from '../utils/workflow/dsl/patterns.js';
|
|
8
|
+
import { getSourceText, extractActorFromOptions, } from '../utils/workflow/dsl/patterns.js';
|
|
9
9
|
/**
|
|
10
10
|
* Extract a workflow step's display name without letting a non-static name
|
|
11
11
|
* (e.g. a function call) abort the scan. The step name is cosmetic, so a
|
|
@@ -86,14 +86,18 @@ export function collectInvokedRPCs(steps, rpcs) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
* Scan for workflow.do(), workflow.
|
|
89
|
+
* Scan for workflow.do(), workflow.expectEventually(), workflow.sleep(), and
|
|
90
|
+
* workflow.cancel() calls to extract workflow steps
|
|
90
91
|
*/
|
|
91
92
|
function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
92
93
|
// Look for property access expressions: workflow.do or workflow.sleep
|
|
93
94
|
if (ts.isPropertyAccessExpression(node)) {
|
|
94
95
|
const { name } = node;
|
|
95
96
|
// Check if this is accessing 'do' or 'sleep' property
|
|
96
|
-
if (name.text === 'do' ||
|
|
97
|
+
if (name.text === 'do' ||
|
|
98
|
+
name.text === 'sleep' ||
|
|
99
|
+
name.text === 'cancel' ||
|
|
100
|
+
name.text === 'expectEventually') {
|
|
97
101
|
// Check if the parent is a call expression
|
|
98
102
|
const parent = node.parent;
|
|
99
103
|
if (ts.isCallExpression(parent) && parent.expression === node) {
|
|
@@ -113,6 +117,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
113
117
|
type: 'rpc',
|
|
114
118
|
stepName,
|
|
115
119
|
rpcName,
|
|
120
|
+
actor: extractActorFromOptions(optionsArg),
|
|
116
121
|
});
|
|
117
122
|
state.rpc.invokedFunctions.add(rpcName);
|
|
118
123
|
}
|
|
@@ -125,6 +130,21 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
125
130
|
});
|
|
126
131
|
}
|
|
127
132
|
}
|
|
133
|
+
else if (name.text === 'expectEventually' && args.length >= 4) {
|
|
134
|
+
// workflow.expectEventually(stepName, rpcName, data, predicate, options?)
|
|
135
|
+
const stepName = extractStepName(args[0], checker);
|
|
136
|
+
if (isStringLike(args[1], checker)) {
|
|
137
|
+
const rpcName = extractStringLiteral(args[1], checker);
|
|
138
|
+
steps.push({
|
|
139
|
+
type: 'rpc',
|
|
140
|
+
stepName,
|
|
141
|
+
rpcName,
|
|
142
|
+
expectEventually: true,
|
|
143
|
+
actor: extractActorFromOptions(args.length >= 5 ? args[4] : undefined),
|
|
144
|
+
});
|
|
145
|
+
state.rpc.invokedFunctions.add(rpcName);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
128
148
|
else if (name.text === 'sleep' && args.length >= 2) {
|
|
129
149
|
// workflow.sleep(stepName, duration)
|
|
130
150
|
const stepNameArg = args[0];
|
|
@@ -282,9 +302,19 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
282
302
|
// For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
|
|
283
303
|
// patterns the DSL extractor doesn't handle (array+push, nested Promise.all
|
|
284
304
|
// with identifier args, etc.) are still registered as invoked functions.
|
|
305
|
+
// When DSL extraction already produced steps this pass is registration-only:
|
|
306
|
+
// appending to `steps` would duplicate nodes and clobber the graph.
|
|
285
307
|
if (wrapperType !== 'dsl') {
|
|
286
|
-
|
|
308
|
+
const sink = steps.length > 0 ? [] : steps;
|
|
309
|
+
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, sink);
|
|
287
310
|
}
|
|
311
|
+
// Actor names the flow's steps run as (user flows) — powers the console
|
|
312
|
+
// personas view and the User Flow graph badges.
|
|
313
|
+
const actorNames = [
|
|
314
|
+
...new Set(steps
|
|
315
|
+
.map((s) => ('actor' in s ? s.actor : undefined))
|
|
316
|
+
.filter((a) => typeof a === 'string')),
|
|
317
|
+
];
|
|
288
318
|
state.workflows.meta[workflowName] = {
|
|
289
319
|
pikkuFuncId,
|
|
290
320
|
name: workflowName,
|
|
@@ -298,6 +328,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
298
328
|
tags,
|
|
299
329
|
expose,
|
|
300
330
|
userFlow: wrapperType === 'user-flow' ? true : undefined,
|
|
331
|
+
actors: actorNames.length > 0 ? actorNames : undefined,
|
|
301
332
|
};
|
|
302
333
|
// User flows are pure stories of remote RPCs (same rule as client-side CLI
|
|
303
334
|
// renderers): the func may only destructure logger/config — everything else
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { isWorkflowDoCall, isWorkflowSleepCall, isWorkflowSuspendCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
|
|
2
|
+
import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
|
|
3
3
|
import { validateNoDisallowedPatterns, validateAwaitedCalls, formatValidationErrors, } from './validation.js';
|
|
4
4
|
import { extractStringLiteral, extractNumberLiteral, } from '../../extract-node-value.js';
|
|
5
5
|
/**
|
|
@@ -386,9 +386,12 @@ function extractRpcStep(call, context, outputVar) {
|
|
|
386
386
|
const rpcName = extractStringLiteral(args[1], context.checker);
|
|
387
387
|
// Extract inputs from third argument
|
|
388
388
|
const inputs = args.length >= 3 ? extractInputSources(args[2], context) : undefined;
|
|
389
|
-
//
|
|
390
|
-
const
|
|
391
|
-
|
|
389
|
+
// do(step, rpc, data, options?) vs expectEventually(step, rpc, data, predicate, options?)
|
|
390
|
+
const expectEventually = isWorkflowExpectEventuallyCall(call);
|
|
391
|
+
const optionsIndex = expectEventually ? 4 : 3;
|
|
392
|
+
const optionsArg = args.length > optionsIndex ? args[optionsIndex] : undefined;
|
|
393
|
+
const options = optionsArg && ts.isObjectLiteralExpression(optionsArg)
|
|
394
|
+
? extractStepOptions(optionsArg, context)
|
|
392
395
|
: undefined;
|
|
393
396
|
return {
|
|
394
397
|
type: 'rpc',
|
|
@@ -397,6 +400,8 @@ function extractRpcStep(call, context, outputVar) {
|
|
|
397
400
|
outputVar,
|
|
398
401
|
inputs,
|
|
399
402
|
options,
|
|
403
|
+
actor: extractActorFromOptions(optionsArg),
|
|
404
|
+
expectEventually: expectEventually || undefined,
|
|
400
405
|
};
|
|
401
406
|
}
|
|
402
407
|
catch (error) {
|
|
@@ -3,9 +3,20 @@ import * as ts from 'typescript';
|
|
|
3
3
|
* Pattern detection helpers for DSL workflow extraction
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* Check if a call expression is workflow.do()
|
|
6
|
+
* Check if a call expression is workflow.do() or workflow.expectEventually()
|
|
7
|
+
* (both are RPC steps; expectEventually is the polling variant used by user flows)
|
|
7
8
|
*/
|
|
8
9
|
export declare function isWorkflowDoCall(node: ts.CallExpression, checker: ts.TypeChecker): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Check if a call expression is workflow.expectEventually()
|
|
12
|
+
*/
|
|
13
|
+
export declare function isWorkflowExpectEventuallyCall(node: ts.CallExpression): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Extract the actor NAME from a step options object literal:
|
|
16
|
+
* `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
|
|
17
|
+
* or not statically resolvable.
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractActorFromOptions(optionsArg: ts.Expression | undefined): string | undefined;
|
|
9
20
|
/**
|
|
10
21
|
* Check if a call expression is workflow.sleep()
|
|
11
22
|
*/
|
|
@@ -3,17 +3,52 @@ import * as ts from 'typescript';
|
|
|
3
3
|
* Pattern detection helpers for DSL workflow extraction
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* Check if a call expression is workflow.do()
|
|
6
|
+
* Check if a call expression is workflow.do() or workflow.expectEventually()
|
|
7
|
+
* (both are RPC steps; expectEventually is the polling variant used by user flows)
|
|
7
8
|
*/
|
|
8
9
|
export function isWorkflowDoCall(node, checker) {
|
|
9
10
|
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
10
11
|
return false;
|
|
11
12
|
}
|
|
12
13
|
const propAccess = node.expression;
|
|
13
|
-
return (propAccess.name.text === 'do'
|
|
14
|
+
return ((propAccess.name.text === 'do' ||
|
|
15
|
+
propAccess.name.text === 'expectEventually') &&
|
|
14
16
|
ts.isIdentifier(propAccess.expression) &&
|
|
15
17
|
propAccess.expression.text === 'workflow');
|
|
16
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if a call expression is workflow.expectEventually()
|
|
21
|
+
*/
|
|
22
|
+
export function isWorkflowExpectEventuallyCall(node) {
|
|
23
|
+
return (ts.isPropertyAccessExpression(node.expression) &&
|
|
24
|
+
node.expression.name.text === 'expectEventually' &&
|
|
25
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
26
|
+
node.expression.expression.text === 'workflow');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Extract the actor NAME from a step options object literal:
|
|
30
|
+
* `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
|
|
31
|
+
* or not statically resolvable.
|
|
32
|
+
*/
|
|
33
|
+
export function extractActorFromOptions(optionsArg) {
|
|
34
|
+
if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg))
|
|
35
|
+
return;
|
|
36
|
+
for (const prop of optionsArg.properties) {
|
|
37
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
38
|
+
ts.isIdentifier(prop.name) &&
|
|
39
|
+
prop.name.text === 'actor') {
|
|
40
|
+
const init = prop.initializer;
|
|
41
|
+
if (ts.isPropertyAccessExpression(init)) {
|
|
42
|
+
return init.name.text;
|
|
43
|
+
}
|
|
44
|
+
if (ts.isElementAccessExpression(init) &&
|
|
45
|
+
ts.isStringLiteral(init.argumentExpression)) {
|
|
46
|
+
return init.argumentExpression.text;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
17
52
|
/**
|
|
18
53
|
* Check if a call expression is workflow.sleep()
|
|
19
54
|
*/
|
|
@@ -58,6 +58,12 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
58
58
|
rpcName: step.rpcName,
|
|
59
59
|
next: nextNodeId,
|
|
60
60
|
};
|
|
61
|
+
if (step.actor) {
|
|
62
|
+
node.actor = step.actor;
|
|
63
|
+
}
|
|
64
|
+
if (step.expectEventually) {
|
|
65
|
+
node.expectEventually = true;
|
|
66
|
+
}
|
|
61
67
|
if (step.inputs) {
|
|
62
68
|
if (step.inputs === 'passthrough') {
|
|
63
69
|
// Entire data is passed through - store as reference to trigger
|
|
@@ -304,8 +310,10 @@ export function convertDslToGraph(workflowName, meta) {
|
|
|
304
310
|
name: workflowName,
|
|
305
311
|
pikkuFuncId: meta.pikkuFuncId,
|
|
306
312
|
source,
|
|
313
|
+
title: meta.title,
|
|
307
314
|
description: meta.description,
|
|
308
315
|
tags: meta.tags,
|
|
316
|
+
actors: meta.actors,
|
|
309
317
|
context: meta.context,
|
|
310
318
|
nodes: nodesRecord,
|
|
311
319
|
entryNodeIds,
|
|
@@ -96,6 +96,10 @@ export interface FunctionNode extends BaseNode {
|
|
|
96
96
|
outputVar?: string;
|
|
97
97
|
/** Hash of nodeId + RPC input/output schemas for version detection */
|
|
98
98
|
stepHash?: string;
|
|
99
|
+
/** User-flow actor name this step runs as (workflow.do {actor: actors.x}) */
|
|
100
|
+
actor?: string;
|
|
101
|
+
/** True for workflow.expectEventually polling steps (user flows) */
|
|
102
|
+
expectEventually?: boolean;
|
|
99
103
|
}
|
|
100
104
|
/**
|
|
101
105
|
* Flow node - control flow only, no RPC call
|
|
@@ -136,10 +140,14 @@ export interface SerializedWorkflowGraph {
|
|
|
136
140
|
pikkuFuncId: string;
|
|
137
141
|
/** Source type: 'dsl' for pikkuWorkflowFunc, 'graph' for pikkuWorkflowGraph */
|
|
138
142
|
source: WorkflowSourceType;
|
|
143
|
+
/** Optional short display name */
|
|
144
|
+
title?: string;
|
|
139
145
|
/** Optional description */
|
|
140
146
|
description?: string;
|
|
141
147
|
/** Tags for organization */
|
|
142
148
|
tags?: string[];
|
|
149
|
+
/** Actor names a user flow's steps run as */
|
|
150
|
+
actors?: string[];
|
|
143
151
|
/** If true, workflow always executes inline without queues */
|
|
144
152
|
inline?: boolean;
|
|
145
153
|
/** Workflow context/state variables (from Zod schema) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.34",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
|
|
38
|
-
"@pikku/core": "^0.12.
|
|
38
|
+
"@pikku/core": "^0.12.50",
|
|
39
39
|
"openapi-types": "^12.1.3",
|
|
40
40
|
"path-to-regexp": "^8.3.0",
|
|
41
41
|
"ts-json-schema-generator": "2.9.1-next.16",
|
package/src/add/add-workflow.ts
CHANGED
|
@@ -16,7 +16,10 @@ import {
|
|
|
16
16
|
getPropertyValue,
|
|
17
17
|
} from '../utils/get-property-value.js'
|
|
18
18
|
import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js'
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
getSourceText,
|
|
21
|
+
extractActorFromOptions,
|
|
22
|
+
} from '../utils/workflow/dsl/patterns.js'
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Extract a workflow step's display name without letting a non-static name
|
|
@@ -86,7 +89,8 @@ export function collectInvokedRPCs(
|
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
/**
|
|
89
|
-
* Scan for workflow.do(), workflow.
|
|
92
|
+
* Scan for workflow.do(), workflow.expectEventually(), workflow.sleep(), and
|
|
93
|
+
* workflow.cancel() calls to extract workflow steps
|
|
90
94
|
*/
|
|
91
95
|
function getWorkflowInvocations(
|
|
92
96
|
node: ts.Node,
|
|
@@ -100,7 +104,12 @@ function getWorkflowInvocations(
|
|
|
100
104
|
const { name } = node
|
|
101
105
|
|
|
102
106
|
// Check if this is accessing 'do' or 'sleep' property
|
|
103
|
-
if (
|
|
107
|
+
if (
|
|
108
|
+
name.text === 'do' ||
|
|
109
|
+
name.text === 'sleep' ||
|
|
110
|
+
name.text === 'cancel' ||
|
|
111
|
+
name.text === 'expectEventually'
|
|
112
|
+
) {
|
|
104
113
|
// Check if the parent is a call expression
|
|
105
114
|
const parent = node.parent
|
|
106
115
|
if (ts.isCallExpression(parent) && parent.expression === node) {
|
|
@@ -125,6 +134,7 @@ function getWorkflowInvocations(
|
|
|
125
134
|
type: 'rpc',
|
|
126
135
|
stepName,
|
|
127
136
|
rpcName,
|
|
137
|
+
actor: extractActorFromOptions(optionsArg),
|
|
128
138
|
})
|
|
129
139
|
state.rpc.invokedFunctions.add(rpcName)
|
|
130
140
|
} else if (isFunctionLike(secondArg)) {
|
|
@@ -135,6 +145,20 @@ function getWorkflowInvocations(
|
|
|
135
145
|
description: description || '<dynamic>',
|
|
136
146
|
})
|
|
137
147
|
}
|
|
148
|
+
} else if (name.text === 'expectEventually' && args.length >= 4) {
|
|
149
|
+
// workflow.expectEventually(stepName, rpcName, data, predicate, options?)
|
|
150
|
+
const stepName = extractStepName(args[0], checker)
|
|
151
|
+
if (isStringLike(args[1], checker)) {
|
|
152
|
+
const rpcName = extractStringLiteral(args[1], checker)
|
|
153
|
+
steps.push({
|
|
154
|
+
type: 'rpc',
|
|
155
|
+
stepName,
|
|
156
|
+
rpcName,
|
|
157
|
+
expectEventually: true,
|
|
158
|
+
actor: extractActorFromOptions(args.length >= 5 ? args[4] : undefined),
|
|
159
|
+
})
|
|
160
|
+
state.rpc.invokedFunctions.add(rpcName)
|
|
161
|
+
}
|
|
138
162
|
} else if (name.text === 'sleep' && args.length >= 2) {
|
|
139
163
|
// workflow.sleep(stepName, duration)
|
|
140
164
|
const stepNameArg = args[0]
|
|
@@ -337,10 +361,23 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
337
361
|
// For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
|
|
338
362
|
// patterns the DSL extractor doesn't handle (array+push, nested Promise.all
|
|
339
363
|
// with identifier args, etc.) are still registered as invoked functions.
|
|
364
|
+
// When DSL extraction already produced steps this pass is registration-only:
|
|
365
|
+
// appending to `steps` would duplicate nodes and clobber the graph.
|
|
340
366
|
if (wrapperType !== 'dsl') {
|
|
341
|
-
|
|
367
|
+
const sink: WorkflowStepMeta[] = steps.length > 0 ? [] : steps
|
|
368
|
+
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, sink)
|
|
342
369
|
}
|
|
343
370
|
|
|
371
|
+
// Actor names the flow's steps run as (user flows) — powers the console
|
|
372
|
+
// personas view and the User Flow graph badges.
|
|
373
|
+
const actorNames = [
|
|
374
|
+
...new Set(
|
|
375
|
+
steps
|
|
376
|
+
.map((s) => ('actor' in s ? s.actor : undefined))
|
|
377
|
+
.filter((a): a is string => typeof a === 'string')
|
|
378
|
+
),
|
|
379
|
+
]
|
|
380
|
+
|
|
344
381
|
state.workflows.meta[workflowName] = {
|
|
345
382
|
pikkuFuncId,
|
|
346
383
|
name: workflowName,
|
|
@@ -354,6 +391,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
354
391
|
tags,
|
|
355
392
|
expose,
|
|
356
393
|
userFlow: wrapperType === 'user-flow' ? true : undefined,
|
|
394
|
+
actors: actorNames.length > 0 ? actorNames : undefined,
|
|
357
395
|
}
|
|
358
396
|
|
|
359
397
|
// User flows are pure stories of remote RPCs (same rule as client-side CLI
|
|
@@ -21,6 +21,8 @@ import type {
|
|
|
21
21
|
} from '@pikku/core/workflow'
|
|
22
22
|
import {
|
|
23
23
|
isWorkflowDoCall,
|
|
24
|
+
isWorkflowExpectEventuallyCall,
|
|
25
|
+
extractActorFromOptions,
|
|
24
26
|
isWorkflowSleepCall,
|
|
25
27
|
isWorkflowSuspendCall,
|
|
26
28
|
isThrowCancelException,
|
|
@@ -553,10 +555,15 @@ function extractRpcStep(
|
|
|
553
555
|
const inputs =
|
|
554
556
|
args.length >= 3 ? extractInputSources(args[2], context) : undefined
|
|
555
557
|
|
|
556
|
-
//
|
|
558
|
+
// do(step, rpc, data, options?) vs expectEventually(step, rpc, data, predicate, options?)
|
|
559
|
+
const expectEventually = isWorkflowExpectEventuallyCall(call)
|
|
560
|
+
const optionsIndex = expectEventually ? 4 : 3
|
|
561
|
+
const optionsArg =
|
|
562
|
+
args.length > optionsIndex ? args[optionsIndex] : undefined
|
|
563
|
+
|
|
557
564
|
const options =
|
|
558
|
-
|
|
559
|
-
? extractStepOptions(
|
|
565
|
+
optionsArg && ts.isObjectLiteralExpression(optionsArg)
|
|
566
|
+
? extractStepOptions(optionsArg, context)
|
|
560
567
|
: undefined
|
|
561
568
|
|
|
562
569
|
return {
|
|
@@ -566,6 +573,8 @@ function extractRpcStep(
|
|
|
566
573
|
outputVar,
|
|
567
574
|
inputs,
|
|
568
575
|
options,
|
|
576
|
+
actor: extractActorFromOptions(optionsArg),
|
|
577
|
+
expectEventually: expectEventually || undefined,
|
|
569
578
|
}
|
|
570
579
|
} catch (error) {
|
|
571
580
|
context.errors.push({
|
|
@@ -5,7 +5,8 @@ import * as ts from 'typescript'
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Check if a call expression is workflow.do()
|
|
8
|
+
* Check if a call expression is workflow.do() or workflow.expectEventually()
|
|
9
|
+
* (both are RPC steps; expectEventually is the polling variant used by user flows)
|
|
9
10
|
*/
|
|
10
11
|
export function isWorkflowDoCall(
|
|
11
12
|
node: ts.CallExpression,
|
|
@@ -17,12 +18,57 @@ export function isWorkflowDoCall(
|
|
|
17
18
|
|
|
18
19
|
const propAccess = node.expression
|
|
19
20
|
return (
|
|
20
|
-
propAccess.name.text === 'do'
|
|
21
|
+
(propAccess.name.text === 'do' ||
|
|
22
|
+
propAccess.name.text === 'expectEventually') &&
|
|
21
23
|
ts.isIdentifier(propAccess.expression) &&
|
|
22
24
|
propAccess.expression.text === 'workflow'
|
|
23
25
|
)
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Check if a call expression is workflow.expectEventually()
|
|
30
|
+
*/
|
|
31
|
+
export function isWorkflowExpectEventuallyCall(
|
|
32
|
+
node: ts.CallExpression
|
|
33
|
+
): boolean {
|
|
34
|
+
return (
|
|
35
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
36
|
+
node.expression.name.text === 'expectEventually' &&
|
|
37
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
38
|
+
node.expression.expression.text === 'workflow'
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract the actor NAME from a step options object literal:
|
|
44
|
+
* `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
|
|
45
|
+
* or not statically resolvable.
|
|
46
|
+
*/
|
|
47
|
+
export function extractActorFromOptions(
|
|
48
|
+
optionsArg: ts.Expression | undefined
|
|
49
|
+
): string | undefined {
|
|
50
|
+
if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg)) return
|
|
51
|
+
for (const prop of optionsArg.properties) {
|
|
52
|
+
if (
|
|
53
|
+
ts.isPropertyAssignment(prop) &&
|
|
54
|
+
ts.isIdentifier(prop.name) &&
|
|
55
|
+
prop.name.text === 'actor'
|
|
56
|
+
) {
|
|
57
|
+
const init = prop.initializer
|
|
58
|
+
if (ts.isPropertyAccessExpression(init)) {
|
|
59
|
+
return init.name.text
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
ts.isElementAccessExpression(init) &&
|
|
63
|
+
ts.isStringLiteral(init.argumentExpression)
|
|
64
|
+
) {
|
|
65
|
+
return init.argumentExpression.text
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
26
72
|
/**
|
|
27
73
|
* Check if a call expression is workflow.sleep()
|
|
28
74
|
*/
|
|
@@ -93,6 +93,12 @@ function convertStepToNode(
|
|
|
93
93
|
rpcName: step.rpcName,
|
|
94
94
|
next: nextNodeId,
|
|
95
95
|
}
|
|
96
|
+
if (step.actor) {
|
|
97
|
+
node.actor = step.actor
|
|
98
|
+
}
|
|
99
|
+
if (step.expectEventually) {
|
|
100
|
+
node.expectEventually = true
|
|
101
|
+
}
|
|
96
102
|
if (step.inputs) {
|
|
97
103
|
if (step.inputs === 'passthrough') {
|
|
98
104
|
// Entire data is passed through - store as reference to trigger
|
|
@@ -403,8 +409,10 @@ export function convertDslToGraph(
|
|
|
403
409
|
name: workflowName,
|
|
404
410
|
pikkuFuncId: meta.pikkuFuncId,
|
|
405
411
|
source,
|
|
412
|
+
title: meta.title,
|
|
406
413
|
description: meta.description,
|
|
407
414
|
tags: meta.tags,
|
|
415
|
+
actors: meta.actors,
|
|
408
416
|
context: meta.context,
|
|
409
417
|
nodes: nodesRecord,
|
|
410
418
|
entryNodeIds,
|
|
@@ -142,6 +142,10 @@ export interface FunctionNode extends BaseNode {
|
|
|
142
142
|
outputVar?: string
|
|
143
143
|
/** Hash of nodeId + RPC input/output schemas for version detection */
|
|
144
144
|
stepHash?: string
|
|
145
|
+
/** User-flow actor name this step runs as (workflow.do {actor: actors.x}) */
|
|
146
|
+
actor?: string
|
|
147
|
+
/** True for workflow.expectEventually polling steps (user flows) */
|
|
148
|
+
expectEventually?: boolean
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
/**
|
|
@@ -191,10 +195,14 @@ export interface SerializedWorkflowGraph {
|
|
|
191
195
|
pikkuFuncId: string
|
|
192
196
|
/** Source type: 'dsl' for pikkuWorkflowFunc, 'graph' for pikkuWorkflowGraph */
|
|
193
197
|
source: WorkflowSourceType
|
|
198
|
+
/** Optional short display name */
|
|
199
|
+
title?: string
|
|
194
200
|
/** Optional description */
|
|
195
201
|
description?: string
|
|
196
202
|
/** Tags for organization */
|
|
197
203
|
tags?: string[]
|
|
204
|
+
/** Actor names a user flow's steps run as */
|
|
205
|
+
actors?: string[]
|
|
198
206
|
/** If true, workflow always executes inline without queues */
|
|
199
207
|
inline?: boolean
|
|
200
208
|
/** Workflow context/state variables (from Zod schema) */
|