@uipath/maestro-builder-sdk 5.4.1 → 6.0.1
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/dist/check.js +205 -57
- package/dist/core/actions.d.ts +21 -17
- package/dist/core/hitl-routing.d.ts +47 -0
- package/dist/core/hitl-routing.js +33 -0
- package/dist/core/step-ports.d.ts +37 -0
- package/dist/core/step-ports.js +102 -0
- package/dist/decompile.js +91 -2
- package/dist/flow-expr-check.js +11 -0
- package/dist/flow-sdk.d.ts +61 -0
- package/dist/flow-sdk.js +61 -0
- package/dist/generators/_connections.py +44 -0
- package/dist/generators/_resolve.py +52 -6
- package/dist/generators/prepare_connector.py +172 -0
- package/dist/serialize.js +83 -6
- package/package.json +2 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The exits an action declares that the AUTHOR named — a human task's outcomes,
|
|
3
|
+
* an HTTP call's response branches — and the port each one leaves on.
|
|
4
|
+
*
|
|
5
|
+
* This is what `.stepSwitch()` routes over, and the reason it exists as a table
|
|
6
|
+
* rather than as two more `if (spec.kind === …)` arms: `serialize` has to turn
|
|
7
|
+
* an arm's `value` into a port, `check` has to say which values are legal and
|
|
8
|
+
* which were left out, and `decompile` has to turn a port back into a value.
|
|
9
|
+
* Those three read the same list here instead of each carrying their own copy
|
|
10
|
+
* of "how a human task names its ports" and "how an HTTP branch names its
|
|
11
|
+
* ports".
|
|
12
|
+
*
|
|
13
|
+
* `undefined` means the family has no author-declared fan-out at all, which is
|
|
14
|
+
* what makes `.stepSwitch()` on it an error rather than a no-op.
|
|
15
|
+
*
|
|
16
|
+
* The `error` port is deliberately NOT here. It is declared by the definition
|
|
17
|
+
* rather than by the author, it exists on families that have no other fan-out,
|
|
18
|
+
* and it is already routed by `.onError()` / `.stepToList('error', …)`. Folding
|
|
19
|
+
* it in would make every action a `.stepSwitch` candidate and would put failure
|
|
20
|
+
* handling and outcome routing in one list, where an omitted arm would mean two
|
|
21
|
+
* different things.
|
|
22
|
+
*/
|
|
23
|
+
import { hitlRoutesPerOutcome, outcomeSlug } from './hitl-routing.js';
|
|
24
|
+
/**
|
|
25
|
+
* The author-declared exits of `spec`, or a `NoExits` explaining why there are
|
|
26
|
+
* none to route.
|
|
27
|
+
*/
|
|
28
|
+
export function declaredExits(spec, pinnedVersion) {
|
|
29
|
+
const kind = spec && 'kind' in spec ? spec.kind : undefined;
|
|
30
|
+
if (kind === 'hitl') {
|
|
31
|
+
const inputs = spec?.inputs;
|
|
32
|
+
const outcomes = (inputs?.outcomes ?? []);
|
|
33
|
+
if (outcomes.length === 0) {
|
|
34
|
+
return { reason: 'it declares no outcomes', suggestion: `outcomes: ['Approve', 'Reject']` };
|
|
35
|
+
}
|
|
36
|
+
if (!hitlRoutesPerOutcome(inputs, pinnedVersion)) {
|
|
37
|
+
// WHY it kept the single exit decides what the author can do about it, so
|
|
38
|
+
// the caller gets the specific one rather than "ports are off".
|
|
39
|
+
if (inputs?.outcomePorts === false) {
|
|
40
|
+
return {
|
|
41
|
+
reason: 'outcomePorts: false keeps its single `completed` exit',
|
|
42
|
+
suggestion: 'Drop `outcomePorts: false`.',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (inputs?.variant !== undefined) {
|
|
46
|
+
return {
|
|
47
|
+
reason: `the '${inputs.variant}' variant has no per-outcome definition version to select`,
|
|
48
|
+
suggestion: `Drop the variant, or use .step() and switch on out('<step>', 'Action').`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (pinnedVersion !== undefined) {
|
|
52
|
+
return {
|
|
53
|
+
reason: `{ version: '${pinnedVersion}' } pins the definition whose only exit is 'completed'`,
|
|
54
|
+
suggestion: 'Drop the pin.',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (outcomes.length === 1) {
|
|
58
|
+
return {
|
|
59
|
+
reason: 'a single outcome has a single exit, so there is nothing to route',
|
|
60
|
+
suggestion: 'Use .step() — the next step follows it unconditionally.',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
reason: 'every outcome ends the process, so no exit distinguishes them',
|
|
65
|
+
suggestion: 'Use .step().',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return outcomes.map((o, i) => {
|
|
69
|
+
const name = (typeof o === 'string' ? o : o?.name) ?? '';
|
|
70
|
+
return {
|
|
71
|
+
value: String(name),
|
|
72
|
+
port: `outcome-${outcomeSlug(name, i)}`,
|
|
73
|
+
...(typeof o !== 'string' && o?.action === 'End' ? { endsProcess: true } : {}),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (kind === 'http') {
|
|
78
|
+
const branches = (spec?.inputs?.branches
|
|
79
|
+
?? []);
|
|
80
|
+
if (branches.length === 0) {
|
|
81
|
+
return {
|
|
82
|
+
reason: 'it declares no response branches',
|
|
83
|
+
suggestion: `branches: [{ name: 'rateLimited', condition: … }]`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// The fall-through is an exit like any other here — it is what `.switch()`
|
|
87
|
+
// spells as its default arm, and leaving it out is the same omission as
|
|
88
|
+
// leaving out a branch.
|
|
89
|
+
return [
|
|
90
|
+
...branches.map((b) => ({ value: String(b?.name ?? ''), port: `branch-${b?.name ?? ''}` })),
|
|
91
|
+
{ value: 'default', port: 'default', isDefault: true },
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
reason: `a ${kind ?? 'custom'} step declares no author-named exits`,
|
|
96
|
+
suggestion: 'Use .step(); route failures with .onError().',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Narrowing helper — `declaredExits` returns one or the other. */
|
|
100
|
+
export function hasExits(r) {
|
|
101
|
+
return Array.isArray(r);
|
|
102
|
+
}
|
package/dist/decompile.js
CHANGED
|
@@ -408,16 +408,39 @@ class Graph {
|
|
|
408
408
|
const first = outcomes[0]?.id;
|
|
409
409
|
return typeof first === 'string' && first !== '' ? `outcome-${first}` : undefined;
|
|
410
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* Every declared exit of `id`, when ALL of them are wired — the `.stepSwitch`
|
|
413
|
+
* shape. `undefined` otherwise, which leaves the node with a tacit main path
|
|
414
|
+
* and its siblings as side arms.
|
|
415
|
+
*/
|
|
416
|
+
fullyWiredExits(id) {
|
|
417
|
+
const node = this.byId.get(id);
|
|
418
|
+
const declared = node ? declaredExitsOfNode(node) : undefined;
|
|
419
|
+
if (declared === undefined || declared.length < 2)
|
|
420
|
+
return undefined;
|
|
421
|
+
const wired = new Set(this.outEdges(id).map((e) => e.sourcePort));
|
|
422
|
+
if (!declared.every((d) => wired.has(d.port)))
|
|
423
|
+
return undefined;
|
|
424
|
+
return new Set(declared.map((d) => d.port));
|
|
425
|
+
}
|
|
411
426
|
/**
|
|
412
427
|
* Out-edges excluding the side paths recovered as port lists: `error`,
|
|
413
428
|
* `branch-*`, and every `outcome-*` except the primary one.
|
|
429
|
+
*
|
|
430
|
+
* UNLESS every declared exit is wired. Then the node genuinely FORKS — there
|
|
431
|
+
* is no main path, each arm is a successor — and the post-dominator has to see
|
|
432
|
+
* that or it computes the join as the primary arm's first node. Which it did:
|
|
433
|
+
* a converged two-outcome task decompiled with one arm empty and the other
|
|
434
|
+
* swallowing the tail, because `ipdom(review)` came back as `publish`.
|
|
414
435
|
*/
|
|
415
436
|
successorEdges(id) {
|
|
416
437
|
const primary = this.primaryOutcomePort(id);
|
|
438
|
+
const forked = this.fullyWiredExits(id);
|
|
417
439
|
return this.outEdges(id).filter((e) => e.sourcePort !== 'error'
|
|
418
440
|
&& !ARTIFACT_PORTS.has(e.sourcePort)
|
|
419
|
-
&&
|
|
420
|
-
|
|
441
|
+
&& (forked?.has(e.sourcePort)
|
|
442
|
+
|| (!e.sourcePort.startsWith('branch-')
|
|
443
|
+
&& (!e.sourcePort.startsWith('outcome-') || e.sourcePort === primary))));
|
|
421
444
|
}
|
|
422
445
|
}
|
|
423
446
|
// ─── Node → step-spec source ──────────────────────────────────────────────────
|
|
@@ -1952,6 +1975,25 @@ function region(cursor, stop, scope, ipdom, ctx, refInto) {
|
|
|
1952
1975
|
}
|
|
1953
1976
|
// A regular action step (mock / script / http / transform / connector / …).
|
|
1954
1977
|
const spec = emitStepSpec(node, ctx.imp, ctx.inputNames, ctx.o, ctx.graph);
|
|
1978
|
+
// `.stepSwitch` when the node's author-declared exits are ALL wired and the
|
|
1979
|
+
// arms rejoin: that graph has no tacit main path to recover, so the
|
|
1980
|
+
// `.step` + `.stepToList` form below cannot express it. It used to come back
|
|
1981
|
+
// as `.stepToRef("end")` — a reference to the synthetic End node, which
|
|
1982
|
+
// `compile` then refused (REF_UNKNOWN_TARGET), so a converged port graph did
|
|
1983
|
+
// not round-trip at all. Reachable from the designer long before
|
|
1984
|
+
// `.stepSwitch` existed; the arms just had nothing to decompile INTO.
|
|
1985
|
+
const exitPorts = portArmExits(node, ctx);
|
|
1986
|
+
if (exitPorts && exitPorts.length > 1) {
|
|
1987
|
+
const r = ipdom.get(node.id) ?? stop;
|
|
1988
|
+
const armSrcs = exitPorts.map(({ value, port }) => {
|
|
1989
|
+
const armSegs = region(portTarget(ctx.graph, node.id, port), r, scope, ipdom, ctx, refInto);
|
|
1990
|
+
return `{ value: ${str(value)}, body: ${armCb('b', armSegs)} }`;
|
|
1991
|
+
});
|
|
1992
|
+
const armList = `[\n${armSrcs.map((a) => reindent(a, ' ')).join(',\n')},\n]`;
|
|
1993
|
+
segs.push(`.stepSwitch(${str(node.id)}, ${spec}, ${armList}${optsArg(node, ctx)})`);
|
|
1994
|
+
cursor = r === EXIT ? undefined : r;
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1955
1997
|
segs.push(`.step(${str(node.id)}, ${spec}${optsArg(node, ctx)})`);
|
|
1956
1998
|
// An error handler on this step (source port `error` + `errorHandlingEnabled`).
|
|
1957
1999
|
const succ = scopeSucc(ctx.graph, node.id, scope);
|
|
@@ -2037,6 +2079,53 @@ function forwardReach(entries, scope, graph) {
|
|
|
2037
2079
|
}
|
|
2038
2080
|
return seen;
|
|
2039
2081
|
}
|
|
2082
|
+
/**
|
|
2083
|
+
* The node's author-declared exits when EVERY one of them is wired — the shape
|
|
2084
|
+
* `.stepSwitch` authors. `undefined` when the node has no such exits, or when
|
|
2085
|
+
* one is unwired, in which case the `.step` + `.stepToList` recovery still
|
|
2086
|
+
* applies and the unwired port stays unwired.
|
|
2087
|
+
*
|
|
2088
|
+
* Deliberately strict about "every one": a partially wired node still has a
|
|
2089
|
+
* tacit main path, and recovering THAT as `.stepSwitch` would invent arms the
|
|
2090
|
+
* flow does not have.
|
|
2091
|
+
*/
|
|
2092
|
+
function portArmExits(node, ctx) {
|
|
2093
|
+
// One predicate, on the Graph, because `successorEdges` has to agree with this
|
|
2094
|
+
// exactly: if the CFG forks here the arms must be emitted as arms, and if it
|
|
2095
|
+
// does not they must not be.
|
|
2096
|
+
if (ctx.graph.fullyWiredExits(node.id) === undefined)
|
|
2097
|
+
return undefined;
|
|
2098
|
+
return declaredExitsOfNode(node);
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* The exits a node declares, read back off the EMITTED artifact rather than off
|
|
2102
|
+
* an authored spec — `inputs.schema.outcomes` for a human task at 1.1/1.2,
|
|
2103
|
+
* `inputs.branches` plus `default` for an HTTP call. The authoring-side twin is
|
|
2104
|
+
* `declaredExits` in `core/step-ports.ts`; they answer about different inputs,
|
|
2105
|
+
* which is why this is not a call into it.
|
|
2106
|
+
*/
|
|
2107
|
+
function declaredExitsOfNode(node) {
|
|
2108
|
+
const inputs = (node.inputs ?? {});
|
|
2109
|
+
if (String(node.type).startsWith('uipath.human-in-the-loop')) {
|
|
2110
|
+
const version = String(node.typeVersion ?? '');
|
|
2111
|
+
if (version !== '1.1' && version !== '1.2')
|
|
2112
|
+
return undefined;
|
|
2113
|
+
const outcomes = (inputs.schema?.outcomes ?? []);
|
|
2114
|
+
if (outcomes.length === 0)
|
|
2115
|
+
return undefined;
|
|
2116
|
+
return outcomes.map((o) => ({ value: String(o?.name ?? ''), port: `outcome-${o?.id ?? ''}` }));
|
|
2117
|
+
}
|
|
2118
|
+
if (node.type === T.http || node.type === T.httpV2) {
|
|
2119
|
+
const branches = (inputs.branches ?? []);
|
|
2120
|
+
if (branches.length === 0)
|
|
2121
|
+
return undefined;
|
|
2122
|
+
return [
|
|
2123
|
+
...branches.map((b) => ({ value: String(b?.name ?? ''), port: `branch-${b?.name ?? ''}` })),
|
|
2124
|
+
{ value: 'default', port: 'default' },
|
|
2125
|
+
];
|
|
2126
|
+
}
|
|
2127
|
+
return undefined;
|
|
2128
|
+
}
|
|
2040
2129
|
/**
|
|
2041
2130
|
* Emit a `.switch(...)` from a `core.logic.switch` node. Each case's serialized
|
|
2042
2131
|
* expression is `<discriminant> === <JSON-literal>`; the discriminant (shared by
|
package/dist/flow-expr-check.js
CHANGED
|
@@ -85,6 +85,10 @@ function addStepNames(steps, roots) {
|
|
|
85
85
|
if (s.default)
|
|
86
86
|
addStepNames(s.default, roots);
|
|
87
87
|
break;
|
|
88
|
+
case 'stepSwitch':
|
|
89
|
+
for (const c of s.cases)
|
|
90
|
+
addStepNames(c.body, roots);
|
|
91
|
+
break;
|
|
88
92
|
case 'loop':
|
|
89
93
|
addStepNames(s.body, roots);
|
|
90
94
|
break;
|
|
@@ -145,6 +149,13 @@ function walk(steps, roots, out) {
|
|
|
145
149
|
walk(s.default, caseRoots, out);
|
|
146
150
|
break;
|
|
147
151
|
}
|
|
152
|
+
case 'stepSwitch':
|
|
153
|
+
// The action's own inputs, then each arm — no discriminant to bind, the
|
|
154
|
+
// exits are named by the step rather than compared against a value.
|
|
155
|
+
deepExprs(s.spec.inputs, s.name, roots, out);
|
|
156
|
+
for (const c of s.cases)
|
|
157
|
+
walk(c.body, roots, out);
|
|
158
|
+
break;
|
|
148
159
|
case 'loop':
|
|
149
160
|
pushExpr(s.collection.js, s.collection.literal, s.name, roots, out);
|
|
150
161
|
if (s.options?.completionCondition) {
|
package/dist/flow-sdk.d.ts
CHANGED
|
@@ -292,6 +292,19 @@ export interface SwitchArm {
|
|
|
292
292
|
label?: string;
|
|
293
293
|
body: Step[];
|
|
294
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* One arm of a built `.stepSwitch` — the same shape as {@link SwitchArm}, but
|
|
297
|
+
* `value` names an exit the ACTION declares (a human-task outcome, an HTTP
|
|
298
|
+
* response branch) rather than a value to compare against. No `label`: a port
|
|
299
|
+
* edge carries none, and the exit's own name is already the label the canvas
|
|
300
|
+
* draws.
|
|
301
|
+
*/
|
|
302
|
+
export interface PortArm {
|
|
303
|
+
/** The exit this arm routes — a human-task outcome name, an HTTP branch name, or `'default'`. */
|
|
304
|
+
value: string;
|
|
305
|
+
/** The steps that run on that exit. Ends in `.return()` to be terminal; otherwise it converges. */
|
|
306
|
+
body: Step[];
|
|
307
|
+
}
|
|
295
308
|
/**
|
|
296
309
|
* Options shared by every builder method that creates a definition-backed node:
|
|
297
310
|
* `version` selects the exact node definition to compile against, and `updates`
|
|
@@ -425,6 +438,12 @@ export type Step = {
|
|
|
425
438
|
cases: SwitchArm[];
|
|
426
439
|
default?: Step[];
|
|
427
440
|
options?: NodeOptions;
|
|
441
|
+
} | {
|
|
442
|
+
kind: 'stepSwitch';
|
|
443
|
+
name: string;
|
|
444
|
+
spec: FlowActionSpec;
|
|
445
|
+
cases: PortArm[];
|
|
446
|
+
options?: NodeOptions;
|
|
428
447
|
} | {
|
|
429
448
|
kind: 'loop';
|
|
430
449
|
name: string;
|
|
@@ -693,6 +712,48 @@ declare class StepList {
|
|
|
693
712
|
* @returns This builder, so calls chain.
|
|
694
713
|
*/
|
|
695
714
|
step(name: string, spec: FlowActionSpec | FlowAction, options?: NodeOptions): this;
|
|
715
|
+
/**
|
|
716
|
+
* Add an action node and route EVERY exit it declares, one arm per exit.
|
|
717
|
+
*
|
|
718
|
+
* The symmetric form of `.step()` + `.stepToList()`. Where those make the
|
|
719
|
+
* first exit the tacit next step and the rest side branches, this makes all
|
|
720
|
+
* of them arms of one construct — so nothing about a human task's routing
|
|
721
|
+
* depends on which outcome happens to be listed first.
|
|
722
|
+
*
|
|
723
|
+
* Arms behave exactly like `.switch()`'s: one that ends in `.return()` is
|
|
724
|
+
* terminal, and one that does not CONVERGES, so the step after the
|
|
725
|
+
* `.stepSwitch` fans in from every arm that reaches it. (That is the
|
|
726
|
+
* difference from `.stepToList`, whose arms get an End of their own.)
|
|
727
|
+
*
|
|
728
|
+
* `value` names an exit the ACTION declares — a human task's outcome name, an
|
|
729
|
+
* HTTP response branch's name, or `'default'` for HTTP's fall-through — not a
|
|
730
|
+
* value to compare against. `check` refuses a value the step does not declare
|
|
731
|
+
* and warns about a declared exit left out, which compiles to an End node.
|
|
732
|
+
*
|
|
733
|
+
* @param name - The step's id, as `.step()`.
|
|
734
|
+
* @param spec - What the node does, from an action factory.
|
|
735
|
+
* @param cases - One arm per declared exit.
|
|
736
|
+
* @param options - Node options, as `.step()`.
|
|
737
|
+
* @returns This builder, so calls chain.
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* **Route a human task's outcomes, and converge on one return**
|
|
741
|
+
* ```ts
|
|
742
|
+
* export default flow('draft-review')
|
|
743
|
+
* .output({ summary: types.string })
|
|
744
|
+
* .var('summary', types.string)
|
|
745
|
+
* .stepSwitch('review', hitl({ fields: [], outcomes: ['Publish', 'Revise'] }), [
|
|
746
|
+
* { value: 'Publish', body: (b) => b.step('publish', script({ code: 'return "published";', returns: 'string' }), { updates: { summary: out('publish') } }) },
|
|
747
|
+
* { value: 'Revise', body: (b) => b.step('sendBack', script({ code: 'return "revise";', returns: 'string' }), { updates: { summary: out('sendBack') } }) },
|
|
748
|
+
* ])
|
|
749
|
+
* .return({ summary: v('summary') })
|
|
750
|
+
* .build();
|
|
751
|
+
* ```
|
|
752
|
+
*/
|
|
753
|
+
stepSwitch(name: string, spec: FlowActionSpec | FlowAction, cases: {
|
|
754
|
+
value: string;
|
|
755
|
+
body: (b: ArmBuilder) => void;
|
|
756
|
+
}[], options?: NodeOptions): this;
|
|
696
757
|
/**
|
|
697
758
|
* Handle the PREVIOUS step's failure: if it fails, the flow runs `bodyFn`'s
|
|
698
759
|
* steps instead of continuing.
|
package/dist/flow-sdk.js
CHANGED
|
@@ -303,6 +303,67 @@ class StepList {
|
|
|
303
303
|
this.steps.push({ kind: 'action', name, spec: spec, ...(options ? { options } : {}) });
|
|
304
304
|
return this;
|
|
305
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Add an action node and route EVERY exit it declares, one arm per exit.
|
|
308
|
+
*
|
|
309
|
+
* The symmetric form of `.step()` + `.stepToList()`. Where those make the
|
|
310
|
+
* first exit the tacit next step and the rest side branches, this makes all
|
|
311
|
+
* of them arms of one construct — so nothing about a human task's routing
|
|
312
|
+
* depends on which outcome happens to be listed first.
|
|
313
|
+
*
|
|
314
|
+
* Arms behave exactly like `.switch()`'s: one that ends in `.return()` is
|
|
315
|
+
* terminal, and one that does not CONVERGES, so the step after the
|
|
316
|
+
* `.stepSwitch` fans in from every arm that reaches it. (That is the
|
|
317
|
+
* difference from `.stepToList`, whose arms get an End of their own.)
|
|
318
|
+
*
|
|
319
|
+
* `value` names an exit the ACTION declares — a human task's outcome name, an
|
|
320
|
+
* HTTP response branch's name, or `'default'` for HTTP's fall-through — not a
|
|
321
|
+
* value to compare against. `check` refuses a value the step does not declare
|
|
322
|
+
* and warns about a declared exit left out, which compiles to an End node.
|
|
323
|
+
*
|
|
324
|
+
* @param name - The step's id, as `.step()`.
|
|
325
|
+
* @param spec - What the node does, from an action factory.
|
|
326
|
+
* @param cases - One arm per declared exit.
|
|
327
|
+
* @param options - Node options, as `.step()`.
|
|
328
|
+
* @returns This builder, so calls chain.
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* **Route a human task's outcomes, and converge on one return**
|
|
332
|
+
* ```ts
|
|
333
|
+
* export default flow('draft-review')
|
|
334
|
+
* .output({ summary: types.string })
|
|
335
|
+
* .var('summary', types.string)
|
|
336
|
+
* .stepSwitch('review', hitl({ fields: [], outcomes: ['Publish', 'Revise'] }), [
|
|
337
|
+
* { value: 'Publish', body: (b) => b.step('publish', script({ code: 'return "published";', returns: 'string' }), { updates: { summary: out('publish') } }) },
|
|
338
|
+
* { value: 'Revise', body: (b) => b.step('sendBack', script({ code: 'return "revise";', returns: 'string' }), { updates: { summary: out('sendBack') } }) },
|
|
339
|
+
* ])
|
|
340
|
+
* .return({ summary: v('summary') })
|
|
341
|
+
* .build();
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
stepSwitch(name, spec, cases, options) {
|
|
345
|
+
if (!Array.isArray(cases) || cases.length === 0) {
|
|
346
|
+
throw new TypeError(`.stepSwitch('${name}'): needs at least one arm. With nothing to route, use .step().`);
|
|
347
|
+
}
|
|
348
|
+
const arms = cases.map((c) => {
|
|
349
|
+
const arm = new ArmBuilder();
|
|
350
|
+
c.body?.(arm);
|
|
351
|
+
// A `.switch()` arm's label becomes `inputs.cases[].label` on the decision
|
|
352
|
+
// node. There is no such slot here: an edge carries no label, and the port
|
|
353
|
+
// is already named by the outcome or branch it leaves. Refusing beats
|
|
354
|
+
// accepting it and dropping it on the floor.
|
|
355
|
+
if (arm.armLabel !== undefined) {
|
|
356
|
+
throw new TypeError(`.stepSwitch('${name}') arm '${c.value}': .label() has nowhere to go — a port edge carries no `
|
|
357
|
+
+ `label, and the exit is named by "${c.value}" already. Use options.label to rename the NODE.`);
|
|
358
|
+
}
|
|
359
|
+
return { value: String(c.value), body: arm.steps };
|
|
360
|
+
});
|
|
361
|
+
this.steps.push({
|
|
362
|
+
kind: 'stepSwitch', name, spec: spec, cases: arms,
|
|
363
|
+
...(options ? { options } : {}),
|
|
364
|
+
});
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
306
367
|
/**
|
|
307
368
|
* Handle the PREVIOUS step's failure: if it fails, the flow runs `bodyFn`'s
|
|
308
369
|
* steps instead of continuing.
|
|
@@ -30,6 +30,8 @@ __all__ = [
|
|
|
30
30
|
"ConnectionError_",
|
|
31
31
|
"byoa_connections",
|
|
32
32
|
"byoa_listing",
|
|
33
|
+
"candidate_connections",
|
|
34
|
+
"candidate_listing",
|
|
33
35
|
"discover_connection",
|
|
34
36
|
"lookup_connection",
|
|
35
37
|
"merge_bindings",
|
|
@@ -130,6 +132,48 @@ def discover_connection(connector_key: str, name: str | None = None) -> dict:
|
|
|
130
132
|
return rows[0]
|
|
131
133
|
|
|
132
134
|
|
|
135
|
+
def candidate_connections(connector_key: str, name: str | None = None) -> list[dict]:
|
|
136
|
+
"""Every enabled connection for `connector_key`, own folder FIRST.
|
|
137
|
+
|
|
138
|
+
`discover_connection` answers "which one" and stops at the first non-empty
|
|
139
|
+
scope, which is right for picking a connection and wrong for retrying one:
|
|
140
|
+
when the own-folder listing holds exactly one, the tenant-wide connections
|
|
141
|
+
are never even looked at. So a lookup that exhausts the personal-workspace
|
|
142
|
+
connection reports a bad VALUE while the record sits in another folder's
|
|
143
|
+
connection (flow-builder-sdk#744, measured on two Jira connections where
|
|
144
|
+
only the second carried project `TS`).
|
|
145
|
+
|
|
146
|
+
Ordering is the same preference `discover_connection` encodes — the caller's
|
|
147
|
+
own folder is the likeliest and is tried first — but nothing is dropped.
|
|
148
|
+
Deduplicated by `Id`, because the tenant-wide listing repeats the own-folder
|
|
149
|
+
rows.
|
|
150
|
+
|
|
151
|
+
This does NOT decide anything, and in particular does not raise on several
|
|
152
|
+
matches: choosing among them is the caller's job, and the only honest way to
|
|
153
|
+
choose is evidence (a lookup that resolves on exactly one). That keeps this
|
|
154
|
+
module's rule intact — ambiguity is never resolved by guessing.
|
|
155
|
+
"""
|
|
156
|
+
ordered: list[dict] = []
|
|
157
|
+
seen: set[str] = set()
|
|
158
|
+
for all_folders in (False, True):
|
|
159
|
+
for row in _matching(_list(all_folders=all_folders), connector_key, name):
|
|
160
|
+
identifier = str(row.get("Id", ""))
|
|
161
|
+
if identifier and identifier in seen:
|
|
162
|
+
continue
|
|
163
|
+
seen.add(identifier)
|
|
164
|
+
ordered.append(row)
|
|
165
|
+
return ordered
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def candidate_listing(rows: list[dict], indent: str = " ") -> str:
|
|
169
|
+
"""The candidates, one pasteable `--connection-id` line each."""
|
|
170
|
+
return "\n".join(
|
|
171
|
+
f"{indent}{row.get('Name')} --connection-id {row.get('Id')}"
|
|
172
|
+
f" (folder: {row.get('Folder')})"
|
|
173
|
+
for row in rows
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
133
177
|
def _enabled_listing(rows: list[dict], connector_key: str) -> str:
|
|
134
178
|
"""The enabled connections for `connector_key`, one per line, pasteable."""
|
|
135
179
|
enabled = _matching(rows, connector_key, None)
|
|
@@ -25,6 +25,7 @@ import subprocess
|
|
|
25
25
|
import sys
|
|
26
26
|
|
|
27
27
|
__all__ = [
|
|
28
|
+
"LookupExhausted",
|
|
28
29
|
"ResolutionError",
|
|
29
30
|
"parse_resolve_flag",
|
|
30
31
|
"resolve_one",
|
|
@@ -55,6 +56,36 @@ class ResolutionError(Exception):
|
|
|
55
56
|
"""A lookup that could not be resolved, with the reason a human needs."""
|
|
56
57
|
|
|
57
58
|
|
|
59
|
+
class LookupExhausted(ResolutionError):
|
|
60
|
+
"""The collection was read to the end on THIS connection and had no match.
|
|
61
|
+
|
|
62
|
+
Split out from its parent because the two failures want opposite responses
|
|
63
|
+
and the caller cannot tell them apart from a message. A transport or
|
|
64
|
+
permission failure says nothing about whether the value exists — retrying it
|
|
65
|
+
on another connection would hide a broken call behind a confident "no
|
|
66
|
+
connection has it". An exhaustive miss is positive evidence about this
|
|
67
|
+
connection specifically, and is the signal `prepare` uses to demote it and
|
|
68
|
+
try the next candidate (flow-builder-sdk#744).
|
|
69
|
+
|
|
70
|
+
`capped` flips that: a scan stopped at the record cap did NOT reach the end,
|
|
71
|
+
so it is not evidence of absence and must not demote anything.
|
|
72
|
+
|
|
73
|
+
The attributes carry the counts so a caller can report per-candidate results
|
|
74
|
+
without re-parsing the sentence.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, message: str, *, field: str, collection: str, by: str,
|
|
78
|
+
value: str, scanned: int, pages: int, capped: bool) -> None:
|
|
79
|
+
super().__init__(message)
|
|
80
|
+
self.field = field
|
|
81
|
+
self.collection = collection
|
|
82
|
+
self.by = by
|
|
83
|
+
self.value = value
|
|
84
|
+
self.scanned = scanned
|
|
85
|
+
self.pages = pages
|
|
86
|
+
self.capped = capped
|
|
87
|
+
|
|
88
|
+
|
|
58
89
|
def parse_resolve_flag(raw: str) -> tuple[str, str, str]:
|
|
59
90
|
"""`channel:profile.email=dustin@example.com` -> (field, by, value).
|
|
60
91
|
|
|
@@ -261,13 +292,28 @@ def resolve_one(
|
|
|
261
292
|
query = f"{query}&nextPage={token}" if query else f"nextPage={token}"
|
|
262
293
|
|
|
263
294
|
capped = scanned >= MAX_SCANNED or pages > PAGE_GUARD
|
|
264
|
-
|
|
295
|
+
# This states the FACT and stops; `prepare` owns the remedy, because
|
|
296
|
+
# `prepare` is what knows the other candidate connections.
|
|
297
|
+
#
|
|
298
|
+
# It deliberately carries NO "check the value" and NO `uip is resources`
|
|
299
|
+
# pointer. The old text led with both: it named only the value as suspect
|
|
300
|
+
# when the connection is equally suspect, and then sent the reader to a
|
|
301
|
+
# tenant crawl the skill explicitly rules out ("Tenant discovery is not a
|
|
302
|
+
# phase of either loop", references/CLI-LOOP.md). An agent followed that
|
|
303
|
+
# advice into exactly what it had been told not to do — 50 of 94 Bash calls
|
|
304
|
+
# hand-crawling Integration Service, 41 of them paging this collection, and
|
|
305
|
+
# the run hit its ceiling without producing a flow (flow-builder-sdk#744).
|
|
306
|
+
#
|
|
307
|
+
# The listing command is still offered, once, by the caller's terminal
|
|
308
|
+
# report — where every connection has been tried and seeing what IS there
|
|
309
|
+
# is finally the next step rather than a detour.
|
|
310
|
+
raise LookupExhausted(
|
|
265
311
|
f"{field}: no record in {collection!r} has {by}={value!r} "
|
|
266
312
|
f"(scanned {scanned} record(s) across {pages + 1} page(s)"
|
|
267
313
|
+ (f"; STOPPED AT THE {MAX_SCANNED}-record cap, so the value may be "
|
|
268
|
-
f"further in" if capped
|
|
269
|
-
|
|
270
|
-
f"
|
|
271
|
-
|
|
272
|
-
|
|
314
|
+
f"further in" if capped
|
|
315
|
+
else "; collection exhausted")
|
|
316
|
+
+ f") on connection {connection_id}.",
|
|
317
|
+
field=field, collection=collection, by=by, value=value,
|
|
318
|
+
scanned=scanned, pages=pages + 1, capped=capped,
|
|
273
319
|
)
|