@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/check.js
CHANGED
|
@@ -7,64 +7,22 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Expr, SCHEDULE_PRESETS, DELAY_PRESETS, VARIANT_OPERATION, parseIxpProjectType, flatNameOf, flatNameSegmentsOf, FLAT_SEP } from './flow-sdk.js';
|
|
9
9
|
import { FlowNode } from './core/node-classes.js';
|
|
10
|
-
import { isLookupToken, resolvedValue, unresolvedLookupMessage, } from './core/lookups.js';
|
|
11
10
|
import { QUEUE_ITEM_RECORD_SCHEMA } from './core/queue-record.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
11
|
+
import { checkEventResolution, checkEventSubscription, eventParamsFor, } from './core/event-checks.js';
|
|
12
|
+
import { FLOW_EXPR_DIALECT, checkBindingLabels, checkConnectorContract as coreCheckConnectorContract, checkConnectorLookups as coreCheckConnectorLookups, checkConnectorSchema as coreCheckConnectorSchema, collectConnectionUses, connectionConnectorConflicts, } from './core/connector-checks.js';
|
|
13
|
+
import { prepareCommand } from './core/cli-spelling.js';
|
|
15
14
|
import { isAnswerField } from './core/hitl-answers.js';
|
|
16
15
|
import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from './core/hitl-routing.js';
|
|
17
16
|
import { childStepLists, declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
|
|
18
17
|
import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
|
|
19
18
|
import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
|
|
20
|
-
import { prepareCommand, resolveFlagArg } from './core/cli-spelling.js';
|
|
21
|
-
import { EVENT_BYOA_UNVERIFIED, byoaUnverified } from './core/byoa.js';
|
|
22
19
|
export function check(built, opts = {}) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
diags.push(...connectionConnectorConflicts(connectionUsage, opts));
|
|
28
|
-
return diags;
|
|
20
|
+
let diags = [];
|
|
21
|
+
const uses = collectConnectionUses(() => { diags = checkFlow(built, [], opts); });
|
|
22
|
+
for (const { id, message } of connectionConnectorConflicts(uses, opts)) {
|
|
23
|
+
diags.push({ level: 'error', code: 'CONNECTION_CONNECTOR_CONFLICT', step: id, message });
|
|
29
24
|
}
|
|
30
|
-
|
|
31
|
-
connectionUsage = outer;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* The connection uses {@link checkBindingLabels} saw during the current
|
|
36
|
-
* `check()` — the flow and every child flow, which share one `bindings[]`.
|
|
37
|
-
* Module state rather than a `CheckOpts` field because `check` is synchronous
|
|
38
|
-
* and this is not an input; `check()` saves and restores it, so a nested call
|
|
39
|
-
* cannot mix two flows.
|
|
40
|
-
*/
|
|
41
|
-
let connectionUsage;
|
|
42
|
-
/**
|
|
43
|
-
* CONNECTION_CONNECTOR_CONFLICT — the rule `serialize` enforces on the emitted
|
|
44
|
-
* document (`connectionConnectors`, bindings-v2.ts), reported here with the same
|
|
45
|
-
* sentence so `check` does not print `✓ no issues` for a source `compile` then
|
|
46
|
-
* refuses. Labels are grouped by what they resolve to, so two labels bound to
|
|
47
|
-
* one connection id are one connection, as they are in the `.flow`.
|
|
48
|
-
*/
|
|
49
|
-
function connectionConnectorConflicts(uses, opts) {
|
|
50
|
-
const byConnection = new Map();
|
|
51
|
-
for (const use of uses) {
|
|
52
|
-
const key = opts.bindings?.resolve(use.label) ?? use.label;
|
|
53
|
-
const byConnector = byConnection.get(key) ?? new Map();
|
|
54
|
-
byConnection.set(key, byConnector);
|
|
55
|
-
byConnector.set(use.connector, [...(byConnector.get(use.connector) ?? []), use.step]);
|
|
56
|
-
}
|
|
57
|
-
const out = [];
|
|
58
|
-
for (const [key, byConnector] of byConnection) {
|
|
59
|
-
if (byConnector.size < 2)
|
|
60
|
-
continue;
|
|
61
|
-
out.push({
|
|
62
|
-
level: 'error', code: 'CONNECTION_CONNECTOR_CONFLICT',
|
|
63
|
-
step: [...byConnector.values()][1][0],
|
|
64
|
-
message: connectionConnectorConflictMessage(key, byConnector),
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
25
|
+
return diags;
|
|
68
26
|
}
|
|
69
27
|
/**
|
|
70
28
|
* `ancestors` is the chain of child flows currently being checked, innermost
|
|
@@ -405,7 +363,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
405
363
|
const target = pair.targetConnector;
|
|
406
364
|
checkBindingLabels(`managed http "${httpStep}"`, pair, opts, (code, message) => diags.push({ level: 'error', code, step: httpStep, message }), undefined,
|
|
407
365
|
// The connector its CONNECTION belongs to: the target, not the wrapper.
|
|
408
|
-
{
|
|
366
|
+
{ id: httpStep, connector: typeof target === 'string' && target !== '' ? target : 'uipath-uipath-http' });
|
|
409
367
|
}
|
|
410
368
|
const seen = new Set();
|
|
411
369
|
for (const b of s.spec.inputs.branches ?? []) {
|
|
@@ -497,7 +455,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
497
455
|
checkAgent(s.name, s.spec.inputs, diags);
|
|
498
456
|
}
|
|
499
457
|
if (s.spec.kind === 'inlineAgent') {
|
|
500
|
-
checkInlineAgent(s.name, s.spec.inputs, diags);
|
|
458
|
+
checkInlineAgent(s.name, s.spec.inputs, diags, opts);
|
|
501
459
|
}
|
|
502
460
|
if (s.spec.kind === 'queueItem')
|
|
503
461
|
checkQueueItem(s.name, s.spec.inputs, diags);
|
|
@@ -584,7 +542,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
584
542
|
});
|
|
585
543
|
checkEventSubscription(s.spec.subscription, `waitForEvent "${stepName}"`, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts));
|
|
586
544
|
checkEventResolution(s.spec.subscription, `waitForEvent "${stepName}"`, 'event', opts, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts));
|
|
587
|
-
checkBindingLabels(`waitForEvent "${stepName}"`, s.spec.subscription, opts, push('error'), eventTarget(s.spec.subscription), {
|
|
545
|
+
checkBindingLabels(`waitForEvent "${stepName}"`, s.spec.subscription, opts, push('error'), eventTarget(s.spec.subscription), { id: stepName, connector: eventTarget(s.spec.subscription)?.key });
|
|
588
546
|
}
|
|
589
547
|
if (s.spec.kind === 'subflow') {
|
|
590
548
|
// Only a real child goes in the map: a missing one is reported by
|
|
@@ -1274,234 +1232,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
1274
1232
|
}
|
|
1275
1233
|
return diags;
|
|
1276
1234
|
}
|
|
1277
|
-
/** Reject a wrapper hop when the runtime shape of a list has been measured. */
|
|
1278
|
-
/**
|
|
1279
|
-
* The lookup rules: a pasted id, and an unresolved token.
|
|
1280
|
-
*
|
|
1281
|
-
* These are the strongest discoverability route in the design, because they
|
|
1282
|
-
* need no prior knowledge and arrive in the loop the author is already in. An
|
|
1283
|
-
* author who pastes `WT9RC72KU` into a lookup field is not being careless, they
|
|
1284
|
-
* are being uninformed — nothing anywhere told them the field was resolvable —
|
|
1285
|
-
* and the fix is to tell them at the point of the mistake.
|
|
1286
|
-
*
|
|
1287
|
-
* Both are WARNINGS, not errors. A literal id is legal and sometimes correct
|
|
1288
|
-
* (a value that genuinely does not move), and the unresolved case is already a
|
|
1289
|
-
* hard failure at compile, where the same message is repeated. Making it an
|
|
1290
|
-
* error here would stop a `check` that is otherwise clean from telling the
|
|
1291
|
-
* author about everything ELSE that is wrong, which is the whole point of
|
|
1292
|
-
* running the cheap pass first.
|
|
1293
|
-
*/
|
|
1294
|
-
function checkConnectorLookups(step, spec, opts, diags) {
|
|
1295
|
-
const specs = opts.lookups?.(spec.key, spec.action, spec.version, spec.object);
|
|
1296
|
-
const resolutions = opts.resolutions ?? {};
|
|
1297
|
-
// One LOOKUP_LITERAL_ID per field: a multiselect of three pasted ids is one
|
|
1298
|
-
// mistake with one remedy, not three.
|
|
1299
|
-
const literalWarned = new Set();
|
|
1300
|
-
const visit = (value, path) => {
|
|
1301
|
-
if (isLookupToken(value)) {
|
|
1302
|
-
// A match key the field does not declare is certainly wrong, and the
|
|
1303
|
-
// spec that says so is already in hand. `fields.reporter.id` is searchable
|
|
1304
|
-
// by `displayName | accountId`; `.by('name', …)` used to pass here as an
|
|
1305
|
-
// ordinary unresolved lookup, and the prepare command this warning prints
|
|
1306
|
-
// would then be REFUSED by the resolver — the mistake surviving both the
|
|
1307
|
-
// check and its own suggested fix. `spec.by` is the field's own
|
|
1308
|
-
// declaration, so it is complete by construction and may hard-error.
|
|
1309
|
-
const named = path.join('.');
|
|
1310
|
-
const declared = specs?.[named];
|
|
1311
|
-
const by = value.__lookup.by;
|
|
1312
|
-
if (declared && typeof by === 'string' && !declared.by.includes(by)) {
|
|
1313
|
-
const { target, call } = lookupPhrases(named, declared, spec);
|
|
1314
|
-
diags.push({
|
|
1315
|
-
level: 'error',
|
|
1316
|
-
code: 'LOOKUP_UNKNOWN_MATCH_KEY',
|
|
1317
|
-
message: `Connector step "${step}": \`${named}\` has no match key \`${by}\`. It resolves `
|
|
1318
|
-
+ `against \`${target}\`, which is searchable by `
|
|
1319
|
-
+ `${declared.by.map((b) => `\`${b}\``).join(' or ')}. Resolving on `
|
|
1320
|
-
+ `\`${by}\` is refused, so recording it would fail too:\n`
|
|
1321
|
-
+ ` ${call}`,
|
|
1322
|
-
step,
|
|
1323
|
-
});
|
|
1324
|
-
return;
|
|
1325
|
-
}
|
|
1326
|
-
if (resolvedValue(value, resolutions) === undefined) {
|
|
1327
|
-
diags.push({
|
|
1328
|
-
level: 'warning',
|
|
1329
|
-
code: 'LOOKUP_UNRESOLVED',
|
|
1330
|
-
message: `Connector step "${step}": ` + unresolvedLookupMessage(value)
|
|
1331
|
-
+ `\nCompile will fail until this is recorded. Running it now, before `
|
|
1332
|
-
+ `compiling, is the cheap order: this pass has already found everything `
|
|
1333
|
-
+ `else that is wrong.`,
|
|
1334
|
-
step,
|
|
1335
|
-
});
|
|
1336
|
-
}
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
if (Array.isArray(value)) {
|
|
1340
|
-
// An element is named by the WIRE name the lookup table is keyed on —
|
|
1341
|
-
// `users[*]`, `contacts[*].id` — not by its index. `users.0` matched no
|
|
1342
|
-
// spec, so a pasted id, a token with a bad match key, or a runtime value
|
|
1343
|
-
// inside a multiselect passed every rule below unseen. A name that already
|
|
1344
|
-
// is the array's (a flat `'users[*]'` key, or a field typed as an array)
|
|
1345
|
-
// stays as it is.
|
|
1346
|
-
const last = path[path.length - 1] ?? '';
|
|
1347
|
-
const element = last.endsWith('[*]') || specs?.[path.join('.')]
|
|
1348
|
-
? path
|
|
1349
|
-
: [...path.slice(0, -1), `${last}[*]`];
|
|
1350
|
-
value.forEach((v) => visit(v, element));
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
if (value && typeof value === 'object' && !(value instanceof Expr)) {
|
|
1354
|
-
for (const [k, v] of Object.entries(value)) {
|
|
1355
|
-
visit(v, [...path, k]);
|
|
1356
|
-
}
|
|
1357
|
-
return;
|
|
1358
|
-
}
|
|
1359
|
-
if (!specs)
|
|
1360
|
-
return;
|
|
1361
|
-
const field = path.join('.');
|
|
1362
|
-
const lookup = specs[field];
|
|
1363
|
-
if (!lookup)
|
|
1364
|
-
return;
|
|
1365
|
-
if (value instanceof Expr && !value.literal) {
|
|
1366
|
-
const d = runtimeLookupDiagnostic(step, spec, field, lookup, value);
|
|
1367
|
-
if (d)
|
|
1368
|
-
diags.push(d);
|
|
1369
|
-
return;
|
|
1370
|
-
}
|
|
1371
|
-
// A pasted id, whether bare or wrapped in `lit()`.
|
|
1372
|
-
const literal = value instanceof Expr ? value.value : value;
|
|
1373
|
-
if (typeof literal !== 'string')
|
|
1374
|
-
return;
|
|
1375
|
-
if (literalWarned.has(field))
|
|
1376
|
-
return;
|
|
1377
|
-
literalWarned.add(field);
|
|
1378
|
-
const { target, call } = lookupPhrases(field, lookup, spec);
|
|
1379
|
-
diags.push({
|
|
1380
|
-
level: 'warning',
|
|
1381
|
-
code: 'LOOKUP_LITERAL_ID',
|
|
1382
|
-
message: `Connector step "${step}": \`${field}\` is a lookup field — its value is an id from `
|
|
1383
|
-
+ `\`${target}\`, searchable by ${lookup.by.map((b) => `\`${b}\``).join(', ')}. `
|
|
1384
|
-
+ `A literal id is meaningless in review and does not survive a move to another `
|
|
1385
|
-
+ `connection: it resolves to nothing there, or to something else, and neither `
|
|
1386
|
-
+ `compile nor deploy can tell. Write what you mean and resolve it late:\n`
|
|
1387
|
-
+ ` ${call}\n`
|
|
1388
|
-
+ `then record the value once with \`${prepareCommand(spec.key, spec.action)} `
|
|
1389
|
-
+ `--resolve ${resolveFlagArg(field, lookup.by[0] ?? 'id', '<value>')}\`.`,
|
|
1390
|
-
step,
|
|
1391
|
-
});
|
|
1392
|
-
};
|
|
1393
|
-
for (const [name, value] of Object.entries(spec.inputs ?? {}))
|
|
1394
|
-
visit(value, [name]);
|
|
1395
|
-
}
|
|
1396
|
-
/**
|
|
1397
|
-
* The schema rules: an unprepared object, and unprepared custom fields.
|
|
1398
|
-
*
|
|
1399
|
-
* Same reasoning as the lookup rules above — the author needs no prior
|
|
1400
|
-
* knowledge, and the diagnostic arrives in the loop they are already in, with
|
|
1401
|
-
* the one command that discharges it. Both are WARNINGS: compile accepts a
|
|
1402
|
-
* connection-scoped object (stamped `objectNameUnverified`) and passes the
|
|
1403
|
-
* inputs through, so `check` is the one place the gap is visible BEFORE a
|
|
1404
|
-
* flow dispatches against a schema nothing validated.
|
|
1405
|
-
*
|
|
1406
|
-
* The custom-fields rule is deliberately narrower than "unprepared op": a step
|
|
1407
|
-
* that uses only baked fields owes nothing, and warning on it anyway teaches
|
|
1408
|
-
* the author that the warning is noise. It fires only for inputs OUTSIDE the
|
|
1409
|
-
* tenant-agnostic snapshot — the moment hand-guessing actually starts.
|
|
1410
|
-
*/
|
|
1411
|
-
function checkConnectorSchema(step, spec, opts, diags) {
|
|
1412
|
-
const readiness = opts.schemaReadiness?.(spec.key, spec.action, spec.version, spec.object);
|
|
1413
|
-
if (!readiness || readiness.prepared)
|
|
1414
|
-
return;
|
|
1415
|
-
const prepare = prepareCommand(spec.key, spec.action);
|
|
1416
|
-
if (readiness.kind === 'object') {
|
|
1417
|
-
diags.push({
|
|
1418
|
-
level: 'warning',
|
|
1419
|
-
code: 'OBJECT_UNPREPARED',
|
|
1420
|
-
message: `Connector step "${step}": object "${spec.object}" has no locally materialized schema — `
|
|
1421
|
-
+ `this operation resolves its object against the connection, so its fields cannot be `
|
|
1422
|
-
+ `validated here and compile will emit it unverified. Prepare it once:\n`
|
|
1423
|
-
+ ` ${prepare} --object ${spec.object}\n`
|
|
1424
|
-
+ `The name is matched against the connected catalog (exact, then case-insensitive on `
|
|
1425
|
-
+ `name and display name); several matches fail with the candidates to choose from, and `
|
|
1426
|
-
+ `the prepared descriptor is the import to switch to.`,
|
|
1427
|
-
step,
|
|
1428
|
-
...(readiness.canonical ? { suggestion: readiness.canonical } : {}),
|
|
1429
|
-
});
|
|
1430
|
-
return;
|
|
1431
|
-
}
|
|
1432
|
-
if (readiness.kind === 'customFields') {
|
|
1433
|
-
// Match by full dotted path, the same descent `checkConnectorLookups`
|
|
1434
|
-
// uses 100 lines up — a top-level comparison would let `contact:
|
|
1435
|
-
// { cf_custom: … }` slide because `contact` is a baked head, and a nested
|
|
1436
|
-
// hand-guessed custom field is precisely the moment this warning is for.
|
|
1437
|
-
// Wire-array markers are stripped from the baked names (`users[*]` is
|
|
1438
|
-
// authored as `users`).
|
|
1439
|
-
const baked = (readiness.bakedFields ?? []).map((name) => name.replace(/\[\*\]/g, ''));
|
|
1440
|
-
const unknown = [];
|
|
1441
|
-
const visit = (value, path) => {
|
|
1442
|
-
const dotted = path.join('.');
|
|
1443
|
-
// A baked field, or deeper than one — the name is in the vocabulary;
|
|
1444
|
-
// whether the VALUE fits is compile's business, not this warning's.
|
|
1445
|
-
if (baked.some((b) => b === dotted || dotted.startsWith(b + '.')))
|
|
1446
|
-
return;
|
|
1447
|
-
// The head of baked fields (`contact` when `contact.email` is baked):
|
|
1448
|
-
// descend to find the precise gap underneath it. Array elements keep
|
|
1449
|
-
// their parent's path — the baked names index arrays as `items[*].sku`,
|
|
1450
|
-
// normalized above to `items.sku` — so a guessed field inside a list
|
|
1451
|
-
// element (`lineItems: [{ cf_custom }]`) warns exactly like one under
|
|
1452
|
-
// an object head.
|
|
1453
|
-
if (baked.some((b) => b.startsWith(dotted + '.'))) {
|
|
1454
|
-
if (Array.isArray(value)) {
|
|
1455
|
-
for (const element of value)
|
|
1456
|
-
visit(element, path);
|
|
1457
|
-
}
|
|
1458
|
-
else if (value && typeof value === 'object'
|
|
1459
|
-
&& !(value instanceof Expr) && !isLookupToken(value)) {
|
|
1460
|
-
for (const [k, v] of Object.entries(value)) {
|
|
1461
|
-
visit(v, [...path, k]);
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
return;
|
|
1465
|
-
}
|
|
1466
|
-
// Neither a baked name nor on the way to one — this IS the gap,
|
|
1467
|
-
// reported once at its head.
|
|
1468
|
-
unknown.push(dotted);
|
|
1469
|
-
};
|
|
1470
|
-
for (const [name, value] of Object.entries(spec.inputs ?? {}))
|
|
1471
|
-
visit(value, [name]);
|
|
1472
|
-
// Array elements share their parent's path, so one guessed field across
|
|
1473
|
-
// many list rows is one gap, not one per row.
|
|
1474
|
-
const distinct = [...new Set(unknown)];
|
|
1475
|
-
if (distinct.length === 0)
|
|
1476
|
-
return;
|
|
1477
|
-
diags.push({
|
|
1478
|
-
level: 'warning',
|
|
1479
|
-
code: 'CUSTOM_FIELDS_UNPREPARED',
|
|
1480
|
-
message: `Connector step "${step}": ${distinct.map((n) => `\`${n}\``).join(', ')} `
|
|
1481
|
-
+ `${distinct.length === 1 ? 'is' : 'are'} not in the tenant-agnostic snapshot, and this `
|
|
1482
|
-
+ `operation resolves its real field set — custom fields included — from a live `
|
|
1483
|
-
+ `connection. Do not hand-guess the names; materialize the schema once:\n`
|
|
1484
|
-
+ ` ${prepare}\n`
|
|
1485
|
-
+ `then author against the prepared descriptor (its field names and doc comments are `
|
|
1486
|
-
+ `the vocabulary) and re-run check.`,
|
|
1487
|
-
step,
|
|
1488
|
-
});
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
/**
|
|
1492
|
-
* The field contract and the binding names `compile` enforces, reported here first.
|
|
1493
|
-
*
|
|
1494
|
-
* Both rules run only with the facts in hand (a library for the fields, a
|
|
1495
|
-
* bindings.json for the names); without them `check` stays a pure function of the
|
|
1496
|
-
* built flow and says nothing, as before.
|
|
1497
|
-
*/
|
|
1498
|
-
function checkConnectorContract(step, spec, opts, diags) {
|
|
1499
|
-
const inputErr = opts.connectorInputs?.({ key: spec.key, action: spec.action, version: spec.version, object: spec.object }, spec.inputs ?? {});
|
|
1500
|
-
if (inputErr) {
|
|
1501
|
-
diags.push({ level: 'error', code: 'CONNECTOR_INPUT', step, message: `Connector step "${step}": ${inputErr}` });
|
|
1502
|
-
}
|
|
1503
|
-
checkBindingLabels(`Connector step "${step}"`, spec, opts, (code, message) => diags.push({ level: 'error', code, step, message }), { key: spec.key, action: spec.action }, { step, connector: spec.key });
|
|
1504
|
-
}
|
|
1505
1235
|
/** What `registry prepare` takes for an event subscription: its connector key and curated event name. */
|
|
1506
1236
|
function eventTarget(sub) {
|
|
1507
1237
|
const { connector, event } = (sub ?? {});
|
|
@@ -1510,139 +1240,32 @@ function eventTarget(sub) {
|
|
|
1510
1240
|
: undefined;
|
|
1511
1241
|
}
|
|
1512
1242
|
/**
|
|
1513
|
-
* The
|
|
1514
|
-
*
|
|
1515
|
-
*
|
|
1516
|
-
*
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
* Silent without a bindings.json, and for a label that resolves to anything
|
|
1520
|
-
* other than its own name (stub values are compile's CONNECTION_STUB warning).
|
|
1243
|
+
* The connector rules live in `core/connector-checks.ts`, shared with BPMN —
|
|
1244
|
+
* they were always a function of the connector corpus and the step's own
|
|
1245
|
+
* `{ key, action, version, object, inputs, connection, folder }`, never of
|
|
1246
|
+
* anything flow-shaped. These three adapters add what IS flow-shaped: the
|
|
1247
|
+
* `Connector step "…"` vocabulary, the `step` field on the diagnostic, and the
|
|
1248
|
+
* `$vars`/`Expr` dialect its inputs are written in.
|
|
1521
1249
|
*/
|
|
1522
|
-
function
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
connectionUsage?.push({ step: use.step, label, connector: use.connector });
|
|
1528
|
-
}
|
|
1529
|
-
const bindings = opts.bindings;
|
|
1530
|
-
if (!bindings || !labels)
|
|
1531
|
-
return;
|
|
1532
|
-
for (const attribute of ['connection', 'folder']) {
|
|
1533
|
-
const name = labels[attribute];
|
|
1534
|
-
if (typeof name !== 'string')
|
|
1535
|
-
continue;
|
|
1536
|
-
const resolved = bindings.resolve(name);
|
|
1537
|
-
if (resolved === name) {
|
|
1538
|
-
report('BINDING_SELF_NAME', bindingSelfNameMessage(where, name, attribute, target));
|
|
1539
|
-
}
|
|
1540
|
-
else if (resolved === undefined) {
|
|
1541
|
-
const declared = undeclaredBinding(bindings, name);
|
|
1542
|
-
if (declared) {
|
|
1543
|
-
report('BINDING_UNDECLARED', bindingUndeclaredMessage(where, name, attribute, declared, target));
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1250
|
+
function connectorSite(step, diags) {
|
|
1251
|
+
return {
|
|
1252
|
+
where: `Connector step "${step}"`,
|
|
1253
|
+
report: (d) => diags.push({ ...d, step }),
|
|
1254
|
+
};
|
|
1547
1255
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
*
|
|
1552
|
-
* The `call` names BOTH authoring forms, and names which one the alias belongs
|
|
1553
|
-
* to. Printing `lookup(<Descriptor>, 'entityName').byDisplayName('<value>')`
|
|
1554
|
-
* alone — the old text — was a remedy with an unstated precondition: aliases
|
|
1555
|
-
* are generated onto the descriptor, and `<Descriptor>` is a placeholder the
|
|
1556
|
-
* reader has to fill from somewhere. An author holding the connector key and
|
|
1557
|
-
* action instead (the form the library's Generic CRUD half is authored in, and
|
|
1558
|
-
* the majority of its lookup fields) substitutes those for the placeholder,
|
|
1559
|
-
* keeps the alias, and gets `lookup(...).byDisplayName is not a function`. Six
|
|
1560
|
-
* flow-eval tasks did precisely that, each losing a check-edit-check round to
|
|
1561
|
-
* a message that had told them to.
|
|
1562
|
-
*/
|
|
1563
|
-
function lookupPhrases(field, lookup, spec) {
|
|
1564
|
-
const target = `${lookup.objectName ?? lookup.path}.${lookup.value}`;
|
|
1565
|
-
const by = lookup.by[0] ?? 'id';
|
|
1566
|
-
const alias = Object.keys(lookup.aliases)[0];
|
|
1567
|
-
// The key/action form first: it needs no import, so it is the one line that
|
|
1568
|
-
// is paste-ready from the message alone.
|
|
1569
|
-
const generic = `lookup('${spec.key}', '${spec.action}', '${field}').by('${by}', '<value>')`;
|
|
1570
|
-
const call = alias
|
|
1571
|
-
? `${generic}\n or \`lookup(D, '${field}').${alias}('<value>')\`, where D is the `
|
|
1572
|
-
+ `descriptor \`${prepareCommand(spec.key, spec.action)}\` generates — the aliases exist only there`
|
|
1573
|
-
: generic;
|
|
1574
|
-
return { target, call };
|
|
1256
|
+
function checkConnectorLookups(step, spec, opts, diags) {
|
|
1257
|
+
const { where, report } = connectorSite(step, diags);
|
|
1258
|
+
coreCheckConnectorLookups(where, spec, opts, FLOW_EXPR_DIALECT, report);
|
|
1575
1259
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
const EMAIL_LEAF = /e[-_]?mail|principal/i;
|
|
1584
|
-
/**
|
|
1585
|
-
* `id`, `accountId`, `channelKey`, `record_id`, `CHANNEL_KEY` — the author is
|
|
1586
|
-
* already naming an id. Case-sensitive on purpose: `valid`, `paid` and `monkey`
|
|
1587
|
-
* end in the same letters and are not ids.
|
|
1588
|
-
*/
|
|
1589
|
-
const ID_LEAF = /(?:^|[a-z0-9])(?:Id|ID|Key|KEY)$|(?:^|_)(?:id|key|ID|KEY|Id|Key)$/;
|
|
1590
|
-
/**
|
|
1591
|
-
* The lookup rule for a RUNTIME value.
|
|
1592
|
-
*
|
|
1593
|
-
* `check` cannot evaluate `$vars.start.output.senderEmail`, but it does know what
|
|
1594
|
-
* the field it lands in must carry — `reporter.accountId` — because the descriptor
|
|
1595
|
-
* says so. That is the fact the author of the 09-01 escalation flow never saw: the
|
|
1596
|
-
* generated type read `id: ConnectorLookupValue<string>`, the field was required,
|
|
1597
|
-
* the only person-shaped input in scope was an e-mail, and nothing between the
|
|
1598
|
-
* source and the tenant said an address is not an account id. Jira did, at run
|
|
1599
|
-
* time, as `[102003] Integration Services bad request`.
|
|
1600
|
-
*
|
|
1601
|
-
* Three tiers, decided by the LEAF the expression reads, because that name is
|
|
1602
|
-
* the only signal available before the flow runs:
|
|
1603
|
-
* - id-shaped (`…Id`, `…Key`, or the lookup's own `value`): silence. The author
|
|
1604
|
-
* has said it is an id; a mistaken id is `LOOKUP_LITERAL_ID`'s move-between-
|
|
1605
|
-
* connections problem, not this one.
|
|
1606
|
-
* - e-mail-shaped, into a lookup that neither SENDS an e-mail (`value`) nor
|
|
1607
|
-
* SEARCHES by one (`by`): an ERROR. Jira's reporter (`accountId`, by
|
|
1608
|
-
* displayName/accountId) is the measured case — the provider rejects the
|
|
1609
|
-
* address, and stopping here is cheaper than a faulted run. Into a lookup
|
|
1610
|
-
* whose `value` is itself an e-mail (SendGrid `from` → `from_email`, PagerDuty
|
|
1611
|
-
* `From` → `email`, 19 fields in the library): silence — the address is the
|
|
1612
|
-
* id. Into one that only searches by e-mail (ActiveCampaign `contact` → `id`,
|
|
1613
|
-
* by `email`, 139 fields): a WARNING — the provider indexes people by address,
|
|
1614
|
-
* so some accept it in the id slot and some do not; the reader decides.
|
|
1615
|
-
* - anything else (a name, a whole output object, a concatenation): a WARNING
|
|
1616
|
-
* that states the identity, so the reader can decide.
|
|
1617
|
-
*/
|
|
1618
|
-
function runtimeLookupDiagnostic(step, spec, field, lookup, value) {
|
|
1619
|
-
const refs = extractRefs(value.js);
|
|
1620
|
-
const single = refs.length === 1 && value.js.trim() === refs[0].raw ? refs[0] : undefined;
|
|
1621
|
-
const leaf = single ? (single.path.length ? single.path[single.path.length - 1] : single.root) : undefined;
|
|
1622
|
-
// The lookup's value may be dotted (`properties.email`): compare against its last segment too.
|
|
1623
|
-
const valueLeaf = lookup.value.split('.').pop() ?? lookup.value;
|
|
1624
|
-
const valueIsEmail = EMAIL_LEAF.test(lookup.value);
|
|
1625
|
-
if (leaf && (ID_LEAF.test(leaf)
|
|
1626
|
-
|| leaf.toLowerCase() === lookup.value.toLowerCase()
|
|
1627
|
-
|| leaf.toLowerCase() === valueLeaf.toLowerCase()
|
|
1628
|
-
|| (valueIsEmail && EMAIL_LEAF.test(leaf))))
|
|
1629
|
-
return undefined;
|
|
1630
|
-
const { target, call } = lookupPhrases(field, lookup, spec);
|
|
1631
|
-
const searchesByEmail = lookup.by.some((b) => EMAIL_LEAF.test(b));
|
|
1632
|
-
const isEmail = !!leaf && EMAIL_LEAF.test(leaf) && !valueIsEmail && !searchesByEmail;
|
|
1633
|
-
const message = `Connector step "${step}": \`${field}\` is a lookup field bound to the runtime expression `
|
|
1634
|
-
+ `\`${value.js}\`. At run time it must carry a \`${target}\` — the id the connector stores — `
|
|
1635
|
-
+ `not a name or an address: the connector rejects anything else when the flow runs, and `
|
|
1636
|
-
+ `neither compile nor validate can tell. Resolve the value you know instead:\n`
|
|
1637
|
-
+ ` ${call}\n`
|
|
1638
|
-
+ `then record it once with \`${prepareCommand(spec.key, spec.action)} `
|
|
1639
|
-
+ `--resolve ${resolveFlagArg(field, lookup.by[0] ?? 'id', '<value>')}\`; or read the id from a step whose `
|
|
1640
|
-
+ `output returned one (\`${lookup.value}\`).`
|
|
1641
|
-
+ (isEmail
|
|
1642
|
-
? `\n\`${single.raw}\` reads \`${leaf}\`, an e-mail address, not a \`${target}\`.`
|
|
1643
|
-
: '');
|
|
1644
|
-
return { level: isEmail ? 'error' : 'warning', code: 'LOOKUP_RUNTIME_VALUE', message, step };
|
|
1260
|
+
function checkConnectorSchema(step, spec, opts, diags) {
|
|
1261
|
+
const { where, report } = connectorSite(step, diags);
|
|
1262
|
+
coreCheckConnectorSchema(where, spec, opts, FLOW_EXPR_DIALECT, report);
|
|
1263
|
+
}
|
|
1264
|
+
function checkConnectorContract(step, spec, opts, diags) {
|
|
1265
|
+
const { where, report } = connectorSite(step, diags);
|
|
1266
|
+
coreCheckConnectorContract(where, spec, opts, report, step);
|
|
1645
1267
|
}
|
|
1268
|
+
/** Reject a wrapper hop when the runtime shape of a list has been measured. */
|
|
1646
1269
|
function checkLoopListEnvelope(loop, connectors, opts, diags) {
|
|
1647
1270
|
const read = /^\$vars\.([^.]+)\.output\.([A-Za-z_$][\w$]*)$/.exec(loop.collection.js);
|
|
1648
1271
|
if (!read || !opts.listEnvelope)
|
|
@@ -1980,305 +1603,6 @@ const INTERVAL_WORDS = [
|
|
|
1980
1603
|
* Without a library (`uip maestro flow check` run with no `--library`) the fields
|
|
1981
1604
|
* are unknown, so the warning keeps the generic wording.
|
|
1982
1605
|
*/
|
|
1983
|
-
/**
|
|
1984
|
-
* Ask the library what event parameters a subscription's operation declares.
|
|
1985
|
-
* Returns `undefined` when the subscription is too broken to look up (its own
|
|
1986
|
-
* diagnostics say so) or when no resolver was supplied — both mean "unknown", and
|
|
1987
|
-
* unknown fails open.
|
|
1988
|
-
*/
|
|
1989
|
-
function eventParamsFor(subscription, opts) {
|
|
1990
|
-
if (!opts.eventParams)
|
|
1991
|
-
return undefined;
|
|
1992
|
-
const sub = subscription;
|
|
1993
|
-
if (typeof sub?.connector !== 'string' || typeof sub?.event !== 'string')
|
|
1994
|
-
return undefined;
|
|
1995
|
-
if (sub.connector === '' || sub.event === '')
|
|
1996
|
-
return undefined;
|
|
1997
|
-
// A GENERIC event's schema is per OBJECT once the library carries baked
|
|
1998
|
-
// per-object variants, so the object is part of the subscription's identity
|
|
1999
|
-
// here. Without it the lookup asks for a node type that resolves to no
|
|
2000
|
-
// single variant and answers "unknown", which reads as EVENT_UNPREPARED on
|
|
2001
|
-
// an event the library actually knows.
|
|
2002
|
-
const object = typeof sub.object === 'string' && sub.object !== ''
|
|
2003
|
-
? sub.object : undefined;
|
|
2004
|
-
return opts.eventParams(sub.connector, sub.event, object);
|
|
2005
|
-
}
|
|
2006
|
-
/**
|
|
2007
|
-
* The event-side resolution rules — the trigger twin of `checkConnectorLookups`,
|
|
2008
|
-
* plus three rules with no activity counterpart:
|
|
2009
|
-
*
|
|
2010
|
-
* - `EVENT_UNKNOWN` — an ERROR: a library is loaded and it cannot resolve the
|
|
2011
|
-
* subscription, so `compile` will refuse it. The message is the resolver's
|
|
2012
|
-
* own (`eventResolveError`), the same text compile throws. Checked first; when
|
|
2013
|
-
* it fires, EVENT_UNPREPARED and the filter rules below are skipped for this
|
|
2014
|
-
* subscription, since there is no entry for them to read.
|
|
2015
|
-
* - `EVENT_UNPREPARED` — the event RESOLVES but the library knows nothing
|
|
2016
|
-
* about its parameters (or no `eventResolveError` resolver was supplied):
|
|
2017
|
-
* the fallback case for a connector the shipped library was not generated
|
|
2018
|
-
* against. A warning, because compile still succeeds. Prints the one
|
|
2019
|
-
* command that makes the event checkable.
|
|
2020
|
-
* - `EVENT_BYOA_UNVERIFIED` — an ERROR: the event only fires on a BYOA
|
|
2021
|
-
* (bring-your-own-app) connection, and the bound connection id has no record
|
|
2022
|
-
* of `prepare` having checked it (`core/byoa.ts` has the rule and when it
|
|
2023
|
-
* stays silent). A standard connection deploys and the trigger never fires;
|
|
2024
|
-
* the usual way in is binding the connection by preparing one of the
|
|
2025
|
-
* connector's ACTIONS, which checks nothing about BYOA. `compile` throws the
|
|
2026
|
-
* same text.
|
|
2027
|
-
* - `EVENT_FILTER_UNKNOWN_FIELD` — a filter leaf naming a field outside the
|
|
2028
|
-
* prepared vocabulary is an ERROR, not a warning, because the platform's own
|
|
2029
|
-
* configure DROPS an unmatched leaf and reports Success: the deployed
|
|
2030
|
-
* trigger then fires on events the filter should exclude, silently. Matching
|
|
2031
|
-
* is case-sensitive, so the near-miss hint checks case first. When the
|
|
2032
|
-
* vocabulary is unknown or known-empty this stays quiet — fail open.
|
|
2033
|
-
*
|
|
2034
|
-
* Unresolved `where` lookups follow the activity rule exactly: a WARNING here
|
|
2035
|
-
* (compile repeats it as the hard failure), with the recorded answer looked up
|
|
2036
|
-
* under the event-form node type — the form `prepare` stores.
|
|
2037
|
-
*/
|
|
2038
|
-
function checkEventResolution(subscription, where, kind, opts, error, warn, eventParams) {
|
|
2039
|
-
const sub = subscription;
|
|
2040
|
-
if (!sub || typeof sub.connector !== 'string' || typeof sub.event !== 'string'
|
|
2041
|
-
|| sub.connector === '' || sub.event === '')
|
|
2042
|
-
return;
|
|
2043
|
-
// Captured because the guard above narrows `sub.connector` / `sub.event` only
|
|
2044
|
-
// in THIS scope: a property narrowing does not survive into the `filters`
|
|
2045
|
-
// callback below, where they are `unknown` again. The old messages hid that
|
|
2046
|
-
// by interpolating them into template literals, which accept anything.
|
|
2047
|
-
const connectorKey = sub.connector;
|
|
2048
|
-
const eventName = sub.event;
|
|
2049
|
-
// Exactly what `serialize` passes to `resolveEvent` — `sub.version` and
|
|
2050
|
-
// `sub.object` as the author wrote them — so the verdict is compile's.
|
|
2051
|
-
const unresolved = opts.eventResolveError?.(kind, {
|
|
2052
|
-
connector: connectorKey,
|
|
2053
|
-
event: eventName,
|
|
2054
|
-
...(typeof sub.version === 'string' ? { version: sub.version } : {}),
|
|
2055
|
-
...(typeof sub.object === 'string' ? { object: sub.object } : {}),
|
|
2056
|
-
});
|
|
2057
|
-
if (unresolved !== undefined) {
|
|
2058
|
-
error('EVENT_UNKNOWN', `${where}: ${unresolved}`);
|
|
2059
|
-
return;
|
|
2060
|
-
}
|
|
2061
|
-
const object = typeof sub.object === 'string' && sub.object !== '' ? sub.object : undefined;
|
|
2062
|
-
const byoa = opts.eventFacts && byoaUnverified(where, { connector: connectorKey, event: eventName, ...(object ? { object } : {}), connection: sub.connection }, opts.eventFacts(connectorKey, eventName, object), opts.byoa, opts.bindings);
|
|
2063
|
-
if (byoa)
|
|
2064
|
-
error(EVENT_BYOA_UNVERIFIED, byoa);
|
|
2065
|
-
if (eventParams?.objectUnprepared && object) {
|
|
2066
|
-
// The generic template answers for every object, so resolution SUCCEEDS
|
|
2067
|
-
// and nothing downstream notices that the object named here has no
|
|
2068
|
-
// schema: no unprepared warning, and a filter vocabulary that fails open.
|
|
2069
|
-
// That is the silent case this whole surface exists to remove, one level
|
|
2070
|
-
// up from the activity it was first found on.
|
|
2071
|
-
warn('OBJECT_UNPREPARED', `${where}: object \`${object}\` has no prepared schema for `
|
|
2072
|
-
+ `\`${sub.connector}\` \`${sub.event}\`, and this connector's objects come from `
|
|
2073
|
-
+ `the connection, so the library cannot carry one. Materialize it:\n`
|
|
2074
|
-
+ ` ${prepareCommand(connectorKey, eventName)} --object ${object}\n`
|
|
2075
|
-
+ `Until then its event parameters and filterable fields are unknown and `
|
|
2076
|
-
+ `nothing here can check them.`);
|
|
2077
|
-
}
|
|
2078
|
-
if (opts.eventParams && eventParams === undefined) {
|
|
2079
|
-
warn('EVENT_UNPREPARED', `${where}: the library knows nothing about connector event \`${sub.connector}\` \`${sub.event}\` — `
|
|
2080
|
-
+ `its parameters and filterable fields cannot be verified offline. Prepare it against a live connection:\n`
|
|
2081
|
-
+ ` ${prepareCommand(connectorKey, eventName)}\n`
|
|
2082
|
-
+ `and re-run check: the prepared overlay makes this event checkable.`);
|
|
2083
|
-
}
|
|
2084
|
-
const resolutions = opts.resolutions ?? {};
|
|
2085
|
-
const scope = sub.where && typeof sub.where === 'object'
|
|
2086
|
-
? Object.entries(sub.where) : [];
|
|
2087
|
-
for (const [key, value] of scope) {
|
|
2088
|
-
if (!isLookupToken(value))
|
|
2089
|
-
continue;
|
|
2090
|
-
if (resolvedValue(value, resolutions) !== undefined)
|
|
2091
|
-
continue;
|
|
2092
|
-
warn('LOOKUP_UNRESOLVED', `${where}: \`where.${key}\` ` + unresolvedLookupMessage(value)
|
|
2093
|
-
+ `\nCompile will fail until this is recorded.`);
|
|
2094
|
-
}
|
|
2095
|
-
const names = opts.eventFilterFields?.(sub.connector, sub.event, object);
|
|
2096
|
-
if (names && names.length > 0 && Array.isArray(sub.filters)) {
|
|
2097
|
-
const known = new Set(names);
|
|
2098
|
-
sub.filters.forEach((leaf, i) => {
|
|
2099
|
-
const { read } = readEventFilter(leaf);
|
|
2100
|
-
if (!read || known.has(read.field))
|
|
2101
|
-
return;
|
|
2102
|
-
const caseSlip = names.find((n) => n.toLowerCase() === read.field.toLowerCase());
|
|
2103
|
-
// A case slip is a mistake whatever the vocabulary's completeness — the
|
|
2104
|
-
// platform matches case-sensitively, so `Subject` is dropped even where
|
|
2105
|
-
// `subject` exists. An unrecognized name is only certainly wrong when
|
|
2106
|
-
// the vocabulary is COMPLETE; where required selectors decide the field
|
|
2107
|
-
// set and nothing can supply them, refusing a legal field would be the
|
|
2108
|
-
// worse error, so it warns.
|
|
2109
|
-
const selectors = eventParams?.selectors ?? [];
|
|
2110
|
-
// A hybrid connector's custom filter fields are left for `prepare` BY
|
|
2111
|
-
// DESIGN, so the baked list is a strict subset of the tenant's and a
|
|
2112
|
-
// name outside it may be perfectly real. Erroring there tells the author
|
|
2113
|
-
// the platform will drop a field it will actually honour, and the only
|
|
2114
|
-
// way out is deleting a correct filter — "not in my snapshot" mistaken
|
|
2115
|
-
// for "wrong", which is the trap CUSTOM_FIELDS_UNPREPARED was narrowed
|
|
2116
|
-
// around for activities. Same shape here, and the same remedy: name the
|
|
2117
|
-
// prepare that closes the gap.
|
|
2118
|
-
if (eventParams?.customFieldsUnprepared && !caseSlip) {
|
|
2119
|
-
warn('CUSTOM_FIELDS_UNPREPARED', `${where}: filters[${i}] names \`${read.field}\`, which is not in the `
|
|
2120
|
-
+ `tenant-agnostic vocabulary for \`${sub.connector}\` \`${sub.event}\`. This `
|
|
2121
|
-
+ `connector discovers CUSTOM FIELDS per connection, so the baked list is a `
|
|
2122
|
-
+ `subset by design and this name may be a real field of yours. Prepare the `
|
|
2123
|
-
+ `event against your connection to check it for real:\n`
|
|
2124
|
-
+ ` ${prepareCommand(connectorKey, eventName)}`
|
|
2125
|
-
+ `${object ? ` --object ${object}` : ''}\n`
|
|
2126
|
-
+ `Baked fields: ${names.slice(0, 12).join(', ')}${names.length > 12 ? ', …' : ''}.`);
|
|
2127
|
-
return;
|
|
2128
|
-
}
|
|
2129
|
-
// Same shape as the branch above, for the gap preparing cannot close.
|
|
2130
|
-
// `customFieldsUnprepared` says "run prepare and this resolves";
|
|
2131
|
-
// `filtersPartial` says "no call reaches these fields at all", so the
|
|
2132
|
-
// remedy is the designer, not another command.
|
|
2133
|
-
//
|
|
2134
|
-
// Yields to the selector branch below, which reaches the same WARNING by
|
|
2135
|
-
// a more specific route: where `project` / `issuetype` select the schema
|
|
2136
|
-
// it can name them, and "these two values decide the field set" is more
|
|
2137
|
-
// use to an author than "this connector reads its schema from the
|
|
2138
|
-
// connection". Both are true of Jira; the named one is the better message.
|
|
2139
|
-
if (eventParams?.filtersPartial && !caseSlip && selectors.length === 0) {
|
|
2140
|
-
warn('EVENT_FILTER_UNKNOWN_FIELD', `${where}: filters[${i}] names \`${read.field}\`, which is not in the known `
|
|
2141
|
-
+ `filter vocabulary for \`${sub.connector}\` \`${sub.event}\`. This connector `
|
|
2142
|
-
+ `discovers custom fields per connection, and the trigger metadata call takes no `
|
|
2143
|
-
+ `parent values (\`uip is triggers describe\` has no \`-f\`), so the list below is a `
|
|
2144
|
-
+ `subset NO prepare can complete — this name may be a real field of yours. `
|
|
2145
|
-
+ `Verify it against the trigger in the designer before deploying.\n`
|
|
2146
|
-
+ `Known fields: ${names.slice(0, 12).join(', ')}${names.length > 12 ? ', …' : ''}.`);
|
|
2147
|
-
return;
|
|
2148
|
-
}
|
|
2149
|
-
const baseline = selectors.length > 0 && !caseSlip;
|
|
2150
|
-
(baseline ? warn : error)('EVENT_FILTER_UNKNOWN_FIELD', `${where}: filters[${i}] names \`${read.field}\`, which is not a filterable field of `
|
|
2151
|
-
+ `\`${sub.connector}\` \`${sub.event}\`. The platform DROPS an unmatched leaf and the deployed `
|
|
2152
|
-
+ `trigger fires on events this filter should exclude. `
|
|
2153
|
-
+ (caseSlip
|
|
2154
|
-
? `Did you mean \`${caseSlip}\`? Matching is case-sensitive.`
|
|
2155
|
-
: `Filterable fields: ${names.slice(0, 12).join(', ')}${names.length > 12 ? ', …' : ''}.`)
|
|
2156
|
-
+ (baseline
|
|
2157
|
-
? ` This is a WARNING rather than an error because \`${selectors.join('`, `')}\` `
|
|
2158
|
-
+ `select this event's schema and no discovery call accepts their values, so the `
|
|
2159
|
-
+ `list above is a baseline the real vocabulary extends. Verify the name against `
|
|
2160
|
-
+ `the trigger in the designer before deploying.`
|
|
2161
|
-
: ''));
|
|
2162
|
-
});
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
function checkEventSubscription(subscription, where, at, warn, eventParams) {
|
|
2166
|
-
const sub = subscription;
|
|
2167
|
-
if (!sub || typeof sub !== 'object') {
|
|
2168
|
-
at('EVENT_NO_SUBSCRIPTION', `${where} has no event subscription. Pass one: `
|
|
2169
|
-
+ `{ connector: 'uipath-microsoft-outlook365', event: 'email-received', where: { parentFolderId: '<id>' } }.`);
|
|
2170
|
-
return;
|
|
2171
|
-
}
|
|
2172
|
-
if (typeof sub.connector !== 'string' || sub.connector === '') {
|
|
2173
|
-
at('EVENT_NO_CONNECTOR', `${where} has no \`connector\` key (e.g. 'uipath-microsoft-outlook365').`);
|
|
2174
|
-
}
|
|
2175
|
-
if (typeof sub.event !== 'string' || sub.event === '') {
|
|
2176
|
-
at('EVENT_NO_EVENT', `${where} has no \`event\` name (e.g. 'email-received').`);
|
|
2177
|
-
}
|
|
2178
|
-
const scope = sub.where;
|
|
2179
|
-
const scopeEntries = scope && typeof scope === 'object'
|
|
2180
|
-
? Object.entries(scope)
|
|
2181
|
-
.filter(([, v]) => v !== undefined && v !== null && String(v) !== '')
|
|
2182
|
-
: [];
|
|
2183
|
-
const scopeKeys = scopeEntries.map(([k]) => k);
|
|
2184
|
-
// The object an author put in `where` by mistake — `objectName` is what the
|
|
2185
|
-
// platform's own `node configure --detail` calls it, so it is the natural slip.
|
|
2186
|
-
const scopeObject = scopeEntries.find(([k]) => /^(objectName|entityName|object)$/i.test(k))?.[1];
|
|
2187
|
-
const object = typeof sub.object === 'string' && sub.object !== '' ? sub.object : undefined;
|
|
2188
|
-
const say = warn ?? at;
|
|
2189
|
-
// The OBJECT half of the contract. A GENERIC event (`record-created` on Data
|
|
2190
|
-
// Fabric, Salesforce, ServiceNow, Jira, …) fires for ONE object of the connection
|
|
2191
|
-
// and its library entry names none — so `object` is REQUIRED, and its absence is
|
|
2192
|
-
// an error: the node would carry `objectName: ""`, validate, and be refused by
|
|
2193
|
-
// the platform's dispatcher (or watch nothing). A CURATED event has its object
|
|
2194
|
-
// built in, so an `object` there names a choice that does not exist.
|
|
2195
|
-
if (eventParams?.generic) {
|
|
2196
|
-
if (object === undefined) {
|
|
2197
|
-
at('EVENT_GENERIC_NO_OBJECT', `${where} subscribes to a GENERIC event: \`${String(sub.connector)}\` \`${String(sub.event)}\` fires for ONE `
|
|
2198
|
-
+ `object of the connection (an entity, a table, a custom object) and the library carries no default. `
|
|
2199
|
-
+ `Name it with \`object\`.`
|
|
2200
|
-
+ (scopeObject !== undefined
|
|
2201
|
-
? ` \`where: { ${scopeKeys.join(', ')} }\` is not it — \`where\` holds event PARAMETERS, and this `
|
|
2202
|
-
+ `operation takes none; the value you put there is the object.`
|
|
2203
|
-
: ` \`uip is triggers objects ${String(sub.connector)} <EVENT> --connection-id <id>\` lists the objects `
|
|
2204
|
-
+ `the bound connection exposes.`), `object: '${scopeObject !== undefined ? String(scopeObject) : '<Object>'}'`);
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
else if (object !== undefined && eventParams?.known) {
|
|
2208
|
-
at('EVENT_OBJECT_NOT_GENERIC', `${where} passes \`object: '${object}'\`, but \`${String(sub.connector)}\` \`${String(sub.event)}\` is a `
|
|
2209
|
-
+ `CURATED event whose object is built in`
|
|
2210
|
-
+ (eventParams.object ? ` (\`${eventParams.object}\`)` : '')
|
|
2211
|
-
+ `. \`object\` names the object of a GENERIC event only (\`record-created\` and its kin); drop it here.`, undefined);
|
|
2212
|
-
}
|
|
2213
|
-
// The operation declares no event parameters: an empty `where` is the ONLY
|
|
2214
|
-
// correct authoring, and a non-empty one names fields the operation has not got.
|
|
2215
|
-
if (eventParams?.known && eventParams.names.length === 0) {
|
|
2216
|
-
if (scopeKeys.length > 0) {
|
|
2217
|
-
say('EVENT_SCOPE_NOT_TAKEN', `${where} passes \`where: { ${scopeKeys.join(', ')} }\`, but this connector operation takes NO event `
|
|
2218
|
-
+ `parameters — its library entry declares \`inputSchema.fields: []\`, and \`uip is triggers describe\` `
|
|
2219
|
-
+ `reports \`EventParameters: []\`. The subscription is scoped by the CONNECTION alone, so these keys `
|
|
2220
|
-
+ `are carried into \`detail.eventParameters\` and matched against nothing. `
|
|
2221
|
-
+ (eventParams.generic && scopeObject !== undefined
|
|
2222
|
-
? `The object this GENERIC event watches goes in \`object\`, not \`where\`.`
|
|
2223
|
-
: `Filter on the PAYLOAD instead if you meant to narrow it.`), eventParams.generic && scopeObject !== undefined
|
|
2224
|
-
? `object: '${String(scopeObject)}'`
|
|
2225
|
-
: `where: {}`);
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
else if (scopeKeys.length === 0 && !eventParams?.generic) {
|
|
2229
|
-
// Whether omitting the scope is a mistake is not knowable here — the platform
|
|
2230
|
-
// accepts an unscoped subscription (it listens to everything the connection can
|
|
2231
|
-
// see) and 12 deployed Outlook flows that do exactly that scored 1.0. Name the
|
|
2232
|
-
// fields the operation actually has and let the author decide.
|
|
2233
|
-
const fields = eventParams?.known && eventParams.names.length > 0
|
|
2234
|
-
? `Its event parameters are: ${eventParams.names.map((n) => `\`${n}\``).join(', ')}`
|
|
2235
|
-
: "\`where\` carries the connector's event parameters — for Outlook email-received that is the mail folder";
|
|
2236
|
-
say('EVENT_NO_SCOPE', `${where} has no \`where\`, so its subscription is UNSCOPED: it listens to everything the bound `
|
|
2237
|
-
+ `connection can see, not the one thing you named. That deploys and runs — it is broader than you `
|
|
2238
|
-
+ `probably meant, which is why this is a warning and not an error. ${fields}. `
|
|
2239
|
-
+ `The names and their ids come from the bound connection: `
|
|
2240
|
-
+ `\`uip is triggers describe <connector> <EVENT> <object> --connection-id <id>\` lists them, and `
|
|
2241
|
-
+ `\`uip is resources run list <connector> MailFolder --connection-id <id>\` resolves a folder id. `
|
|
2242
|
-
+ `If the operation takes no event parameters at all — HTTP Webhook is one — an empty \`where\` `
|
|
2243
|
-
+ `is correct, and with a library on hand this warning does not fire.`, eventParams?.known && eventParams.names.length > 0
|
|
2244
|
-
? `where: { ${eventParams.names[0]}: '<id>' }`
|
|
2245
|
-
: `where: { parentFolderId: '<mail folder id>' }`);
|
|
2246
|
-
}
|
|
2247
|
-
const filters = sub.filters;
|
|
2248
|
-
if (filters !== undefined) {
|
|
2249
|
-
if (!Array.isArray(filters)) {
|
|
2250
|
-
at('EVENT_FILTERS_NOT_ARRAY', `${where}'s \`filters\` must be an ARRAY of { field, <operator>: value } — filters are positional and `
|
|
2251
|
-
+ `carry author-supplied field names, so an object map cannot express them.`, `filters: [{ field: 'subject', contains: 'Report' }]`);
|
|
2252
|
-
}
|
|
2253
|
-
else {
|
|
2254
|
-
const operators = Object.keys(EVENT_FILTER_OPERATORS).join(', ');
|
|
2255
|
-
filters.forEach((f, i) => {
|
|
2256
|
-
const leaf = f;
|
|
2257
|
-
if (!leaf || typeof leaf.field !== 'string' || leaf.field === '') {
|
|
2258
|
-
at('EVENT_FILTER_NO_FIELD', `${where}'s filters[${i}] has no \`field\` — name a field on the event payload, e.g. 'subject'.`);
|
|
2259
|
-
}
|
|
2260
|
-
const { keys, read } = readEventFilter(leaf);
|
|
2261
|
-
if (keys.length === 0) {
|
|
2262
|
-
at('EVENT_FILTER_NO_VALUE', `${where}'s filters[${i}] has no operator. Write exactly one of ${operators} with its value — `
|
|
2263
|
-
+ `\`contains\` is a case-sensitive substring test, the same one Integration Service applies `
|
|
2264
|
-
+ `server-side; \`lessThan\`/\`greaterThan\` order a number or an ISO date.`, `{ field: '${typeof leaf?.field === 'string' ? leaf.field : 'subject'}', contains: '<text>' }`);
|
|
2265
|
-
}
|
|
2266
|
-
else if (keys.length > 1) {
|
|
2267
|
-
at('EVENT_FILTER_MANY_OPERATORS', `${where}'s filters[${i}] carries ${keys.length} operators (${keys.join(', ')}); a condition has exactly `
|
|
2268
|
-
+ `one. Write two conditions instead — they are ANDed.`);
|
|
2269
|
-
}
|
|
2270
|
-
else if (!read) {
|
|
2271
|
-
at('EVENT_FILTER_NO_VALUE', `${where}'s filters[${i}] gives \`${keys[0]}\` a value that is not a string, number or boolean.`);
|
|
2272
|
-
}
|
|
2273
|
-
else {
|
|
2274
|
-
const problem = eventFilterProblem(read);
|
|
2275
|
-
if (problem)
|
|
2276
|
-
at('EVENT_FILTER_BAD_VALUE', `${where}'s filters[${i}] on \`${read.field}\`: ${problem}`);
|
|
2277
|
-
}
|
|
2278
|
-
});
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
1606
|
/**
|
|
2283
1607
|
* `{ updates }` writes a declared flow variable when the node completes. The
|
|
2284
1608
|
* assignable targets are `.var()` variables and flow outputs — assigning to an
|
|
@@ -2338,7 +1662,7 @@ function checkTrigger(spec, diags, opts = {}) {
|
|
|
2338
1662
|
const sub = spec.subscription;
|
|
2339
1663
|
checkEventSubscription(sub, 'trigger', at, push('warning'), eventParamsFor(sub, opts));
|
|
2340
1664
|
checkEventResolution(sub, 'trigger', 'trigger', opts, at, push('warning'), eventParamsFor(sub, opts));
|
|
2341
|
-
checkBindingLabels('event trigger', sub, opts, at, eventTarget(sub), {
|
|
1665
|
+
checkBindingLabels('event trigger', sub, opts, at, eventTarget(sub), { id: 'trigger', connector: eventTarget(sub)?.key });
|
|
2342
1666
|
return;
|
|
2343
1667
|
}
|
|
2344
1668
|
// manual() is the explicit spelling of the default — nothing to validate.
|
|
@@ -3510,7 +2834,7 @@ export function jsonTextDirective(text, returnKeys) {
|
|
|
3510
2834
|
* ALSO asks for JSON text makes the model pack its whole answer, serialized, into
|
|
3511
2835
|
* one string field. See {@link jsonTextDirective}.
|
|
3512
2836
|
*/
|
|
3513
|
-
function checkInlineAgent(step, inputs, diags) {
|
|
2837
|
+
function checkInlineAgent(step, inputs, diags, opts = {}) {
|
|
3514
2838
|
const at = (code, message, suggestion) => {
|
|
3515
2839
|
diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
|
|
3516
2840
|
};
|
|
@@ -3793,6 +3117,24 @@ function checkInlineAgent(step, inputs, diags) {
|
|
|
3793
3117
|
at('INLINE_AGENT_TOOL_NO_OPERATION', `Inline agent step "${step}" declares a connector tool without ${blank(c.connector) ? 'a `connector`' : 'an `operation`'}. `
|
|
3794
3118
|
+ `Both name the curated-library entry the tool is resolved from — the same pair \`connector(…)\` takes.`, `{ kind: 'connector', connector: 'uipath-atlassian-jira', operation: 'create-issue' }`);
|
|
3795
3119
|
}
|
|
3120
|
+
// A connector tool calls Integration Service on a CONNECTION, exactly as a
|
|
3121
|
+
// connector step does — and before #867 there was no way to give it one, so
|
|
3122
|
+
// the tool compiled with none anywhere (no binding, no resource.json row)
|
|
3123
|
+
// and could only fail at run time. Both halves, as `registry prepare` writes them.
|
|
3124
|
+
const labels = t;
|
|
3125
|
+
const missing = ['connection', 'folder'].filter((k) => blank(labels[k]));
|
|
3126
|
+
if (missing.length > 0) {
|
|
3127
|
+
const key = blank(c.connector) ? '<connector-key>' : String(c.connector);
|
|
3128
|
+
const op = blank(c.operation) ? '<operation>' : String(c.operation);
|
|
3129
|
+
at('INLINE_AGENT_TOOL_CONNECTOR_NO_CONNECTION', `Inline agent step "${step}"'s connector tool "${key}/${op}" has no ${missing.map((m) => `\`${m}\``).join(' or ')}. `
|
|
3130
|
+
+ `The tool calls Integration Service on a tenant connection: without one the agent's tool has no `
|
|
3131
|
+
+ `connection binding, its resource.json carries none, and the solution's Resources never list it. `
|
|
3132
|
+
+ `Name the bindings.json labels the same way \`connector()\` takes them — \`${prepareCommand(key, op)}\` `
|
|
3133
|
+
+ `finds the connection and writes both entries.`, `{ kind: 'connector', connector: '${key}', operation: '${op}', connection: '<connection label>', folder: '<folder label>' }`);
|
|
3134
|
+
}
|
|
3135
|
+
else {
|
|
3136
|
+
checkBindingLabels(`Inline agent step "${step}"'s connector tool`, labels, opts, (code, message) => at(code, message), blank(c.connector) ? undefined : { key: String(c.connector), ...(blank(c.operation) ? {} : { action: String(c.operation) }) });
|
|
3137
|
+
}
|
|
3796
3138
|
}
|
|
3797
3139
|
else if (kind === 'ixp') {
|
|
3798
3140
|
const x = t;
|