@uipath/maestro-builder-sdk 6.10.1 → 6.10.3

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.
@@ -116,11 +116,23 @@ export async function enrichEntry(ctx, entry) {
116
116
  }
117
117
  if (!truthy(opName))
118
118
  return entry; // nothing to do
119
+ const failuresBefore = ctx.failedFieldDescribes.length;
119
120
  let data = await isDescribeOperation(ctx, connectorKey, objectName, opName);
120
121
  if (!truthy(data)) {
121
122
  const alt = await discoverOperationName(ctx, entry);
122
- if (truthy(alt) && alt !== opName)
123
+ if (truthy(alt) && alt !== opName) {
124
+ // The registry's verb did not name an operation the object has, so its
125
+ // `-f` rejection is about a request that is now superseded: the CLI maps
126
+ // `List` to GET, and Data Service `QueryEntityRecordsCurated` exposes
127
+ // only POST ("No api-type ObjectAction matched for fields [entityName]
128
+ // on operation 'List'"). Kept, it made `prepare` refuse
129
+ // PARENT_FIELDS_REJECTED even when the retry below materialised the
130
+ // schema (flow-builder-sdk#818: 169 refusals across 15 runs on
131
+ // query-entity-records and get-entity-record-by-id). Only the describe
132
+ // whose answer is used may speak for the parent set.
133
+ ctx.failedFieldDescribes.length = failuresBefore;
123
134
  data = await isDescribeOperation(ctx, connectorKey, objectName, alt);
135
+ }
124
136
  }
125
137
  if (!truthy(data))
126
138
  return entry; // connector might be a stub or recently removed
@@ -187,16 +187,27 @@ export declare function candidateResolvesAllLine(label: string, lookups: number)
187
187
  export declare function candidateExhaustedLine(label: string, field: string, scanned: number): string;
188
188
  /** The terminal report's line for a candidate whose lookup failed for another reason: the error, verbatim. */
189
189
  export declare function candidateErrorLine(label: string, error: unknown): string;
190
+ /**
191
+ * The command that shows what a lookup's collection holds on one connection.
192
+ *
193
+ * Every refusal that ends in "check the value" carries it: the value the author
194
+ * wrote may be one the task never named (an invented Slack channel), and
195
+ * without this line the only way to learn the real values is to leave the
196
+ * documented loop. The skills eval customer-escalation dead-ended exactly
197
+ * there on 2026-09-25 (UiPath/flow-builder-sdk#817).
198
+ */
199
+ export declare function collectionListingHint(connectorKey: string, collection: string, connectionId: string): string;
190
200
  /**
191
201
  * An explicit `--connection-id` / `--connection` is the author's decision and
192
202
  * is not overridden. The alternatives are still named, because the whole cost
193
203
  * of this bug was not knowing they existed. `first` is the exhausted lookup's
194
- * own message; `others` the pasteable listing of the other candidates.
204
+ * own message; `others` the pasteable listing of the other candidates; `hint`
205
+ * the {@link collectionListingHint} for the pinned connection.
195
206
  */
196
- export declare function pinnedConnectionExhaustedMessage(first: string, connectorKey: string, othersCount: number, othersListing: string): string;
207
+ export declare function pinnedConnectionExhaustedMessage(first: string, connectorKey: string, othersCount: number, othersListing: string, hint: string): string;
197
208
  /** The listing of other connections itself failed; the original lookup failure is what the author asked about. */
198
209
  export declare function candidateListingFailedMessage(first: string, error: unknown): string;
199
- export declare function onlyConnectionMessage(first: string, connectorKey: string): string;
210
+ export declare function onlyConnectionMessage(first: string, connectorKey: string, hint: string): string;
200
211
  /** Two or more candidates resolve everything: reported, never chosen between. */
201
212
  export declare function severalConnectionsResolveMessage(count: number, connectorKey: string, listing: string): string;
