@uipath/maestro-builder-sdk 6.14.4 → 6.15.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.
Files changed (47) hide show
  1. package/dist/api-index.md +129 -129
  2. package/dist/api-members.md +420 -416
  3. package/dist/bpmn/bpmn-sdk.d.ts +16 -0
  4. package/dist/bpmn/bpmn-sdk.js +2 -0
  5. package/dist/bpmn/check-cli.js +22 -5
  6. package/dist/bpmn/check.d.ts +15 -1
  7. package/dist/bpmn/check.js +69 -2
  8. package/dist/bpmn/compile-cli.js +24 -14
  9. package/dist/bpmn/connector.js +29 -14
  10. package/dist/bpmn/intsvc.js +5 -2
  11. package/dist/bpmn/serialize.d.ts +2 -0
  12. package/dist/bpmn/serialize.js +1 -1
  13. package/dist/case/compile-cli.js +6 -9
  14. package/dist/case/serialize.js +1 -1
  15. package/dist/check-cli.js +6 -35
  16. package/dist/check.d.ts +16 -148
  17. package/dist/check.js +53 -711
  18. package/dist/cli/commands/bpmn.js +15 -0
  19. package/dist/cli-library.d.ts +6 -25
  20. package/dist/cli-library.js +6 -73
  21. package/dist/compile-cli.js +14 -8
  22. package/dist/core/actions.d.ts +19 -0
  23. package/dist/core/cli-library.d.ts +47 -0
  24. package/dist/core/cli-library.js +150 -0
  25. package/dist/core/connector-checks.d.ts +226 -0
  26. package/dist/core/connector-checks.js +518 -0
  27. package/dist/core/connector-inputs.d.ts +80 -0
  28. package/dist/core/connector-inputs.js +453 -0
  29. package/dist/core/event-checks.d.ts +137 -0
  30. package/dist/core/event-checks.js +321 -0
  31. package/dist/decompile.js +1 -1
  32. package/dist/registry/generators/markdown/sections/example.d.ts +7 -1
  33. package/dist/registry/generators/markdown/sections/example.js +70 -5
  34. package/dist/registry/generators/markdown/sections/lookups.js +29 -8
  35. package/dist/registry/prepare/messages.js +6 -1
  36. package/dist/registry/prepare/source-import.d.ts +3 -3
  37. package/dist/registry/prepare/source-import.js +15 -4
  38. package/dist/registry/prepare/source-labels.js +31 -4
  39. package/dist/serialize.d.ts +11 -11
  40. package/dist/serialize.js +285 -428
  41. package/lib/README.md +1 -0
  42. package/lib/library-json.samples/index.json +9 -0
  43. package/lib/library-json.samples/uipath-uipath-airdk/web-search@1.0.0.json +118 -0
  44. package/lib/library-json.samples/uipath-uipath-airdk/web-search@1.0.0.v1def.json +258 -0
  45. package/package.json +2 -2
  46. /package/dist/{event-filters.d.ts → core/event-filters.d.ts} +0 -0
  47. /package/dist/{event-filters.js → core/event-filters.js} +0 -0
