@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
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* When `registry get uipath.connector.<key>.<action>` finds nothing — and the
|
|
5
5
|
* event namespaces do not have it either — the one question left is what the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* catalog actually serves for that connector, so the refusal can list it: its
|
|
7
|
+
* actions AND its events, because the author's name may have meant either.
|
|
8
|
+
* That is one `registry search <key>`.
|
|
9
|
+
*
|
|
10
|
+
* `registry get` and `registry search` both answer from the CLI's local node
|
|
11
|
+
* index and pull the tenant catalog only when that index is missing (uipath-cli
|
|
12
|
+
* `packages/maestro-sdk/src/registry/node-sync-service.ts:307-323`, `:570-597`),
|
|
13
|
+
* so what they list can be stale; the refusal says how to refresh it.
|
|
9
14
|
*
|
|
10
15
|
* Ported from `_registry_actions` / `_closest_actions` in
|
|
11
16
|
* `prepare_connector.py`. The fetch goes through the injected {@link UipClient};
|
|
@@ -13,6 +18,7 @@
|
|
|
13
18
|
*/
|
|
14
19
|
import { compareCodePoints, sortedStrings } from "../compat/compare.js";
|
|
15
20
|
import { registrySearch } from "../uip/commands.js";
|
|
21
|
+
import { failureReason, findJsonDocument, tail } from "../uip/envelope.js";
|
|
16
22
|
import { isPlainObject, pyOr } from "./support.js";
|
|
17
23
|
/**
|
|
18
24
|
* The action ids in a `registry search <key>` reply, sorted.
|
|
@@ -20,7 +26,8 @@ import { isPlainObject, pyOr } from "./support.js";
|
|
|
20
26
|
* Exact prefix only: the same search returns
|
|
21
27
|
* `uipath.agent.resource.tool.connector.<key>.<action>` and
|
|
22
28
|
* `uipath.connector.trigger.<key>.<event>`, which are not actions of this node
|
|
23
|
-
* and would read as alternatives an author could pass.
|
|
29
|
+
* and would read as alternatives an author could pass as an action. The events
|
|
30
|
+
* are listed separately, by {@link registryEventIds}.
|
|
24
31
|
*/
|
|
25
32
|
export function registryActionIds(payload, connectorKey) {
|
|
26
33
|
// `json.loads(stdout).get("Data") or []`: a reply that is not an object has
|
|
@@ -74,21 +81,88 @@ export function closestActions(action, candidates) {
|
|
|
74
81
|
});
|
|
75
82
|
return scored.map(({ candidate }) => candidate);
|
|
76
83
|
}
|
|
84
|
+
/** Levenshtein distance, two rows. Short strings only (operation ids). */
|
|
85
|
+
function editDistance(a, b) {
|
|
86
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
87
|
+
for (let i = 1; i <= a.length; i++) {
|
|
88
|
+
const cur = [i];
|
|
89
|
+
for (let j = 1; j <= b.length; j++) {
|
|
90
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
91
|
+
}
|
|
92
|
+
prev = cur;
|
|
93
|
+
}
|
|
94
|
+
return prev[b.length];
|
|
95
|
+
}
|
|
77
96
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
97
|
+
* {@link closestActions}, kept only where the candidate is plausibly what was
|
|
98
|
+
* meant: it shares MOST of the wanted id's words (`list-all-records` →
|
|
99
|
+
* `list-records`), or it is a near spelling (`record-creatd` → `record-created`,
|
|
100
|
+
* `send-mail` → `send-email`). One shared word is not enough: `record-created`
|
|
101
|
+
* shares "record" with every Data Fabric action, and offering
|
|
102
|
+
* `update-entity-record` as the closest match to an EVENT name points the
|
|
103
|
+
* author at the wrong namespace.
|
|
81
104
|
*/
|
|
82
|
-
export
|
|
83
|
-
const
|
|
84
|
-
|
|
105
|
+
export function plausibleMatches(wanted, candidates) {
|
|
106
|
+
const words = new Set(wanted.split("-"));
|
|
107
|
+
const limit = Math.max(2, Math.floor(wanted.length / 3));
|
|
108
|
+
const kept = closestActions(wanted, candidates)
|
|
109
|
+
.map((candidate, rank) => ({ candidate, rank, distance: editDistance(wanted, candidate) }))
|
|
110
|
+
.filter(({ candidate, distance }) => {
|
|
111
|
+
const shared = [...words].filter((word) => candidate.split("-").includes(word)).length;
|
|
112
|
+
return shared * 2 > words.size || distance <= limit;
|
|
113
|
+
});
|
|
114
|
+
// Nearest spelling first; the word-overlap rank breaks ties.
|
|
115
|
+
kept.sort((a, b) => a.distance - b.distance || a.rank - b.rank);
|
|
116
|
+
return kept.map(({ candidate }) => candidate);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The event ids in a `registry search <key>` reply, sorted, each with its forms.
|
|
120
|
+
*
|
|
121
|
+
* The form is kept because the two can come and go separately: on 2026-09-16
|
|
122
|
+
* the Data Fabric `.event.` pair was still served after its `.trigger.` pair
|
|
123
|
+
* was gone, and `prepare` probes the `.trigger.` form.
|
|
124
|
+
*/
|
|
125
|
+
export function registryEventIds(payload, connectorKey) {
|
|
126
|
+
if (!isPlainObject(payload))
|
|
85
127
|
return [];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
payload = JSON.parse(result.stdout);
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
128
|
+
const data = pyOr(payload.Data, []);
|
|
129
|
+
if (!Array.isArray(data))
|
|
91
130
|
return [];
|
|
131
|
+
const forms = new Map();
|
|
132
|
+
for (const entry of data) {
|
|
133
|
+
if (!isPlainObject(entry))
|
|
134
|
+
continue;
|
|
135
|
+
const nodeType = pyOr(entry.NodeType, "");
|
|
136
|
+
if (typeof nodeType !== "string")
|
|
137
|
+
continue;
|
|
138
|
+
for (const form of ["trigger", "event"]) {
|
|
139
|
+
const prefix = `uipath.connector.${form}.${connectorKey}.`;
|
|
140
|
+
if (!nodeType.startsWith(prefix))
|
|
141
|
+
continue;
|
|
142
|
+
const rest = nodeType.slice(prefix.length);
|
|
143
|
+
if (rest.includes("."))
|
|
144
|
+
continue;
|
|
145
|
+
if (!forms.has(rest))
|
|
146
|
+
forms.set(rest, new Set());
|
|
147
|
+
forms.get(rest).add(form);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return sortedStrings(forms.keys()).map((id) => ({
|
|
151
|
+
id,
|
|
152
|
+
forms: ["trigger", "event"].filter((form) => forms.get(id).has(form)),
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* `registry search <key>`, read for both catalogs. This runs only on the
|
|
157
|
+
* failure path, to tell the author what the catalog does serve.
|
|
158
|
+
*/
|
|
159
|
+
export async function registryCatalog(client, connectorKey, timeoutMs) {
|
|
160
|
+
const result = await client.run(registrySearch(connectorKey), timeoutMs === undefined ? undefined : { timeoutMs });
|
|
161
|
+
if (result.code !== 0)
|
|
162
|
+
return { actions: [], events: [], failure: failureReason(result) };
|
|
163
|
+
const payload = findJsonDocument(result.stdout);
|
|
164
|
+
if (payload === undefined) {
|
|
165
|
+
return { actions: [], events: [], failure: `it did not answer JSON: ${tail(result.stdout.trim()) || "no output"}` };
|
|
92
166
|
}
|
|
93
|
-
return registryActionIds(payload, connectorKey);
|
|
167
|
+
return { actions: registryActionIds(payload, connectorKey), events: registryEventIds(payload, connectorKey) };
|
|
94
168
|
}
|
|
@@ -23,10 +23,9 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
25
25
|
import { pyJsonStringify } from "../codecs/pyjson.js";
|
|
26
|
-
import { pyStrip } from "../compat/pystr.js";
|
|
27
26
|
import { Refusal, messageOf } from "../errors.js";
|
|
28
27
|
import { isConnectionsList } from "../uip/commands.js";
|
|
29
|
-
import { dataItems, parseEnvelope } from "../uip/envelope.js";
|
|
28
|
+
import { dataItems, failureReason, parseEnvelope } from "../uip/envelope.js";
|
|
30
29
|
import { isPlainObject, notJsonReason, pyGet, pyOr, pyRepr, pyStrAny, pyTruthy } from "./support.js";
|
|
31
30
|
/** No usable connection, with the reason and the way forward (`_connections.ConnectionError_`). */
|
|
32
31
|
export class ConnectionRefusal extends Refusal {
|
|
@@ -44,7 +43,7 @@ export class ConnectionRefusal extends Refusal {
|
|
|
44
43
|
export async function listConnections(client, options) {
|
|
45
44
|
const result = await client.run(isConnectionsList({ allFolders: options.allFolders, byoa: options.byoa }));
|
|
46
45
|
if (result.code !== 0) {
|
|
47
|
-
throw new ConnectionRefusal(`listing connections failed: ${
|
|
46
|
+
throw new ConnectionRefusal(`listing connections failed: ${failureReason(result)}`, "CONNECTION_LISTING_FAILED");
|
|
48
47
|
}
|
|
49
48
|
const envelope = parseEnvelope(result.stdout);
|
|
50
49
|
if (envelope === undefined) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BuildContext } from "../library-build/context.js";
|
|
1
|
+
import { type BuildContext, type RegistryMiss } from "../library-build/context.js";
|
|
2
2
|
import { type EventFacts } from "../library-build/events.js";
|
|
3
3
|
import { type Dict } from "../library-build/py.js";
|
|
4
4
|
import type { LibraryEntry } from "../model/library.js";
|
|
@@ -87,10 +87,19 @@ export interface EventPrepareResult {
|
|
|
87
87
|
filterFields: unknown[];
|
|
88
88
|
definition: EventDefinition;
|
|
89
89
|
}
|
|
90
|
+
/** `<key> <action>` is not an event: the `.trigger.` probe missed, and why. */
|
|
91
|
+
export interface EventMiss {
|
|
92
|
+
missed: RegistryMiss;
|
|
93
|
+
}
|
|
90
94
|
/**
|
|
91
|
-
* `_prepare_event`. Returns
|
|
92
|
-
* either, so the caller can
|
|
93
|
-
* everything else is handled here,
|
|
94
|
-
* already be the connection: the
|
|
95
|
+
* `_prepare_event`. Returns an {@link EventMiss} when `<key> <action>` is not
|
|
96
|
+
* an event either — carrying the probe's reason, so the caller can put it in
|
|
97
|
+
* the one refusal that names both namespaces; everything else is handled here,
|
|
98
|
+
* refusals included. `ctx.connectionId` must already be the connection: the
|
|
99
|
+
* trigger definition only answers with one.
|
|
100
|
+
*
|
|
101
|
+
* The probe is quiet. The Python printed a `WARN: registry get failed for …`
|
|
102
|
+
* here, and the port printed it with an empty reason, because the CLI's reason
|
|
103
|
+
* is on stdout; the reason now travels in the result instead.
|
|
95
104
|
*/
|
|
96
|
-
export declare function prepareEvent(ctx: BuildContext, options: EventPrepareOptions): Promise<EventPrepareResult |
|
|
105
|
+
export declare function prepareEvent(ctx: BuildContext, options: EventPrepareOptions): Promise<EventPrepareResult | EventMiss>;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* through its reporter, in the Python's order.
|
|
21
21
|
*/
|
|
22
22
|
import { extractCanonical, nodeOf } from "../library-build/canonical.js";
|
|
23
|
-
import {
|
|
23
|
+
import { registryProbe } from "../library-build/context.js";
|
|
24
24
|
import { EventDiscoveryError, eventFacts, mergeDescribeIntoNode, triggersDescribe, triggersObjects, unionEventParameters, } from "../library-build/events.js";
|
|
25
25
|
import { get, isRecord } from "../library-build/py.js";
|
|
26
26
|
import { planLibrary } from "../library-build/plan.js";
|
|
@@ -114,10 +114,15 @@ function fieldsOf(section) {
|
|
|
114
114
|
return Array.isArray(fields) ? fields : [];
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
|
-
* `_prepare_event`. Returns
|
|
118
|
-
* either, so the caller can
|
|
119
|
-
* everything else is handled here,
|
|
120
|
-
* already be the connection: the
|
|
117
|
+
* `_prepare_event`. Returns an {@link EventMiss} when `<key> <action>` is not
|
|
118
|
+
* an event either — carrying the probe's reason, so the caller can put it in
|
|
119
|
+
* the one refusal that names both namespaces; everything else is handled here,
|
|
120
|
+
* refusals included. `ctx.connectionId` must already be the connection: the
|
|
121
|
+
* trigger definition only answers with one.
|
|
122
|
+
*
|
|
123
|
+
* The probe is quiet. The Python printed a `WARN: registry get failed for …`
|
|
124
|
+
* here, and the port printed it with an empty reason, because the CLI's reason
|
|
125
|
+
* is on stdout; the reason now travels in the result instead.
|
|
121
126
|
*/
|
|
122
127
|
export async function prepareEvent(ctx, options) {
|
|
123
128
|
const { connectorKey, action, connectionId, out } = options;
|
|
@@ -125,13 +130,16 @@ export async function prepareEvent(ctx, options) {
|
|
|
125
130
|
const info = (line) => ctx.reporter.info(line);
|
|
126
131
|
const warn = (line) => ctx.reporter.warn(line);
|
|
127
132
|
const { trigger: triggerNodeType, event: eventNodeType } = eventNodeTypes(connectorKey, action);
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
return
|
|
131
|
-
const payload = ctx.caches.registry.read(
|
|
133
|
+
const probe = await registryProbe(ctx, triggerNodeType);
|
|
134
|
+
if ("miss" in probe)
|
|
135
|
+
return { missed: probe.miss };
|
|
136
|
+
const payload = ctx.caches.registry.read(probe.name);
|
|
132
137
|
const canonical = extractCanonical(nodeOf(payload));
|
|
133
|
-
if (canonical === undefined)
|
|
134
|
-
|
|
138
|
+
if (canonical === undefined) {
|
|
139
|
+
// An answer, but not one this code can read as a trigger definition. Not
|
|
140
|
+
// "absent", so the caller must not say it is.
|
|
141
|
+
return { missed: { nodeType: triggerNodeType, reason: "the registry answered, but not with a node definition", notFound: false } };
|
|
142
|
+
}
|
|
135
143
|
if (pyTruthy(options.fields))
|
|
136
144
|
throw new PrepareRefusal(eventParentFieldsMessage(), "EVENT_NO_PARENT_FIELDS");
|
|
137
145
|
if (options.allObjects)
|
|
@@ -55,7 +55,9 @@ export interface ResolutionPlan {
|
|
|
55
55
|
export declare class ResolutionsExhausted extends LookupExhausted {
|
|
56
56
|
readonly first: LookupExhausted;
|
|
57
57
|
readonly plan: ResolutionPlan;
|
|
58
|
-
constructor(first: LookupExhausted, plan: ResolutionPlan
|
|
58
|
+
constructor(first: LookupExhausted, plan: ResolutionPlan,
|
|
59
|
+
/** Every miss of the run, when there was more to say than `first`'s own message. */
|
|
60
|
+
message?: string);
|
|
59
61
|
}
|
|
60
62
|
/** What one `--resolve` run needs; `objectName` narrows the overlay entry the specs are read from. */
|
|
61
63
|
export interface ResolveRun {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { lookupKey } from "../../core/lookups.js";
|
|
21
21
|
import { pyJsonStringify } from "../codecs/pyjson.js";
|
|
22
22
|
import { sortedStrings } from "../compat/compare.js";
|
|
23
|
-
import { PrepareRefusal, resolutionLine, unknownLookupMessage, wroteResolutionsLine } from "./messages.js";
|
|
23
|
+
import { PrepareRefusal, incompleteRunMessage, notTriedLine, resolutionLine, unknownLookupMessage, wroteResolutionsLine, } from "./messages.js";
|
|
24
24
|
import { LedgerRefusal, RESOLUTIONS_FILENAME, ledgerPath, readLedger, writeLedger } from "./objects.js";
|
|
25
25
|
import { lookupSpecsFromOverlay } from "./overlay.js";
|
|
26
26
|
import { LookupExhausted, orderResolutions, parseResolveFlag, resolveOne } from "./resolve.js";
|
|
@@ -87,8 +87,10 @@ export function findResolution(existing, nodeType, field, by, value) {
|
|
|
87
87
|
export class ResolutionsExhausted extends LookupExhausted {
|
|
88
88
|
first;
|
|
89
89
|
plan;
|
|
90
|
-
constructor(first, plan
|
|
91
|
-
|
|
90
|
+
constructor(first, plan,
|
|
91
|
+
/** Every miss of the run, when there was more to say than `first`'s own message. */
|
|
92
|
+
message = first.message) {
|
|
93
|
+
super(message, first);
|
|
92
94
|
this.first = first;
|
|
93
95
|
this.plan = plan;
|
|
94
96
|
this.name = "ResolutionsExhausted";
|
|
@@ -117,13 +119,29 @@ export async function writeResolutions(run, specs) {
|
|
|
117
119
|
const recorded = pyGet(ledger, "resolutions", null);
|
|
118
120
|
const existing = isPlainObject(recorded) ? { ...recorded } : {};
|
|
119
121
|
const resolved = new Map();
|
|
122
|
+
// An exhaustive miss no longer ends the run: the rest are still tried, so ONE
|
|
123
|
+
// run reports every value this connection does not hold — a multiselect of
|
|
124
|
+
// three names used to cost three runs to learn about three misses. A lookup
|
|
125
|
+
// addressed through a field that missed is skipped, since its path cannot be
|
|
126
|
+
// built. The WRITE stays all-or-nothing; see the throw below.
|
|
127
|
+
const misses = [];
|
|
128
|
+
const unresolved = new Set();
|
|
129
|
+
const notTried = [];
|
|
130
|
+
const answered = [];
|
|
120
131
|
for (const [field, by, value] of ordered) {
|
|
132
|
+
const blockedBy = (table.get(field)?.dependsOn ?? []).find((prerequisite) => unresolved.has(prerequisite));
|
|
133
|
+
if (blockedBy !== undefined) {
|
|
134
|
+
unresolved.add(field);
|
|
135
|
+
notTried.push(notTriedLine(field, by, value, blockedBy));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
121
138
|
const key = resolutionKey(run.nodeType, field, by, value);
|
|
122
139
|
const found = findResolution(existing, run.nodeType, field, by, value);
|
|
123
140
|
if (found && !run.refreshLookups) {
|
|
124
141
|
const kept = pyStrAny(pyGet(found.row, "value", null));
|
|
125
142
|
resolved.set(field, kept);
|
|
126
143
|
run.reporter.info(resolutionLine(field, by, value, kept, true));
|
|
144
|
+
answered.push(resolutionLine(field, by, value, kept, true));
|
|
127
145
|
continue;
|
|
128
146
|
}
|
|
129
147
|
let got;
|
|
@@ -152,8 +170,25 @@ export async function writeResolutions(run, specs) {
|
|
|
152
170
|
// refusal, whose text already names the remedy ("the value may be
|
|
153
171
|
// further in"). Upstream's own tests never covered the row, which is
|
|
154
172
|
// how it shipped unimplemented.
|
|
155
|
-
|
|
156
|
-
|
|
173
|
+
//
|
|
174
|
+
// Only that exhaustive miss is collected; every other refusal — capped,
|
|
175
|
+
// transport, unsearchable `by` — still ends the run at once, as before,
|
|
176
|
+
// so none of them can reach the cross-connection retry.
|
|
177
|
+
if (error instanceof LookupExhausted && !error.capped) {
|
|
178
|
+
misses.push(error);
|
|
179
|
+
unresolved.add(field);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// An exhaustive miss collected EARLIER still decides the run: it is
|
|
183
|
+
// evidence against this connection, and before the misses were collected
|
|
184
|
+
// it would have ended the run and started the cross-connection retry
|
|
185
|
+
// before this lookup was ever tried. So that retry still runs, and its
|
|
186
|
+
// refusal names the collected misses and then this error.
|
|
187
|
+
if (misses.length > 0) {
|
|
188
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
189
|
+
const lines = [...misses.map((miss) => miss.message), ...notTried, text.startsWith(`${field}:`) ? text : `${field}: ${text}`];
|
|
190
|
+
throw new ResolutionsExhausted(misses[0], { specs: table, ordered }, incompleteRunMessage(lines, [], answered, ordered.length));
|
|
191
|
+
}
|
|
157
192
|
throw error;
|
|
158
193
|
}
|
|
159
194
|
resolved.set(field, got);
|
|
@@ -163,6 +198,21 @@ export async function writeResolutions(run, specs) {
|
|
|
163
198
|
delete existing[found.key];
|
|
164
199
|
existing[key] = { nodeType: run.nodeType, field, by, match: value, value: got };
|
|
165
200
|
run.reporter.info(resolutionLine(field, by, value, got));
|
|
201
|
+
answered.push(resolutionLine(field, by, value, got));
|
|
202
|
+
}
|
|
203
|
+
// ALL-OR-NOTHING, deliberately, even though the rows that DID resolve are
|
|
204
|
+
// correct for this connection. A ledger row carries no connection: it is
|
|
205
|
+
// keyed `[nodeType, field, by, value]`, and a later run REUSES a recorded row
|
|
206
|
+
// without asking again unless `--refresh-lookups` is passed (above). So a
|
|
207
|
+
// partial write from connection A would hand A's workspace ids to the next
|
|
208
|
+
// run bound to connection B — the 2026-09-23 run resolved one recipient on
|
|
209
|
+
// one Slack workspace and then re-pinned to another. Until rows are keyed by
|
|
210
|
+
// connection, a run that misses records nothing and says what it found.
|
|
211
|
+
if (misses.length > 0) {
|
|
212
|
+
const message = misses.length === 1 && notTried.length === 0 && answered.length === 0
|
|
213
|
+
? misses[0].message
|
|
214
|
+
: incompleteRunMessage(misses.map((miss) => miss.message), notTried, answered, ordered.length);
|
|
215
|
+
throw new ResolutionsExhausted(misses[0], { specs: table, ordered }, message);
|
|
166
216
|
}
|
|
167
217
|
const written = { ...ledger, schemaVersion: "1", resolutions: existing };
|
|
168
218
|
writeLedger(path, written);
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* `core/cli-spelling.ts`) without this module reading the environment.
|
|
22
22
|
*/
|
|
23
23
|
import { Refusal } from "../errors.js";
|
|
24
|
+
import { type RegistryCatalog } from "./actions.js";
|
|
24
25
|
export declare const PREFIX = "prepare-connector: ";
|
|
25
26
|
/** The spelling the Python hard-coded into every pasteable re-run. */
|
|
26
27
|
export declare const PREPARE_COMMAND = "npx flow-sdk registry prepare";
|
|
@@ -45,16 +46,43 @@ export declare function connectionLine(row: Record<string, unknown>, connectionI
|
|
|
45
46
|
export declare function bindingsLine(symbolicName: string, bindingsPath: string): string;
|
|
46
47
|
/** `_missing_generic_schema_message`: a Generic action that expanded to nothing. */
|
|
47
48
|
export declare function missingGenericSchemaMessage(operation: string, objectName?: string | null): string;
|
|
49
|
+
/** One `registry get` probe that missed, as a refusal quotes it. */
|
|
50
|
+
export interface ProbeMiss {
|
|
51
|
+
nodeType: string;
|
|
52
|
+
/** The CLI's reason, never empty (`failureReason`). */
|
|
53
|
+
reason: string;
|
|
54
|
+
}
|
|
48
55
|
/**
|
|
49
|
-
*
|
|
50
|
-
* registry
|
|
51
|
-
*
|
|
56
|
+
* What to do when `<key> <name>` is neither an action nor an event: both
|
|
57
|
+
* `registry get` probes answered "Node not found". Pure over its inputs.
|
|
58
|
+
*
|
|
59
|
+
* It names both probes, because the author's name may have meant either
|
|
60
|
+
* namespace and the SDK cannot know which (so the code is OPERATION_UNKNOWN,
|
|
61
|
+
* not ACTION_UNKNOWN), and it lists both catalogs `registry search <key>`
|
|
62
|
+
* served. Until 2026-09 this was an action-only message: it named the action
|
|
63
|
+
* node type, said "Re-run with a registry action id" and listed only actions,
|
|
64
|
+
* because `registryActionIds` removes event ids by design. For
|
|
65
|
+
* `uipath-uipath-dataservice record-created` — an EVENT the catalog had stopped
|
|
66
|
+
* serving — that sent the author after action ids (2026-09-23 eval,
|
|
67
|
+
* `skill-flow-datafabric-trigger-lifecycle`, v2 command 33).
|
|
52
68
|
*
|
|
53
69
|
* NOT auto-substituted, deliberately: the baked action id and the registry key
|
|
54
70
|
* are different namespaces, and guessing between them would emit a node both
|
|
55
71
|
* offline validators accept and the runtime dispatches to the wrong operation.
|
|
56
72
|
*/
|
|
57
|
-
export declare function
|
|
73
|
+
export declare function unresolvedOperationMessage(input: {
|
|
74
|
+
connectorKey: string;
|
|
75
|
+
name: string;
|
|
76
|
+
probes: readonly ProbeMiss[];
|
|
77
|
+
catalog: RegistryCatalog;
|
|
78
|
+
}): string;
|
|
79
|
+
/**
|
|
80
|
+
* A `registry get` probe failed for a reason other than "Node not found" — an
|
|
81
|
+
* error inside the CLI, a refusal, a timeout, a reply that is not JSON. That is
|
|
82
|
+
* not evidence the node is absent, so this refusal says so and carries the
|
|
83
|
+
* CLI's own reasons instead of listing alternatives.
|
|
84
|
+
*/
|
|
85
|
+
export declare function registryGetFailedMessage(connectorKey: string, name: string, probes: readonly ProbeMiss[]): string;
|
|
58
86
|
export declare function noCanonicalEntryMessage(nodeType: string): string;
|
|
59
87
|
/** The `-f` names do not match any api-type ObjectAction; `specs` are the actions' parent lists. */
|
|
60
88
|
export declare function parentFieldNamesMismatchMessage(given: readonly string[], specs: ReadonlyArray<readonly [string, readonly string[]]>): string;
|
|
@@ -111,6 +139,14 @@ export declare function lookupVocabularyLines(connectorKey: string, action: stri
|
|
|
111
139
|
export declare function unknownLookupMessage(unknown: readonly string[], nodeType: string, known: readonly string[]): string;
|
|
112
140
|
/** ` <field>: <by>=<value> -> <id>`, with ` (already resolved)` when the ledger answered. */
|
|
113
141
|
export declare function resolutionLine(field: string, by: string, value: string, got: unknown, alreadyResolved?: boolean): string;
|
|
142
|
+
/** A lookup skipped because the field its collection path is built from did not resolve. */
|
|
143
|
+
export declare function notTriedLine(field: string, by: string, value: string, prerequisite: string): string;
|
|
144
|
+
/**
|
|
145
|
+
* Every miss of one `--resolve` run, then what DID resolve. Nothing is recorded
|
|
146
|
+
* when anything misses, and the reader needs both halves to fix every value in
|
|
147
|
+
* one pass rather than one per run.
|
|
148
|
+
*/
|
|
149
|
+
export declare function incompleteRunMessage(misses: readonly string[], notTried: readonly string[], answered: readonly string[], requested: number): string;
|
|
114
150
|
export declare function wroteResolutionsLine(count: number, ledgerPath: string): string;
|
|
115
151
|
/**
|
|
116
152
|
* `generated N connector module(s) → <out>` — the descriptor generator's own
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* `core/cli-spelling.ts`) without this module reading the environment.
|
|
22
22
|
*/
|
|
23
23
|
import { Refusal, messageOf } from "../errors.js";
|
|
24
|
-
import {
|
|
24
|
+
import { plausibleMatches } from "./actions.js";
|
|
25
25
|
import { pyGet, pyOr, pyRepr, pyStrAny, pyTruthy } from "./support.js";
|
|
26
26
|
export const PREFIX = "prepare-connector: ";
|
|
27
27
|
/** The spelling the Python hard-coded into every pasteable re-run. */
|
|
@@ -74,33 +74,98 @@ export function missingGenericSchemaMessage(operation, objectName) {
|
|
|
74
74
|
"not need connection-specific fields, use the baked connector descriptor " +
|
|
75
75
|
"without custom fields instead.");
|
|
76
76
|
}
|
|
77
|
+
function probeLines(probes) {
|
|
78
|
+
return probes.map((probe) => ` registry get ${probe.nodeType} → ${probe.reason}`);
|
|
79
|
+
}
|
|
80
|
+
/** `record-created (trigger, event)`: an event id with the forms the registry served it in. */
|
|
81
|
+
function eventLabel(event) {
|
|
82
|
+
return `${event.id} (${event.forms.join(", ")})`;
|
|
83
|
+
}
|
|
84
|
+
/** How many of `wanted`'s hyphen tokens `candidate` shares. */
|
|
85
|
+
function sharedTokens(wanted, candidate) {
|
|
86
|
+
const tokens = new Set(candidate.split("-"));
|
|
87
|
+
return [...new Set(wanted.split("-"))].filter((token) => tokens.has(token)).length;
|
|
88
|
+
}
|
|
77
89
|
/**
|
|
78
|
-
*
|
|
79
|
-
* registry
|
|
80
|
-
*
|
|
90
|
+
* What to do when `<key> <name>` is neither an action nor an event: both
|
|
91
|
+
* `registry get` probes answered "Node not found". Pure over its inputs.
|
|
92
|
+
*
|
|
93
|
+
* It names both probes, because the author's name may have meant either
|
|
94
|
+
* namespace and the SDK cannot know which (so the code is OPERATION_UNKNOWN,
|
|
95
|
+
* not ACTION_UNKNOWN), and it lists both catalogs `registry search <key>`
|
|
96
|
+
* served. Until 2026-09 this was an action-only message: it named the action
|
|
97
|
+
* node type, said "Re-run with a registry action id" and listed only actions,
|
|
98
|
+
* because `registryActionIds` removes event ids by design. For
|
|
99
|
+
* `uipath-uipath-dataservice record-created` — an EVENT the catalog had stopped
|
|
100
|
+
* serving — that sent the author after action ids (2026-09-23 eval,
|
|
101
|
+
* `skill-flow-datafabric-trigger-lifecycle`, v2 command 33).
|
|
81
102
|
*
|
|
82
103
|
* NOT auto-substituted, deliberately: the baked action id and the registry key
|
|
83
104
|
* are different namespaces, and guessing between them would emit a node both
|
|
84
105
|
* offline validators accept and the runtime dispatches to the wrong operation.
|
|
85
106
|
*/
|
|
86
|
-
export function
|
|
107
|
+
export function unresolvedOperationMessage(input) {
|
|
108
|
+
const { connectorKey: key, name, probes, catalog } = input;
|
|
87
109
|
const lines = [
|
|
88
|
-
`${PREFIX}
|
|
110
|
+
`${PREFIX}\`${key} ${name}\` is neither an action nor an event the registry serves:`,
|
|
111
|
+
...probeLines(probes),
|
|
89
112
|
"",
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
113
|
+
// flow-v1 `packages/flow-core/src/manifest/fetchers.ts:286`, `:336-337`: the
|
|
114
|
+
// node catalog is the typecache Search/Catalog, and a trigger node exists
|
|
115
|
+
// only where it serves one.
|
|
116
|
+
"A Flow node exists only if the tenant's Flow node catalog serves it. An event that",
|
|
117
|
+
"`uip is triggers objects` lists but this registry does not cannot be prepared or",
|
|
118
|
+
"compiled, and hand-writing its node JSON does not make it offered.",
|
|
93
119
|
];
|
|
94
|
-
if (
|
|
95
|
-
lines.push("", `\`registry search ${
|
|
120
|
+
if (catalog.failure !== undefined) {
|
|
121
|
+
lines.push("", `\`registry search ${key}\` failed: ${catalog.failure}`, "so what the registry does serve for it could not be listed.");
|
|
96
122
|
return lines.join("\n");
|
|
97
123
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
if (catalog.events.length > 0) {
|
|
125
|
+
const close = plausibleMatches(name, catalog.events.map((event) => event.id));
|
|
126
|
+
const byId = new Map(catalog.events.map((event) => [event.id, event]));
|
|
127
|
+
if (close.length > 0)
|
|
128
|
+
lines.push("", `Closest event to "${name}": ${close.slice(0, 3).map((id) => eventLabel(byId.get(id))).join(", ")}`);
|
|
129
|
+
lines.push("", `Events the registry serves for ${key}:`, ...catalog.events.map((event) => ` ${eventLabel(event)}`));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
lines.push("", `\`registry search ${key}\` lists no \`uipath.connector.{trigger,event}.${key}.*\` node: the`, `registry serves no event for ${key}.`);
|
|
133
|
+
}
|
|
134
|
+
if (catalog.actions.length > 0) {
|
|
135
|
+
const close = plausibleMatches(name, catalog.actions);
|
|
136
|
+
if (close.length > 0)
|
|
137
|
+
lines.push("", `Closest action to "${name}": ${close.slice(0, 3).join(", ")}`);
|
|
138
|
+
// The Generic explanation only where the name reads like one of these
|
|
139
|
+
// actions (most of its words shared): `list-all-records` against
|
|
140
|
+
// `list-records`, not `record-created` against `create-entity-record`,
|
|
141
|
+
// where it would point an event request at action ids again.
|
|
142
|
+
const wanted = new Set(name.split("-")).size;
|
|
143
|
+
if (close.length > 0 && sharedTokens(name, close[0]) * 2 > wanted) {
|
|
144
|
+
lines.push("The baked library's action id is not always the registry's key — a Generic", "op is `list-all-records` in the library and materializes per object, while", "the registry serves the un-expanded form. For an action, re-run with a registry action id.");
|
|
145
|
+
}
|
|
146
|
+
lines.push("", `All actions the registry serves for ${key}:`, ...catalog.actions.map((a) => ` ${a}`));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
lines.push("", `\`registry search ${key}\` lists no action for ${key} either.`);
|
|
150
|
+
}
|
|
151
|
+
lines.push("", "`registry get` and `registry search` answer from the CLI's local copy of the catalog.", "If it looks stale, refresh it first: `uip maestro flow registry pull --force`.");
|
|
102
152
|
return lines.join("\n");
|
|
103
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* A `registry get` probe failed for a reason other than "Node not found" — an
|
|
156
|
+
* error inside the CLI, a refusal, a timeout, a reply that is not JSON. That is
|
|
157
|
+
* not evidence the node is absent, so this refusal says so and carries the
|
|
158
|
+
* CLI's own reasons instead of listing alternatives.
|
|
159
|
+
*/
|
|
160
|
+
export function registryGetFailedMessage(connectorKey, name, probes) {
|
|
161
|
+
return [
|
|
162
|
+
`${PREFIX}could not tell whether \`${connectorKey} ${name}\` is an action or an event: a registry`,
|
|
163
|
+
"lookup failed, which does not mean the node is absent.",
|
|
164
|
+
...probeLines(probes),
|
|
165
|
+
"",
|
|
166
|
+
"Resolve the failure above and re-run prepare.",
|
|
167
|
+
].join("\n");
|
|
168
|
+
}
|
|
104
169
|
export function noCanonicalEntryMessage(nodeType) {
|
|
105
170
|
return `${PREFIX}no canonical entry for ${nodeType} (unexpected registry shape).`;
|
|
106
171
|
}
|
|
@@ -262,6 +327,23 @@ export function unknownLookupMessage(unknown, nodeType, known) {
|
|
|
262
327
|
export function resolutionLine(field, by, value, got, alreadyResolved = false) {
|
|
263
328
|
return ` ${field}: ${by}=${value} -> ${pyStrAny(got)}${alreadyResolved ? " (already resolved)" : ""}`;
|
|
264
329
|
}
|
|
330
|
+
/** A lookup skipped because the field its collection path is built from did not resolve. */
|
|
331
|
+
export function notTriedLine(field, by, value, prerequisite) {
|
|
332
|
+
return `${field}: ${by}=${pyRepr(value)} not tried — its collection is addressed through ${pyRepr(prerequisite)}, which did not resolve.`;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Every miss of one `--resolve` run, then what DID resolve. Nothing is recorded
|
|
336
|
+
* when anything misses, and the reader needs both halves to fix every value in
|
|
337
|
+
* one pass rather than one per run.
|
|
338
|
+
*/
|
|
339
|
+
export function incompleteRunMessage(misses, notTried, answered, requested) {
|
|
340
|
+
const failed = misses.length + notTried.length;
|
|
341
|
+
return (`${failed} of ${requested} lookup(s) did not resolve, so nothing was recorded:\n` +
|
|
342
|
+
[...misses, ...notTried].map((line) => ` ${line}`).join("\n") +
|
|
343
|
+
(answered.length > 0
|
|
344
|
+
? `\nThese resolved, and are recorded only once every lookup of the run does:\n${answered.join("\n")}`
|
|
345
|
+
: ""));
|
|
346
|
+
}
|
|
265
347
|
export function wroteResolutionsLine(count, ledgerPath) {
|
|
266
348
|
return ` wrote ${count} resolution(s) to ${ledgerPath}`;
|
|
267
349
|
}
|