202
213
  /**
@@ -443,13 +443,27 @@ export function candidateExhaustedLine(label, field, scanned) {
443
443
  export function candidateErrorLine(label, error) {
444
444
  return ` ${label} ${messageOf(error)}`;
445
445
  }
446
+ /**
447
+ * The command that shows what a lookup's collection holds on one connection.
448
+ *
449
+ * Every refusal that ends in "check the value" carries it: the value the author
450
+ * wrote may be one the task never named (an invented Slack channel), and
451
+ * without this line the only way to learn the real values is to leave the
452
+ * documented loop. The skills eval customer-escalation dead-ended exactly
453
+ * there on 2026-09-25 (UiPath/flow-builder-sdk#817).
454
+ */
455
+ export function collectionListingHint(connectorKey, collection, connectionId) {
456
+ return (`To see what ${pyRepr(collection)} holds on this connection:\n` +
457
+ ` uip is resources run list ${connectorKey} ${collection} --connection-id ${connectionId}`);
458
+ }
446
459
  /**
447
460
  * An explicit `--connection-id` / `--connection` is the author's decision and
448
461
  * is not overridden. The alternatives are still named, because the whole cost
449
462
  * of this bug was not knowing they existed. `first` is the exhausted lookup's
450
- * own message; `others` the pasteable listing of the other candidates.
463
+ * own message; `others` the pasteable listing of the other candidates; `hint`
464
+ * the {@link collectionListingHint} for the pinned connection.
451
465
  */
452
- export function pinnedConnectionExhaustedMessage(first, connectorKey, othersCount, othersListing) {
466
+ export function pinnedConnectionExhaustedMessage(first, connectorKey, othersCount, othersListing, hint) {
453
467
  let extra = "";
454
468
  if (othersCount > 0) {
455
469
  extra =
@@ -457,14 +471,16 @@ export function pinnedConnectionExhaustedMessage(first, connectorKey, othersCoun
457
471
  `${othersListing}\n` +
458
472
  "Re-run against one of those, or check the value. This run kept the connection you named.";
459
473
  }
460
- return `${PREFIX}${first}${extra}`;
474
+ return `${PREFIX}${first}${extra}\n${hint}`;
461
475
  }
462
476
  /** The listing of other connections itself failed; the original lookup failure is what the author asked about. */
463
477
  export function candidateListingFailedMessage(first, error) {
464
478
  return `${PREFIX}${first}\n(could not list other connections: ${messageOf(error)})`;
465
479
  }
466
- export function onlyConnectionMessage(first, connectorKey) {
467
- return (`${PREFIX}${first}\n` + `This is the only enabled ${connectorKey} connection on the tenant, so the value is what to check.`);
480
+ export function onlyConnectionMessage(first, connectorKey, hint) {
481
+ return (`${PREFIX}${first}\n` +
482
+ `This is the only enabled ${connectorKey} connection on the tenant, so the value is what to check.\n` +
483
+ hint);
468
484
  }
469
485
  /** Two or more candidates resolve everything: reported, never chosen between. */
470
486
  export function severalConnectionsResolveMessage(count, connectorKey, listing) {
@@ -484,9 +500,7 @@ export function noConnectionResolvesMessage(connectorKey, first, lines, connecti
484
500
  `${first.field}: ${first.by}=${pyRepr(first.value)}\n` +
485
501
  `${lines.join("\n")}\n` +
486
502
  "Every candidate was read to the end, so the value is what to check now. " +
487
- `To see what ${pyRepr(first.collection)} holds:\n` +
488
- ` uip is resources run list ${connectorKey} ${first.collection} ` +
489
- `--connection-id ${connectionId}`);
503
+ collectionListingHint(connectorKey, first.collection, connectionId));
490
504
  }
491
505
  // ---------------------------------------------------------------------------
492
506
  // The event path
@@ -122,6 +122,8 @@ export type OverlayLookupSpec = {
122
122
  objectName?: string;
123
123
  dependsOn?: string[];
124
124
  filterPattern?: string;
125
+ /** The child collection of a tree lookup, `{Field}` placeholders filled from the parent record. */
126
+ childPath?: string;
125
127
  };
126
128
  /**
127
129
  * `_lookup_specs_from_overlay`: the prepared operation's lookup specs, read
@@ -426,6 +426,9 @@ function withFilterPatterns(specs, catalogFields) {
426
426
  const filterPattern = references.get(field)?.filterPattern;
427
427
  if (pyTruthy(filterPattern))
428
428
  copy.filterPattern = pyStrAny(filterPattern);
429
+ const childPath = references.get(field)?.childPath;
430
+ if (pyTruthy(childPath))
431
+ copy.childPath = pyStrAny(childPath);
429
432
  out.set(field, copy);
430
433
  }
431
434
  return out;
@@ -93,8 +93,17 @@ export interface ResolveSpec {
93
93
  value?: string;
94
94
  filterPattern?: string;
95
95
  dependsOn?: readonly string[];
96
+ /** A tree lookup's child collection: `/EventFolderPicker?parentReference={ID}&includeFiles=true`. */
97
+ childPath?: string;
96
98
  [key: string]: unknown;
97
99
  }
100
+ /**
101
+ * A tree walk lists one child collection per parent it opens. Google Drive's
102
+ * picker root holds three records (`My Drive`, `Shared with me`, `Shared
103
+ * drives`), so a bare-name search opens three; the cap only matters for a
104
+ * root that is itself a long list.
105
+ */
106
+ export declare const MAX_TREE_LISTINGS = 25;
98
107
  /**
99
108
  * `channel:profile.email=dustin@example.com` -> `[field, by, value]`.
100
109
  *
@@ -221,4 +230,14 @@ export declare function scanCollection(client: UipClient, connectorKey: string,
221
230
  * the subclass the caller's retry keys on; every other failure is the parent.
222
231
  */
223
232
  export declare function resolveOne(client: UipClient, connectorKey: string, spec: ResolveSpec, field: string, by: string, value: string, connectionId: string, resolved?: Resolved): Promise<string>;
233
+ /**
234
+ * The child collection of `parent`: `childPath` with each `{Field}` read off
235
+ * the parent record (`{ID}` → the folder's id), split into collection and
236
+ * query as the root path is. `undefined` when the parent lacks a field the
237
+ * path needs — it cannot be opened.
238
+ */
239
+ export declare function childCollection(childPath: string, parent: unknown, resolved: Resolved): {
240
+ collection: string;
241
+ query: string | undefined;
242
+ } | undefined;
224
243
  export {};
@@ -102,6 +102,13 @@ export class LookupExhausted extends ResolutionRefusal {
102
102
  this.near = facts.near;
103
103
  }
104
104
  }