package/dist/serialize.js CHANGED
@@ -23,7 +23,23 @@ import { declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
23
23
  import { assertNodeEnvelope } from './core/node-envelope.js';
24
24
  import { buildConfiguration, transportHttpMethod } from './config.js';
25
25
  import { stableId } from './core/stable-id.js';
26
- import { readEventFilter, eventFilterProblem, eventFilterJmes, eventFilterTreeLeaf } from './event-filters.js';
26
+ import { readEventFilter, eventFilterProblem, eventFilterJmes, eventFilterTreeLeaf } from './core/event-filters.js';
27
+ import { FLOW_EXPR_DIALECT } from './core/connector-checks.js';
28
+ import { buildConnectorInputs, connectorInputErrors as coreConnectorInputErrors, multipartBodyFieldName } from './core/connector-inputs.js';
29
+ /**
30
+ * What `compile` would refuse these connector inputs with, or `undefined` when it
31
+ * would accept them — Flow's binding of the shared validator.
32
+ *
33
+ * `render` is not optional here: `renderDeep` is what REFUSES a lookup token in a
34
+ * position no lookup is resolved from (inside an opaque `object` input, say), and
35
+ * dropping it would make `check` quietly accept what `compile` throws on.
36
+ */
37
+ export function connectorInputErrors(rc, inputs, resolutions = {}, lookups = {}) {
38
+ return coreConnectorInputErrors(rc, inputs, resolutions, lookups, {
39
+ dialect: FLOW_EXPR_DIALECT,
40
+ render: (value) => renderDeep(value, NO_RENAME),
41
+ });
42
+ }
27
43
  import { isLookupToken, resolvedValue, unresolvedLookupMessage, } from './core/lookups.js';
28
44
  import { declaredScriptShape, inferScriptShape } from './core/script-return.js';
29
45
  import { migrateToSchemaVersion } from './migrate.js';
@@ -980,8 +996,9 @@ const TOOL_RESOURCE_TYPE = {
980
996
  *
981
997
  * * `ixp`, `clientside` — measured to fail exactly that way (`ixp` leaves the
982
998
  * flow failing validate; `clientside` drops the four inputs its node needs).
983
- * * `connector` — `uip agent refresh` GENERATES it from the flow's connector
984
- * node, reconstructing a whole `properties` block the SDK should not duplicate.
999
+ * * `connector` — has its own writer, {@link connectorToolResourceJson}: its
1000
+ * node carries the configuration in `inputs.detail`, which the strip keeps,
1001
+ * and the file is the one `uip agent refresh` itself derives from it.
985
1002
  * * `mcp`, `a2a` — need the server's `availableTools` and the remote agent's
986
1003
  * card, which are tenant reads.
987
1004
  * * `builtin`, `httpRequest` — need a `properties.toolType` that maps
@@ -1202,6 +1219,225 @@ function connectorToolDef(rc) {
1202
1219
  inputDefaults: {},
1203
1220
  };
1204
1221
  }
1222
+ /** The folder key a connection with no folder binding carries in its resource.json. */
1223
+ const NO_FOLDER_KEY = '00000000-0000-0000-0000-000000000000';
1224
+ /**
1225
+ * The library's operation description without the `(<Connector Name>) ` prefix
1226
+ * the v1def's `display.description` carries — the model reads this text, and
1227
+ * the designer's own resource.json has the bare sentence.
1228
+ */
1229
+ function connectorToolDescription(rc) {
1230
+ const text = String(rc.v1def?.display?.description ?? rc.entry?.display?.description ?? '');
1231
+ const prefix = `(${rc.connectorName}) `;
1232
+ return rc.connectorName && text.startsWith(prefix) ? text.slice(prefix.length) : text;
1233
+ }
1234
+ /** An agent-decided value: `{{prompt: "<what the model is told>"}}`. */
1235
+ function promptChip(f) {
1236
+ const text = f.description || f.displayName || f.name;
1237
+ return `{{prompt: "${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"}}`;
1238
+ }
1239
+ const isPromptChip = (v) => typeof v === 'string' && v.startsWith('{{prompt');
1240
+ /** The text inside `{{prompt: "…"}}`, unescaped; `''` for a bare `{{prompt}}`. */
1241
+ function chipText(value) {
1242
+ const m = /\{\{\s*prompt:\s*"((?:[^"\\]|\\.)*)"/.exec(value);
1243
+ return m ? m[1].replace(/\\(.)/g, '$1') : '';
1244
+ }
1245
+ /** A field's enum as `{ name, value }` rows, the platform's spelling; `undefined` when it has none. */
1246
+ function enumRows(raw) {
1247
+ if (!Array.isArray(raw) || raw.length === 0)
1248
+ return undefined;
1249
+ return raw.map((e) => (typeof e === 'string'
1250
+ ? { name: e, value: e }
1251
+ : { name: String(e?.name ?? e?.value ?? ''), value: String(e?.value ?? e?.name ?? '') }));
1252
+ }
1253
+ /**
1254
+ * The `inputs.detail` of a connector-tool node — the same key set a
1255
+ * `connector()` step's node carries, with the request buckets filled the way
1256
+ * `uip maestro flow node configure` fills them for a `tool.connector.*` node
1257
+ * (uipcli `buildAgentToolParamsFromMetadata`): every BODY field is
1258
+ * agent-decided (`{{prompt: "<description>"}}`) except a single-value enum,
1259
+ * which is its one constant (the model would otherwise guess — `google` for
1260
+ * `GoogleCustomSearch`); a QUERY/PATH parameter with a declared default sends
1261
+ * it, and otherwise is agent-decided too.
1262
+ */
1263
+ function connectorToolDetail(rc, conn, folder, seed) {
1264
+ const params = Array.isArray(rc.entry?.operation?.parameters) ? rc.entry.operation.parameters : [];
1265
+ const buckets = { body: {}, query: {}, path: {} };
1266
+ for (const f of rc.fields ?? []) {
1267
+ const loc = f.location ?? 'body';
1268
+ if (loc === 'body') {
1269
+ const only = enumRows(f.enum);
1270
+ buckets.body[f.name] = only && only.length === 1 ? only[0].value : promptChip(f);
1271
+ }
1272
+ else if (loc === 'query' || loc === 'path') {
1273
+ const dflt = params.find((p) => p?.name === f.name)?.defaultValue;
1274
+ buckets[loc][f.name] = dflt !== undefined && dflt !== null && dflt !== '' ? dflt : promptChip(f);
1275
+ }
1276
+ }
1277
+ const parts = rc.multipartParts ?? [];
1278
+ return {
1279
+ connector: rc.connectorKey,
1280
+ method: transportHttpMethod(rc.httpMethod),
1281
+ endpoint: rc.endpoint,
1282
+ ...(rc.objectName ? { objectName: rc.objectName } : {}),
1283
+ inputMetadata: parts.length
1284
+ ? { type: 'multipart', multipart: { bodyFieldName: multipartBodyFieldName(parts) } }
1285
+ : {},
1286
+ errorState: { hasError: false },
1287
+ telemetryData: { connectorKey: rc.connectorKey, connectorName: rc.connectorName },
1288
+ configuration: buildConfiguration(rc.entry, rc.version, {}, seed),
1289
+ ...(rc.activityTypeId ? { uiPathActivityTypeId: rc.activityTypeId } : {}),
1290
+ ...(conn ? { connectionId: conn, connectionResourceId: conn } : {}),
1291
+ ...(folder ? { connectionFolderKey: folder } : {}),
1292
+ bodyParameters: buckets.body,
1293
+ ...(Object.keys(buckets.path).length ? { pathParameters: buckets.path } : {}),
1294
+ ...(Object.keys(buckets.query).length ? { queryParameters: buckets.query } : {}),
1295
+ ...(parts.length ? { multipartParameters: parts.map((p) => ({ name: p.name, dataType: p.dataType })) } : {}),
1296
+ };
1297
+ }
1298
+ /** Walk (creating) the object chain for a dotted field name; returns the parent schema and the leaf key. */
1299
+ function nestedSlot(root, name) {
1300
+ const segments = name.split('.');
1301
+ let parent = root;
1302
+ for (const seg of segments.slice(0, -1)) {
1303
+ let child = parent.properties[seg];
1304
+ if (!child || child.type !== 'object') {
1305
+ child = { type: 'object', properties: {} };
1306
+ parent.properties[seg] = child;
1307
+ }
1308
+ parent = child;
1309
+ }
1310
+ return { parent, leaf: segments[segments.length - 1] };
1311
+ }
1312
+ /**
1313
+ * The `resources/<id>/resource.json` of a CONNECTOR tool (#867) — the file the
1314
+ * agent runtime registers the tool from, and the one that puts the connection
1315
+ * under the solution's Resources.
1316
+ *
1317
+ * The shape is uipcli's `mapConnectorToolToResource` (the writer behind `uip
1318
+ * agent refresh --inline-in-flow`, itself ported from flow-workbench's
1319
+ * `agent-connector-tool` mapper), in its metadata-enriched form: field types,
1320
+ * enums, `required` and the response fields come from the curated library,
1321
+ * which carries the same declarations that command fetches from Integration
1322
+ * Service. Where that writer and a Studio Web export differ, this follows the
1323
+ * writer: `enum`/`oneOf` only on agent-decided fields (a static field with an
1324
+ * enum breaks the runtime's model build), no `additionalProperties`, and
1325
+ * array-of-object response fields (`results[*]`) left out.
1326
+ *
1327
+ * The values come from the node's own `detail` — the same source refresh reads —
1328
+ * so a later refresh rewrites this file to itself.
1329
+ */
1330
+ function connectorToolResourceJson(ctx, id) {
1331
+ const { rc, detail } = ctx;
1332
+ const valueOf = (raw) => raw !== null && typeof raw === 'object' ? JSON.stringify(raw) : String(raw ?? '');
1333
+ const bucketOf = { body: 'bodyParameters', query: 'queryParameters', path: 'pathParameters' };
1334
+ const fields = [];
1335
+ for (const f of rc.fields ?? []) {
1336
+ const loc = f.location ?? 'body';
1337
+ if (loc === 'multipart')
1338
+ continue;
1339
+ const value = valueOf(detail[bucketOf[loc]]?.[f.name]);
1340
+ fields.push({
1341
+ name: f.name,
1342
+ fieldLocation: loc,
1343
+ type: f.type || 'string',
1344
+ ...(loc === 'body' && enumRows(f.enum)
1345
+ ? { enumValues: enumRows(f.enum) } : {}),
1346
+ required: f.required === true,
1347
+ description: f.description ?? '',
1348
+ ...(f.displayName ? { displayName: f.displayName } : {}),
1349
+ value,
1350
+ isPrompt: isPromptChip(value),
1351
+ });
1352
+ }
1353
+ for (const part of (detail.multipartParameters ?? [])) {
1354
+ fields.push({
1355
+ name: part.name, fieldLocation: 'multipart', type: part.dataType || 'string',
1356
+ required: false, description: '', value: '{{prompt}}', isPrompt: true,
1357
+ });
1358
+ }
1359
+ const inputSchema = { type: 'object', properties: {} };
1360
+ for (const f of fields) {
1361
+ if (f.fieldLocation === 'multipart')
1362
+ continue;
1363
+ const { parent, leaf } = nestedSlot(inputSchema, f.name);
1364
+ const prop = { type: f.type, title: f.displayName ?? leaf, description: f.description };
1365
+ if (f.isPrompt && f.enumValues) {
1366
+ prop.enum = f.enumValues.map((e) => e.value);
1367
+ prop.oneOf = f.enumValues.map((e) => ({ const: e.value, title: e.name }));
1368
+ }
1369
+ parent.properties[leaf] = prop;
1370
+ if (f.required)
1371
+ (parent.required ??= []).push(leaf);
1372
+ }
1373
+ const outputSchema = { type: 'object', properties: {}, definitions: {} };
1374
+ for (const f of (rc.entry?.outputSchema?.fields ?? [])) {
1375
+ if (typeof f?.name !== 'string' || f.name.includes('[*]'))
1376
+ continue;
1377
+ const { parent, leaf } = nestedSlot(outputSchema, f.name);
1378
+ parent.properties[leaf] = {
1379
+ type: f.type || 'string',
1380
+ title: f.displayName ?? leaf,
1381
+ ...(f.description ? { description: f.description } : {}),
1382
+ };
1383
+ }
1384
+ const multipart = detail.inputMetadata?.type === 'multipart';
1385
+ return {
1386
+ $resourceType: 'tool',
1387
+ name: ctx.name,
1388
+ description: ctx.description,
1389
+ location: 'external',
1390
+ type: 'integration',
1391
+ inputSchema,
1392
+ outputSchema,
1393
+ settings: {},
1394
+ properties: {
1395
+ toolPath: rc.endpoint ?? '',
1396
+ objectName: rc.objectName ?? '',
1397
+ toolDisplayName: ctx.name,
1398
+ toolDescription: ctx.description,
1399
+ // The operation KEY (`GETBYID` for a get-by-id), not the transport verb:
1400
+ // the runtime sends it to Integration Service to pick the operation.
1401
+ method: rc.httpMethod,
1402
+ connection: {
1403
+ id: detail.connectionId ?? '',
1404
+ name: rc.connectorName,
1405
+ elementInstanceId: 0,
1406
+ apiBaseUri: '',
1407
+ state: 'enabled',
1408
+ isDefault: false,
1409
+ connector: { key: rc.connectorKey, name: rc.connectorName, image: '' },
1410
+ folder: { key: detail.connectionFolderKey ?? NO_FOLDER_KEY, path: '' },
1411
+ },
1412
+ parameters: fields.map((f, i) => ({
1413
+ name: f.name,
1414
+ displayName: f.displayName ?? f.name,
1415
+ type: f.type,
1416
+ fieldLocation: f.fieldLocation,
1417
+ value: f.value,
1418
+ // uipcli's fallback: an undescribed agent-decided field keeps the chip's own text.
1419
+ description: f.description || (f.isPrompt ? chipText(f.value) : ''),
1420
+ position: f.required ? 'primary' : 'secondary',
1421
+ sortOrder: i + 1,
1422
+ required: f.required,
1423
+ fieldVariant: f.isPrompt ? 'dynamic' : 'static',
1424
+ isCascading: false,
1425
+ dynamic: f.isPrompt,
1426
+ enumValues: f.enumValues ?? null,
1427
+ loadReferenceOptionsByDefault: null,
1428
+ dynamicBehavior: [],
1429
+ reference: null,
1430
+ })),
1431
+ bodyStructure: multipart
1432
+ ? { contentType: 'multipart', jsonBodySection: detail.inputMetadata.multipart?.bodyFieldName ?? 'body' }
1433
+ : { contentType: 'json' },
1434
+ },
1435
+ guardrail: { policies: [] },
1436
+ iconUrl: ctx.iconUrl,
1437
+ id,
1438
+ isEnabled: true,
1439
+ };
1440
+ }
1205
1441
  /**
1206
1442
  * The tool kinds that wrap a DEPLOYED tenant resource, and everything that
1207
1443
  * differs between them. Transcribed from `registry get` over one live example
@@ -4434,12 +4670,16 @@ export function serialize(built, opts = {}) {
4434
4670
  const { entries: connBindings } = connectionResourceBindings({ def: rc.v1def, connectorKey: rc.connectorKey, connectionId: conn, folderKey: folder }, resourceBindings);
4435
4671
  for (const b of connBindings)
4436
4672
  resourceBindings.set(b.id, b);
4437
- const connectorInputs = buildConnectorInputs(rc, spec.inputs, scope.rename, opts.library.lookupResolutions ?? {},
4438
- // Only for the missing-required hint; a test double that is not a full
4439
- // Library simply gets the plain message.
4440
- typeof opts.library.lookupSpecs === 'function'
4441
- ? opts.library.lookupSpecs(spec.key, spec.action, spec.version, spec.object)
4442
- : {});
4673
+ const connectorInputs = buildConnectorInputs(rc, spec.inputs, {
4674
+ dialect: FLOW_EXPR_DIALECT,
4675
+ render: (value) => renderDeep(value, scope.rename),
4676
+ resolutions: opts.library.lookupResolutions ?? {},
4677
+ // Only for the missing-required hint; a test double that is not a full
4678
+ // Library simply gets the plain message.
4679
+ lookups: typeof opts.library.lookupSpecs === 'function'
4680
+ ? opts.library.lookupSpecs(spec.key, spec.action, spec.version, spec.object)
4681
+ : {},
4682
+ });
4443
4683
  const replayInputs = {
4444
4684
  ...(connectorInputs.bodyParameters ?? {}),
4445
4685
  ...(connectorInputs.pathParameters ?? {}),
@@ -4712,6 +4952,7 @@ export function serialize(built, opts = {}) {
4712
4952
  let tdef;
4713
4953
  let label;
4714
4954
  let extraInputs = {};
4955
+ let connectorTool;
4715
4956
  if (tr.kind === 'builtin') {
4716
4957
  tdef = clone(BUILTIN_TOOL_DEFS[tr.tool]);
4717
4958
  if (!tdef)
@@ -4754,10 +4995,33 @@ export function serialize(built, opts = {}) {
4754
4995
  const rc = opts.library.resolve(tr.connector, tr.operation, tr.version, tr.object);
4755
4996
  tdef = connectorToolDef(rc);
4756
4997
  label = tr.name ?? rc.v1def?.display?.label ?? tr.operation;
4757
- // The connector tool's configuration (connection, folder) is bound at
4758
- // the agent/solution layer, not on the node: the deployed corpus
4759
- // carries `inputs: {}` and a top-level `bindings: []` even for a
4760
- // connector tool, so nothing is emitted here beyond the identity.
4998
+ // The connection rides the same three places it does for a
4999
+ // `connector()` step, because the platform reads it from the same
5000
+ // three (#867):
5001
+ // * the node's `inputs.detail` — what `uip maestro flow node configure`
5002
+ // writes on a `tool.connector.*` node, what `uip agent refresh
5003
+ // --inline-in-flow` builds the tool's resource.json FROM, and what
5004
+ // its shell-ify pass keeps ("where a connector tool's request config
5005
+ // lives"). It is also what `bindings_v2.json` is derived from.
5006
+ // * the top-level `bindings[]` pair the def's `model.context`
5007
+ // placeholders (`<bindings.… connection>` / `<bindings.FolderKey>`)
5008
+ // name.
5009
+ // * the agent's `resources/<id>/resource.json` (below).
5010
+ // Emitting none of them left the tool with no connection anywhere.
5011
+ const where = `inline agent "${step.name}"'s connector tool "${tr.connector}/${tr.operation}"`;
5012
+ const target = { key: tr.connector, action: tr.operation };
5013
+ const conn = resolveBinding(opts, tr.connection, 'connection', where, target);
5014
+ const folder = resolveBinding(opts, tr.folder, 'folder', where, target);
5015
+ const { entries: connBindings } = connectionResourceBindings({ def: rc.v1def, connectorKey: rc.connectorKey, connectionId: conn, folderKey: folder }, resourceBindings);
5016
+ for (const b of connBindings)
5017
+ resourceBindings.set(b.id, b);
5018
+ const detail = connectorToolDetail(rc, conn, folder, `${step.name}Tool`);
5019
+ extraInputs = { detail };
5020
+ connectorTool = {
5021
+ rc, detail, name: label,
5022
+ description: tr.description ?? connectorToolDescription(rc),
5023
+ iconUrl: String(tdef.display?.icon ?? ''),
5024
+ };
4761
5025
  }
4762
5026
  else if (tr.kind === 'mcp') {
4763
5027
  // Per-instance def minted like the ixp family's; the node carries the
@@ -4837,6 +5101,10 @@ export function serialize(built, opts = {}) {
4837
5101
  const tid = uid(scope, `${step.name}Tool`);
4838
5102
  const tnode = makeNode(tdef.nodeType, tid, label, { parentId, def: tdef });
4839
5103
  tnode.display = { ...tnode.display, shape: 'circle' };
5104
+ // `uip agent refresh` reads the tool's description off the node's display
5105
+ // first, so the node carries the one the resource.json does.
5106
+ if (connectorTool)
5107
+ tnode.display.description = connectorTool.description;
4840
5108
  // The resource model lives on the DEFINITION only (CX's rule for
4841
5109
  // context, and what the deployed corpus carries for tools).
4842
5110
  delete tnode.model;
@@ -4855,8 +5123,10 @@ export function serialize(built, opts = {}) {
4855
5123
  addEdge(scope, { node: id, port: 'tool' }, tid, 'input');
4856
5124
  // The other half, for the kinds whose file is DERIVABLE — see
4857
5125
  // `toolResourceJson` for which are not, and why each is left to the
4858
- // generator or the designer rather than guessed.
4859
- const toolResource = toolResourceJson(tr, { id: toolResourceId });
5126
+ // designer rather than guessed.
5127
+ const toolResource = connectorTool
5128
+ ? connectorToolResourceJson(connectorTool, toolResourceId)
5129
+ : toolResourceJson(tr, { id: toolResourceId });
4860
5130
  if (toolResource !== undefined) {
4861
5131
  sidecars.push({
4862
5132
  path: `${source}/resources/${toolResourceId}/resource.json`,
@@ -6365,419 +6635,6 @@ export function assertBindingPlaceholdersResolve(flow) {
6365
6635
  + `"invalid GUID <bindings.…>") and refuses to publish. This is a serializer defect, not an authoring `
6366
6636
  + `error; report it with the flow source.`);
6367
6637
  }
6368
- /**
6369
- * Validate a connector's activity inputs against its declared field list and
6370
- * return them ROUTED to the wire slots the platform reads. Catches the three
6371
- * connector-input mistakes — missing required, unknown key, type mismatch — at
6372
- * compile, where the author can act on them.
6373
- *
6374
- * Generated descriptors accept natural nested objects and arrays; the legacy
6375
- * string form also accepts exact flat registry field ids. Routing remains the
6376
- * library's business because a field's location is a property of the operation.
6377
- * Each projected registry field lands in the slot its declaration names:
6378
- *
6379
- * ```jsonc
6380
- * // connector('uipath-atlassian-jira','get-issue', { issueId: …, project: 'IN', issuetype: 'Task' })
6381
- * "inputs": { "detail": {
6382
- * "endpoint": "/curated_get_issue/{issueId}",
6383
- * "pathParameters": { "issueId": "IN-28" },
6384
- * "queryParameters": { "project": "IN", "issuetype": "Task" },
6385
- * "bodyParameters": {} } }
6386
- * ```
6387
- *
6388
- * That is the platform's own encoding, measured on the deployed corpus: 1,283
6389
- * connector nodes in the nightly artifacts, 384 carrying `pathParameters` and
6390
- * 576 carrying `queryParameters`, always as a FLAT map keyed by the parameter's
6391
- * DECLARED name (`send_as`, `issuetype`, `channel_id` keep their spelling — no
6392
- * camelCasing) and always as a SIBLING of `bodyParameters` rather than as a
6393
- * location-tagged entry inside it.
6394
- */
6395
- /** For a missing required lookup field: what it sends and how to resolve it. Empty for a plain field. */
6396
- function missingLookupHint(field, lookup) {
6397
- if (!lookup)
6398
- return '';
6399
- const target = `${lookup.objectName ?? lookup.path}.${lookup.value}`;
6400
- const alias = Object.keys(lookup.aliases)[0];
6401
- const call = alias
6402
- ? `lookup(<Descriptor>, '${field}').${alias}('<value>')`
6403
- : `lookup(<Descriptor>, '${field}').by('${lookup.by[0] ?? 'id'}', '<value>')`;
6404
- return ` — a lookup id (\`${target}\`), not a name or address: ${call}`;
6405
- }
6406
- /**
6407
- * What `compile` would refuse these connector inputs with, or `undefined` when it
6408
- * would accept them — the SAME code path `buildConnectorInputs` runs at emit time,
6409
- * so `check` and `compile` cannot disagree about a missing required field, an
6410
- * unknown or mistyped input, or a container part passed as a value. Unresolved
6411
- * lookups are the one thing skipped: `check` already reports them as
6412
- * LOOKUP_UNRESOLVED with the resolution recipe.
6413
- */
6414
- export function connectorInputErrors(rc, inputs, resolutions = {}, lookups = {}) {
6415
- try {
6416
- buildConnectorInputs(rc, inputs, NO_RENAME, resolutions, lookups, { unresolvedLookups: 'ignore' });
6417
- return undefined;
6418
- }
6419
- catch (e) {
6420
- return e instanceof Error ? e.message : String(e);
6421
- }
6422
- }
6423
- function buildConnectorInputs(rc, inputs, rename = NO_RENAME, resolutions = {}, lookups = {}, policy = {}) {
6424
- const fields = (rc.fields ?? []);
6425
- const byName = new Map(fields.map((f) => [f.name, f]));
6426
- const errs = validateNestedConnectorKeys(fields, inputs, byName);
6427
- const routed = {
6428
- bodyParameters: {}, pathParameters: {}, queryParameters: {},
6429
- };
6430
- // The author's values for FILE parts, by part name. The part LIST itself comes
6431
- // from the library (`rc.multipartParts`): a string-typed part is a transport
6432
- // container the runtime composes from `bodyParameters`, so it is listed without
6433
- // a value and is never an author input — see `readFields` in core/library.ts.
6434
- const partValues = new Map();
6435
- for (const f of fields) {
6436
- const flat = Object.prototype.hasOwnProperty.call(inputs, f.name);
6437
- let nested = flat
6438
- ? { found: true, value: inputs[f.name], projectedArrays: 0 }
6439
- : readConnectorPath(inputs, f.name);
6440
- if (!nested.found) {
6441
- // A required LOOKUP field is missing far more often because the author does
6442
- // not know what to put in it than because they forgot it exists: name the
6443
- // id it sends and the helper that finds one, so "(string)" is not read as
6444
- // "any string" — an e-mail into `fields.reporter.id` is exactly that misreading.
6445
- if (f.required)
6446
- errs.push(`missing required input "${f.name}" (${f.type ?? 'value'})${missingLookupHint(f.name, lookups[f.name])}`);
6447
- continue;
6448
- }
6449
- // An unresolved lookup is swapped for its recorded id, or reported. It is
6450
- // NOT rendered as a placeholder: emitting something for a value nobody
6451
- // resolved would produce a flow that compiles and then talks to the wrong
6452
- // record, which is exactly the failure the lookup surface exists to remove.
6453
- //
6454
- // A token is swapped wherever the field's value can hold one: the whole
6455
- // value, or an element of an array at any depth. A multiselect is
6456
- // `users: [lookup(…), lookup(…)]` — the form `ConnectorLookupValue`'s array
6457
- // branch types and the generated `## Example` prints — and until this swapped
6458
- // only the whole-value case, so every token inside the array reached
6459
- // validation as an object ("expected string, got object") with every lookup
6460
- // recorded. An unresolved element under 'ignore' becomes `null`, which
6461
- // validation skips, so `check` still type-checks the resolved siblings.
6462
- const unresolved = [];
6463
- const swapped = swapLookupTokens(nested.value, resolutions, unresolved);
6464
- if (unresolved.length) {
6465
- if (policy.unresolvedLookups !== 'ignore') {
6466
- for (const token of unresolved)
6467
- errs.push(unresolvedLookupMessage(token));
6468
- continue;
6469
- }
6470
- if (isLookupToken(nested.value))
6471
- continue;
6472
- }
6473
- nested = { ...nested, value: swapped };
6474
- // A FLAT key whose wire name ends in `[*]` names one array: an array value
6475
- // there is checked element by element, as the nested form is. A flat scalar
6476
- // keeps depth 0 and is emitted as the scalar it is — the product wraps it
6477
- // into a one-element list (flow-v1 `convertDotNotationToNested`).
6478
- const depth = flat
6479
- ? (f.name.endsWith('[*]') && Array.isArray(nested.value) ? 1 : 0)
6480
- : nested.projectedArrays;
6481
- validateConnectorFieldValue(f, nested.value, depth, errs);
6482
- if (f.location === 'multipart') {
6483
- partValues.set(f.name, renderDeep(nested.value, rename));
6484
- continue;
6485
- }
6486
- const slot = SLOT_FOR_LOCATION[f.location ?? 'body'] ?? 'bodyParameters';
6487
- routed[slot][f.name] = renderDeep(nested.value, rename);
6488
- }
6489
- if (errs.length) {
6490
- // Name the location of each required field: a "missing required input" on a
6491
- // path parameter reads very differently from one on a body field, and the
6492
- // author's fix is the same either way (add it to the same flat `inputs` map).
6493
- const req = fields
6494
- .filter((f) => f.required)
6495
- .map((f) => (f.location && f.location !== 'body' ? `${f.name} (${f.location})` : f.name))
6496
- .join(', ') || '(none required)';
6497
- // Phase 1 propagates connector discovery capability to every activity. When
6498
- // one of those activities rejects an unknown field, point at the live-schema
6499
- // preparation path; fully static activities on the same connector still
6500
- // compile normally and never reach this hint.
6501
- const schemaDynamic = !!rc.entry?.runtime?.requiresConnectionForSchema;
6502
- const hasUnknown = errs.some((e) => e.startsWith('unknown input'));
6503
- let hint = '';
6504
- if (schemaDynamic && hasUnknown) {
6505
- const prefix = `uipath.connector.${rc.connectorKey}.`;
6506
- const action = String(rc.nodeType).startsWith(prefix)
6507
- ? String(rc.nodeType).slice(prefix.length)
6508
- // `.pop()` on a split is never empty at runtime, but its type admits
6509
- // `undefined`; the node type itself is the only sane fallback.
6510
- : String(rc.nodeType).split('.').pop() ?? String(rc.nodeType);
6511
- hint =
6512
- ` This connector supports connection-specific object or field discovery, and the ` +
6513
- `static library does not contain this input. Run ` +
6514
- `\`${prepareCommand(rc.connectorKey, action)}\` ` +
6515
- `to load them ` +
6516
- `(writes ./.flow-sdk/connectors-local/, which the compiler auto-detects). If that still ` +
6517
- `does not carry this input, the operation resolves its fields from the VALUES of ` +
6518
- `parent inputs: resolve every parent and re-run with ` +
6519
- `\`-f <parent-field>=<value>\` for each — too few parents is refused, and a ` +
6520
- `wrong value resolves back to just the parents. ` +
6521
- `See the connector guide's parent-field loop.`;
6522
- }
6523
- throw new Error(`connector ${rc.nodeType}: ${errs.join('; ')}. Required fields: ${req}.${hint}`);
6524
- }
6525
- // Every declared part, in declaration order, as the designer lists them: the
6526
- // container part valueless, a file part with the author's value when given. A
6527
- // test double that resolves no `multipartParts` falls back to its multipart
6528
- // fields, which are all file parts by construction.
6529
- const declaredParts = rc.multipartParts
6530
- ?? fields.filter((f) => f.location === 'multipart').map((f) => ({ name: f.name, dataType: f.type ?? 'string' }));
6531
- const multipartParameters = declaredParts.map((part) => (partValues.has(part.name)
6532
- ? { name: part.name, dataType: part.dataType, value: partValues.get(part.name) }
6533
- : { name: part.name, dataType: part.dataType }));
6534
- // A FIXED slot order, so a recompile is byte-identical no matter which order
6535
- // the author listed the inputs in.
6536
- const slots = multipartParameters.length === 0 || Object.keys(routed.bodyParameters).length > 0
6537
- ? { bodyParameters: routed.bodyParameters }
6538
- : {};
6539
- if (Object.keys(routed.pathParameters).length)
6540
- slots.pathParameters = routed.pathParameters;
6541
- if (Object.keys(routed.queryParameters).length)
6542
- slots.queryParameters = routed.queryParameters;
6543
- if (multipartParameters.length) {
6544
- slots.multipartParameters = multipartParameters;
6545
- slots.inputMetadata = { type: 'multipart', multipart: { bodyFieldName: multipartBodyFieldName(declaredParts) } };
6546
- }
6547
- return slots;
6548
- }
6549
- /**
6550
- * `value` with every lookup token swapped for its recorded id: the value itself,
6551
- * or the elements of an array at any depth. An unrecorded token is pushed to
6552
- * `unresolved` and stands as `null`, so an array keeps its length.
6553
- *
6554
- * Objects are NOT descended: a lookup's place is a field's value or an element
6555
- * of an array field (`ConnectorLookupValue`), and a nested path such as
6556
- * `contacts[*].id` reaches here already projected to its leaf array. A token
6557
- * anywhere else is refused by {@link renderDeep}.
6558
- */
6559
- function swapLookupTokens(value, resolutions, unresolved) {
6560
- if (isLookupToken(value)) {
6561
- const resolved = resolvedValue(value, resolutions);
6562
- if (resolved === undefined) {
6563
- unresolved.push(value);
6564
- return null;
6565
- }
6566
- return resolved;
6567
- }
6568
- if (Array.isArray(value))
6569
- return value.map((item) => swapLookupTokens(item, resolutions, unresolved));
6570
- return value;
6571
- }
6572
- /**
6573
- * `inputMetadata.multipart.bodyFieldName` names the CONTAINER part — the one the
6574
- * runtime composes from `bodyParameters`. Measured on the designer's own nodes in
6575
- * the eval archive: `body` ×413, `RagRequest` ×1 (GenAI `content-generation`, whose
6576
- * string part is named that), and `body` on 91 of 93 file-only operations. A
6577
- * mis-named container is fatal at dispatch — hardcoding `body` for GenAI made
6578
- * Integration Service answer `400 The request body is empty` (verified live
6579
- * 2026-09-02) — so it is the string part's own name, with `body` only as the
6580
- * default for an operation that has no string part.
6581
- */
6582
- function multipartBodyFieldName(parts) {
6583
- const containers = parts.filter((p) => p.dataType.toLowerCase() !== 'file');
6584
- return containers.length === 1 ? containers[0].name : 'body';
6585
- }
6586
- const SLOT_FOR_LOCATION = {
6587
- body: 'bodyParameters',
6588
- query: 'queryParameters',
6589
- path: 'pathParameters',
6590
- };
6591
- function connectorPath(name) {
6592
- return name.split('.').map((raw) => ({
6593
- key: raw.endsWith('[*]') ? raw.slice(0, -3) : raw,
6594
- array: raw.endsWith('[*]'),
6595
- }));
6596
- }
6597
- /** Read a natural nested authoring value using a registry path such as
6598
- * `contacts[*].id`. Wildcards project one value per array element so the Flow
6599
- * bodyParameters map can retain the registry's exact field ids. */
6600
- function readConnectorPath(inputs, name) {
6601
- const parts = connectorPath(name);
6602
- const read = (value, index) => {
6603
- if (index >= parts.length)
6604
- return { found: true, value, projectedArrays: 0 };
6605
- if (!value || typeof value !== 'object' || value instanceof Expr || Array.isArray(value))
6606
- return { found: false, projectedArrays: 0 };
6607
- const part = parts[index];
6608
- const object = value;
6609
- if (!Object.prototype.hasOwnProperty.call(object, part.key))
6610
- return { found: false, projectedArrays: 0 };
6611
- const next = object[part.key];
6612
- if (!part.array)
6613
- return read(next, index + 1);
6614
- if (!Array.isArray(next))
6615
- return { found: false, projectedArrays: 0 };
6616
- if (index === parts.length - 1)
6617
- return { found: true, value: next, projectedArrays: 1 };
6618
- const projected = next.map((item) => read(item, index + 1));
6619
- if (!projected.some((item) => item.found))
6620
- return { found: false, projectedArrays: 0 };
6621
- return {
6622
- found: true,
6623
- value: projected.map((item) => item.found ? item.value : null),
6624
- projectedArrays: 1 + Math.max(0, ...projected.map((item) => item.projectedArrays)),
6625
- };
6626
- };
6627
- return read(inputs, 0);
6628
- }
6629
- function buildConnectorInputTrie(fields) {
6630
- const root = { children: new Map(), array: false };
6631
- for (const field of fields) {
6632
- let cursor = root;
6633
- for (const part of connectorPath(field.name)) {
6634
- let child = cursor.children.get(part.key);
6635
- if (!child) {
6636
- child = { children: new Map(), array: part.array };
6637
- cursor.children.set(part.key, child);
6638
- }
6639
- child.array ||= part.array;
6640
- cursor = child;
6641
- }
6642
- cursor.terminal = field;
6643
- }
6644
- return root;
6645
- }
6646
- /** Reject nested typos with the same unknown-key message as flat keys. */
6647
- function validateNestedConnectorKeys(fields, inputs, exact) {
6648
- const root = buildConnectorInputTrie(fields);
6649
- const errs = [];
6650
- const visit = (value, node, path) => {
6651
- if (value instanceof Expr)
6652
- return;
6653
- if (node.array) {
6654
- if (!Array.isArray(value)) {
6655
- errs.push(`input "${path}" expected array, got ${typeof value}`);
6656
- return;
6657
- }
6658
- for (const item of value)
6659
- visit(item, { ...node, array: false }, `${path}[*]`);
6660
- return;
6661
- }
6662
- // An opaque object/array terminal owns its complete value; there is no
6663
- // child metadata with which to validate its internal keys.
6664
- if (node.terminal && node.children.size === 0)
6665
- return;
6666
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
6667
- if (node.children.size > 0)
6668
- errs.push(`input "${path}" expected object, got ${Array.isArray(value) ? 'array' : typeof value}`);
6669
- return;
6670
- }
6671
- for (const [key, childValue] of Object.entries(value)) {
6672
- const child = node.children.get(key);
6673
- const childPath = path ? `${path}.${key}` : key;
6674
- if (!child) {
6675
- const near = nearestField(childPath, fields.map((f) => f.name));
6676
- errs.push(`unknown input "${childPath}"${near ? ` — did you mean "${near}"?` : ''}`);
6677
- }
6678
- else {
6679
- visit(childValue, child, childPath);
6680
- }
6681
- }
6682
- };
6683
- for (const [key, value] of Object.entries(inputs)) {
6684
- if (exact.has(key))
6685
- continue; // backwards-compatible flat authoring
6686
- const node = root.children.get(key);
6687
- if (!node) {
6688
- const near = nearestField(key, [...exact.keys()]);
6689
- errs.push(`unknown input "${key}"${near ? ` — did you mean "${near}"?` : ''}`);
6690
- }
6691
- else {
6692
- visit(value, node, key);
6693
- }
6694
- }
6695
- return errs;
6696
- }
6697
- function validateConnectorFieldValue(field, value, projectedArrays, errs) {
6698
- if (projectedArrays > 0) {
6699
- if (!Array.isArray(value)) {
6700
- errs.push(`input "${field.name}" expected array projection, got ${typeof value}`);
6701
- return;
6702
- }
6703
- for (const item of value) {
6704
- if (item !== null && item !== undefined)
6705
- validateConnectorFieldValue(field, item, projectedArrays - 1, errs);
6706
- }
6707
- return;
6708
- }
6709
- if (value instanceof Expr || value === null || value === undefined)
6710
- return;
6711
- if (field.type === 'array') {
6712
- if (!Array.isArray(value))
6713
- errs.push(`input "${field.name}" expected array, got ${typeof value}`);
6714
- return;
6715
- }
6716
- if (field.type === 'object') {
6717
- if (typeof value !== 'object' || Array.isArray(value))
6718
- errs.push(`input "${field.name}" expected object, got ${Array.isArray(value) ? 'array' : typeof value}`);
6719
- return;
6720
- }
6721
- if (field.type === 'string' || field.type === 'number' || field.type === 'boolean') {
6722
- const actual = typeof value;
6723
- const ok = actual === field.type ||
6724
- (field.type === 'number' && actual === 'string' && value !== '' && !isNaN(Number(value)));
6725
- if (!ok)
6726
- errs.push(`input "${field.name}" expected ${field.type}, got ${actual}`);
6727
- }
6728
- }
6729
- /**
6730
- * The closest declared field name to `key`, for the "did you mean …?" half of an
6731
- * unknown-input error.
6732
- *
6733
- * Three rules, tried in order: an exact case-insensitive match (`pagesize` →
6734
- * `pageSize`), a substring either way (`summary` → `fields.summary`), and — the
6735
- * one added for query/path parameters — a small EDIT DISTANCE. The first two miss
6736
- * every substitution typo: `projekt` and `project` share no containment, so the
6737
- * suggestion was simply absent, and it is absent exactly where it is needed most.
6738
- * Parameter names are unfamiliar (`issuetype`, `nextPage`, `expansionLevel`), so
6739
- * the author has no memory to fall back on.
6740
- *
6741
- * The threshold scales with the name's length (⌈len/4⌉, at least 1, at most 3), so
6742
- * a short name needs a near-exact miss and a long one may be off by a few — and
6743
- * two genuinely different fields are never conflated.
6744
- */
6745
- function nearestField(key, names) {
6746
- const lk = key.toLowerCase();
6747
- const exact = names.find((n) => n.toLowerCase() === lk);
6748
- if (exact)
6749
- return exact;
6750
- const contained = names.find((n) => n.toLowerCase().includes(lk) || lk.includes(n.toLowerCase()));
6751
- if (contained)
6752
- return contained;
6753
- const budget = Math.min(3, Math.max(1, Math.ceil(lk.length / 4)));
6754
- let best;
6755
- let bestD = budget + 1;
6756
- for (const n of names) {
6757
- const d = editDistance(lk, n.toLowerCase());
6758
- if (d < bestD) {
6759
- bestD = d;
6760
- best = n;
6761
- }
6762
- }
6763
- return bestD <= budget ? best : undefined;
6764
- }
6765
- /** Levenshtein distance, two rows — enough for field-name suggestions. */
6766
- function editDistance(a, b) {
6767
- if (a === b)
6768
- return 0;
6769
- if (Math.abs(a.length - b.length) > 3)
6770
- return 99; // cheap bail: beyond any budget
6771
- let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
6772
- for (let i = 1; i <= a.length; i++) {
6773
- const row = [i];
6774
- for (let j = 1; j <= b.length; j++) {
6775
- row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
6776
- }
6777
- prev = row;
6778
- }
6779
- return prev[b.length];
6780
- }
6781
6638
  /**
6782
6639
  * Emit a transform's `inputs` in the canonical shape the node's runtime reads:
6783
6640
  * `collection` as a BARE variable path (not `=js:`-prefixed — it is a path