@uipath/maestro-builder-sdk 6.11.0 → 6.11.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.
|
@@ -146,7 +146,10 @@ export async function writeResolutions(run, specs) {
|
|
|
146
146
|
}
|
|
147
147
|
let got;
|
|
148
148
|
try {
|
|
149
|
-
|
|
149
|
+
const notes = [];
|
|
150
|
+
got = await resolveOne(run.client, run.connectorKey, table.get(field), field, by, value, run.connectionId, resolved, notes);
|
|
151
|
+
for (const note of notes)
|
|
152
|
+
run.reporter.info(note);
|
|
150
153
|
}
|
|
151
154
|
catch (error) {
|
|
152
155
|
// The collection was read to the END on this connection and the value
|
|
@@ -229,7 +229,7 @@ export declare function scanCollection(client: UipClient, connectorKey: string,
|
|
|
229
229
|
* A collection read to its end with no match throws {@link LookupExhausted},
|
|
230
230
|
* the subclass the caller's retry keys on; every other failure is the parent.
|
|
231
231
|
*/
|
|
232
|
-
export declare function resolveOne(client: UipClient, connectorKey: string, spec: ResolveSpec, field: string, by: string, value: string, connectionId: string, resolved?: Resolved): Promise<string>;
|
|
232
|
+
export declare function resolveOne(client: UipClient, connectorKey: string, spec: ResolveSpec, field: string, by: string, value: string, connectionId: string, resolved?: Resolved, notes?: string[]): Promise<string>;
|
|
233
233
|
/**
|
|
234
234
|
* The child collection of `parent`: `childPath` with each `{Field}` read off
|
|
235
235
|
* the parent record (`{ID}` → the folder's id), split into collection and
|
|
@@ -449,7 +449,7 @@ export async function* scanCollection(client, connectorKey, collection, connecti
|
|
|
449
449
|
* A collection read to its end with no match throws {@link LookupExhausted},
|
|
450
450
|
* the subclass the caller's retry keys on; every other failure is the parent.
|
|
451
451
|
*/
|
|
452
|
-
export async function resolveOne(client, connectorKey, spec, field, by, value, connectionId, resolved = new Map()) {
|
|
452
|
+
export async function resolveOne(client, connectorKey, spec, field, by, value, connectionId, resolved = new Map(), notes) {
|
|
453
453
|
const searchableBy = Array.isArray(spec.by) ? spec.by : [];
|
|
454
454
|
if (!searchableBy.includes(by)) {
|
|
455
455
|
const searchable = searchableBy.map(pyRepr).join(", ");
|
|
@@ -482,13 +482,15 @@ export async function resolveOne(client, connectorKey, spec, field, by, value, c
|
|
|
482
482
|
// pasting a literal id (flow-builder-sdk#818). A server-side filter still
|
|
483
483
|
// wins: it answers from the whole collection in one request.
|
|
484
484
|
if (pyTruthy(spec.childPath) && !pyTruthy(filterPattern)) {
|
|
485
|
-
return resolveInTree(client, connectorKey, spec, field, by, value, connectionId, resolved, collection, query, send);
|
|
485
|
+
return resolveInTree(client, connectorKey, spec, field, by, value, connectionId, resolved, collection, query, send, notes);
|
|
486
486
|
}
|
|
487
487
|
const scan = scanCollection(client, connectorKey, collection, connectionId, query);
|
|
488
488
|
// What the collection DOES hold at `by`, for the refusal: kept only while it
|
|
489
489
|
// could still be listed in full, plus the first case-insensitive equal.
|
|
490
490
|
const held = [];
|
|
491
|
-
|
|
491
|
+
// Records whose `by` equals the value ignoring case only, first per distinct
|
|
492
|
+
// spelling: the fallback when no record matches exactly.
|
|
493
|
+
const caseOnly = new Map();
|
|
492
494
|
let step = await scan.next();
|
|
493
495
|
while (!step.done) {
|
|
494
496
|
for (const record of step.value) {
|
|
@@ -497,8 +499,8 @@ export async function resolveOne(client, connectorKey, spec, field, by, value, c
|
|
|
497
499
|
const text = pyStrAny(at);
|
|
498
500
|
if (held.length <= CANDIDATE_LIMIT && !held.includes(text))
|
|
499
501
|
held.push(text);
|
|
500
|
-
if (
|
|
501
|
-
|
|
502
|
+
if (text !== value && text.toLowerCase() === value.toLowerCase() && !caseOnly.has(text))
|
|
503
|
+
caseOnly.set(text, record);
|
|
502
504
|
}
|
|
503
505
|
if (matchRecord(record, by, value)) {
|
|
504
506
|
const got = readPath(record, send);
|
|
@@ -512,6 +514,27 @@ export async function resolveOne(client, connectorKey, spec, field, by, value, c
|
|
|
512
514
|
step = await scan.next();
|
|
513
515
|
}
|
|
514
516
|
const { scanned, pages, capped } = step.value;
|
|
517
|
+
// No exact match anywhere in the collection: try the case-only ones. Exact
|
|
518
|
+
// first, because an id picked by a looser rule while the exact record sat
|
|
519
|
+
// on a later page would be the wrong id. Only once the scan has read to the
|
|
520
|
+
// END — a capped scan may still hold the exact record further in, so it
|
|
521
|
+
// keeps the refusal and the hint.
|
|
522
|
+
//
|
|
523
|
+
// `send_as:name=user` against `available_tokens` ('Bot', 'User') and
|
|
524
|
+
// `E2E-testing` against `E2E-Testing` each cost a refused `prepare` and a
|
|
525
|
+
// re-run whose only change was the letter case the refusal had already
|
|
526
|
+
// named (flow-builder-sdk#818). One spelling is unambiguous, so it is taken
|
|
527
|
+
// and said; two spellings that differ only in case are two records, and
|
|
528
|
+
// choosing between them would be a guess.
|
|
529
|
+
const near = caseOnly.size > 0 ? [...caseOnly.keys()][0] : undefined;
|
|
530
|
+
if (!capped && caseOnly.size > 0) {
|
|
531
|
+
const record = findRecord([...caseOnly.values()], field, by, value, notes);
|
|
532
|
+
const got = readPath(record, send);
|
|
533
|
+
if (got === null) {
|
|
534
|
+
throw new ResolutionRefusal(`${field}: matched a record on ${by}=${pyRepr(pyStrAny(readPath(record, by)))}, but it carries no ${pyRepr(send)} to send.`, "RESOLVE_NO_VALUE");
|
|
535
|
+
}
|
|
536
|
+
return pyStrAny(got);
|
|
537
|
+
}
|
|
515
538
|
// This states the FACT and stops; `prepare` owns the remedy, because
|
|
516
539
|
// `prepare` is what knows the other candidate connections.
|
|
517
540
|
//
|
|
@@ -529,8 +552,8 @@ export async function resolveOne(client, connectorKey, spec, field, by, value, c
|
|
|
529
552
|
// is finally the next step rather than a detour.
|
|
530
553
|
//
|
|
531
554
|
// What it DOES carry is what the collection holds, when that is small enough
|
|
532
|
-
// to read in one line, and
|
|
533
|
-
//
|
|
555
|
+
// to read in one line, and — when the cap stopped the scan — a case-only
|
|
556
|
+
// near miss that was NOT taken: facts about this connection, not a remedy.
|
|
534
557
|
//
|
|
535
558
|
// Never under a server-side filter: the scan then read only the records the
|
|
536
559
|
// filter returned (`name like '%Coder Eval%'` gives 'Coder Evals' and 'Coder
|
|
@@ -541,13 +564,43 @@ export async function resolveOne(client, connectorKey, spec, field, by, value, c
|
|
|
541
564
|
`(scanned ${scanned} record(s) across ${pages + 1} page(s)` +
|
|
542
565
|
(capped ? `; STOPPED AT THE ${MAX_SCANNED}-record cap, so the value may be further in` : "; collection exhausted") +
|
|
543
566
|
`) on connection ${connectionId}.` +
|
|
544
|
-
(near !== undefined
|
|
567
|
+
(near !== undefined
|
|
568
|
+
? ` Did you mean ${pyRepr(near)}? It differs only in case and was not taken: the scan stopped before the end, where an exact match may still be.`
|
|
569
|
+
: "") +
|
|
545
570
|
(candidates !== undefined ? ` It holds: ${candidates.map(pyRepr).join(", ")}.` : ""), {
|
|
546
571
|
field, collection, by, value, scanned, pages: pages + 1, capped,
|
|
547
572
|
...(candidates !== undefined ? { candidates } : {}),
|
|
548
573
|
...(near !== undefined ? { near } : {}),
|
|
549
574
|
});
|
|
550
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* The record in `records` matching `value` on `by`: exact first, else the one
|
|
578
|
+
* case-only spelling (reported through `notes`), else `undefined`. Two
|
|
579
|
+
* case-only spellings are a refusal, as in the flat scan.
|
|
580
|
+
*/
|
|
581
|
+
function findRecord(records, field, by, value, notes) {
|
|
582
|
+
const exact = records.find((record) => matchRecord(record, by, value));
|
|
583
|
+
if (exact !== undefined)
|
|
584
|
+
return exact;
|
|
585
|
+
const caseOnly = new Map();
|
|
586
|
+
for (const record of records) {
|
|
587
|
+
const at = readPath(record, by);
|
|
588
|
+
if (at === null || at === undefined)
|
|
589
|
+
continue;
|
|
590
|
+
const text = pyStrAny(at);
|
|
591
|
+
if (text !== value && text.toLowerCase() === value.toLowerCase() && !caseOnly.has(text))
|
|
592
|
+
caseOnly.set(text, record);
|
|
593
|
+
}
|
|
594
|
+
if (caseOnly.size === 0)
|
|
595
|
+
return undefined;
|
|
596
|
+
if (caseOnly.size > 1) {
|
|
597
|
+
throw new ResolutionRefusal(`${field}: no record has ${by}=${pyRepr(value)} exactly, and ${caseOnly.size} differ from it only in case: ` +
|
|
598
|
+
`${[...caseOnly.keys()].map(pyRepr).join(", ")}. They are different records; pass the one you mean exactly.`, "RESOLVE_CASE_AMBIGUOUS");
|
|
599
|
+
}
|
|
600
|
+
const [[text, record]] = [...caseOnly.entries()];
|
|
601
|
+
notes?.push(` ${field}: no record has ${by}=${pyRepr(value)} exactly; took ${pyRepr(text)}, the only one that differs from it only in case`);
|
|
602
|
+
return record;
|
|
603
|
+
}
|
|
551
604
|
async function listAll(client, connectorKey, collection, connectionId, query) {
|
|
552
605
|
const records = [];
|
|
553
606
|
const scan = scanCollection(client, connectorKey, collection, connectionId, query);
|
|
@@ -602,7 +655,7 @@ function isContainer(record) {
|
|
|
602
655
|
* connections. A path whose segment is missing from a listing read to the end
|
|
603
656
|
* IS evidence, and is reported as an exhaustive miss like a flat scan's.
|
|
604
657
|
*/
|
|
605
|
-
async function resolveInTree(client, connectorKey, spec, field, by, value, connectionId, resolved, rootCollection, rootQuery, send) {
|
|
658
|
+
async function resolveInTree(client, connectorKey, spec, field, by, value, connectionId, resolved, rootCollection, rootQuery, send, notes) {
|
|
606
659
|
const childPath = pyStrAny(spec.childPath);
|
|
607
660
|
const answer = (record) => {
|
|
608
661
|
const got = readPath(record, send);
|
|
@@ -625,12 +678,14 @@ async function resolveInTree(client, connectorKey, spec, field, by, value, conne
|
|
|
625
678
|
const exact = root.records.find((record) => matchRecord(record, by, value));
|
|
626
679
|
if (exact !== undefined)
|
|
627
680
|
return answer(exact);
|
|
681
|
+
// A whole-value case-only match at the root is deferred: a path value, or a
|
|
682
|
+
// bare name one level down, may still match exactly.
|
|
628
683
|
const segments = value.split("/").map((segment) => pyStrip(segment)).filter((segment) => segment !== "");
|
|
629
684
|
if (segments.length > 1) {
|
|
630
685
|
let level = root;
|
|
631
686
|
let where = rootCollection;
|
|
632
687
|
for (const [i, segment] of segments.entries()) {
|
|
633
|
-
const hit = level.records
|
|
688
|
+
const hit = findRecord(level.records, field, by, segment, notes);
|
|
634
689
|
if (hit === undefined) {
|
|
635
690
|
const held = level.records.map((record) => pyStrAny(readPath(record, by))).filter((text) => text !== "None");
|
|
636
691
|
const shown = !level.outcome.capped && held.length > 0 && held.length <= CANDIDATE_LIMIT ? ` It holds: ${held.map(pyRepr).join(", ")}.` : "";
|
|
@@ -651,6 +706,7 @@ async function resolveInTree(client, connectorKey, spec, field, by, value, conne
|
|
|
651
706
|
}
|
|
652
707
|
}
|
|
653
708
|
// Bare name: one level below every root container.
|
|
709
|
+
const below = [];
|
|
654
710
|
for (const parent of root.records) {
|
|
655
711
|
if (!isContainer(parent))
|
|
656
712
|
continue;
|
|
@@ -660,10 +716,16 @@ async function resolveInTree(client, connectorKey, spec, field, by, value, conne
|
|
|
660
716
|
if (child === undefined)
|
|
661
717
|
continue;
|
|
662
718
|
const level = await list(child.collection, child.query);
|
|
663
|
-
const
|
|
664
|
-
if (
|
|
665
|
-
return answer(
|
|
719
|
+
const exactBelow = level.records.find((record) => matchRecord(record, by, value));
|
|
720
|
+
if (exactBelow !== undefined)
|
|
721
|
+
return answer(exactBelow);
|
|
722
|
+
below.push(...level.records);
|
|
666
723
|
}
|
|
724
|
+
// No exact match at either depth: the case-only one, root and one level
|
|
725
|
+
// down together, so a spelling in each is two records and a refusal.
|
|
726
|
+
const loose = findRecord([...root.records, ...below], field, by, value, notes);
|
|
727
|
+
if (loose !== undefined)
|
|
728
|
+
return answer(loose);
|
|
667
729
|
const top = root.records.map((record) => pyStrAny(readPath(record, by))).filter((text) => text !== "None");
|
|
668
730
|
const example = `${top[0] ?? "<top-level>"}/<folder>/${value}`;
|
|
669
731
|
throw new LookupExhausted(`${field}: no record in ${pyRepr(rootCollection)} or one level below it has ${by}=${pyRepr(value)} ` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "6.11.
|
|
3
|
+
"version": "6.11.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",
|
|
@@ -92,5 +92,5 @@
|
|
|
92
92
|
"@types/node": "^22.7.0",
|
|
93
93
|
"esbuild": "^0.28.1"
|
|
94
94
|
},
|
|
95
|
-
"gitref": "
|
|
95
|
+
"gitref": "85e9b8d11773bb7e83aed5deb38eea2ecd25bd2a"
|
|
96
96
|
}
|