@uipath/maestro-builder-sdk 6.8.4 → 6.8.6
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/api-index.md +1 -1
- package/dist/check-cli.js +13 -16
- package/dist/check.d.ts +20 -0
- package/dist/check.js +46 -12
- package/dist/cli/commands/flow.js +2 -0
- package/dist/cli-library.d.ts +21 -0
- package/dist/cli-library.js +56 -0
- package/dist/compile-cli.js +12 -11
- package/dist/core/cli-spelling.d.ts +10 -0
- package/dist/core/cli-spelling.js +24 -0
- package/dist/core/library.d.ts +20 -0
- package/dist/core/library.js +97 -20
- package/dist/core/lookups.js +4 -2
- package/dist/core/node-classes.d.ts +8 -0
- package/dist/core/node-classes.js +27 -3
- package/dist/core/typed-raw-node.d.ts +97 -0
- package/dist/core/typed-raw-node.js +199 -0
- package/dist/registry/generators/markdown/sections/lookups.js +20 -5
- package/dist/registry/library-build/build.js +2 -1
- package/dist/registry/library-build/context.d.ts +26 -0
- package/dist/registry/library-build/context.js +25 -12
- package/dist/registry/library-build/events.js +3 -2
- package/dist/registry/prepare/actions.d.ts +42 -5
- package/dist/registry/prepare/actions.js +90 -16
- package/dist/registry/prepare/connections.js +2 -3
- package/dist/registry/prepare/event.d.ts +15 -6
- package/dist/registry/prepare/event.js +19 -11
- package/dist/registry/prepare/ledger.d.ts +3 -1
- package/dist/registry/prepare/ledger.js +55 -5
- package/dist/registry/prepare/messages.d.ts +40 -4
- package/dist/registry/prepare/messages.js +97 -15
- package/dist/registry/prepare/pipeline.js +19 -10
- package/dist/registry/prepare/resolve.d.ts +27 -2
- package/dist/registry/prepare/resolve.js +71 -8
- package/dist/registry/uip/envelope.d.ts +44 -9
- package/dist/registry/uip/envelope.js +80 -0
- package/dist/serialize.js +82 -9
- package/lib/library-json.samples/index.json +18 -0
- package/lib/library-json.samples/uipath-getresponse-getresponse/send-newsletter@1.0.0.json +159 -0
- package/lib/library-json.samples/uipath-getresponse-getresponse/send-newsletter@1.0.0.v1def.json +323 -0
- package/lib/library-json.samples/uipath-salesforce-slack/create-group-direct-message@1.0.0.json +149 -0
- package/lib/library-json.samples/uipath-salesforce-slack/create-group-direct-message@1.0.0.v1def.json +237 -0
- package/package.json +2 -2
|
@@ -28,17 +28,17 @@ import { resolve } from "node:path";
|
|
|
28
28
|
import { PACKAGE_NAME } from "../../package-name.js";
|
|
29
29
|
import { afterLast } from "../compat/pystr.js";
|
|
30
30
|
import { extractCanonical, isHttpPassthrough, nodeOf } from "../library-build/canonical.js";
|
|
31
|
-
import { createBuildContext, isResourcesList,
|
|
31
|
+
import { createBuildContext, isResourcesList, registryProbe } from "../library-build/context.js";
|
|
32
32
|
import { enrichEntry } from "../library-build/enrich.js";
|
|
33
33
|
import { expandGenericForConnector, genericTemplatesByOp } from "../library-build/generic-expand.js";
|
|
34
34
|
import { planLibrary } from "../library-build/plan.js";
|
|
35
35
|
import { MemoryResponseCache } from "../uip/response-cache.js";
|
|
36
|
-
import {
|
|
36
|
+
import { registryCatalog } from "./actions.js";
|
|
37
37
|
import { discoverConnection, lookupConnection, mergeBindings, restoreBindings, snapshotBindings, } from "./connections.js";
|
|
38
38
|
import { prepareEvent } from "./event.js";
|
|
39
39
|
import { descriptorUsage } from "./handoff.js";
|
|
40
40
|
import { ResolutionsExhausted, allParentsResolved, resolvedParentValues, writeResolutions } from "./ledger.js";
|
|
41
|
-
import { PREPARE_COMMAND, PrepareRefusal, bindingsLine, candidateLabel, connectionLine, generatedModulesLine, genericActionNeedsObjectMessage, inputFieldNote, keptSchemaMessage, lookupVocabularyLines, missingGenericSchemaMessage, missingParentsNote, nextLine, noCanonicalEntryMessage, objectFlagsOnlyForGenericMessage, objectNoteLine, objectRecordedLine, parentFieldNamesMismatchMessage, parentOnlyMessage, prefixed, preparedSummaryLine, rejectedParentFieldsMessage, rerunAgainstLine, resolvedParentsNote, tsNote,
|
|
41
|
+
import { PREPARE_COMMAND, PrepareRefusal, bindingsLine, candidateLabel, connectionLine, generatedModulesLine, genericActionNeedsObjectMessage, inputFieldNote, keptSchemaMessage, lookupVocabularyLines, missingGenericSchemaMessage, missingParentsNote, nextLine, noCanonicalEntryMessage, objectFlagsOnlyForGenericMessage, objectNoteLine, objectRecordedLine, parentFieldNamesMismatchMessage, parentOnlyMessage, prefixed, preparedSummaryLine, rejectedParentFieldsMessage, rerunAgainstLine, resolvedParentsNote, registryGetFailedMessage, tsNote, unresolvedOperationMessage, } from "./messages.js";
|
|
42
42
|
import { ObjectMatchRefusal, RESOLUTIONS_FILENAME, matchObject, recordObjectResolution } from "./objects.js";
|
|
43
43
|
import { countInputFields, keepPreparedSchema, lookupSpecsFromOverlay, mergeFilesInto, parentFields as overlayParentFields, schemaActionParents, } from "./overlay.js";
|
|
44
44
|
import { declaredParents, objectActionRequest, objectActionSpecs, parseFields } from "./parents.js";
|
|
@@ -163,7 +163,10 @@ export async function prepareConnector(options, ports) {
|
|
|
163
163
|
connectionId: connected.connectionId,
|
|
164
164
|
connection: options.connection,
|
|
165
165
|
pinned,
|
|
166
|
-
}, error.plan,
|
|
166
|
+
}, error.plan,
|
|
167
|
+
// The wrapper, not `error.first`: same facts, and its message names every
|
|
168
|
+
// miss of the run rather than only the first.
|
|
169
|
+
error);
|
|
167
170
|
// Undo the bindings this run wrote for the connection that cannot serve
|
|
168
171
|
// the lookups, then start over pinned to the one that can. Re-running
|
|
169
172
|
// rather than switching in place is deliberate: the overlay and its
|
|
@@ -204,8 +207,8 @@ async function prepareOnConnection(options, ports, connected) {
|
|
|
204
207
|
const { connectionId, bound, rollback } = connected;
|
|
205
208
|
const ctx = createBuildContext({ client: ports.client, caches: ports.caches ?? memoryCaches(), reporter: ports.reporter, concurrency: 1, timeoutMs: ports.timeoutMs }, { connectionId, parentFields });
|
|
206
209
|
const nodeType = `uipath.connector.${connectorKey}.${action}`;
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
210
|
+
const actionProbe = await registryProbe(ctx, nodeType);
|
|
211
|
+
if ("miss" in actionProbe) {
|
|
209
212
|
// Not an activity. Connector EVENTS live one namespace over, and their
|
|
210
213
|
// registry definition only answers with a connection, which this command
|
|
211
214
|
// has by now. Same loop, different twin pair.
|
|
@@ -227,13 +230,19 @@ async function prepareOnConnection(options, ports, connected) {
|
|
|
227
230
|
bindings: rollback,
|
|
228
231
|
prepareCommand: command,
|
|
229
232
|
});
|
|
230
|
-
if (
|
|
233
|
+
if (!("missed" in event)) {
|
|
231
234
|
return { kind: "event", nodeType: event.eventNodeType, connectionId, bound, objectName: event.objectName };
|
|
232
235
|
}
|
|
233
|
-
const
|
|
234
|
-
|
|
236
|
+
const probes = [actionProbe.miss, event.missed];
|
|
237
|
+
// Only the CLI's "Node not found" says a node is absent. Any other failure
|
|
238
|
+
// of either lookup is reported as the failure it is, never as a verdict.
|
|
239
|
+
if (probes.some((probe) => !probe.notFound)) {
|
|
240
|
+
throw new PrepareRefusal(registryGetFailedMessage(connectorKey, action, probes), "REGISTRY_GET_FAILED");
|
|
241
|
+
}
|
|
242
|
+
const catalog = await registryCatalog(ports.client, connectorKey, ports.timeoutMs);
|
|
243
|
+
throw new PrepareRefusal(unresolvedOperationMessage({ connectorKey, name: action, probes, catalog }), "OPERATION_UNKNOWN");
|
|
235
244
|
}
|
|
236
|
-
const payload = ctx.caches.registry.read(
|
|
245
|
+
const payload = ctx.caches.registry.read(actionProbe.name);
|
|
237
246
|
// The registry's own declaration of parent inputs, read from the same reply.
|
|
238
247
|
const declared = declaredParents(payload);
|
|
239
248
|
let entry = extractCanonical(nodeOf(payload));
|
|
@@ -36,7 +36,21 @@ export interface LookupExhaustedFacts {
|
|
|
36
36
|
pages: number;
|
|
37
37
|
/** True when a cap, not the collection's end, stopped the scan. */
|
|
38
38
|
capped: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Every distinct value the scan saw at `by`, when the whole collection was
|
|
41
|
+
* read and holds at most {@link CANDIDATE_LIMIT} records — what to pick from.
|
|
42
|
+
*/
|
|
43
|
+
candidates?: readonly string[];
|
|
44
|
+
/** A value the scan saw that equals the wanted one ignoring case; the match itself is exact. */
|
|
45
|
+
near?: string;
|
|
39
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A collection this small is listed in full when a value is not in it. The
|
|
49
|
+
* 2026-09-23 run guessed `send_as` 'Bot token' from the field's description
|
|
50
|
+
* while `available_tokens` held two records, 'Bot' and 'User', and the refusal
|
|
51
|
+
* said only "scanned 2 record(s)".
|
|
52
|
+
*/
|
|
53
|
+
export declare const CANDIDATE_LIMIT = 20;
|
|
40
54
|
/**
|
|
41
55
|
* The collection was read to the end on THIS connection and had no match
|
|
42
56
|
* (`_resolve.LookupExhausted`).
|
|
@@ -63,6 +77,8 @@ export declare class LookupExhausted extends ResolutionRefusal {
|
|
|
63
77
|
readonly scanned: number;
|
|
64
78
|
readonly pages: number;
|
|
65
79
|
readonly capped: boolean;
|
|
80
|
+
readonly candidates?: readonly string[];
|
|
81
|
+
readonly near?: string;
|
|
66
82
|
constructor(message: string, facts: LookupExhaustedFacts);
|
|
67
83
|
}
|
|
68
84
|
/** One `--resolve <field>:<by>=<value>`, parsed. */
|
|
@@ -97,8 +113,17 @@ type SpecTable = ReadonlyMap<string, ResolveSpec> | Readonly<Record<string, Reso
|
|
|
97
113
|
* unsubstituted `{placeholder}` in a URL, and the error the connector returns
|
|
98
114
|
* for that names neither field.
|
|
99
115
|
*
|
|
100
|
-
* A field requested
|
|
101
|
-
*
|
|
116
|
+
* A field requested more than once keeps its first position and EVERY distinct
|
|
117
|
+
* `by`/`value`, in the order given; an exact repeat collapses. DELIBERATE
|
|
118
|
+
* DIVERGENCE FROM THE PYTHON, whose dict kept one request per field — its LAST
|
|
119
|
+
* `by`/`value` — and dropped the rest without a word. That is fatal for a
|
|
120
|
+
* multiselect: `--resolve users[*]:…=A --resolve users[*]:…=B --resolve
|
|
121
|
+
* users[*]:…=C`, one command as the library page prints it, resolved and
|
|
122
|
+
* recorded only C (2026-09-23, skill-flow-ipe-multiselect), and the probe of
|
|
123
|
+
* the other connections asked only about C too.
|
|
124
|
+
*
|
|
125
|
+
* A PREREQUISITE given more than one value is refused: its id is substituted
|
|
126
|
+
* into the dependent's collection path, and one placeholder takes one id.
|
|
102
127
|
*/
|
|
103
128
|
export declare function orderResolutions(requested: readonly ResolveRequest[], specs: SpecTable): ResolveRequest[];
|
|
104
129
|
/**
|
|
@@ -22,6 +22,7 @@ import { pyFloatRepr } from "../codecs/pyjson.js";
|
|
|
22
22
|
import { pyStrip, pyStripChars, splitOnce } from "../compat/pystr.js";
|
|
23
23
|
import { Refusal } from "../errors.js";
|
|
24
24
|
import { isResourcesRunList } from "../uip/commands.js";
|
|
25
|
+
import { failureReason } from "../uip/envelope.js";
|
|
25
26
|
import { isPlainObject, notJsonReason, pyOr, pyRepr, pyStrAny, pyTruthy } from "./support.js";
|
|
26
27
|
/**
|
|
27
28
|
* A scan has to stop somewhere, and the caps must be set against the MEASURED
|
|
@@ -50,6 +51,13 @@ export class ResolutionRefusal extends Refusal {
|
|
|
50
51
|
this.name = "ResolutionRefusal";
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* A collection this small is listed in full when a value is not in it. The
|
|
56
|
+
* 2026-09-23 run guessed `send_as` 'Bot token' from the field's description
|
|
57
|
+
* while `available_tokens` held two records, 'Bot' and 'User', and the refusal
|
|
58
|
+
* said only "scanned 2 record(s)".
|
|
59
|
+
*/
|
|
60
|
+
export const CANDIDATE_LIMIT = 20;
|
|
53
61
|
/**
|
|
54
62
|
* The collection was read to the end on THIS connection and had no match
|
|
55
63
|
* (`_resolve.LookupExhausted`).
|
|
@@ -76,6 +84,8 @@ export class LookupExhausted extends ResolutionRefusal {
|
|
|
76
84
|
scanned;
|
|
77
85
|
pages;
|
|
78
86
|
capped;
|
|
87
|
+
candidates;
|
|
88
|
+
near;
|
|
79
89
|
constructor(message, facts) {
|
|
80
90
|
super(message, "RESOLVE_NO_MATCH");
|
|
81
91
|
this.name = "LookupExhausted";
|
|
@@ -86,6 +96,10 @@ export class LookupExhausted extends ResolutionRefusal {
|
|
|
86
96
|
this.scanned = facts.scanned;
|
|
87
97
|
this.pages = facts.pages;
|
|
88
98
|
this.capped = facts.capped;
|
|
99
|
+
if (facts.candidates !== undefined)
|
|
100
|
+
this.candidates = facts.candidates;
|
|
101
|
+
if (facts.near !== undefined)
|
|
102
|
+
this.near = facts.near;
|
|
89
103
|
}
|
|
90
104
|
}
|
|
91
105
|
/**
|
|
@@ -123,13 +137,27 @@ function specOf(specs, field) {
|
|
|
123
137
|
* unsubstituted `{placeholder}` in a URL, and the error the connector returns
|
|
124
138
|
* for that names neither field.
|
|
125
139
|
*
|
|
126
|
-
* A field requested
|
|
127
|
-
*
|
|
140
|
+
* A field requested more than once keeps its first position and EVERY distinct
|
|
141
|
+
* `by`/`value`, in the order given; an exact repeat collapses. DELIBERATE
|
|
142
|
+
* DIVERGENCE FROM THE PYTHON, whose dict kept one request per field — its LAST
|
|
143
|
+
* `by`/`value` — and dropped the rest without a word. That is fatal for a
|
|
144
|
+
* multiselect: `--resolve users[*]:…=A --resolve users[*]:…=B --resolve
|
|
145
|
+
* users[*]:…=C`, one command as the library page prints it, resolved and
|
|
146
|
+
* recorded only C (2026-09-23, skill-flow-ipe-multiselect), and the probe of
|
|
147
|
+
* the other connections asked only about C too.
|
|
148
|
+
*
|
|
149
|
+
* A PREREQUISITE given more than one value is refused: its id is substituted
|
|
150
|
+
* into the dependent's collection path, and one placeholder takes one id.
|
|
128
151
|
*/
|
|
129
152
|
export function orderResolutions(requested, specs) {
|
|
130
153
|
const byField = new Map();
|
|
131
|
-
for (const request of requested)
|
|
132
|
-
byField.
|
|
154
|
+
for (const request of requested) {
|
|
155
|
+
const same = byField.get(request[0]);
|
|
156
|
+
if (same === undefined)
|
|
157
|
+
byField.set(request[0], [request]);
|
|
158
|
+
else if (!same.some(([, by, value]) => by === request[1] && value === request[2]))
|
|
159
|
+
same.push(request);
|
|
160
|
+
}
|
|
133
161
|
const ordered = [];
|
|
134
162
|
const placed = new Set();
|
|
135
163
|
const place = (field, trail) => {
|
|
@@ -141,7 +169,14 @@ export function orderResolutions(requested, specs) {
|
|
|
141
169
|
}
|
|
142
170
|
const dependsOn = specOf(specs, field)?.dependsOn;
|
|
143
171
|
for (const prerequisite of Array.isArray(dependsOn) ? dependsOn : []) {
|
|
144
|
-
|
|
172
|
+
const given = byField.get(prerequisite);
|
|
173
|
+
if (given !== undefined && given.length > 1) {
|
|
174
|
+
throw new ResolutionRefusal(`cannot resolve ${pyRepr(field)}: its collection is addressed through ` +
|
|
175
|
+
`${pyRepr(prerequisite)}, which was given ${given.length} values ` +
|
|
176
|
+
`(${given.map(([, by, value]) => `${by}=${pyRepr(value)}`).join(", ")}). ` +
|
|
177
|
+
`The path takes one id; keep one --resolve ${prerequisite}:<by>=<value> and re-run.`, "RESOLVE_AMBIGUOUS_PREREQUISITE");
|
|
178
|
+
}
|
|
179
|
+
if (given !== undefined) {
|
|
145
180
|
place(prerequisite, [...trail, field]);
|
|
146
181
|
}
|
|
147
182
|
else {
|
|
@@ -151,7 +186,7 @@ export function orderResolutions(requested, specs) {
|
|
|
151
186
|
}
|
|
152
187
|
}
|
|
153
188
|
placed.add(field);
|
|
154
|
-
ordered.push(byField.get(field));
|
|
189
|
+
ordered.push(...byField.get(field));
|
|
155
190
|
};
|
|
156
191
|
for (const [field] of requested)
|
|
157
192
|
place(field, []);
|
|
@@ -327,7 +362,7 @@ export function parseListingDocument(text) {
|
|
|
327
362
|
export async function listPage(client, connectorKey, collection, connectionId, query) {
|
|
328
363
|
const result = await client.run(isResourcesRunList(connectorKey, collection, connectionId, query));
|
|
329
364
|
if (result.code !== 0) {
|
|
330
|
-
throw new ResolutionRefusal(`listing ${collection} failed: ${
|
|
365
|
+
throw new ResolutionRefusal(`listing ${collection} failed: ${failureReason(result)}`, "RESOLVE_LISTING_FAILED");
|
|
331
366
|
}
|
|
332
367
|
const envelope = parseListingDocument(result.stdout);
|
|
333
368
|
if (!isPlainObject(envelope)) {
|
|
@@ -433,9 +468,21 @@ export async function resolveOne(client, connectorKey, spec, field, by, value, c
|
|
|
433
468
|
query = query ? `${query}&${filtered}` : filtered;
|
|
434
469
|
}
|
|
435
470
|
const scan = scanCollection(client, connectorKey, collection, connectionId, query);
|
|
471
|
+
// What the collection DOES hold at `by`, for the refusal: kept only while it
|
|
472
|
+
// could still be listed in full, plus the first case-insensitive equal.
|
|
473
|
+
const held = [];
|
|
474
|
+
let near;
|
|
436
475
|
let step = await scan.next();
|
|
437
476
|
while (!step.done) {
|
|
438
477
|
for (const record of step.value) {
|
|
478
|
+
const at = readPath(record, by);
|
|
479
|
+
if (at !== null && at !== undefined) {
|
|
480
|
+
const text = pyStrAny(at);
|
|
481
|
+
if (held.length <= CANDIDATE_LIMIT && !held.includes(text))
|
|
482
|
+
held.push(text);
|
|
483
|
+
if (near === undefined && text !== value && text.toLowerCase() === value.toLowerCase())
|
|
484
|
+
near = text;
|
|
485
|
+
}
|
|
439
486
|
if (matchRecord(record, by, value)) {
|
|
440
487
|
const got = readPath(record, send);
|
|
441
488
|
if (got === null) {
|
|
@@ -463,8 +510,24 @@ export async function resolveOne(client, connectorKey, spec, field, by, value, c
|
|
|
463
510
|
// The listing command is still offered, once, by the caller's terminal
|
|
464
511
|
// report — where every connection has been tried and seeing what IS there
|
|
465
512
|
// is finally the next step rather than a detour.
|
|
513
|
+
//
|
|
514
|
+
// What it DOES carry is what the collection holds, when that is small enough
|
|
515
|
+
// to read in one line, and a case-only near miss: facts about this
|
|
516
|
+
// connection, not a remedy. `matchRecord` stays exact — nothing is picked.
|
|
517
|
+
//
|
|
518
|
+
// Never under a server-side filter: the scan then read only the records the
|
|
519
|
+
// filter returned (`name like '%Coder Eval%'` gives 'Coder Evals' and 'Coder
|
|
520
|
+
// Eval Test'), and listing that subset as what the collection "holds" would
|
|
521
|
+
// be false. 693 library operations declare a `filterPattern`.
|
|
522
|
+
const candidates = !capped && !pyTruthy(filterPattern) && scanned <= CANDIDATE_LIMIT && held.length > 0 ? held : undefined;
|
|
466
523
|
throw new LookupExhausted(`${field}: no record in ${pyRepr(collection)} has ${by}=${pyRepr(value)} ` +
|
|
467
524
|
`(scanned ${scanned} record(s) across ${pages + 1} page(s)` +
|
|
468
525
|
(capped ? `; STOPPED AT THE ${MAX_SCANNED}-record cap, so the value may be further in` : "; collection exhausted") +
|
|
469
|
-
`) on connection ${connectionId}
|
|
526
|
+
`) on connection ${connectionId}.` +
|
|
527
|
+
(near !== undefined ? ` Did you mean ${pyRepr(near)}? The match is exact, case included.` : "") +
|
|
528
|
+
(candidates !== undefined ? ` It holds: ${candidates.map(pyRepr).join(", ")}.` : ""), {
|
|
529
|
+
field, collection, by, value, scanned, pages: pages + 1, capped,
|
|
530
|
+
...(candidates !== undefined ? { candidates } : {}),
|
|
531
|
+
...(near !== undefined ? { near } : {}),
|
|
532
|
+
});
|
|
470
533
|
}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Reading what `uip … --output json` prints.
|
|
3
|
-
*
|
|
4
|
-
* The CLI is chatty: an update banner, a tool-version warning and a
|
|
5
|
-
* `ManifestClient` diagnostic can precede the JSON document, and the document
|
|
6
|
-
* itself is a `{ Result, Data, Message, … }` envelope whose key casing has
|
|
7
|
-
* varied between tools. Three Python callers each had their own loop for
|
|
8
|
-
* finding the document; this is the one loop, and the one accessor.
|
|
9
|
-
*/
|
|
1
|
+
import type { UipResult } from "./client.js";
|
|
10
2
|
/** The shape every `uip` JSON response shares, read case-insensitively. */
|
|
11
3
|
export interface UipEnvelope {
|
|
12
4
|
Result?: unknown;
|
|
@@ -35,3 +27,46 @@ export declare function isSuccess(envelope: UipEnvelope | undefined): boolean;
|
|
|
35
27
|
export declare function dataItems(envelope: UipEnvelope | undefined): unknown[];
|
|
36
28
|
/** The last `limit` characters of a stream, for a message. */
|
|
37
29
|
export declare function tail(text: string, limit?: number): string;
|
|
30
|
+
/**
|
|
31
|
+
* Why a `uip` call failed, as text a message can quote after a colon. Never empty.
|
|
32
|
+
*
|
|
33
|
+
* `uip … --output json` prints its FAILURE envelope on stdout, not stderr:
|
|
34
|
+
* `OutputFormatter.error` hands it to `logOutput`, which writes through
|
|
35
|
+
* `sink.writeOut` (uipath-cli `packages/common/src/formatter.ts:1657-1688` and
|
|
36
|
+
* `:654-675`). For example, `registry get` of a node the catalog does not serve
|
|
37
|
+
* exits 1 with this on stdout:
|
|
38
|
+
*
|
|
39
|
+
* ```json
|
|
40
|
+
* { "Result": "Failure", "Message": "Node not found",
|
|
41
|
+
* "Instructions": "No node found with node type: uipath.connector.trigger.…" }
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* and, on stderr, nothing, or a `[ManifestClient] fetchDynamicNodes ok: …`
|
|
45
|
+
* diagnostic, or a self-update banner. Callers that quoted stderr printed
|
|
46
|
+
* `registry get failed for <nodeType>: ` with nothing after the colon (2026-09-23
|
|
47
|
+
* eval, `skill-flow-datafabric-trigger-lifecycle`, v2 command 33).
|
|
48
|
+
*
|
|
49
|
+
* So the envelope is read first. The order is:
|
|
50
|
+
*
|
|
51
|
+
* 1. a call the client refused (`OfflineUipClient`): no process ran, and its
|
|
52
|
+
* stderr already says so (`offline: refused to run …`), so that text is kept;
|
|
53
|
+
* 2. a call killed by its timeout: `timed out`, whatever partial output it left;
|
|
54
|
+
* 3. a non-Success envelope on stdout (found past any banner, keys read
|
|
55
|
+
* case-insensitively): `<Message>: <Instructions>`, or whichever is present;
|
|
56
|
+
* 4. the end of stderr, then the end of stdout;
|
|
57
|
+
* 5. `no output`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function failureReason(result: UipResult): string;
|
|
60
|
+
/**
|
|
61
|
+
* True when a failed `registry get` answered the CLI's own "not in the catalog"
|
|
62
|
+
* envelope, `Message: "Node not found"` (uipath-cli
|
|
63
|
+
* `packages/maestro-sdk/src/registry/command.ts:595-600`).
|
|
64
|
+
*
|
|
65
|
+
* Only that answer says the node is ABSENT. Every other failure — `Error
|
|
66
|
+
* retrieving node` (`command.ts:586-591`, which carries the thrown error in
|
|
67
|
+
* `Instructions`), a refusal, a timeout, output that is not JSON — says the
|
|
68
|
+
* lookup did not work, which is a different fact with a different remedy. The
|
|
69
|
+
* `Message` is the discriminator because the envelope's `ErrorCode` is the
|
|
70
|
+
* generic `unknown_error` for both (`formatter.ts:1490-1502`).
|
|
71
|
+
*/
|
|
72
|
+
export declare function isNodeNotFound(result: UipResult): boolean;
|
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
* itself is a `{ Result, Data, Message, … }` envelope whose key casing has
|
|
7
7
|
* varied between tools. Three Python callers each had their own loop for
|
|
8
8
|
* finding the document; this is the one loop, and the one accessor.
|
|
9
|
+
*
|
|
10
|
+
* It is also the one place that decides why a call FAILED ({@link failureReason}).
|
|
11
|
+
* Callers outside `registry/uip/` do not read a result's stderr themselves;
|
|
12
|
+
* `tests/uip-stderr-gate.test.ts` holds them to that.
|
|
9
13
|
*/
|
|
14
|
+
import { pyStrip } from "../compat/pystr.js";
|
|
10
15
|
/**
|
|
11
16
|
* The first JSON document in `text`, or `undefined`.
|
|
12
17
|
*
|
|
@@ -82,3 +87,78 @@ export function dataItems(envelope) {
|
|
|
82
87
|
export function tail(text, limit = 300) {
|
|
83
88
|
return text.length <= limit ? text : text.slice(text.length - limit);
|
|
84
89
|
}
|
|
90
|
+
/** A non-empty trimmed string, or `undefined`. */
|
|
91
|
+
function said(value) {
|
|
92
|
+
if (typeof value !== "string")
|
|
93
|
+
return undefined;
|
|
94
|
+
const text = pyStrip(value);
|
|
95
|
+
return text === "" ? undefined : text;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Why a `uip` call failed, as text a message can quote after a colon. Never empty.
|
|
99
|
+
*
|
|
100
|
+
* `uip … --output json` prints its FAILURE envelope on stdout, not stderr:
|
|
101
|
+
* `OutputFormatter.error` hands it to `logOutput`, which writes through
|
|
102
|
+
* `sink.writeOut` (uipath-cli `packages/common/src/formatter.ts:1657-1688` and
|
|
103
|
+
* `:654-675`). For example, `registry get` of a node the catalog does not serve
|
|
104
|
+
* exits 1 with this on stdout:
|
|
105
|
+
*
|
|
106
|
+
* ```json
|
|
107
|
+
* { "Result": "Failure", "Message": "Node not found",
|
|
108
|
+
* "Instructions": "No node found with node type: uipath.connector.trigger.…" }
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* and, on stderr, nothing, or a `[ManifestClient] fetchDynamicNodes ok: …`
|
|
112
|
+
* diagnostic, or a self-update banner. Callers that quoted stderr printed
|
|
113
|
+
* `registry get failed for <nodeType>: ` with nothing after the colon (2026-09-23
|
|
114
|
+
* eval, `skill-flow-datafabric-trigger-lifecycle`, v2 command 33).
|
|
115
|
+
*
|
|
116
|
+
* So the envelope is read first. The order is:
|
|
117
|
+
*
|
|
118
|
+
* 1. a call the client refused (`OfflineUipClient`): no process ran, and its
|
|
119
|
+
* stderr already says so (`offline: refused to run …`), so that text is kept;
|
|
120
|
+
* 2. a call killed by its timeout: `timed out`, whatever partial output it left;
|
|
121
|
+
* 3. a non-Success envelope on stdout (found past any banner, keys read
|
|
122
|
+
* case-insensitively): `<Message>: <Instructions>`, or whichever is present;
|
|
123
|
+
* 4. the end of stderr, then the end of stdout;
|
|
124
|
+
* 5. `no output`.
|
|
125
|
+
*/
|
|
126
|
+
export function failureReason(result) {
|
|
127
|
+
if (result.refused === true)
|
|
128
|
+
return said(result.stderr) ?? "not run (offline client refused the call)";
|
|
129
|
+
if (result.timedOut === true)
|
|
130
|
+
return "timed out (the call was killed before it answered)";
|
|
131
|
+
const envelope = parseEnvelope(result.stdout);
|
|
132
|
+
if (envelope !== undefined && !isSuccess(envelope)) {
|
|
133
|
+
const parts = [said(getCI(envelope, "Message")), said(getCI(envelope, "Instructions"))].filter((part) => part !== undefined);
|
|
134
|
+
if (parts.length > 0)
|
|
135
|
+
return parts.join(": ");
|
|
136
|
+
}
|
|
137
|
+
const stderr = said(result.stderr);
|
|
138
|
+
if (stderr !== undefined)
|
|
139
|
+
return tail(stderr);
|
|
140
|
+
const stdout = said(result.stdout);
|
|
141
|
+
if (stdout !== undefined)
|
|
142
|
+
return tail(stdout);
|
|
143
|
+
return "no output";
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* True when a failed `registry get` answered the CLI's own "not in the catalog"
|
|
147
|
+
* envelope, `Message: "Node not found"` (uipath-cli
|
|
148
|
+
* `packages/maestro-sdk/src/registry/command.ts:595-600`).
|
|
149
|
+
*
|
|
150
|
+
* Only that answer says the node is ABSENT. Every other failure — `Error
|
|
151
|
+
* retrieving node` (`command.ts:586-591`, which carries the thrown error in
|
|
152
|
+
* `Instructions`), a refusal, a timeout, output that is not JSON — says the
|
|
153
|
+
* lookup did not work, which is a different fact with a different remedy. The
|
|
154
|
+
* `Message` is the discriminator because the envelope's `ErrorCode` is the
|
|
155
|
+
* generic `unknown_error` for both (`formatter.ts:1490-1502`).
|
|
156
|
+
*/
|
|
157
|
+
export function isNodeNotFound(result) {
|
|
158
|
+
if (result.code === 0 || result.refused === true || result.timedOut === true)
|
|
159
|
+
return false;
|
|
160
|
+
const envelope = parseEnvelope(result.stdout);
|
|
161
|
+
if (envelope === undefined || isSuccess(envelope))
|
|
162
|
+
return false;
|
|
163
|
+
return said(getCI(envelope, "Message"))?.toLowerCase() === "node not found";
|
|
164
|
+
}
|
package/dist/serialize.js
CHANGED
|
@@ -13,6 +13,7 @@ import { dirname, join } from 'node:path';
|
|
|
13
13
|
import { Expr, toExpr, SCHEDULE_PRESETS, DELAY_PRESETS, parseIxpProjectType, flatNameOf, flatNameSegmentsOf } from './flow-sdk.js';
|
|
14
14
|
import { FlowAction, FlowTrigger } from './core/node-classes.js';
|
|
15
15
|
import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
|
|
16
|
+
import { dataFabricUnknownInputs } from './core/typed-raw-node.js';
|
|
16
17
|
import { bindingSelfNameMessage } from './core/binding-messages.js';
|
|
17
18
|
import { OUTCOME_HANDLE_NODE_TYPES, assertHitlExitInvariant, hitlExitModel, hitlExitViolation, outcomeSlug } from './core/hitl-routing.js';
|
|
18
19
|
import { declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
|
|
@@ -3474,8 +3475,18 @@ function eventDetail(rc, sub, nodeId, opts) {
|
|
|
3474
3475
|
errorState: { issues: [] },
|
|
3475
3476
|
};
|
|
3476
3477
|
}
|
|
3477
|
-
/**
|
|
3478
|
+
/**
|
|
3479
|
+
* Recursively render Exprs nested inside objects/arrays (for connector inputs).
|
|
3480
|
+
*
|
|
3481
|
+
* A lookup token is REFUSED, never rendered. `buildConnectorInputs` swaps every
|
|
3482
|
+
* token it can place — a field's whole value, or an element of an array — so a
|
|
3483
|
+
* token that reaches here sits where nothing resolves it: inside an object value,
|
|
3484
|
+
* or in a custom node's inputs. Recursing into it would put `{"__lookup": …}` on
|
|
3485
|
+
* the wire where the connector expects an id.
|
|
3486
|
+
*/
|
|
3478
3487
|
function renderDeep(val, rename = NO_RENAME) {
|
|
3488
|
+
if (isLookupToken(val))
|
|
3489
|
+
throw new Error(misplacedLookupMessage(val));
|
|
3479
3490
|
if (val instanceof Expr)
|
|
3480
3491
|
return renderValue(val, rename);
|
|
3481
3492
|
if (Array.isArray(val))
|
|
@@ -3488,6 +3499,13 @@ function renderDeep(val, rename = NO_RENAME) {
|
|
|
3488
3499
|
}
|
|
3489
3500
|
return val;
|
|
3490
3501
|
}
|
|
3502
|
+
/** Why {@link renderDeep} refused a token: where a lookup CAN go, and what it would have emitted. */
|
|
3503
|
+
function misplacedLookupMessage(token) {
|
|
3504
|
+
const { field, by, value } = token.__lookup;
|
|
3505
|
+
return (`lookup('${field}') (${by}=${JSON.stringify(value)}) is in a position no lookup is resolved from: ` +
|
|
3506
|
+
`a lookup() must be the whole value of a connector input, or an element of an array input. ` +
|
|
3507
|
+
`Emitting it here would send a raw {"__lookup": …} object where the connector expects an id.`);
|
|
3508
|
+
}
|
|
3491
3509
|
function newScope(outputs, vars = []) {
|
|
3492
3510
|
return {
|
|
3493
3511
|
nodes: [],
|
|
@@ -3911,7 +3929,14 @@ export function serialize(built, opts = {}) {
|
|
|
3911
3929
|
display: { label, ...(def?.display?.icon ? { icon: def.display.icon } : {}) },
|
|
3912
3930
|
};
|
|
3913
3931
|
col++;
|
|
3914
|
-
|
|
3932
|
+
// A Data Fabric node gets no node-level `model`, whichever path emits it
|
|
3933
|
+
// (the typed verbs, or a rawNode through emitContribution). Studio Web
|
|
3934
|
+
// never writes one (flow-core node.ts: "`node.model` is never written";
|
|
3935
|
+
// canvas `nodeToInstance` omits it), and export seeds it from the
|
|
3936
|
+
// definition (services bpmn-to-xml.ts), so a copy here can only go stale.
|
|
3937
|
+
// `outputs` stay: the canvas persists them (`source: '=response'`), and
|
|
3938
|
+
// `variables.nodes[]` is derived from them.
|
|
3939
|
+
if (def?.model && !type.startsWith('core.datafabric.'))
|
|
3915
3940
|
node.model = subNodeId(clone(def.model), id);
|
|
3916
3941
|
if (def?.outputDefinition)
|
|
3917
3942
|
node.outputs = subNodeId(clone(def.outputDefinition), id);
|
|
@@ -4153,6 +4178,12 @@ export function serialize(built, opts = {}) {
|
|
|
4153
4178
|
const connectorRefusal = connectorRawNodeRefusal(def.nodeType);
|
|
4154
4179
|
if (connectorRefusal)
|
|
4155
4180
|
throw new Error(`${where}: ${connectorRefusal}`);
|
|
4181
|
+
// A Data Fabric key the product never reads (`_sortOptions`) is refused for
|
|
4182
|
+
// every contribution, whichever class or SDK copy made it: `validate`
|
|
4183
|
+
// cannot see it, and the node would ignore it at run time.
|
|
4184
|
+
const unknownInput = dataFabricUnknownInputs(def.nodeType, def.version, contribution.inputs);
|
|
4185
|
+
if (unknownInput?.severity === 'error')
|
|
4186
|
+
throw new Error(`${where}: ${unknownInput.message}`);
|
|
4156
4187
|
const id = stepUid(scope, stepName);
|
|
4157
4188
|
const node = makeNode(def.nodeType, id, stepName, { parentId, def, requestedVersion });
|
|
4158
4189
|
node.inputs = renderDeep(contribution.inputs ?? {}, scope.rename);
|
|
@@ -6118,16 +6149,35 @@ function buildConnectorInputs(rc, inputs, rename = NO_RENAME, resolutions = {},
|
|
|
6118
6149
|
// NOT rendered as a placeholder: emitting something for a value nobody
|
|
6119
6150
|
// resolved would produce a flow that compiles and then talks to the wrong
|
|
6120
6151
|
// record, which is exactly the failure the lookup surface exists to remove.
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6152
|
+
//
|
|
6153
|
+
// A token is swapped wherever the field's value can hold one: the whole
|
|
6154
|
+
// value, or an element of an array at any depth. A multiselect is
|
|
6155
|
+
// `users: [lookup(…), lookup(…)]` — the form `ConnectorLookupValue`'s array
|
|
6156
|
+
// branch types and the generated `## Example` prints — and until this swapped
|
|
6157
|
+
// only the whole-value case, so every token inside the array reached
|
|
6158
|
+
// validation as an object ("expected string, got object") with every lookup
|
|
6159
|
+
// recorded. An unresolved element under 'ignore' becomes `null`, which
|
|
6160
|
+
// validation skips, so `check` still type-checks the resolved siblings.
|
|
6161
|
+
const unresolved = [];
|
|
6162
|
+
const swapped = swapLookupTokens(nested.value, resolutions, unresolved);
|
|
6163
|
+
if (unresolved.length) {
|
|
6164
|
+
if (policy.unresolvedLookups !== 'ignore') {
|
|
6165
|
+
for (const token of unresolved)
|
|
6166
|
+
errs.push(unresolvedLookupMessage(token));
|
|
6126
6167
|
continue;
|
|
6127
6168
|
}
|
|
6128
|
-
|
|
6169
|
+
if (isLookupToken(nested.value))
|
|
6170
|
+
continue;
|
|
6129
6171
|
}
|
|
6130
|
-
|
|
6172
|
+
nested = { ...nested, value: swapped };
|
|
6173
|
+
// A FLAT key whose wire name ends in `[*]` names one array: an array value
|
|
6174
|
+
// there is checked element by element, as the nested form is. A flat scalar
|
|
6175
|
+
// keeps depth 0 and is emitted as the scalar it is — the product wraps it
|
|
6176
|
+
// into a one-element list (flow-v1 `convertDotNotationToNested`).
|
|
6177
|
+
const depth = flat
|
|
6178
|
+
? (f.name.endsWith('[*]') && Array.isArray(nested.value) ? 1 : 0)
|
|
6179
|
+
: nested.projectedArrays;
|
|
6180
|
+
validateConnectorFieldValue(f, nested.value, depth, errs);
|
|
6131
6181
|
if (f.location === 'multipart') {
|
|
6132
6182
|
partValues.set(f.name, renderDeep(nested.value, rename));
|
|
6133
6183
|
continue;
|
|
@@ -6195,6 +6245,29 @@ function buildConnectorInputs(rc, inputs, rename = NO_RENAME, resolutions = {},
|
|
|
6195
6245
|
}
|
|
6196
6246
|
return slots;
|
|
6197
6247
|
}
|
|
6248
|
+
/**
|
|
6249
|
+
* `value` with every lookup token swapped for its recorded id: the value itself,
|
|
6250
|
+
* or the elements of an array at any depth. An unrecorded token is pushed to
|
|
6251
|
+
* `unresolved` and stands as `null`, so an array keeps its length.
|
|
6252
|
+
*
|
|
6253
|
+
* Objects are NOT descended: a lookup's place is a field's value or an element
|
|
6254
|
+
* of an array field (`ConnectorLookupValue`), and a nested path such as
|
|
6255
|
+
* `contacts[*].id` reaches here already projected to its leaf array. A token
|
|
6256
|
+
* anywhere else is refused by {@link renderDeep}.
|
|
6257
|
+
*/
|
|
6258
|
+
function swapLookupTokens(value, resolutions, unresolved) {
|
|
6259
|
+
if (isLookupToken(value)) {
|
|
6260
|
+
const resolved = resolvedValue(value, resolutions);
|
|
6261
|
+
if (resolved === undefined) {
|
|
6262
|
+
unresolved.push(value);
|
|
6263
|
+
return null;
|
|
6264
|
+
}
|
|
6265
|
+
return resolved;
|
|
6266
|
+
}
|
|
6267
|
+
if (Array.isArray(value))
|
|
6268
|
+
return value.map((item) => swapLookupTokens(item, resolutions, unresolved));
|
|
6269
|
+
return value;
|
|
6270
|
+
}
|
|
6198
6271
|
/**
|
|
6199
6272
|
* `inputMetadata.multipart.bodyFieldName` names the CONTAINER part — the one the
|
|
6200
6273
|
* runtime composes from `bodyParameters`. Measured on the designer's own nodes in
|
|
@@ -100,6 +100,15 @@
|
|
|
100
100
|
"objectName": "create_issue",
|
|
101
101
|
"path": "uipath-atlassian-jira/generic/create_issue/create@1.0.0.json"
|
|
102
102
|
},
|
|
103
|
+
{
|
|
104
|
+
"nodeType": "uipath.connector.uipath-getresponse-getresponse.send-newsletter",
|
|
105
|
+
"version": "1.0.0",
|
|
106
|
+
"connectorKey": "uipath-getresponse-getresponse",
|
|
107
|
+
"label": "Send Newsletter",
|
|
108
|
+
"activityType": "Curated",
|
|
109
|
+
"objectName": "NewslettersSend-draft",
|
|
110
|
+
"path": "uipath-getresponse-getresponse/send-newsletter@1.0.0.json"
|
|
111
|
+
},
|
|
103
112
|
{
|
|
104
113
|
"nodeType": "uipath.connector.uipath-google-vertex.execute-google-vertex-agent",
|
|
105
114
|
"version": "1.0.0",
|
|
@@ -136,6 +145,15 @@
|
|
|
136
145
|
"objectName": "triggerFlow",
|
|
137
146
|
"path": "uipath-microsoft-powerautomate/invoke-a-microsoft-power-automate-flow@1.0.0.json"
|
|
138
147
|
},
|
|
148
|
+
{
|
|
149
|
+
"nodeType": "uipath.connector.uipath-salesforce-slack.create-group-direct-message",
|
|
150
|
+
"version": "1.0.0",
|
|
151
|
+
"connectorKey": "uipath-salesforce-slack",
|
|
152
|
+
"label": "Create Group Direct Message",
|
|
153
|
+
"activityType": "Curated",
|
|
154
|
+
"objectName": "conversations_open",
|
|
155
|
+
"path": "uipath-salesforce-slack/create-group-direct-message@1.0.0.json"
|
|
156
|
+
},
|
|
139
157
|
{
|
|
140
158
|
"nodeType": "uipath.connector.uipath-salesforce-slack.send-message-to-user",
|
|
141
159
|
"version": "1.0.0",
|