105
+ /**
106
+ * A tree walk lists one child collection per parent it opens. Google Drive's
107
+ * picker root holds three records (`My Drive`, `Shared with me`, `Shared
108
+ * drives`), so a bare-name search opens three; the cap only matters for a
109
+ * root that is itself a long list.
110
+ */
111
+ export const MAX_TREE_LISTINGS = 25;
105
112
  /**
106
113
  * `channel:profile.email=dustin@example.com` -> `[field, by, value]`.
107
114
  *
@@ -467,6 +474,16 @@ export async function resolveOne(client, connectorKey, spec, field, by, value, c
467
474
  const filtered = pyStrAny(filterPattern).replaceAll("{filter}", value);
468
475
  query = query ? `${query}&${filtered}` : filtered;
469
476
  }
477
+ // A tree (`childPath`) is walked rather than scanned: its root is a handful
478
+ // of containers and every real record sits below one. Scanning only the root
479
+ // could never match a Google Drive file — the root is `My Drive`, `Shared
480
+ // with me`, `Shared drives` — and the agent fell back to paging
481
+ // `uip is resources run list … --query parentReference=…` by hand and
482
+ // pasting a literal id (flow-builder-sdk#818). A server-side filter still
483
+ // wins: it answers from the whole collection in one request.
484
+ if (pyTruthy(spec.childPath) && !pyTruthy(filterPattern)) {
485
+ return resolveInTree(client, connectorKey, spec, field, by, value, connectionId, resolved, collection, query, send);
486
+ }
470
487
  const scan = scanCollection(client, connectorKey, collection, connectionId, query);
471
488
  // What the collection DOES hold at `by`, for the refusal: kept only while it
472
489
  // could still be listed in full, plus the first case-insensitive equal.
@@ -531,3 +548,127 @@ export async function resolveOne(client, connectorKey, spec, field, by, value, c
531
548
  ...(near !== undefined ? { near } : {}),
532
549
  });
533
550
  }
551
+ async function listAll(client, connectorKey, collection, connectionId, query) {
552
+ const records = [];
553
+ const scan = scanCollection(client, connectorKey, collection, connectionId, query);
554
+ let step = await scan.next();
555
+ while (!step.done) {
556
+ records.push(...step.value);
557
+ step = await scan.next();
558
+ }
559
+ return { records, outcome: step.value };
560
+ }
561
+ /**
562
+ * The child collection of `parent`: `childPath` with each `{Field}` read off
563
+ * the parent record (`{ID}` → the folder's id), split into collection and
564
+ * query as the root path is. `undefined` when the parent lacks a field the
565
+ * path needs — it cannot be opened.
566
+ */
567
+ export function childCollection(childPath, parent, resolved) {
568
+ let missing = false;
569
+ const filled = substitute(childPath, resolved).replace(/\{([^{}]+)\}/g, (_whole, name) => {
570
+ const at = readPath(parent, name);
571
+ if (at === null || at === undefined || at === "") {
572
+ missing = true;
573
+ return "";
574
+ }
575
+ return pyStrAny(at);
576
+ });
577
+ if (missing)
578
+ return undefined;
579
+ const [beforeQuery, afterQuery] = splitOnce(filled, "?");
580
+ return { collection: pyStripChars(beforeQuery, "/"), query: afterQuery || undefined };
581
+ }
582
+ /** A record the walk may open: anything not marked as a leaf (`isFolder: false`). */
583
+ function isContainer(record) {
584
+ const flag = readPath(record, "isFolder");
585
+ return flag !== false && pyStrAny(flag).toLowerCase() !== "false";
586
+ }
587
+ /**
588
+ * {@link resolveOne} for a tree lookup (`childPath` declared).
589
+ *
590
+ * Two forms of value, both matched EXACTLY on `by`:
591
+ *
592
+ * - a path, `My Drive/Reports/q3.csv`: each segment is matched in the listing
593
+ * the previous one opened, starting at the root. One listing per segment, so
594
+ * depth costs nothing extra and nothing is guessed about where the item is.
595
+ * - a bare name, `q3.csv`: the root is searched, then one level below every
596
+ * root container. That reaches a Drive file at the top of `My Drive` — the
597
+ * 2026-09-25 run's `000_coder_evals.txt` — in 1 + 3 listings. Deeper items
598
+ * need the path form, and the refusal says so.
599
+ *
600
+ * A bare-name miss is `capped`: the walk did not read the whole tree, so it is
601
+ * no evidence the value is absent and must not send `prepare` probing other
602
+ * connections. A path whose segment is missing from a listing read to the end
603
+ * IS evidence, and is reported as an exhaustive miss like a flat scan's.
604
+ */
605
+ async function resolveInTree(client, connectorKey, spec, field, by, value, connectionId, resolved, rootCollection, rootQuery, send) {
606
+ const childPath = pyStrAny(spec.childPath);
607
+ const answer = (record) => {
608
+ const got = readPath(record, send);
609
+ if (got === null) {
610
+ throw new ResolutionRefusal(`${field}: matched a record on ${by}=${pyRepr(value)}, but it carries no ${pyRepr(send)} to send.`, "RESOLVE_NO_VALUE");
611
+ }
612
+ return pyStrAny(got);
613
+ };
614
+ let scanned = 0;
615
+ let pages = 0;
616
+ let listings = 0;
617
+ const list = async (collection, query) => {
618
+ const listing = await listAll(client, connectorKey, collection, connectionId, query);
619
+ scanned += listing.outcome.scanned;
620
+ pages += listing.outcome.pages + 1;
621
+ listings += 1;
622
+ return listing;
623
+ };
624
+ const root = await list(rootCollection, rootQuery);
625
+ const exact = root.records.find((record) => matchRecord(record, by, value));
626
+ if (exact !== undefined)
627
+ return answer(exact);
628
+ const segments = value.split("/").map((segment) => pyStrip(segment)).filter((segment) => segment !== "");
629
+ if (segments.length > 1) {
630
+ let level = root;
631
+ let where = rootCollection;
632
+ for (const [i, segment] of segments.entries()) {
633
+ const hit = level.records.find((record) => matchRecord(record, by, segment));
634
+ if (hit === undefined) {
635
+ const held = level.records.map((record) => pyStrAny(readPath(record, by))).filter((text) => text !== "None");
636
+ const shown = !level.outcome.capped && held.length > 0 && held.length <= CANDIDATE_LIMIT ? ` It holds: ${held.map(pyRepr).join(", ")}.` : "";
637
+ const facts = { field, collection: where, by, value, scanned, pages, capped: level.outcome.capped };
638
+ throw new LookupExhausted(`${field}: path ${pyRepr(value)} stops at segment ${i + 1}, ${pyRepr(segment)}: no record in ${pyRepr(where)} ` +
639
+ `has ${by}=${pyRepr(segment)} (walked ${listings} listing(s), ${scanned} record(s)` +
640
+ (level.outcome.capped ? `; STOPPED AT THE ${MAX_SCANNED}-record cap` : "; that listing exhausted") +
641
+ `) on connection ${connectionId}.${shown}`, facts);
642
+ }
643
+ if (i === segments.length - 1)
644
+ return answer(hit);
645
+ const child = childCollection(childPath, hit, resolved);
646
+ if (child === undefined) {
647
+ throw new ResolutionRefusal(`${field}: ${pyRepr(segment)} matched, but its record lacks a field ${pyRepr(childPath)} needs, so it cannot be opened.`, "RESOLVE_TREE_UNOPENABLE");
648
+ }
649
+ where = child.collection;
650
+ level = await list(child.collection, child.query);
651
+ }
652
+ }
653
+ // Bare name: one level below every root container.
654
+ for (const parent of root.records) {
655
+ if (!isContainer(parent))
656
+ continue;
657
+ if (listings > MAX_TREE_LISTINGS)
658
+ break;
659
+ const child = childCollection(childPath, parent, resolved);
660
+ if (child === undefined)
661
+ continue;
662
+ const level = await list(child.collection, child.query);
663
+ const hit = level.records.find((record) => matchRecord(record, by, value));
664
+ if (hit !== undefined)
665
+ return answer(hit);
666
+ }
667
+ const top = root.records.map((record) => pyStrAny(readPath(record, by))).filter((text) => text !== "None");
668
+ const example = `${top[0] ?? "<top-level>"}/<folder>/${value}`;
669
+ throw new LookupExhausted(`${field}: no record in ${pyRepr(rootCollection)} or one level below it has ${by}=${pyRepr(value)} ` +
670
+ `(walked ${listings} listing(s), ${scanned} record(s)) on connection ${connectionId}. ` +
671
+ `This lookup is a tree, and a name is searched only that deep: for an item further down, ` +
672
+ `pass its path from the top, --resolve '${field}:${by}=${example}'.` +
673
+ (top.length > 0 && top.length <= CANDIDATE_LIMIT ? ` The top level holds: ${top.map(pyRepr).join(", ")}.` : ""), { field, collection: rootCollection, by, value, scanned, pages, capped: true });
674
+ }
@@ -1,5 +1,5 @@
1
1
  import { ConnectionRefusal, candidateConnections, candidateListing } from "./connections.js";
