@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.
- package/dist/api-index.md +129 -129
- package/dist/api-members.md +420 -416
- package/dist/bpmn/bpmn-sdk.d.ts +16 -0
- package/dist/bpmn/bpmn-sdk.js +2 -0
- package/dist/bpmn/check-cli.js +22 -5
- package/dist/bpmn/check.d.ts +15 -1
- package/dist/bpmn/check.js +69 -2
- package/dist/bpmn/compile-cli.js +24 -14
- package/dist/bpmn/connector.js +29 -14
- package/dist/bpmn/intsvc.js +5 -2
- package/dist/bpmn/serialize.d.ts +2 -0
- package/dist/bpmn/serialize.js +1 -1
- package/dist/case/compile-cli.js +6 -9
- package/dist/case/serialize.js +1 -1
- package/dist/check-cli.js +6 -35
- package/dist/check.d.ts +16 -148
- package/dist/check.js +53 -711
- package/dist/cli/commands/bpmn.js +15 -0
- package/dist/cli-library.d.ts +6 -25
- package/dist/cli-library.js +6 -73
- package/dist/compile-cli.js +14 -8
- package/dist/core/actions.d.ts +19 -0
- package/dist/core/cli-library.d.ts +47 -0
- package/dist/core/cli-library.js +150 -0
- package/dist/core/connector-checks.d.ts +226 -0
- package/dist/core/connector-checks.js +518 -0
- package/dist/core/connector-inputs.d.ts +80 -0
- package/dist/core/connector-inputs.js +453 -0
- package/dist/core/event-checks.d.ts +137 -0
- package/dist/core/event-checks.js +321 -0
- package/dist/decompile.js +1 -1
- package/dist/registry/generators/markdown/sections/example.d.ts +7 -1
- package/dist/registry/generators/markdown/sections/example.js +70 -5
- package/dist/registry/generators/markdown/sections/lookups.js +29 -8
- package/dist/registry/prepare/messages.js +6 -1
- package/dist/registry/prepare/source-import.d.ts +3 -3
- package/dist/registry/prepare/source-import.js +15 -4
- package/dist/registry/prepare/source-labels.js +31 -4
- package/dist/serialize.d.ts +11 -11
- package/dist/serialize.js +285 -428
- package/lib/README.md +1 -0
- package/lib/library-json.samples/index.json +9 -0
- package/lib/library-json.samples/uipath-uipath-airdk/web-search@1.0.0.json +118 -0
- package/lib/library-json.samples/uipath-uipath-airdk/web-search@1.0.0.v1def.json +258 -0
- package/package.json +2 -2
- /package/dist/{event-filters.d.ts → core/event-filters.d.ts} +0 -0
- /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` —
|
|
984
|
-
* node
|
|
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,
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
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
|
|
4758
|
-
//
|
|
4759
|
-
//
|
|
4760
|
-
//
|
|
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
|
-
//
|
|
4859
|
-
const toolResource =
|
|
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
|