@uipath/maestro-builder-sdk 6.10.2 → 6.11.0
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/cli/commands/registry.js +2 -0
- package/dist/registry/library-build/enrich.js +13 -1
- package/dist/registry/prepare/connections.d.ts +62 -1
- package/dist/registry/prepare/connections.js +132 -7
- package/dist/registry/prepare/messages.d.ts +5 -0
- package/dist/registry/prepare/messages.js +10 -0
- package/dist/registry/prepare/overlay.d.ts +2 -0
- package/dist/registry/prepare/overlay.js +3 -0
- package/dist/registry/prepare/pipeline.d.ts +6 -0
- package/dist/registry/prepare/pipeline.js +15 -4
- package/dist/registry/prepare/resolve.d.ts +19 -0
- package/dist/registry/prepare/resolve.js +141 -0
- package/dist/registry/prepare-cli.d.ts +2 -0
- package/dist/registry/prepare-cli.js +5 -1
- package/package.json +2 -2
|
@@ -130,6 +130,7 @@ export function registerRegistryCommands(program) {
|
|
|
130
130
|
// `uip is connections list` and writes both keys into bindings.json.
|
|
131
131
|
.option('--connection-id <id>', 'connection to read the schema through')
|
|
132
132
|
.option('--connection <name>', 'pick the connection by name when several match')
|
|
133
|
+
.option('--connection-folder <folder>', 'pick the connection in the folder the flow runs in (name or key) when several match')
|
|
133
134
|
.option('--bindings <file>', 'bindings file to update (default: ./bindings.json)')
|
|
134
135
|
.option('--bind-connection <name>', 'symbolic connection name (default: the one label the source uses for this connector, else the connector suffix)')
|
|
135
136
|
.option('--bind-folder <name>', 'symbolic folder name (default: the one folder label the source uses, else shared)')
|
|
@@ -170,6 +171,7 @@ export function registerRegistryCommands(program) {
|
|
|
170
171
|
const argv = [connectorKey, action];
|
|
171
172
|
addValue(argv, '--connection-id', options.connectionId);
|
|
172
173
|
addValue(argv, '--connection', options.connection);
|
|
174
|
+
addValue(argv, '--connection-folder', options.connectionFolder);
|
|
173
175
|
addValue(argv, '--bindings', options.bindings);
|
|
174
176
|
addValue(argv, '--bind-connection', options.bindConnection);
|
|
175
177
|
addValue(argv, '--bind-folder', options.bindFolder);
|
|
@@ -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
|
|
@@ -53,7 +53,68 @@ export declare function selectConnection(rows: readonly ConnectionRow[], connect
|
|
|
53
53
|
* connection where the tenant-wide view is many. Widening first would turn an
|
|
54
54
|
* unambiguous case into an ambiguous one.
|
|
55
55
|
*/
|
|
56
|
-
export declare function discoverConnection(client: UipClient, connectorKey: string, name?: string): Promise<ConnectionRow>;
|
|
56
|
+
export declare function discoverConnection(client: UipClient, connectorKey: string, name?: string, prefer?: ConnectionPreference): Promise<ConnectionRow>;
|
|
57
|
+
/**
|
|
58
|
+
* What settles a choice among several enabled connections for one connector,
|
|
59
|
+
* consulted in this order, each only when the one before it did not decide.
|
|
60
|
+
*
|
|
61
|
+
* The eval tenant holds `is-sandboxes` (Slack), `is.sandboxes.test@gmail.com`
|
|
62
|
+
* (Drive) and the Outlook connections twice each, once in the
|
|
63
|
+
* `uipath-maestro-flow` folder and once in `uipath-maestro-case`. A name
|
|
64
|
+
* cannot tell them apart, and a lookup cannot either when both reach the same
|
|
65
|
+
* workspace — so every connector task paid one refused call and a re-run with
|
|
66
|
+
* `--connection-id` (flow-builder-sdk#818: 13 refusals in one run). What does
|
|
67
|
+
* tell them apart is WHERE the flow runs, and there are two honest sources for
|
|
68
|
+
* that: the author saying so, and the solution having already recorded it.
|
|
69
|
+
*/
|
|
70
|
+
export interface ConnectionPreference {
|
|
71
|
+
/**
|
|
72
|
+
* `--connection-folder`: the folder the flow runs in, by name
|
|
73
|
+
* (case-insensitive, as Orchestrator folder names are) or by folder key.
|
|
74
|
+
*/
|
|
75
|
+
folder?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Connection ids the solution already records for this connector —
|
|
78
|
+
* `resources/<folder>/connection/<connectorKey>/*.json`, whose `key` is the
|
|
79
|
+
* connection id `uip solution resources refresh` bound. See
|
|
80
|
+
* {@link solutionConnectionIds}.
|
|
81
|
+
*/
|
|
82
|
+
solutionIds?: ReadonlySet<string>;
|
|
83
|
+
}
|
|
84
|
+
/** How {@link chooseConnection} settled on its row, for the line that reports it. */
|
|
85
|
+
export type ConnectionChoice = "only" | "folder" | "solution";
|
|
86
|
+
/** True when `row` is in `folder`: its `Folder` name ignoring case, or its `FolderKey` exactly. */
|
|
87
|
+
export declare function inFolder(row: ConnectionRow, folder: string): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* {@link discoverConnection} with the reason it chose. Never guesses: each
|
|
90
|
+
* step either narrows to exactly one row or hands the choice on, and the last
|
|
91
|
+
* step is the refusal listing every candidate.
|
|
92
|
+
*
|
|
93
|
+
* 1. `--connection-folder` given: every enabled connection in the tenant (own
|
|
94
|
+
* folder first, as {@link candidateConnections} orders them) in that
|
|
95
|
+
* folder. Exactly one is the answer; none or several is a refusal, because
|
|
96
|
+
* the author named a folder and a silent fallback would ignore them.
|
|
97
|
+
* 2. Otherwise the default discovery — own folder, then the tenant. One row is
|
|
98
|
+
* the answer as before.
|
|
99
|
+
* 3. Several: those the solution already records. Exactly one is the answer.
|
|
100
|
+
* 4. Otherwise the ambiguity refusal, which names both flags.
|
|
101
|
+
*/
|
|
102
|
+
export declare function chooseConnection(client: UipClient, connectorKey: string, name?: string, prefer?: ConnectionPreference): Promise<{
|
|
103
|
+
row: ConnectionRow;
|
|
104
|
+
by: ConnectionChoice;
|
|
105
|
+
}>;
|
|
106
|
+
/**
|
|
107
|
+
* The connection ids a solution records for `connectorKey`: the `key` of every
|
|
108
|
+
* `resources/<folder>/connection/<connectorKey>/*.json` under each solution
|
|
109
|
+
* (a directory holding a `.uipx`) at `cwd`, above it, or one level below it —
|
|
110
|
+
* the flow-sdk layout keeps the solution as a child of the working directory.
|
|
111
|
+
*
|
|
112
|
+
* `uip solution resources refresh` writes these, and a seeded or re-edited
|
|
113
|
+
* solution carries them, so they are the tenant's own record of which
|
|
114
|
+
* connection this solution deploys with. A file that will not parse, or whose
|
|
115
|
+
* `type` names another connector, contributes nothing.
|
|
116
|
+
*/
|
|
117
|
+
export declare function solutionConnectionIds(cwd: string, connectorKey: string): Set<string>;
|
|
57
118
|
/**
|
|
58
119
|
* Every enabled connection for `connectorKey`, own folder FIRST
|
|
59
120
|
* (`_connections.candidate_connections`).
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* a {@link UipClient} and hand the rows to them. Every refusal's text is kept
|
|
22
22
|
* verbatim — agents read these messages and follow them.
|
|
23
23
|
*/
|
|
24
|
-
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { dirname, join, resolve as resolvePath } from "node:path";
|
|
25
26
|
import { pyJsonStringify } from "../codecs/pyjson.js";
|
|
26
27
|
import { Refusal, messageOf } from "../errors.js";
|
|
27
28
|
import { isConnectionsList } from "../uip/commands.js";
|
|
@@ -102,10 +103,11 @@ export function selectConnection(rows, connectorKey, name, scope = "the tenant")
|
|
|
102
103
|
const names = new Set(matched.map((row) => pyStrAny(row.Name)));
|
|
103
104
|
// Python tested `name is not None`: an empty-string name takes this branch too.
|
|
104
105
|
const remedy = (name !== undefined && name !== null) || names.size < matched.length
|
|
105
|
-
? "They share a name, so --connection <name> cannot pick one;
|
|
106
|
-
"the
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
? "They share a name, so --connection <name> cannot pick one; name " +
|
|
107
|
+
"the folder the flow runs in with --connection-folder <folder>, or " +
|
|
108
|
+
"pass the id of the one you mean with --connection-id:"
|
|
109
|
+
: "Name the one you want with --connection <name>, name its folder " +
|
|
110
|
+
"with --connection-folder <folder>, or pass its id with --connection-id:";
|
|
109
111
|
throw new ConnectionRefusal(`${matched.length} enabled ${connectorKey} connections in ${scope}, and they ` +
|
|
110
112
|
"are not interchangeable — a record that exists in one workspace need " +
|
|
111
113
|
`not exist in another. ${remedy}\n${listed}`, "CONNECTION_AMBIGUOUS");
|
|
@@ -121,14 +123,137 @@ export function selectConnection(rows, connectorKey, name, scope = "the tenant")
|
|
|
121
123
|
* connection where the tenant-wide view is many. Widening first would turn an
|
|
122
124
|
* unambiguous case into an ambiguous one.
|
|
123
125
|
*/
|
|
124
|
-
export async function discoverConnection(client, connectorKey, name) {
|
|
126
|
+
export async function discoverConnection(client, connectorKey, name, prefer = {}) {
|
|
127
|
+
return (await chooseConnection(client, connectorKey, name, prefer)).row;
|
|
128
|
+
}
|
|
129
|
+
/** True when `row` is in `folder`: its `Folder` name ignoring case, or its `FolderKey` exactly. */
|
|
130
|
+
export function inFolder(row, folder) {
|
|
131
|
+
const wanted = folder.trim();
|
|
132
|
+
if (!wanted)
|
|
133
|
+
return false;
|
|
134
|
+
return (pyStrAny(pyGet(row, "Folder", "")).toLowerCase() === wanted.toLowerCase() ||
|
|
135
|
+
pyStrAny(pyGet(row, "FolderKey", "")) === wanted);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* {@link discoverConnection} with the reason it chose. Never guesses: each
|
|
139
|
+
* step either narrows to exactly one row or hands the choice on, and the last
|
|
140
|
+
* step is the refusal listing every candidate.
|
|
141
|
+
*
|
|
142
|
+
* 1. `--connection-folder` given: every enabled connection in the tenant (own
|
|
143
|
+
* folder first, as {@link candidateConnections} orders them) in that
|
|
144
|
+
* folder. Exactly one is the answer; none or several is a refusal, because
|
|
145
|
+
* the author named a folder and a silent fallback would ignore them.
|
|
146
|
+
* 2. Otherwise the default discovery — own folder, then the tenant. One row is
|
|
147
|
+
* the answer as before.
|
|
148
|
+
* 3. Several: those the solution already records. Exactly one is the answer.
|
|
149
|
+
* 4. Otherwise the ambiguity refusal, which names both flags.
|
|
150
|
+
*/
|
|
151
|
+
export async function chooseConnection(client, connectorKey, name, prefer = {}) {
|
|
152
|
+
if (prefer.folder !== undefined && prefer.folder.trim() !== "") {
|
|
153
|
+
const folder = prefer.folder.trim();
|
|
154
|
+
const all = await candidateConnections(client, connectorKey, name);
|
|
155
|
+
const there = all.filter((row) => inFolder(row, folder));
|
|
156
|
+
if (there.length === 1)
|
|
157
|
+
return { row: there[0], by: "folder" };
|
|
158
|
+
if (there.length === 0) {
|
|
159
|
+
const qualifier = name ? ` named ${pyRepr(name)}` : "";
|
|
160
|
+
throw new ConnectionRefusal(`no enabled ${connectorKey} connection${qualifier} in folder ${pyRepr(folder)}. ` +
|
|
161
|
+
(all.length > 0
|
|
162
|
+
? `The enabled ones are:\n${candidateLines(all)}`
|
|
163
|
+
: "List what exists with:\n uip is connections list --all-folders"), "CONNECTION_NONE_IN_FOLDER");
|
|
164
|
+
}
|
|
165
|
+
return { row: selectConnection(there, connectorKey, name, "the tenant"), by: "folder" };
|
|
166
|
+
}
|
|
125
167
|
let rows = enabledConnections(await listConnections(client, { allFolders: false }), connectorKey, name);
|
|
126
168
|
let scope = "your folder";
|
|
127
169
|
if (rows.length === 0) {
|
|
128
170
|
rows = enabledConnections(await listConnections(client, { allFolders: true }), connectorKey, name);
|
|
129
171
|
scope = "the tenant";
|
|
130
172
|
}
|
|
131
|
-
|
|
173
|
+
if (rows.length > 1 && prefer.solutionIds !== undefined && prefer.solutionIds.size > 0) {
|
|
174
|
+
const recorded = rows.filter((row) => prefer.solutionIds.has(pyStrAny(pyGet(row, "Id", ""))));
|
|
175
|
+
if (recorded.length === 1)
|
|
176
|
+
return { row: recorded[0], by: "solution" };
|
|
177
|
+
}
|
|
178
|
+
return { row: selectConnection(rows, connectorKey, name, scope), by: "only" };
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The connection ids a solution records for `connectorKey`: the `key` of every
|
|
182
|
+
* `resources/<folder>/connection/<connectorKey>/*.json` under each solution
|
|
183
|
+
* (a directory holding a `.uipx`) at `cwd`, above it, or one level below it —
|
|
184
|
+
* the flow-sdk layout keeps the solution as a child of the working directory.
|
|
185
|
+
*
|
|
186
|
+
* `uip solution resources refresh` writes these, and a seeded or re-edited
|
|
187
|
+
* solution carries them, so they are the tenant's own record of which
|
|
188
|
+
* connection this solution deploys with. A file that will not parse, or whose
|
|
189
|
+
* `type` names another connector, contributes nothing.
|
|
190
|
+
*/
|
|
191
|
+
export function solutionConnectionIds(cwd, connectorKey) {
|
|
192
|
+
const ids = new Set();
|
|
193
|
+
const solutions = new Set();
|
|
194
|
+
const hasUipx = (dir) => {
|
|
195
|
+
try {
|
|
196
|
+
return readdirSync(dir).some((entry) => entry.endsWith(".uipx"));
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
let dir = resolvePath(cwd);
|
|
203
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
204
|
+
if (hasUipx(dir))
|
|
205
|
+
solutions.add(dir);
|
|
206
|
+
const parent = dirname(dir);
|
|
207
|
+
if (parent === dir)
|
|
208
|
+
break;
|
|
209
|
+
dir = parent;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
for (const entry of readdirSync(cwd, { withFileTypes: true })) {
|
|
213
|
+
if (entry.isDirectory() && hasUipx(join(cwd, entry.name)))
|
|
214
|
+
solutions.add(resolvePath(cwd, entry.name));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// An unreadable cwd records nothing.
|
|
219
|
+
}
|
|
220
|
+
for (const solution of solutions) {
|
|
221
|
+
const resources = join(solution, "resources");
|
|
222
|
+
let folders;
|
|
223
|
+
try {
|
|
224
|
+
folders = readdirSync(resources);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
for (const folder of folders) {
|
|
230
|
+
const connectorDir = join(resources, folder, "connection", connectorKey);
|
|
231
|
+
let files;
|
|
232
|
+
try {
|
|
233
|
+
files = readdirSync(connectorDir).filter((file) => file.endsWith(".json"));
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
for (const file of files) {
|
|
239
|
+
try {
|
|
240
|
+
const resource = pyGet(JSON.parse(readFileSync(join(connectorDir, file), "utf8")), "resource", null);
|
|
241
|
+
if (!isPlainObject(resource))
|
|
242
|
+
continue;
|
|
243
|
+
const type = pyGet(resource, "type", null);
|
|
244
|
+
if (pyTruthy(type) && type !== connectorKey)
|
|
245
|
+
continue;
|
|
246
|
+
const key = pyStrAny(pyGet(resource, "key", ""));
|
|
247
|
+
if (key)
|
|
248
|
+
ids.add(key);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// Not JSON: records nothing.
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return ids;
|
|
132
257
|
}
|
|
133
258
|
/**
|
|
134
259
|
* Every enabled connection for `connectorKey`, own folder FIRST
|
|
@@ -42,6 +42,11 @@ export declare class PrepareRefusal extends Refusal {
|
|
|
42
42
|
export declare function exitText(error: unknown): string;
|
|
43
43
|
/** ` connection: <Name> (<id>)` — `found.get('Name')` renders `None` when the row has none, as Python did. */
|
|
44
44
|
export declare function connectionLine(row: Record<string, unknown>, connectionId: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Why discovery chose this connection when several matched: the folder the
|
|
47
|
+
* author named, or the solution's own record of the connection.
|
|
48
|
+
*/
|
|
49
|
+
export declare function connectionChoiceLine(row: Record<string, unknown>, by: "folder" | "solution", folder?: string): string;
|
|
45
50
|
/** ` bindings: <symbolic name> -> <bindings.json>`, once per row this run changed. */
|
|
46
51
|
export declare function bindingsLine(symbolicName: string, bindingsPath: string): string;
|
|
47
52
|
/** ` (the label Demo.flow.ts uses)` — appended to a bindings line whose name came from the source. */
|
|
@@ -58,6 +58,16 @@ export function exitText(error) {
|
|
|
58
58
|
export function connectionLine(row, connectionId) {
|
|
59
59
|
return ` connection: ${pyStrAny(pyGet(row, "Name", null))} (${connectionId})`;
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Why discovery chose this connection when several matched: the folder the
|
|
63
|
+
* author named, or the solution's own record of the connection.
|
|
64
|
+
*/
|
|
65
|
+
export function connectionChoiceLine(row, by, folder) {
|
|
66
|
+
const where = pyStrAny(row.Folder);
|
|
67
|
+
return by === "folder"
|
|
68
|
+
? ` connection: chose the one in folder ${pyRepr(pyOr(folder, where))} (--connection-folder)`
|
|
69
|
+
: ` connection: chose the one the solution records (resources/*/connection/…, folder: ${where})`;
|
|
70
|
+
}
|
|
61
71
|
/** ` bindings: <symbolic name> -> <bindings.json>`, once per row this run changed. */
|
|
62
72
|
export function bindingsLine(symbolicName, bindingsPath) {
|
|
63
73
|
return ` bindings: ${symbolicName} -> ${bindingsPath}`;
|
|
@@ -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;
|
|
@@ -10,6 +10,12 @@ export interface PrepareOptions {
|
|
|
10
10
|
connectionId?: string;
|
|
11
11
|
/** `--connection <name>`: pick the connection by name when several match. */
|
|
12
12
|
connection?: string;
|
|
13
|
+
/**
|
|
14
|
+
* `--connection-folder <folder>`: the folder the flow runs in (name or key).
|
|
15
|
+
* Picks among several enabled connections; without it, the ids the solution
|
|
16
|
+
* already records are tried before the ambiguity refusal.
|
|
17
|
+
*/
|
|
18
|
+
connectionFolder?: string;
|
|
13
19
|
/** `--bindings` (default `./bindings.json`). */
|
|
14
20
|
bindings?: string;
|
|
15
21
|
/**
|
|
@@ -36,11 +36,11 @@ import { expandGenericForConnector, genericTemplatesByOp } from "../library-buil
|
|
|
36
36
|
import { planLibrary } from "../library-build/plan.js";
|
|
37
37
|
import { MemoryResponseCache } from "../uip/response-cache.js";
|
|
38
38
|
import { registryCatalog } from "./actions.js";
|
|
39
|
-
import {
|
|
39
|
+
import { chooseConnection, listConnections, lookupConnection, mergeBindings, restoreBindings, snapshotBindings, solutionConnectionIds, } from "./connections.js";
|
|
40
40
|
import { prepareEvent } from "./event.js";
|
|
41
41
|
import { descriptorUsage } from "./handoff.js";
|
|
42
42
|
import { ResolutionsExhausted, allParentsResolved, resolvedParentValues, writeResolutions } from "./ledger.js";
|
|
43
|
-
import { PREPARE_COMMAND, PrepareRefusal, ambiguousLabelsMessage, bindingsLine, candidateLabel, connectionLine, generatedModulesLine, genericActionNeedsObjectMessage, inputFieldNote, flagDiffersFromSourceLine, keptSchemaMessage, labelTakenMessage, lookupVocabularyLines, missingGenericSchemaMessage, missingParentsNote, nextLine, noCanonicalEntryMessage, objectFlagsOnlyForGenericMessage, objectNoteLine, objectRecordedLine, parentFieldNamesMismatchMessage, parentOnlyMessage, prefixed, preparedSummaryLine, rejectedParentFieldsMessage, rerunAgainstLine, resolvedParentsNote, registryGetFailedMessage, sharedLabelMessage, sourceLabelSuffix, tsNote, partlyUnreadableLabelLine, unreadableLabelLine, unresolvedOperationMessage, } from "./messages.js";
|
|
43
|
+
import { PREPARE_COMMAND, PrepareRefusal, ambiguousLabelsMessage, connectionChoiceLine, bindingsLine, candidateLabel, connectionLine, generatedModulesLine, genericActionNeedsObjectMessage, inputFieldNote, flagDiffersFromSourceLine, keptSchemaMessage, labelTakenMessage, lookupVocabularyLines, missingGenericSchemaMessage, missingParentsNote, nextLine, noCanonicalEntryMessage, objectFlagsOnlyForGenericMessage, objectNoteLine, objectRecordedLine, parentFieldNamesMismatchMessage, parentOnlyMessage, prefixed, preparedSummaryLine, rejectedParentFieldsMessage, rerunAgainstLine, resolvedParentsNote, registryGetFailedMessage, sharedLabelMessage, sourceLabelSuffix, tsNote, partlyUnreadableLabelLine, unreadableLabelLine, unresolvedOperationMessage, } from "./messages.js";
|
|
44
44
|
import { ObjectMatchRefusal, RESOLUTIONS_FILENAME, matchObject, recordObjectResolution } from "./objects.js";
|
|
45
45
|
import { countInputFields, keepPreparedSchema, lookupSpecsFromOverlay, mergeFilesInto, parentFields as overlayParentFields, schemaActionParents, } from "./overlay.js";
|
|
46
46
|
import { declaredParents, objectActionRequest, objectActionSpecs, parseFields } from "./parents.js";
|
|
@@ -195,12 +195,19 @@ async function connect(options, ports) {
|
|
|
195
195
|
const labels = options.noBindings ? undefined : chooseLabels(options, ports);
|
|
196
196
|
let row;
|
|
197
197
|
let connectionId;
|
|
198
|
+
let chosenBy = "only";
|
|
198
199
|
if (!options.connectionId) {
|
|
199
200
|
// Discover the connection when the caller did not name one, and record it
|
|
200
201
|
// in bindings.json so the author never has to look up either key — the
|
|
201
202
|
// step that removes 4-7 tool calls per task.
|
|
202
|
-
|
|
203
|
+
const cwd = ports.cwd ?? process.cwd();
|
|
204
|
+
const choice = await chooseConnection(ports.client, options.connectorKey, options.connection, {
|
|
205
|
+
folder: options.connectionFolder,
|
|
206
|
+
solutionIds: solutionConnectionIds(cwd, options.connectorKey),
|
|
207
|
+
});
|
|
208
|
+
row = choice.row;
|
|
203
209
|
connectionId = pyStrAny(row.Id);
|
|
210
|
+
chosenBy = choice.by;
|
|
204
211
|
}
|
|
205
212
|
else {
|
|
206
213
|
// An explicit id gets the same gate discovery applies: a `State: Failed`
|
|
@@ -209,6 +216,8 @@ async function connect(options, ports) {
|
|
|
209
216
|
row = await lookupConnection(ports.client, options.connectorKey, connectionId);
|
|
210
217
|
}
|
|
211
218
|
info(connectionLine(row, connectionId));
|
|
219
|
+
if (chosenBy !== "only")
|
|
220
|
+
info(connectionChoiceLine(row, chosenBy, options.connectionFolder));
|
|
212
221
|
if (!labels)
|
|
213
222
|
return { connectionId, row };
|
|
214
223
|
// Recorded on BOTH routes. Until 3.30.5 only discovery wrote it, and the
|
|
@@ -251,7 +260,9 @@ export async function prepareConnector(options, ports) {
|
|
|
251
260
|
// Whether the CALLER chose the connection, recorded before discovery answers
|
|
252
261
|
// with its own. An explicit choice is never second-guessed: the retry below
|
|
253
262
|
// is for a connection this command picked, not for one the author named.
|
|
254
|
-
|
|
263
|
+
// A named folder is a choice too: probing the other folders' connections
|
|
264
|
+
// after the author said which folder the flow runs in would overrule them.
|
|
265
|
+
const pinned = pyTruthy(options.connectionId) || pyTruthy(options.connection) || pyTruthy(options.connectionFolder);
|
|
255
266
|
const connected = await connect(options, ports);
|
|
256
267
|
try {
|
|
257
268
|
return await prepareOnConnection(options, ports, connected);
|
|
@@ -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
|
+
}
|
|
@@ -26,6 +26,8 @@ export interface PrepareConnectorOptions {
|
|
|
26
26
|
connectionId?: string;
|
|
27
27
|
/** Pick the connection by name when several match. */
|
|
28
28
|
connection?: string;
|
|
29
|
+
/** `--connection-folder`: the folder the flow runs in, when several connections match. */
|
|
30
|
+
connectionFolder?: string;
|
|
29
31
|
/** bindings file to add the discovered connection to. */
|
|
30
32
|
bindings?: string;
|
|
31
33
|
bindConnection?: string;
|
|
@@ -15,7 +15,8 @@ import { defaultUipClient } from "./uip/client.js";
|
|
|
15
15
|
const DEFAULT_CONNECTOR_IMPORT = PACKAGE_NAME;
|
|
16
16
|
const USAGE = [
|
|
17
17
|
"Usage: flow-sdk registry prepare <connector-key> --action <name> [--connection-id <id>]",
|
|
18
|
-
" [--connection <name>] [--
|
|
18
|
+
" [--connection <name>] [--connection-folder <folder>]",
|
|
19
|
+
" [--bindings <file>] [--no-bindings]",
|
|
19
20
|
" [--object <name> | --all-objects] [-f NAME=VALUE ...]",
|
|
20
21
|
" [--resolve FIELD:BY=VALUE ...] [--refresh-lookups]",
|
|
21
22
|
" [--out <dir>] [--version <v>] [--import <specifier>]",
|
|
@@ -29,6 +30,7 @@ export function pipelineOptions(options) {
|
|
|
29
30
|
action: options.action,
|
|
30
31
|
connectionId: options.connectionId,
|
|
31
32
|
connection: options.connection,
|
|
33
|
+
connectionFolder: options.connectionFolder,
|
|
32
34
|
bindings: options.bindings,
|
|
33
35
|
bindConnection: options.bindConnection,
|
|
34
36
|
bindFolder: options.bindFolder,
|
|
@@ -101,6 +103,7 @@ export function parseArgs(argv) {
|
|
|
101
103
|
for (const name of [
|
|
102
104
|
"--connection-id",
|
|
103
105
|
"--connection",
|
|
106
|
+
"--connection-folder",
|
|
104
107
|
"--bindings",
|
|
105
108
|
"--bind-connection",
|
|
106
109
|
"--bind-folder",
|
|
@@ -147,6 +150,7 @@ export function parseArgs(argv) {
|
|
|
147
150
|
action: positionals[1],
|
|
148
151
|
connectionId: value(argv, "--connection-id"),
|
|
149
152
|
connection: value(argv, "--connection"),
|
|
153
|
+
connectionFolder: value(argv, "--connection-folder"),
|
|
150
154
|
bindings: value(argv, "--bindings"),
|
|
151
155
|
bindConnection: value(argv, "--bind-connection"),
|
|
152
156
|
bindFolder: value(argv, "--bind-folder"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.11.0",
|
|
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": "fa8ffa3eea8369ff520a8214644c971df0c4af74"
|
|
96
96
|
}
|