@uipath/maestro-builder-sdk 6.68.3 → 6.68.5

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.
@@ -29,9 +29,9 @@ import { defaultBindingsFile } from '../workdir.js';
29
29
  import { flagValue, runWhenInvokedDirectly } from '../cli-run.js';
30
30
  import { ensureTypeScriptRuntime } from '../node-runtime.js';
31
31
  import { CASE_FORMAT_PROFILE } from './format-profile.js';
32
- import { isInSolutionKey, SOLUTION_FOLDER } from './resource-identity.js';
32
+ import { deriveCaseBindingsV2 } from './bindings-v2.js';
33
+ import { BINDINGS_V2_DOCUMENT_VERSION } from '../core/bindings-v2-resource.js';
33
34
  import { sha256Uuid } from '../core/stable-id.js';
34
- import { isRecord } from '../core/node-envelope.js';
35
35
  export async function run(argv) {
36
36
  if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
37
37
  console.error('usage: compile <case.ts | BaseName> [-o <project>/caseplan.case]');
@@ -315,210 +315,16 @@ function syncEntryPoints(caseplan, outPath) {
315
315
  * artifact. `resources refresh` reads bindings_v2.json, not caseplan.json, and
316
316
  * succeeds with zero warnings when the sidecar is stale or empty.
317
317
  *
318
- * Non-connector resources come from the root binding rows only, as in
319
- * `uip maestro case bindings sync`.
320
- * A connector's connection and folder relationship comes from the context[]
321
- * that references both binding ids.
322
- * No-op when compiling outside a scaffolded project.
318
+ * The entries are what `uip maestro case bindings sync` derives
319
+ * ({@link deriveCaseBindingsV2}). No-op when compiling outside a scaffolded
320
+ * project.
323
321
  */
324
322
  function syncBindingsV2(caseplan, outPath) {
325
323
  const sidecarPath = join(dirname(outPath), 'bindings_v2.json');
326
324
  if (!existsSync(sidecarPath))
327
325
  return -1;
328
- const bindings = Array.isArray(caseplan.bindings)
329
- ? caseplan.bindings.filter(isRecord)
330
- : [];
331
- const byId = new Map(bindings
332
- .filter((binding) => typeof binding.id === 'string')
333
- .map((binding) => [binding.id, binding]));
334
- const resources = [];
335
- // One entry per (resource, resourceKey) group of root binding rows, in the
336
- // shape `uip maestro case bindings sync` derives (`deriveCaseBindingsV2` in
337
- // cli case-bindings-service.ts). `validate --strict` compares the two
338
- // entry by entry as serialized JSON, so field order matters as well as
339
- // field values.
340
- const groups = new Map();
341
- for (const binding of bindings) {
342
- const resource = stringValue(binding.resource);
343
- const key = stringValue(binding.resourceKey);
344
- if (!resource || !key || typeof binding.propertyAttribute !== 'string')
345
- continue;
346
- if (resource.toLowerCase() === 'connection')
347
- continue;
348
- const groupKey = `${resource}\0${key}`;
349
- const group = groups.get(groupKey);
350
- if (group)
351
- group.push(binding);
352
- else
353
- groups.set(groupKey, [binding]);
354
- }
355
- const seenConnections = new Set();
356
- const connectionResources = [];
357
- visitRecords(caseplan, (record) => {
358
- if (!Array.isArray(record.context))
359
- return;
360
- const context = new Map(record.context
361
- .filter(isRecord)
362
- .filter((row) => typeof row.name === 'string')
363
- .map((row) => [row.name, row.value]));
364
- const connector = stringValue(context.get('connectorKey'));
365
- const connectionId = referencedBindingValue(context.get('connection'), byId);
366
- const folderKey = referencedBindingValue(context.get('folderKey'), byId);
367
- if (!connector || !connectionId || !folderKey || seenConnections.has(connectionId))
368
- return;
369
- seenConnections.add(connectionId);
370
- connectionResources.push({
371
- resource: 'Connection',
372
- key: connectionId,
373
- value: {
374
- connectionId: { defaultValue: connectionId },
375
- folderKey: { defaultValue: folderKey },
376
- },
377
- metadata: { connector },
378
- });
379
- });
380
- const labels = bindingLabels(caseplan.nodes);
381
- for (const group of groups.values())
382
- resources.push(sidecarResourceEntry(group, labels));
383
- resources.push(...connectionResources);
384
- writeJsonFile(sidecarPath, { version: '2.0', resources });
326
+ const resources = deriveCaseBindingsV2(caseplan);
327
+ writeJsonFile(sidecarPath, { version: BINDINGS_V2_DOCUMENT_VERSION, resources });
385
328
  return resources.length;
386
329
  }
387
- /**
388
- * The Case Designer's label for each value slot of an app or process entry.
389
- * These resources get the Designer shape (cli#5003, MST-16424): an `id`, each
390
- * slot flagged and labelled, and the `BindingsVersion` / `SolutionsSupport`
391
- * metadata without which deploy cannot override the entry.
392
- */
393
- const DESIGNER_SLOT_NAMES = new Map([
394
- ['app', { name: 'App name', folderPath: 'Folder path', solutionsSupport: true }],
395
- ['process', { name: 'Process name', folderPath: 'Folder path', solutionsSupport: true }],
396
- ['ixpDeployment', { name: 'name', folderPath: 'Folder path', solutionsSupport: false }],
397
- ]);
398
- /** The bindings version deploy needs to override an entry. */
399
- const RESOURCE_BINDINGS_VERSION = '2.2';
400
- /**
401
- * One sidecar entry for a group of root binding rows that share a resource and
402
- * a resource key. The subtype comes from the rows only: a Flow's rows carry
403
- * none, so its entry has none either, as the Designer saves it.
404
- *
405
- * `metadata.ActivityName` labels a Designer-shaped entry with the first task
406
- * that references one of its rows ({@link bindingLabels}).
407
- */
408
- function sidecarResourceEntry(group, labels) {
409
- const resource = group[0].resource;
410
- const key = group[0].resourceKey;
411
- const attributeDefault = (attribute) => {
412
- const found = group.find((binding) => binding.propertyAttribute === attribute)?.default;
413
- return typeof found === 'string' ? found : undefined;
414
- };
415
- // An in-solution resource binds an empty folderPath in the caseplan, while
416
- // the sidecar names its folder by the sentinel the key carries.
417
- const folderPath = isInSolutionKey(key) ? SOLUTION_FOLDER : attributeDefault('folderPath');
418
- const name = attributeDefault('name');
419
- const subType = group.map((binding) => binding.resourceSubType).find((found) => found !== undefined);
420
- const designer = DESIGNER_SLOT_NAMES.get(resource);
421
- if (designer) {
422
- const value = {};
423
- if (folderPath !== undefined) {
424
- value.folderPath = { defaultValue: folderPath, isExpression: false, displayName: designer.folderPath };
425
- }
426
- if (name !== undefined) {
427
- value.name = { defaultValue: name, isExpression: false, displayName: designer.name };
428
- }
429
- const activityName = group
430
- .map((binding) => (typeof binding.id === 'string' ? labels.get(binding.id) : undefined))
431
- .find((found) => found !== undefined);
432
- return {
433
- resource,
434
- key,
435
- id: `${resource}${key}`,
436
- value,
437
- metadata: {
438
- ...(activityName !== undefined && { ActivityName: activityName }),
439
- BindingsVersion: RESOURCE_BINDINGS_VERSION,
440
- DisplayLabel: 'name',
441
- ...(designer.solutionsSupport && { SolutionsSupport: 'true' }),
442
- ...(subType !== undefined && { subType }),
443
- },
444
- };
445
- }
446
- const value = {};
447
- if (name !== undefined)
448
- value.name = { defaultValue: name };
449
- if (folderPath !== undefined)
450
- value.folderPath = { defaultValue: folderPath };
451
- // A solution-resource picker (such as a Data Fabric entity) carries its folder key as a slot of its own.
452
- const folderKey = attributeDefault('folderKey');
453
- if (folderKey !== undefined)
454
- value.folderKey = { defaultValue: folderKey };
455
- return {
456
- resource,
457
- key,
458
- value,
459
- metadata: {
460
- BindingsVersion: RESOURCE_BINDINGS_VERSION,
461
- ...(subType !== undefined && { subType }),
462
- },
463
- };
464
- }
465
- /**
466
- * The label of the first object that references each binding id
467
- * (`=bindings.<id>`), in document order: the nearest enclosing `displayName`,
468
- * `display.label` or `label`. The same walk as the CLI's
469
- * `collectBindingLabels`, so both name an entry after the same task.
470
- */
471
- function bindingLabels(nodes) {
472
- const found = new Map();
473
- const ownLabel = (node) => {
474
- const display = isRecord(node.display) ? node.display.label : undefined;
475
- return [node.displayName, display, node.label].map(stringValue).find((label) => label !== undefined);
476
- };
477
- const walk = (node, label) => {
478
- if (typeof node === 'string') {
479
- if (label !== undefined && node.startsWith('=bindings.')) {
480
- const id = node.slice('=bindings.'.length);
481
- if (!found.has(id))
482
- found.set(id, label);
483
- }
484
- return;
485
- }
486
- if (Array.isArray(node)) {
487
- for (const item of node)
488
- walk(item, label);
489
- return;
490
- }
491
- if (!isRecord(node))
492
- return;
493
- const scoped = ownLabel(node) ?? label;
494
- for (const value of Object.values(node))
495
- walk(value, scoped);
496
- };
497
- walk(nodes ?? [], undefined);
498
- return found;
499
- }
500
- function referencedBindingValue(value, bindings) {
501
- const ref = stringValue(value);
502
- if (!ref)
503
- return undefined;
504
- if (!ref.startsWith('=bindings.'))
505
- return ref;
506
- const binding = bindings.get(ref.slice('=bindings.'.length));
507
- return binding && (stringValue(binding.default) ?? stringValue(binding.resourceKey));
508
- }
509
- function visitRecords(value, visit) {
510
- if (Array.isArray(value)) {
511
- for (const item of value)
512
- visitRecords(item, visit);
513
- return;
514
- }
515
- if (!isRecord(value))
516
- return;
517
- visit(value);
518
- for (const child of Object.values(value))
519
- visitRecords(child, visit);
520
- }
521
- function stringValue(value) {
522
- return typeof value === 'string' && value.length > 0 ? value : undefined;
523
- }
524
330
  runWhenInvokedDirectly(import.meta.url, 'compile', run);
@@ -1,7 +1,7 @@
1
1
  import { commentText } from '../core/comment-text.js';
2
2
  import { tsLiteral, tsNumber } from '../core/ts-literal.js';
3
3
  import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS } from './format-profile.js';
4
- import { EXTERNAL_TASK_CONTEXT, externalDescriptorRefusal, externalHybridRefusal, externalServiceType, isExternalExecutionMode } from './external.js';
4
+ import { EXTERNAL_TASK_CONTEXT, EXTERNAL_TASK_EVENT_CONTEXT, externalDescriptorRefusal, externalEventRefusal, externalServiceType, isExternalExecutionMode } from './external.js';
5
5
  import { preserveSchemaVersion } from '../schema-version.js';
6
6
  import { ownEntry } from '../core/own-entry.js';
7
7
  import { isUnrestorableSlotId, taskFingerprint } from './preserve.js';
@@ -618,7 +618,9 @@ function connectorCall(task, refs, library) {
618
618
  /**
619
619
  * Lift an External Agent / External Workflow task saved in the Case Designer's
620
620
  * shape (the one `serialize` emits): a connector activity's context and input
621
- * containers plus the object's `executionType`, which becomes `mode`. The
621
+ * containers plus the object's `executionType`, which becomes `mode`, and the
622
+ * event pair an async / hybrid object answers with (`operation` and `eventMode`
623
+ * rows), which becomes `event`. The
622
624
  * service type must be the one that execution type gives, so a plan whose two
623
625
  * disagree is refused rather than lifted to a source that would compile to
624
626
  * something else.
@@ -637,19 +639,20 @@ function externalCall(task, refs, library) {
637
639
  throw new Error(`${owner} data.bindings must be an empty array; bindings live at case root.`);
638
640
  if (!Array.isArray(data.context))
639
641
  throw new Error(`${owner} requires a resolved context array.`);
640
- // A hybrid task carries an event pair the SDK cannot write back (#1387): refuse
641
- // it by name, before its extra context rows are reported as unknown.
642
- if (data.context.some((row) => row?.name === 'executionType' && row.value === 'hybrid'))
643
- throw new Error(externalHybridRefusal(owner));
644
- // Exactly the context rows serialize writes (the Designer's set): an unknown
645
- // or repeated row would be dropped by the lift and lost on recompile.
642
+ // Exactly the context rows serialize writes (the Designer's set, plus the
643
+ // event pair an async / hybrid object answers with): an unknown or repeated
644
+ // row would be dropped by the lift and lost on recompile.
646
645
  const contextNames = data.context.map((row) => String(row?.name ?? ''));
647
- const unknownContext = contextNames.find((name, at) => !EXTERNAL_TASK_CONTEXT.includes(name) || contextNames.indexOf(name) !== at);
646
+ const unknownContext = contextNames.find((name, at) => !(EXTERNAL_TASK_CONTEXT.includes(name) || EXTERNAL_TASK_EVENT_CONTEXT.includes(name)) || contextNames.indexOf(name) !== at);
648
647
  if (unknownContext !== undefined)
649
648
  throw new Error(`${owner} has unsupported or repeated context row "${unknownContext}".`);
650
649
  const absentContext = EXTERNAL_TASK_CONTEXT.find((name) => !contextNames.includes(name));
651
650
  if (absentContext)
652
651
  throw new Error(`${owner} is missing context row "${absentContext}".`);
652
+ // The pair is written whole or not at all, so half of one cannot be lifted.
653
+ const eventRows = EXTERNAL_TASK_EVENT_CONTEXT.filter((name) => contextNames.includes(name));
654
+ if (eventRows.length === 1)
655
+ throw new Error(`${owner} carries context row "${eventRows[0]}" without "${EXTERNAL_TASK_EVENT_CONTEXT.find((name) => name !== eventRows[0])}"; the event pair is saved whole.`);
653
656
  const context = contextMap(data);
654
657
  const stringValue = (name) => {
655
658
  const row = context.get(name);
@@ -672,6 +675,18 @@ function externalCall(task, refs, library) {
672
675
  const serviceType = externalServiceType(kind, mode);
673
676
  if (data.serviceType !== serviceType)
674
677
  throw new Error(`${owner} has serviceType "${String(data.serviceType ?? '')}", but its executionType "${mode}" saves as "${serviceType}".`);
678
+ // The event the object answers with, as saved: the same rule compile applies,
679
+ // so a lifted task always compiles back (a hybrid task without one does not).
680
+ const event = eventRows.length ? { operation: stringValue('operation'), mode: stringValue('eventMode') } : undefined;
681
+ // A saved plan has no `event` option to drop, so a sync task with event rows
682
+ // gets its own text naming the rows; every other case is compile's rule.
683
+ if (mode === 'sync' && event)
684
+ throw new Error(`${owner} has context rows "operation" (${JSON.stringify(event.operation)}) and "eventMode" ` +
685
+ `(${JSON.stringify(event.mode)}) on executionType "sync"; a sync object answers inline and has no event. ` +
686
+ `Remove the two rows, or fix executionType and serviceType if the object is async or hybrid.`);
687
+ const eventRefused = externalEventRefusal(owner, mode, event);
688
+ if (eventRefused)
689
+ throw new Error(eventRefused);
675
690
  const identity = library.identifyAction({
676
691
  connectorKey: stringValue('connectorKey'),
677
692
  httpMethod: stringValue('method'),
@@ -691,6 +706,7 @@ function externalCall(task, refs, library) {
691
706
  ['connection', tsLiteral(binding('connection'))],
692
707
  ['folder', tsLiteral(binding('folderKey'))],
693
708
  ['mode', tsLiteral(mode)],
709
+ ['event', event ? opts([['operation', tsLiteral(event.operation)], ['mode', tsLiteral(event.mode)]]) : undefined],
694
710
  ['inputs', tsLiteral(inputs)],
695
711
  ]);
696
712
  const method = kind === 'external-agent' ? 'externalAgent' : 'externalWorkflow';
@@ -8,32 +8,43 @@
8
8
  * `buildConnectorInputs`, which `check` already runs as `connectorInputs`.
9
9
  */
10
10
  import type { ResolvedConnector } from '../core/library.js';
11
- import type { ExternalExecutionMode } from './case-sdk.js';
11
+ import type { ExternalEventPair, ExternalExecutionMode } from './case-sdk.js';
12
12
  /**
13
13
  * The context rows `serialize` writes on an external task: the set the Case
14
14
  * Designer saves for a sync object (`uip`'s
15
15
  * `tests/fixtures/external-tasks/designer-saved.json`, the Snowflake Cortex
16
- * agent). `decompile` refuses a task with any other row, since the lift would
17
- * drop it without a word. A `hybrid` task (which also carries `operation` and
18
- * `eventMode`) is refused before this check; see {@link externalHybridRefusal}.
16
+ * agent). An `async` or `hybrid` task may also carry
17
+ * {@link EXTERNAL_TASK_EVENT_CONTEXT}. `decompile` refuses a task with any other
18
+ * row, since the lift would drop it without a word.
19
19
  */
20
20
  export declare const EXTERNAL_TASK_CONTEXT: readonly string[];
21
- /** Check's code for {@link externalHybridRefusal}. */
22
- export declare const EXTERNAL_TASK_HYBRID_UNSUPPORTED = "EXTERNAL_TASK_HYBRID_UNSUPPORTED";
23
21
  /**
24
- * Why an external task on a `hybrid` object cannot be compiled or decompiled
25
- * yet. The Case Designer saves such a task with an event pair (context
26
- * `operation`, e.g. `TRIGGER_CREATED`, and `eventMode`, e.g. `polling`, also in
27
- * the metadata's `activityPropertyConfiguration`) that it reads from live
28
- * Integration Service metadata. The connector library does not carry that pair
29
- * (#1387), so the SDK cannot write the task faithfully, and a guessed pair could
30
- * deploy a task that never completes. `check`, `serialize` and `decompile` all
31
- * refuse with this text.
22
+ * The two context rows that carry the event an `async` / `hybrid` object
23
+ * answers with (`ExternalEventPair`): `operation` (e.g. `TRIGGER_CREATED`) and
24
+ * `eventMode` (e.g. `polling`). The Designer writes them after `objectName`,
25
+ * before `executionType` (the Power Automate flow in `designer-saved.json`).
26
+ */
27
+ export declare const EXTERNAL_TASK_EVENT_CONTEXT: readonly string[];
28
+ /** Check's and compile's code for {@link externalEventRefusal}. */
29
+ export declare const EXTERNAL_TASK_EVENT = "EXTERNAL_TASK_EVENT";
30
+ /** The event modes Integration Service reports for an object's events. */
31
+ export declare function isExternalEventMode(value: unknown): value is ExternalEventPair['mode'];
32
+ /**
33
+ * Why this task's event pair cannot be written, or `undefined`.
34
+ *
35
+ * A `hybrid` object answers through an event, and the Case Designer saves that
36
+ * event with the task (context `operation` and `eventMode`, and the same pair in
37
+ * the metadata's `activityPropertyConfiguration`). Without it the task has
38
+ * nothing to wait on, so `hybrid` needs the pair. An `async` object takes it
39
+ * when given. A `sync` object answers inline and has none, so a pair there is a
40
+ * wrong `mode` or a wrong pair. `check`, `serialize` and `decompile` all use this.
32
41
  *
33
42
  * @param owner - The task as the message names it, e.g. `External Workflow task "Run"`.
34
- * @returns The refusal.
43
+ * @param mode - The task's execution type (already known to be valid).
44
+ * @param event - The authored or saved pair, if any.
45
+ * @returns The refusal, or `undefined`.
35
46
  */
36
- export declare function externalHybridRefusal(owner: string): string;
47
+ export declare function externalEventRefusal(owner: string, mode: ExternalExecutionMode, event: unknown): string | undefined;
37
48
  /** The execution types an external task accepts: the values Integration Service reports. */
38
49
  export declare function isExternalExecutionMode(value: unknown): value is ExternalExecutionMode;
39
50
  /**
@@ -3,34 +3,61 @@ import { CASE_FORMAT_PROFILE } from './format-profile.js';
3
3
  * The context rows `serialize` writes on an external task: the set the Case
4
4
  * Designer saves for a sync object (`uip`'s
5
5
  * `tests/fixtures/external-tasks/designer-saved.json`, the Snowflake Cortex
6
- * agent). `decompile` refuses a task with any other row, since the lift would
7
- * drop it without a word. A `hybrid` task (which also carries `operation` and
8
- * `eventMode`) is refused before this check; see {@link externalHybridRefusal}.
6
+ * agent). An `async` or `hybrid` task may also carry
7
+ * {@link EXTERNAL_TASK_EVENT_CONTEXT}. `decompile` refuses a task with any other
8
+ * row, since the lift would drop it without a word.
9
9
  */
10
10
  export const EXTERNAL_TASK_CONTEXT = [
11
11
  'connectorKey', 'connection', 'resourceKey', 'folderKey', 'objectName', 'method', 'path',
12
12
  'executionType', 'metadata', 'activityConfigurationVersion',
13
13
  ];
14
- /** Check's code for {@link externalHybridRefusal}. */
15
- export const EXTERNAL_TASK_HYBRID_UNSUPPORTED = 'EXTERNAL_TASK_HYBRID_UNSUPPORTED';
16
14
  /**
17
- * Why an external task on a `hybrid` object cannot be compiled or decompiled
18
- * yet. The Case Designer saves such a task with an event pair (context
19
- * `operation`, e.g. `TRIGGER_CREATED`, and `eventMode`, e.g. `polling`, also in
20
- * the metadata's `activityPropertyConfiguration`) that it reads from live
21
- * Integration Service metadata. The connector library does not carry that pair
22
- * (#1387), so the SDK cannot write the task faithfully, and a guessed pair could
23
- * deploy a task that never completes. `check`, `serialize` and `decompile` all
24
- * refuse with this text.
15
+ * The two context rows that carry the event an `async` / `hybrid` object
16
+ * answers with (`ExternalEventPair`): `operation` (e.g. `TRIGGER_CREATED`) and
17
+ * `eventMode` (e.g. `polling`). The Designer writes them after `objectName`,
18
+ * before `executionType` (the Power Automate flow in `designer-saved.json`).
19
+ */
20
+ export const EXTERNAL_TASK_EVENT_CONTEXT = ['operation', 'eventMode'];
21
+ /** Check's and compile's code for {@link externalEventRefusal}. */
22
+ export const EXTERNAL_TASK_EVENT = 'EXTERNAL_TASK_EVENT';
23
+ /** The event modes Integration Service reports for an object's events. */
24
+ export function isExternalEventMode(value) {
25
+ return value === 'polling' || value === 'webhooks';
26
+ }
27
+ /**
28
+ * Why this task's event pair cannot be written, or `undefined`.
29
+ *
30
+ * A `hybrid` object answers through an event, and the Case Designer saves that
31
+ * event with the task (context `operation` and `eventMode`, and the same pair in
32
+ * the metadata's `activityPropertyConfiguration`). Without it the task has
33
+ * nothing to wait on, so `hybrid` needs the pair. An `async` object takes it
34
+ * when given. A `sync` object answers inline and has none, so a pair there is a
35
+ * wrong `mode` or a wrong pair. `check`, `serialize` and `decompile` all use this.
25
36
  *
26
37
  * @param owner - The task as the message names it, e.g. `External Workflow task "Run"`.
27
- * @returns The refusal.
38
+ * @param mode - The task's execution type (already known to be valid).
39
+ * @param event - The authored or saved pair, if any.
40
+ * @returns The refusal, or `undefined`.
28
41
  */
29
- export function externalHybridRefusal(owner) {
30
- return (`${owner} is on a "hybrid" object, which Case cannot compile yet: the connector library does not carry ` +
31
- `the event pair the Case Designer writes for it (operation, e.g. "TRIGGER_CREATED", and eventMode, e.g. "polling"), ` +
32
- `so the task cannot be written faithfully. Add this task in Studio Web for now; ` +
33
- `https://github.com/UiPath/builder-sdk/issues/1387 tracks the library change.`);
42
+ export function externalEventRefusal(owner, mode, event) {
43
+ if (event === undefined) {
44
+ if (mode !== 'hybrid')
45
+ return undefined;
46
+ return (`${owner} is on a "hybrid" object, which answers through an event: pass the event the object reports as ` +
47
+ `event: { operation, mode } (for example { operation: "TRIGGER_CREATED", mode: "polling" }, ` +
48
+ `the "operation" and "eventMode" rows of the case spec result's caseShape.context). Without it the task has nothing to wait on.`);
49
+ }
50
+ if (mode === 'sync')
51
+ return `${owner} is on a "sync" object, which answers inline and has no event; drop event, or fix mode.`;
52
+ const pair = event;
53
+ if (typeof pair !== 'object' || pair === null || typeof pair.operation !== 'string' || !pair.operation.trim())
54
+ return `${owner} event.operation must be the object's non-empty event operation, e.g. "TRIGGER_CREATED".`;
55
+ // Written as given, and Integration Service matches it exactly: " TRIGGER_CREATED" would never match.
56
+ if (pair.operation !== pair.operation.trim())
57
+ return `${owner} event.operation ${JSON.stringify(pair.operation)} has surrounding whitespace; write it exactly as the object reports it, e.g. "TRIGGER_CREATED".`;
58
+ if (!isExternalEventMode(pair.mode))
59
+ return `${owner} event.mode must be "polling" or "webhooks"; got ${JSON.stringify(pair.mode)}.`;
60
+ return undefined;
34
61
  }
35
62
  /** The execution types an external task accepts: the values Integration Service reports. */
36
63
  export function isExternalExecutionMode(value) {
@@ -19,7 +19,7 @@ import { bindingLabelRefusal } from '../core/binding-messages.js';
19
19
  import { resolveEventWhere } from '../core/lookups.js';
20
20
  import { resolveCaseSubscription } from './subscription.js';
21
21
  import { dedupeConditionNames } from './rule-names.js';
22
- import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, EXTERNAL_TASK_HYBRID_UNSUPPORTED, externalDescriptorRefusal, externalHybridRefusal, externalServiceType, isExternalExecutionMode, } from './external.js';
22
+ import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, EXTERNAL_TASK_EVENT, externalDescriptorRefusal, externalEventRefusal, externalServiceType, isExternalExecutionMode, } from './external.js';
23
23
  import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS, caseVersionMismatch } from './format-profile.js';
24
24
  import { isResourceLabelRefusal, resolveResourceRef, resourceIdentity } from './resource-identity.js';
25
25
  import { ownEntry } from '../core/own-entry.js';
@@ -1739,7 +1739,7 @@ function businessRuleTaskData(task, resolver, elementId, ids) {
1739
1739
  * the op's output schema. Connection/folder resolve through `bindings` and are
1740
1740
  * referenced as `=bindings.<id>`, with the binding entry collected at case root.
1741
1741
  */
1742
- function connectorTaskData(spec, resolver, elementId, where, executionType) {
1742
+ function connectorTaskData(spec, resolver, elementId, where, executionType, event) {
1743
1743
  if (!resolver.library)
1744
1744
  throw new CompileRefusal('LIBRARY_REQUIRED', `connector task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
1745
1745
  const rc = resolver.library.resolve(spec.key, spec.action, spec.version, spec.object);
@@ -1787,7 +1787,7 @@ function connectorTaskData(spec, resolver, elementId, where, executionType) {
1787
1787
  // from the `body` slot. This used to drop the whole wire and push a filled
1788
1788
  // file part into the body, where the runtime does not look for it (#1420).
1789
1789
  const fileParts = (slots.multipartParameters ?? []).filter((part) => part.dataType.toLowerCase() === 'file');
1790
- context.push(io('metadata', 'json', { body: connectorMetadata(rc, op, slots, elementId, executionType) }));
1790
+ context.push(io('metadata', 'json', { body: connectorMetadata(rc, op, slots, elementId, executionType, event) }));
1791
1791
  const inputs = [
1792
1792
  ioSlot('pathParameters', pathParameters, `${elementId}`),
1793
1793
  ioSlot('queryParameters', queryParameters, `${elementId}`),
@@ -1816,14 +1816,20 @@ function externalTaskData(kind, spec, resolver, elementId, where) {
1816
1816
  throw new CompileRefusal(EXTERNAL_TASK_DESCRIPTOR_CONTRACT, refused);
1817
1817
  if (!isExternalExecutionMode(spec.mode))
1818
1818
  throw new CompileRefusal('EXTERNAL_TASK_MODE', `${kind} task mode must be the connector object's executionType: "sync", "async" or "hybrid".`);
1819
- if (spec.mode === 'hybrid')
1820
- throw new CompileRefusal(EXTERNAL_TASK_HYBRID_UNSUPPORTED, externalHybridRefusal(where));
1819
+ const eventRefused = externalEventRefusal(where, spec.mode, spec.event);
1820
+ if (eventRefused)
1821
+ throw new CompileRefusal(EXTERNAL_TASK_EVENT, eventRefused);
1821
1822
  if (!spec.connector.connection || !spec.connector.folder)
1822
1823
  throw new CompileRefusal(spec.connector.connection ? 'EXTERNAL_TASK_FOLDER' : 'EXTERNAL_TASK_CONNECTION', `${kind} task requires non-empty connection and folder binding names.`);
1823
- const data = connectorTaskData(spec.connector, resolver, elementId, where, spec.mode);
1824
+ const data = connectorTaskData(spec.connector, resolver, elementId, where, spec.mode, spec.event);
1824
1825
  const context = data.context;
1825
1826
  const metadataAt = context.findIndex((row) => row.name === 'metadata');
1826
- context.splice(metadataAt, 0, io('executionType', 'string', { value: spec.mode }));
1827
+ // The Designer's order: the event pair (when the object answers through one),
1828
+ // then the execution type, just before `metadata`.
1829
+ context.splice(metadataAt, 0, ...(spec.event ? [
1830
+ io('operation', 'string', { value: spec.event.operation }),
1831
+ io('eventMode', 'string', { value: spec.event.mode }),
1832
+ ] : []), io('executionType', 'string', { value: spec.mode }));
1827
1833
  context.push(io('activityConfigurationVersion', 'string', { value: 'v2' }));
1828
1834
  return { ...data, serviceType: externalServiceType(kind, spec.mode) };
1829
1835
  }
@@ -1957,7 +1963,7 @@ function folderBindingRef(resolver, name, connection, site) {
1957
1963
  * Generic activity, and the library version where the platform writes
1958
1964
  * `packageVersion: "1.0.0"` (#1420).
1959
1965
  */
1960
- function connectorMetadata(rc, op, slots, seed, executionType) {
1966
+ function connectorMetadata(rc, op, slots, seed, executionType, event) {
1961
1967
  // The part DECLARATIONS only: a file part's value rides on its `file` input.
1962
1968
  const multipartParameters = (slots.multipartParameters ?? []).map(({ name, dataType }) => ({ name, dataType }));
1963
1969
  // The configuration replays the authored values (a schema action's parent
@@ -1973,6 +1979,9 @@ function connectorMetadata(rc, op, slots, seed, executionType) {
1973
1979
  uiPathActivityTypeId: rc.activityTypeId ?? '',
1974
1980
  errorState: { issues: [] },
1975
1981
  ...(executionType ? { executionType } : {}),
1982
+ // The event an async / hybrid object answers with, named as the Designer
1983
+ // names it here (`eventType`, where the context row is `operation`).
1984
+ ...(event ? { eventType: event.operation, eventMode: event.mode } : {}),
1976
1985
  },
1977
1986
  activityMetadata: {
1978
1987
  activity: {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * One `bindings_v2.json` resource row, and the connection row every artifact
3
+ * writes.
4
+ *
5
+ * Flow (`bindings-v2.ts`) and Case (`case/bindings-v2.ts`) both declare a
6
+ * connector's connection in the project sidecar, and the CLI checks each
7
+ * against its own generator: `uip maestro flow pack` for Flow, `uip maestro
8
+ * case bindings sync` (`connectionEntry` in cli
9
+ * `packages/case-tool/src/services/case-bindings-service.ts`) for Case. Both
10
+ * generators write the same row — the Designer shape, `id` + flagged
11
+ * `ConnectionId` slot + `BindingsVersion` / `UseConnectionService` metadata.
12
+ * The CLI compares rows as serialized JSON, so the key order below is part of
13
+ * the contract.
14
+ *
15
+ * Case used to write its own older row (`connectionId` slot, a lower-case
16
+ * `connector` metadata key) and `validate --strict` reported it stale
17
+ * (builder-sdk#1382). One builder for both artifacts is what keeps the two
18
+ * from drifting apart again.
19
+ */
20
+ /** One declared solution resource. */
21
+ export interface BindingsV2Resource {
22
+ resource: string;
23
+ key: string;
24
+ id?: string;
25
+ value: Record<string, unknown>;
26
+ metadata: Record<string, unknown>;
27
+ [k: string]: unknown;
28
+ }
29
+ /** The `bindings_v2.json` document version every generator writes. */
30
+ export declare const BINDINGS_V2_DOCUMENT_VERSION = "2.0";
31
+ /** The per-resource `metadata.BindingsVersion` deploy needs to override an entry. */
32
+ export declare const RESOURCE_BINDINGS_VERSION = "2.2";
33
+ /** What one connection row is made of. Optional parts are left out of the row when absent. */
34
+ export interface ConnectionResourceParts {
35
+ /** `connection` in a `.flow`, `Connection` in a caseplan: each artifact keeps its own spelling. */
36
+ resource: string;
37
+ /** The connection's resource key. */
38
+ key: string;
39
+ /** The `ConnectionId` slot's value; the key when the binding row has no default. */
40
+ connectionId: string;
41
+ /** The connection binding's name: the slot's `displayName` and `metadata.DisplayLabel`. */
42
+ displayLabel?: string;
43
+ /** The folder key slot. Case writes it; Flow's pack does not. */
44
+ folderKey?: string;
45
+ /** The label of the first step or task that uses the connection. */
46
+ activityName?: string;
47
+ /** The connector the connection belongs to. */
48
+ connector?: string;
49
+ }
50
+ /** The connection row, in the Designer shape both CLI generators derive. */
51
+ export declare function connectionResource(parts: ConnectionResourceParts): BindingsV2Resource;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * One `bindings_v2.json` resource row, and the connection row every artifact
3
+ * writes.
4
+ *
5
+ * Flow (`bindings-v2.ts`) and Case (`case/bindings-v2.ts`) both declare a
6
+ * connector's connection in the project sidecar, and the CLI checks each
7
+ * against its own generator: `uip maestro flow pack` for Flow, `uip maestro
8
+ * case bindings sync` (`connectionEntry` in cli
9
+ * `packages/case-tool/src/services/case-bindings-service.ts`) for Case. Both
10
+ * generators write the same row — the Designer shape, `id` + flagged
11
+ * `ConnectionId` slot + `BindingsVersion` / `UseConnectionService` metadata.
12
+ * The CLI compares rows as serialized JSON, so the key order below is part of
13
+ * the contract.
14
+ *
15
+ * Case used to write its own older row (`connectionId` slot, a lower-case
16
+ * `connector` metadata key) and `validate --strict` reported it stale
17
+ * (builder-sdk#1382). One builder for both artifacts is what keeps the two
18
+ * from drifting apart again.
19
+ */
20
+ /** The `bindings_v2.json` document version every generator writes. */
21
+ export const BINDINGS_V2_DOCUMENT_VERSION = '2.0';
22
+ /** The per-resource `metadata.BindingsVersion` deploy needs to override an entry. */
23
+ export const RESOURCE_BINDINGS_VERSION = '2.2';
24
+ /** The connection row, in the Designer shape both CLI generators derive. */
25
+ export function connectionResource(parts) {
26
+ const { resource, key, connectionId, displayLabel, folderKey, activityName, connector } = parts;
27
+ const value = {
28
+ ConnectionId: {
29
+ defaultValue: connectionId,
30
+ isExpression: false,
31
+ ...(displayLabel !== undefined && { displayName: displayLabel }),
32
+ },
33
+ };
34
+ if (folderKey !== undefined)
35
+ value.folderKey = { defaultValue: folderKey };
36
+ return {
37
+ resource,
38
+ key,
39
+ // The generators concatenate resource and key with no separator.
40
+ id: `${resource}${key}`,
41
+ value,
42
+ metadata: {
43
+ ...(activityName !== undefined && { ActivityName: activityName }),
44
+ BindingsVersion: RESOURCE_BINDINGS_VERSION,
45
+ ...(displayLabel !== undefined && { DisplayLabel: displayLabel }),
46
+ UseConnectionService: 'true',
47
+ ...(connector !== undefined && { Connector: connector }),
48
+ },
49
+ };
50
+ }
@@ -274,8 +274,8 @@ export declare const REFUSALS: {
274
274
  readonly EXTERNAL_TASK_DESCRIPTOR: "lint";
275
275
  readonly EXTERNAL_TASK_DESCRIPTOR_CONTRACT: "lowering";
276
276
  readonly EXTERNAL_TASK_EMPTY_DATA: "lint";
277
+ readonly EXTERNAL_TASK_EVENT: "lowering";
277
278
  readonly EXTERNAL_TASK_FOLDER: "lowering";
278
- readonly EXTERNAL_TASK_HYBRID_UNSUPPORTED: "lowering";
279
279
  readonly EXTERNAL_TASK_MODE: "lowering";
280
280
  readonly EXTRACT_SCHEMA_SHAPE: "lint";
281
281
  readonly FAKE_JOIN: "lint";
@@ -325,8 +325,8 @@ export const REFUSALS = {
325
325
  EXTERNAL_TASK_DESCRIPTOR: 'lint',
326
326
  EXTERNAL_TASK_DESCRIPTOR_CONTRACT: 'lowering',
327
327
  EXTERNAL_TASK_EMPTY_DATA: 'lint',
328
+ EXTERNAL_TASK_EVENT: 'lowering',
328
329
  EXTERNAL_TASK_FOLDER: 'lowering',
329
- EXTERNAL_TASK_HYBRID_UNSUPPORTED: 'lowering',
330
330
  EXTERNAL_TASK_MODE: 'lowering',
331
331
  EXTRACT_SCHEMA_SHAPE: 'lint',
332
332
  FAKE_JOIN: 'lint',