@uipath/maestro-builder-sdk 6.14.5 → 6.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/api-index.md +71 -71
  2. package/dist/api-members.md +324 -323
  3. package/dist/bpmn/bpmn-sdk.d.ts +16 -0
  4. package/dist/bpmn/bpmn-sdk.js +2 -0
  5. package/dist/bpmn/check-cli.js +22 -5
  6. package/dist/bpmn/check.d.ts +15 -1
  7. package/dist/bpmn/check.js +69 -2
  8. package/dist/bpmn/compile-cli.js +24 -14
  9. package/dist/bpmn/connector.js +29 -14
  10. package/dist/bpmn/decompile-cli.js +3 -3
  11. package/dist/bpmn/intsvc.js +5 -2
  12. package/dist/bpmn/serialize.d.ts +2 -0
  13. package/dist/bpmn/serialize.js +1 -1
  14. package/dist/case/compile-cli.js +6 -9
  15. package/dist/case/decompile-cli.js +2 -2
  16. package/dist/case/serialize.js +1 -1
  17. package/dist/check-cli.js +6 -35
  18. package/dist/check.d.ts +16 -148
  19. package/dist/check.js +33 -709
  20. package/dist/cli/commands/bpmn.js +15 -0
  21. package/dist/cli-library.d.ts +6 -25
  22. package/dist/cli-library.js +6 -73
  23. package/dist/core/cli-library.d.ts +47 -0
  24. package/dist/core/cli-library.js +150 -0
  25. package/dist/core/connector-checks.d.ts +226 -0
  26. package/dist/core/connector-checks.js +518 -0
  27. package/dist/core/connector-inputs.d.ts +80 -0
  28. package/dist/core/connector-inputs.js +453 -0
  29. package/dist/core/event-checks.d.ts +137 -0
  30. package/dist/core/event-checks.js +321 -0
  31. package/dist/decompile-cli.js +2 -2
  32. package/dist/decompile.js +1 -1
  33. package/dist/registry/generators/markdown/sections/example.d.ts +7 -1
  34. package/dist/registry/generators/markdown/sections/example.js +70 -5
  35. package/dist/registry/generators/markdown/sections/lookups.js +29 -8
  36. package/dist/registry/prepare/messages.js +6 -1
  37. package/dist/registry/prepare/source-import.d.ts +3 -3
  38. package/dist/registry/prepare/source-import.js +15 -4
  39. package/dist/registry/prepare/source-labels.js +31 -4
  40. package/dist/serialize.d.ts +11 -11
  41. package/dist/serialize.js +27 -420
  42. package/dist/workdir.d.ts +10 -0
  43. package/dist/workdir.js +14 -1
  44. package/package.json +2 -2
  45. /package/dist/{event-filters.d.ts → core/event-filters.d.ts} +0 -0
  46. /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 { EVENT_FILTER_OPERATORS, readEventFilter, eventFilterProblem } from './event-filters.js';
13
- import { extractRefs } from './core/expr-check.js';
14
- import { bindingSelfNameMessage, bindingUndeclaredMessage, connectionConnectorConflictMessage, undeclaredBinding } from './core/binding-messages.js';
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
- const outer = connectionUsage;
24
- connectionUsage = [];
25
- try {
26
- const diags = checkFlow(built, [], opts);
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
- finally {
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
- { step: httpStep, connector: typeof target === 'string' && target !== '' ? target : 'uipath-uipath-http' });
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 ?? []) {
@@ -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), { step: stepName, connector: eventTarget(s.spec.subscription)?.key });
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 binding-label rules `serialize`'s `resolveBinding` enforces, for one
1514
- * `{ connection, folder }` pair — reported at every site that resolves one
1515
- * (connector step, event trigger and `.entryPoint` onEvent, waitForEvent, managed
1516
- * HTTP), with the sentence compile throws. Without this at the event and HTTP
1517
- * sites `check` printed `✓ no issues` for a source `compile` then refused.
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 checkBindingLabels(where, labels, opts, report, target,
1523
- /** The step, and the connector its connection must belong to, for CONNECTION_CONNECTOR_CONFLICT. */
1524
- use) {
1525
- const label = labels?.connection;
1526
- if (use?.connector && typeof label === 'string' && label !== '') {
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
- * The two phrases every lookup diagnostic needs: what the field sends, and how
1550
- * to resolve it.
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
- * `senderEmail`, `emailAddress`, `e_mail`, `userPrincipalName` — an address. Applied
1578
- * to the LEAF an expression reads, and to the lookup's own `value` / `by` names,
1579
- * because whether an address is wrong depends on what the field sends: SendGrid's
1580
- * `from` sends `from_email`, PagerDuty's `From` sends `email` — there an e-mail IS
1581
- * the id. `emailId` is left to the id rule.
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), { step: 'trigger', connector: eventTarget(sub)?.key });
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.