2
- import { PrepareRefusal, candidateErrorLine, candidateExhaustedLine, candidateLabel, candidateListingFailedMessage, candidateResolvesAllLine, noConnectionResolvesMessage, onlyConnectionMessage, pinnedConnectionExhaustedMessage, severalConnectionsResolveMessage, tryingOtherConnectionsLine, } from "./messages.js";
2
+ import { PrepareRefusal, candidateErrorLine, candidateExhaustedLine, candidateLabel, candidateListingFailedMessage, candidateResolvesAllLine, collectionListingHint, noConnectionResolvesMessage, onlyConnectionMessage, pinnedConnectionExhaustedMessage, severalConnectionsResolveMessage, tryingOtherConnectionsLine, } from "./messages.js";
3
3
  import { LookupExhausted, ResolutionRefusal, resolveOne } from "./resolve.js";
4
4
  import { pyGet, pyStrAny } from "./support.js";
5
5
  /**
@@ -61,7 +61,7 @@ export async function retryOtherConnections(ctx, plan, first) {
61
61
  throw error;
62
62
  throw new PrepareRefusal(candidateListingFailedMessage(first.message, error), "LOOKUP_EXHAUSTED_PINNED");
63
63
  }
64
- throw new PrepareRefusal(pinnedConnectionExhaustedMessage(first.message, ctx.connectorKey, others.length, candidateListing(others)), "LOOKUP_EXHAUSTED_PINNED");
64
+ throw new PrepareRefusal(pinnedConnectionExhaustedMessage(first.message, ctx.connectorKey, others.length, candidateListing(others), collectionListingHint(ctx.connectorKey, first.collection, ctx.connectionId)), "LOOKUP_EXHAUSTED_PINNED");
65
65
  }
66
66
  let candidates;
67
67
  try {
@@ -76,7 +76,7 @@ export async function retryOtherConnections(ctx, plan, first) {
76
76
  }
77
77
  const others = candidates.filter((row) => !sameId(row));
78
78
  if (others.length === 0) {
79
- throw new PrepareRefusal(onlyConnectionMessage(first.message, ctx.connectorKey), "LOOKUP_EXHAUSTED_ONLY_CONNECTION");
79
+ throw new PrepareRefusal(onlyConnectionMessage(first.message, ctx.connectorKey, collectionListingHint(ctx.connectorKey, first.collection, ctx.connectionId)), "LOOKUP_EXHAUSTED_ONLY_CONNECTION");
80
80
  }
81
81
  ctx.reporter.info(tryingOtherConnectionsLine(first.field, others.length, ctx.connectorKey));
82
82
  const winners = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.10.1",
3
+ "version": "6.10.3",
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",
@@ -92,5 +92,5 @@
92
92
  "@types/node": "^22.7.0",
93
93
  "esbuild": "^0.28.1"
94
94
  },
95
- "gitref": "388e366bc878b0b6701b2842d56479125f6a9edd"
95
+ "gitref": "d3639fb590ef392d76c174521c9cc1d280f7125a"
96
96
  }