@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
|
@@ -382,6 +382,12 @@ def main() -> None:
|
|
|
382
382
|
# step that removes 4-7 tool calls per task: listing connections, filtering
|
|
383
383
|
# them, and then hunting Orchestrator for a folder key that was on the
|
|
384
384
|
# connection record all along.
|
|
385
|
+
# Whether the CALLER chose the connection, recorded before discovery
|
|
386
|
+
# overwrites `connection_id` with its own answer. An explicit choice is never
|
|
387
|
+
# second-guessed: the retry in `_write_resolutions` is for a connection this
|
|
388
|
+
# command picked, not for one the author named (flow-builder-sdk#744).
|
|
389
|
+
args._connection_pinned = bool(args.connection_id or args.connection)
|
|
390
|
+
|
|
385
391
|
if not args.connection_id:
|
|
386
392
|
try:
|
|
387
393
|
found = _connections.discover_connection(args.key, args.connection)
|
|
@@ -1125,6 +1131,14 @@ def _write_resolutions(args, node_type: str) -> None:
|
|
|
1125
1131
|
args.key, specs[field], field, by, value,
|
|
1126
1132
|
args.connection_id, resolved_values,
|
|
1127
1133
|
)
|
|
1134
|
+
except _resolve.LookupExhausted as error:
|
|
1135
|
+
# The collection was read to the END on this connection and the
|
|
1136
|
+
# value is not in it. That is evidence about the CONNECTION, not
|
|
1137
|
+
# only about the value — so before failing, try the other enabled
|
|
1138
|
+
# connections for this connector. A transport failure is the
|
|
1139
|
+
# sibling exception below and is never retried: retrying it would
|
|
1140
|
+
# dress a broken call up as "no connection has it".
|
|
1141
|
+
_retry_or_exit(args, node_type, specs, ordered, error)
|
|
1128
1142
|
except _resolve.ResolutionError as error:
|
|
1129
1143
|
sys.exit(f"prepare-connector: {error}")
|
|
1130
1144
|
resolved_values[field] = got
|
|
@@ -1143,6 +1157,164 @@ def _write_resolutions(args, node_type: str) -> None:
|
|
|
1143
1157
|
print(f" wrote {len(existing)} resolution(s) to {path}")
|
|
1144
1158
|
|
|
1145
1159
|
|
|
1160
|
+
def _candidate_label(row: dict) -> str:
|
|
1161
|
+
"""`name (folder)` for a per-candidate report line."""
|
|
1162
|
+
return f"{row.get('Name')} ({row.get('Folder')})"
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def _probe_candidate(args, specs, ordered, connection_id: str):
|
|
1166
|
+
"""Resolve every requested lookup against one connection.
|
|
1167
|
+
|
|
1168
|
+
Returns `None` when all of them resolve, or the first reason they did not:
|
|
1169
|
+
`("exhausted", <LookupExhausted>)` or `("error", <ResolutionError>)`. The
|
|
1170
|
+
two are kept apart all the way up, because only the first is evidence about
|
|
1171
|
+
the connection.
|
|
1172
|
+
"""
|
|
1173
|
+
resolved: dict[str, str] = {}
|
|
1174
|
+
for field, by, value in ordered:
|
|
1175
|
+
try:
|
|
1176
|
+
resolved[field] = _resolve.resolve_one(
|
|
1177
|
+
args.key, specs[field], field, by, value, connection_id, resolved,
|
|
1178
|
+
)
|
|
1179
|
+
except _resolve.LookupExhausted as error:
|
|
1180
|
+
return ("exhausted", error)
|
|
1181
|
+
except _resolve.ResolutionError as error:
|
|
1182
|
+
return ("error", error)
|
|
1183
|
+
return None
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def _retry_or_exit(args, node_type: str, specs, ordered, first) -> None:
|
|
1187
|
+
"""Try the other candidate connections, or exit explaining what was tried.
|
|
1188
|
+
|
|
1189
|
+
NEVER RETURNS. Either it re-execs this command pinned to a connection that
|
|
1190
|
+
resolves everything, or it exits with a report.
|
|
1191
|
+
"""
|
|
1192
|
+
if args._connection_pinned:
|
|
1193
|
+
# An explicit --connection-id / --connection is the author's decision and
|
|
1194
|
+
# is not overridden. The alternatives are still named, because the whole
|
|
1195
|
+
# cost of this bug was not knowing they existed.
|
|
1196
|
+
others = [
|
|
1197
|
+
row for row in _connections.candidate_connections(args.key)
|
|
1198
|
+
if str(row.get("Id")) != str(args.connection_id)
|
|
1199
|
+
]
|
|
1200
|
+
extra = ""
|
|
1201
|
+
if others:
|
|
1202
|
+
extra = (
|
|
1203
|
+
f"\n{len(others)} other enabled {args.key} connection"
|
|
1204
|
+
f"{'s' if len(others) > 1 else ''} exist"
|
|
1205
|
+
f"{'' if len(others) > 1 else 's'}:\n"
|
|
1206
|
+
f"{_connections.candidate_listing(others)}\n"
|
|
1207
|
+
"Re-run against one of those, or check the value. This run kept the "
|
|
1208
|
+
"connection you named."
|
|
1209
|
+
)
|
|
1210
|
+
sys.exit(f"prepare-connector: {first}{extra}")
|
|
1211
|
+
|
|
1212
|
+
try:
|
|
1213
|
+
candidates = _connections.candidate_connections(args.key, args.connection)
|
|
1214
|
+
except _connections.ConnectionError_ as error:
|
|
1215
|
+
# The listing itself failed; report the original lookup failure, which is
|
|
1216
|
+
# the thing the author asked about.
|
|
1217
|
+
sys.exit(f"prepare-connector: {first}\n(could not list other connections: {error})")
|
|
1218
|
+
|
|
1219
|
+
others = [row for row in candidates
|
|
1220
|
+
if str(row.get("Id")) != str(args.connection_id)]
|
|
1221
|
+
if not others:
|
|
1222
|
+
sys.exit(
|
|
1223
|
+
f"prepare-connector: {first}\n"
|
|
1224
|
+
f"This is the only enabled {args.key} connection on the tenant, so the "
|
|
1225
|
+
"value is what to check."
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
print(f" {first.field}: not on this connection; trying "
|
|
1229
|
+
f"{len(others)} other {args.key} connection(s)")
|
|
1230
|
+
winners: list[dict] = []
|
|
1231
|
+
report: list[str] = []
|
|
1232
|
+
for row in others:
|
|
1233
|
+
outcome = _probe_candidate(args, specs, ordered, str(row.get("Id")))
|
|
1234
|
+
if outcome is None:
|
|
1235
|
+
winners.append(row)
|
|
1236
|
+
report.append(f" {_candidate_label(row)} resolves all "
|
|
1237
|
+
f"{len(ordered)} lookup(s)")
|
|
1238
|
+
continue
|
|
1239
|
+
kind, error = outcome
|
|
1240
|
+
if kind == "exhausted":
|
|
1241
|
+
report.append(
|
|
1242
|
+
f" {_candidate_label(row)} {error.field}: "
|
|
1243
|
+
f"{error.scanned} record(s) scanned, no match")
|
|
1244
|
+
else:
|
|
1245
|
+
report.append(f" {_candidate_label(row)} {error}")
|
|
1246
|
+
|
|
1247
|
+
if len(winners) == 1:
|
|
1248
|
+
chosen = winners[0]
|
|
1249
|
+
# Undo the bindings this run wrote for the connection that cannot serve
|
|
1250
|
+
# the lookups, then start over pinned to the one that can. Re-running
|
|
1251
|
+
# rather than switching in place is deliberate: the overlay and its
|
|
1252
|
+
# lookup specs are described THROUGH a connection
|
|
1253
|
+
# (`_lookup_specs_from_overlay`), so adopting a different connection
|
|
1254
|
+
# after the describe would leave the overlay describing one connection
|
|
1255
|
+
# while bindings.json names another.
|
|
1256
|
+
_restore_bindings(args)
|
|
1257
|
+
print(f" connection: {_candidate_label(chosen)} resolves "
|
|
1258
|
+
f"{first.field}; re-running against it "
|
|
1259
|
+
f"(--connection-id {chosen.get('Id')})")
|
|
1260
|
+
argv = [sys.executable, *sys.argv,
|
|
1261
|
+
"--connection-id", str(chosen.get("Id"))]
|
|
1262
|
+
# The ledger keys a resolution by [nodeType, field, by, value] and NOT by
|
|
1263
|
+
# connection, so an id recorded against the connection just abandoned
|
|
1264
|
+
# would be reused as "already resolved". Force the re-run to resolve
|
|
1265
|
+
# against the connection it actually pins.
|
|
1266
|
+
if not args.refresh_lookups:
|
|
1267
|
+
argv.append("--refresh-lookups")
|
|
1268
|
+
sys.stdout.flush()
|
|
1269
|
+
try:
|
|
1270
|
+
os.execv(sys.executable, argv)
|
|
1271
|
+
except OSError as error:
|
|
1272
|
+
# `execv` does not return on success, so reaching here means the
|
|
1273
|
+
# re-run could not start. Hand back the command rather than a
|
|
1274
|
+
# traceback: the author is one paste from the answer.
|
|
1275
|
+
pinned = " ".join(argv[2:])
|
|
1276
|
+
sys.exit(
|
|
1277
|
+
f"prepare-connector: {first}\n"
|
|
1278
|
+
f"{_candidate_label(chosen)} resolves it, but this command could "
|
|
1279
|
+
f"not re-run itself ({error}). Run:\n prepare {pinned}"
|
|
1280
|
+
)
|
|
1281
|
+
# Unreachable in production — execv replaced the process. Kept so the
|
|
1282
|
+
# function can never fall through to the report below, which would
|
|
1283
|
+
# contradict the line just printed.
|
|
1284
|
+
raise SystemExit(0)
|
|
1285
|
+
|
|
1286
|
+
if len(winners) > 1:
|
|
1287
|
+
sys.exit(
|
|
1288
|
+
f"prepare-connector: {len(winners)} {args.key} connections resolve "
|
|
1289
|
+
f"every requested lookup, and they are not interchangeable — the ids "
|
|
1290
|
+
f"they return differ per connection. Pick the one you mean:\n"
|
|
1291
|
+
f"{_connections.candidate_listing(winners)}"
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
# Nothing resolved anywhere. Report every candidate and what it held, the
|
|
1295
|
+
# one connection first, so the reader can see the value really is absent
|
|
1296
|
+
# rather than guess which connection was asked.
|
|
1297
|
+
original = next(
|
|
1298
|
+
(row for row in candidates
|
|
1299
|
+
if str(row.get("Id")) == str(args.connection_id)),
|
|
1300
|
+
{"Name": args.connection_id, "Folder": "?"},
|
|
1301
|
+
)
|
|
1302
|
+
lines = [f" {_candidate_label(original)} {first.field}: "
|
|
1303
|
+
f"{first.scanned} record(s) scanned, no match", *report]
|
|
1304
|
+
# HERE is where the collection listing finally belongs: every connection has
|
|
1305
|
+
# been tried, so seeing what the collection actually holds is the next step
|
|
1306
|
+
# rather than the detour it is while a retry is still available.
|
|
1307
|
+
sys.exit(
|
|
1308
|
+
f"prepare-connector: no {args.key} connection could resolve "
|
|
1309
|
+
f"{first.field}: {first.by}={first.value!r}\n"
|
|
1310
|
+
+ "\n".join(lines) + "\n"
|
|
1311
|
+
"Every candidate was read to the end, so the value is what to check now. "
|
|
1312
|
+
f"To see what {first.collection!r} holds:\n"
|
|
1313
|
+
f" uip is resources run list {args.key} {first.collection} "
|
|
1314
|
+
f"--connection-id {args.connection_id}"
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
|
|
1146
1318
|
def _lookup_specs_from_overlay(
|
|
1147
1319
|
out_dir: str, node_type: str, object_name: str | None = None,
|
|
1148
1320
|
) -> dict:
|
package/dist/serialize.js
CHANGED
|
@@ -14,6 +14,8 @@ import { Expr, toExpr, SCHEDULE_PRESETS, DELAY_PRESETS, parseIxpProjectType, fla
|
|
|
14
14
|
import { FlowAction, FlowTrigger } from './core/node-classes.js';
|
|
15
15
|
import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
|
|
16
16
|
import { bindingSelfNameMessage } from './core/binding-messages.js';
|
|
17
|
+
import { hitlRoutesPerOutcome } from './core/hitl-routing.js';
|
|
18
|
+
import { declaredExits, hasExits } from './core/step-ports.js';
|
|
17
19
|
import { buildConfiguration, transportHttpMethod } from './config.js';
|
|
18
20
|
import { stableId } from './core/stable-id.js';
|
|
19
21
|
import { readEventFilter, eventFilterProblem, eventFilterJmes, eventFilterTreeLeaf } from './event-filters.js';
|
|
@@ -38,6 +40,7 @@ function bodyUsesBreak(steps) {
|
|
|
38
40
|
return steps.some((s) => s.kind === 'break'
|
|
39
41
|
|| (s.kind === 'branch' && (bodyUsesBreak(s.then) || bodyUsesBreak(s.otherwise)))
|
|
40
42
|
|| (s.kind === 'switch' && (s.cases.some((c) => bodyUsesBreak(c.body)) || (s.default ? bodyUsesBreak(s.default) : false)))
|
|
43
|
+
|| (s.kind === 'stepSwitch' && s.cases.some((c) => bodyUsesBreak(c.body)))
|
|
41
44
|
|| (s.kind === 'parallel' && s.arms.some(bodyUsesBreak))
|
|
42
45
|
|| (s.kind === 'stepToList' && bodyUsesBreak(s.body)));
|
|
43
46
|
}
|
|
@@ -3676,6 +3679,8 @@ function stepNamesOf(steps, out = []) {
|
|
|
3676
3679
|
s.cases.forEach((c) => stepNamesOf(c.body, out));
|
|
3677
3680
|
stepNamesOf(s.default ?? [], out);
|
|
3678
3681
|
}
|
|
3682
|
+
else if (s.kind === 'stepSwitch')
|
|
3683
|
+
s.cases.forEach((c) => stepNamesOf(c.body, out));
|
|
3679
3684
|
else if (s.kind === 'parallel')
|
|
3680
3685
|
s.arms.forEach((a) => stepNamesOf(a, out));
|
|
3681
3686
|
else if (s.kind === 'loop')
|
|
@@ -4982,18 +4987,24 @@ export function serialize(built, opts = {}) {
|
|
|
4982
4987
|
const id = stepUid(scope, step.name);
|
|
4983
4988
|
const hitlType = hitlNodeType(spec.inputs);
|
|
4984
4989
|
// Rich options select a newer definition, same rule as the scheduled
|
|
4985
|
-
// trigger's cron:
|
|
4986
|
-
// exits) and `exposeError` needs 1.2 (adds the `error`
|
|
4987
|
-
// are the same per-outcome handles).
|
|
4988
|
-
// default, byte-identically.
|
|
4989
|
-
|
|
4990
|
+
// trigger's cron: per-outcome routing needs 1.1 (per-outcome
|
|
4991
|
+
// `outcome-<id>` exits) and `exposeError` needs 1.2 (adds the `error`
|
|
4992
|
+
// output; its exits are the same per-outcome handles). A zero- or
|
|
4993
|
+
// one-outcome task stays on the pinned default, byte-identically.
|
|
4994
|
+
//
|
|
4995
|
+
// More than one outcome INFERS the routing — see `hitlRoutesPerOutcome`
|
|
4996
|
+
// for the four things that turn that off, one of which is an explicit
|
|
4997
|
+
// `{ version }`, which is what keeps decompile→compile exact.
|
|
4998
|
+
const pinned = step.options?.version;
|
|
4999
|
+
const outcomeRouted = hitlRoutesPerOutcome(spec.inputs, pinned);
|
|
4990
5000
|
const neededVersion = spec.inputs?.exposeError ? '1.2' : outcomeRouted ? '1.1' : undefined;
|
|
4991
5001
|
if (neededVersion !== undefined && hitlType !== NODE_TYPE.hitl) {
|
|
4992
5002
|
throw new Error(`"${step.name}": outcomePorts/exposeError need the base human task's ${neededVersion} `
|
|
4993
5003
|
+ `definition — the '${spec.inputs?.variant}' variant has no per-outcome version. `
|
|
4994
5004
|
+ `Drop the variant, or route on out('${step.name}', 'Action') downstream.`);
|
|
4995
5005
|
}
|
|
4996
|
-
|
|
5006
|
+
// Both refusals below can only be reached by an EXPLICIT flag: the
|
|
5007
|
+
// inference already declines on a variant and on a pinned version.
|
|
4997
5008
|
if (neededVersion !== undefined && pinned !== undefined && pinned !== neededVersion) {
|
|
4998
5009
|
throw new Error(`"${step.name}": ${spec.inputs?.exposeError ? 'exposeError' : 'outcomePorts'} needs the task's ${neededVersion} `
|
|
4999
5010
|
+ `definition, but { version: '${pinned}' } pins it. Drop the pin or match it.`);
|
|
@@ -5307,6 +5318,72 @@ export function serialize(built, opts = {}) {
|
|
|
5307
5318
|
else if (step.kind === 'stepToList') {
|
|
5308
5319
|
tails = emitPortList(step, lastAction ?? '?', tails, scope, parentId, breakTarget);
|
|
5309
5320
|
}
|
|
5321
|
+
else if (step.kind === 'stepSwitch') {
|
|
5322
|
+
// The action, then EVERY exit it declares as an arm — no tacit tail.
|
|
5323
|
+
//
|
|
5324
|
+
// The exits are resolved BEFORE the node is emitted, because a spec with
|
|
5325
|
+
// no routable fan-out (a human task pinned to 1.0, a variant, an http
|
|
5326
|
+
// with no branches) must refuse rather than emit a node and then
|
|
5327
|
+
// discover it has one port. `declaredExits` carries the reason.
|
|
5328
|
+
const exits = declaredExits(step.spec, step.options?.version);
|
|
5329
|
+
if (!hasExits(exits)) {
|
|
5330
|
+
throw new Error(`.stepSwitch("${step.name}"): ${exits.reason}, so there are no exits to route.`
|
|
5331
|
+
+ (exits.suggestion ? ` ${exits.suggestion}` : ''));
|
|
5332
|
+
}
|
|
5333
|
+
const action = {
|
|
5334
|
+
kind: 'action', name: step.name, spec: step.spec,
|
|
5335
|
+
...(step.options ? { options: step.options } : {}),
|
|
5336
|
+
};
|
|
5337
|
+
// The tail `emitAction` hands back is the family's tacit exit — the
|
|
5338
|
+
// primary outcome, http's `default`. Discarded on purpose: every exit is
|
|
5339
|
+
// an arm here, so that port is wired by the arm that names it (and
|
|
5340
|
+
// wiring both is flow-builder-sdk#741).
|
|
5341
|
+
emitAction(action, tails, scope, parentId);
|
|
5342
|
+
applyUpdates(action);
|
|
5343
|
+
applyLabel(action);
|
|
5344
|
+
const nodeId = scope.stepNodeId.get(step.name);
|
|
5345
|
+
if (!nodeId)
|
|
5346
|
+
throw new Error(`.stepSwitch("${step.name}"): emitted node not found.`);
|
|
5347
|
+
const armTails = [];
|
|
5348
|
+
const routed = new Set();
|
|
5349
|
+
for (const c of step.cases) {
|
|
5350
|
+
const exit = exits.find((e) => e.value === c.value);
|
|
5351
|
+
if (!exit) {
|
|
5352
|
+
throw new Error(`.stepSwitch("${step.name}"): no exit named ${JSON.stringify(c.value)}. `
|
|
5353
|
+
+ `It declares ${exits.map((e) => JSON.stringify(e.value)).join(', ')}.`);
|
|
5354
|
+
}
|
|
5355
|
+
routed.add(exit.port);
|
|
5356
|
+
// Arms CONVERGE, exactly as `.switch()`'s do: every arm that does not
|
|
5357
|
+
// end in `.return()` contributes its tail, so the next step fans in
|
|
5358
|
+
// from all of them. (`.stepToList` instead gives each arm a private
|
|
5359
|
+
// End, which silently drops the flow's outputs on that path —
|
|
5360
|
+
// flow-builder-sdk#742.)
|
|
5361
|
+
armTails.push(...emit(c.body, [{ node: nodeId, port: exit.port }], scope, parentId, breakTarget));
|
|
5362
|
+
}
|
|
5363
|
+
// A declared exit with no arm gets an End, so the run FINISHES there
|
|
5364
|
+
// rather than reaching a port with no edge and stalling. `check` warns
|
|
5365
|
+
// (STEP_SWITCH_EXIT_UNROUTED) — this is the compile-side half of it.
|
|
5366
|
+
//
|
|
5367
|
+
// `action: 'End'` outcomes get one TOO, and that is deliberate: the
|
|
5368
|
+
// designer draws a handle for every entry in `inputs.schema.outcomes`
|
|
5369
|
+
// whatever its action, so skipping them left a drawn handle wired to
|
|
5370
|
+
// nothing. UiPath/skills' own gate says the same and is stricter than
|
|
5371
|
+
// this was — `assert_outcome_wiring` requires an edge per outcome with
|
|
5372
|
+
// no exemption, and its convention makes every non-primary outcome an
|
|
5373
|
+
// `End`. What the action changes is the WARNING, not the edge: nobody
|
|
5374
|
+
// needs to be told to write an arm for an outcome that ends the run.
|
|
5375
|
+
for (const e of exits) {
|
|
5376
|
+
if (routed.has(e.port))
|
|
5377
|
+
continue;
|
|
5378
|
+
const endId = uid(scope, `${step.name}${e.port.charAt(0).toUpperCase()}${e.port.slice(1)}End`);
|
|
5379
|
+
const end = makeNode(NODE_TYPE.end, endId, 'End', parentId ? { parentId } : {});
|
|
5380
|
+
end.inputs = {};
|
|
5381
|
+
scope.nodes.push(end);
|
|
5382
|
+
addEdge(scope, { node: nodeId, port: e.port }, endId);
|
|
5383
|
+
}
|
|
5384
|
+
tails = armTails;
|
|
5385
|
+
lastAction = step.name;
|
|
5386
|
+
}
|
|
5310
5387
|
else if (step.kind === 'branch') {
|
|
5311
5388
|
const id = stepUid(scope, step.name);
|
|
5312
5389
|
const node = makeNode(NODE_TYPE.decision, id, step.options?.label ?? step.label ?? step.name, { parentId, requestedVersion: step.options?.version });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://docs.uipath.com/maestro",
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"@types/node": "^22.7.0",
|
|
88
88
|
"esbuild": "^0.28.1"
|
|
89
89
|
},
|
|
90
|
-
"gitref": "
|
|
90
|
+
"gitref": "21bfc3d02150e62bfca808d6dfdbf8b4c320870f"
|
|
91
91
|
}
|