@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.
@@ -1,13 +1,6 @@
1
1
  import type { ConnectionUse } from './core/compile-refusal.js';
2
- /** One declared solution resource. */
3
- export interface BindingsV2Resource {
4
- resource: string;
5
- key: string;
6
- id?: string;
7
- value: Record<string, unknown>;
8
- metadata: Record<string, unknown>;
9
- [k: string]: unknown;
10
- }
2
+ import { type BindingsV2Resource } from './core/bindings-v2-resource.js';
3
+ export type { BindingsV2Resource };
11
4
  /** The `bindings_v2.json` document. */
12
5
  export interface BindingsV2Document {
13
6
  version: string;
@@ -55,10 +55,11 @@
55
55
  */
56
56
  import { BindingRefusal, connectionConnectorConflictMessage } from './core/binding-messages.js';
57
57
  import { isRecord } from './core/node-envelope.js';
58
+ import { BINDINGS_V2_DOCUMENT_VERSION, connectionResource, RESOURCE_BINDINGS_VERSION, } from './core/bindings-v2-resource.js';
58
59
  /** The document version pack emits. */
59
- const DOCUMENT_VERSION = '2.0';
60
+ const DOCUMENT_VERSION = BINDINGS_V2_DOCUMENT_VERSION;
60
61
  /** The per-resource metadata version pack stamps. */
61
- const BINDINGS_VERSION = '2.2';
62
+ const BINDINGS_VERSION = RESOURCE_BINDINGS_VERSION;
62
63
  function nonEmptyString(value) {
63
64
  return typeof value === 'string' && value.length > 0 ? value : undefined;
64
65
  }
@@ -235,26 +236,15 @@ export function connectionResources(flow) {
235
236
  const binding = candidates[0];
236
237
  const displayLabel = nonEmptyString(binding.name) ?? key;
237
238
  const label = isRecord(node.display) ? nonEmptyString(node.display.label) : undefined;
238
- resources.push({
239
+ // The same row Case writes (core/bindings-v2-resource.ts); pack writes no folderKey slot.
240
+ resources.push(connectionResource({
239
241
  resource: 'connection',
240
242
  key,
241
- // Pack concatenates resource and key with no separator for a connection.
242
- id: `connection${key}`,
243
- value: {
244
- ConnectionId: {
245
- defaultValue: nonEmptyString(binding.default) ?? key,
246
- isExpression: false,
247
- displayName: displayLabel,
248
- },
249
- },
250
- metadata: {
251
- ActivityName: label ?? nonEmptyString(node.id) ?? key,
252
- BindingsVersion: BINDINGS_VERSION,
253
- DisplayLabel: displayLabel,
254
- UseConnectionService: 'true',
255
- Connector: connector,
256
- },
257
- });
243
+ connectionId: nonEmptyString(binding.default) ?? key,
244
+ displayLabel,
245
+ activityName: label ?? nonEmptyString(node.id) ?? key,
246
+ connector,
247
+ }));
258
248
  }
259
249
  return resources;
260
250
  }
@@ -0,0 +1,3 @@
1
+ import { type BindingsV2Resource } from '../core/bindings-v2-resource.js';
2
+ /** Every resource the caseplan's root bindings declare, in the order their rows first appear. */
3
+ export declare function deriveCaseBindingsV2(caseplan: Record<string, unknown>): BindingsV2Resource[];
@@ -0,0 +1,281 @@
1
+ /**
2
+ * The `bindings_v2.json` resources a caseplan's root `bindings[]` rows declare:
3
+ * the root-row pass of `uip maestro case bindings sync` (`deriveCaseBindingsV2`
4
+ * in cli `packages/case-tool/src/services/case-bindings-service.ts`). It is
5
+ * NOT the whole CLI function; see "Not ported" below.
6
+ *
7
+ * `uip maestro case validate --strict` re-derives the sidecar with that
8
+ * function and compares it to the file on disk entry by entry, as serialized
9
+ * JSON (`STRICT_BINDINGS_SIDECAR_STALE`), so field order matters as well as
10
+ * field values. The CLI is a restricted package the SDK cannot depend on, so
11
+ * this is a copy; `tests/case-bindings-v2-parity.test.ts` holds it to the
12
+ * CLI's own output, one resource kind at a time, from a fixture the real CLI
13
+ * function derived. Re-record that fixture with
14
+ * `bun typescript/tests/fixtures/case-bindings-v2/record.ts <uipath-cli checkout>`.
15
+ *
16
+ * Every entry comes from the root `bindings[]` rows, grouped by
17
+ * `(resource, resourceKey)`:
18
+ *
19
+ * - `Connection` → the connection row Flow writes too
20
+ * ({@link connectionResource}); its connector and activity label come from
21
+ * the connector context that references the row's binding id.
22
+ * - `app` / `process` / `ixpDeployment` → the Case Designer shape.
23
+ * - any other resource → the plain shape.
24
+ *
25
+ * Not ported — the CLI function's other passes:
26
+ *
27
+ * - `EventTrigger` / `Property` entries for a connector start trigger
28
+ * (`deriveEventTriggerResources`). The SDK writes these triggers, so this
29
+ * is a live gap: builder-sdk#1557, a known-gap row in the parity test.
30
+ * - The stored-binding overlay: entries a connector task keeps in its
31
+ * `metadata` context's `body.bindings`, merged into the derived entry's
32
+ * metadata or appended. The serializer's connector `metadata` body has no
33
+ * `bindings` key, so there is nothing to overlay today.
34
+ * - `Role` entries from `metadata.caseRoles`. The SDK neither authors nor
35
+ * preserves `caseRoles` (it is not in preserve.ts's metadata keys).
36
+ *
37
+ * If the serializer starts writing either of the last two, port the pass and
38
+ * add a task for it to the parity fixture (`fixtures/case-bindings-v2/plan.ts`).
39
+ */
40
+ import { isRecord } from '../core/node-envelope.js';
41
+ import { connectionResource, RESOURCE_BINDINGS_VERSION, } from '../core/bindings-v2-resource.js';
42
+ import { isInSolutionKey, SOLUTION_FOLDER } from './resource-identity.js';
43
+ const CONNECTION_RESOURCE = 'Connection';
44
+ /** The placeholder a manual-mode http-request task binds; nothing declares it. */
45
+ const IMPLICIT_CONNECTION = 'ImplicitConnection';
46
+ const CONNECTION_ID_ATTRIBUTE = 'ConnectionId';
47
+ const FOLDER_KEY_ATTRIBUTE = 'folderKey';
48
+ const BINDING_REFERENCE_PREFIX = '=bindings.';
49
+ function asBindingRow(value) {
50
+ if (!isRecord(value))
51
+ return undefined;
52
+ return typeof value.id === 'string'
53
+ && typeof value.resource === 'string'
54
+ && typeof value.resourceKey === 'string'
55
+ && value.resourceKey !== ''
56
+ && typeof value.propertyAttribute === 'string'
57
+ && (value.default === undefined || typeof value.default === 'string')
58
+ ? value
59
+ : undefined;
60
+ }
61
+ /** Every resource the caseplan's root bindings declare, in the order their rows first appear. */
62
+ export function deriveCaseBindingsV2(caseplan) {
63
+ const groups = new Map();
64
+ for (const entry of Array.isArray(caseplan.bindings) ? caseplan.bindings : []) {
65
+ const binding = asBindingRow(entry);
66
+ if (!binding)
67
+ continue;
68
+ if (binding.resource === CONNECTION_RESOURCE && binding.resourceKey === IMPLICIT_CONNECTION)
69
+ continue;
70
+ const groupKey = `${binding.resource}:${binding.resourceKey}`;
71
+ const group = groups.get(groupKey);
72
+ if (group)
73
+ group.push(binding);
74
+ else
75
+ groups.set(groupKey, [binding]);
76
+ }
77
+ const connectorUses = collectConnectorUses(caseplan);
78
+ const labels = bindingLabels(caseplan.nodes);
79
+ return [...groups.values()].map((group) => group[0].resource === CONNECTION_RESOURCE
80
+ ? connectionEntry(group, connectorUses)
81
+ : resourceEntry(group, labels));
82
+ }
83
+ function attributeDefault(group, attribute) {
84
+ return group.find((binding) => binding.propertyAttribute === attribute)?.default;
85
+ }
86
+ /**
87
+ * A connection: its `ConnectionId` row labels it, its `folderKey` row (keyed
88
+ * by the connection, not the folder) gives the folder slot.
89
+ */
90
+ function connectionEntry(group, uses) {
91
+ const key = group[0].resourceKey;
92
+ const use = group.map((binding) => uses.get(binding.id)).find((found) => found !== undefined);
93
+ const label = (group.find((binding) => binding.propertyAttribute === CONNECTION_ID_ATTRIBUTE) ?? group[0]).name;
94
+ return connectionResource({
95
+ resource: CONNECTION_RESOURCE,
96
+ key,
97
+ connectionId: attributeDefault(group, CONNECTION_ID_ATTRIBUTE) ?? key,
98
+ ...(typeof label === 'string' && label !== '' && { displayLabel: label }),
99
+ folderKey: attributeDefault(group, FOLDER_KEY_ATTRIBUTE),
100
+ activityName: use?.activityName,
101
+ connector: use?.connectorKey,
102
+ });
103
+ }
104
+ /**
105
+ * The Case Designer's label for each value slot of an app or process entry.
106
+ * These resources get the Designer shape (cli#5003, MST-16424): an `id`, each
107
+ * slot flagged and labelled, and the `BindingsVersion` / `SolutionsSupport`
108
+ * metadata without which deploy cannot override the entry.
109
+ */
110
+ const DESIGNER_SLOT_NAMES = new Map([
111
+ ['app', { name: 'App name', folderPath: 'Folder path', solutionsSupport: true }],
112
+ ['process', { name: 'Process name', folderPath: 'Folder path', solutionsSupport: true }],
113
+ ['ixpDeployment', { name: 'name', folderPath: 'Folder path', solutionsSupport: false }],
114
+ ]);
115
+ /**
116
+ * One sidecar entry for a group of root binding rows that share a resource and
117
+ * a resource key. The subtype comes from the rows only: a Flow's rows carry
118
+ * none, so its entry has none either, as the Designer saves it.
119
+ *
120
+ * `metadata.ActivityName` labels a Designer-shaped entry with the first task
121
+ * that references one of its rows ({@link bindingLabels}).
122
+ */
123
+ function resourceEntry(group, labels) {
124
+ const resource = group[0].resource;
125
+ const key = group[0].resourceKey;
126
+ // An in-solution resource binds an empty folderPath in the caseplan, while
127
+ // the sidecar names its folder by the sentinel the key carries.
128
+ const folderPath = isInSolutionKey(key) ? SOLUTION_FOLDER : attributeDefault(group, 'folderPath');
129
+ const name = attributeDefault(group, 'name');
130
+ const subType = group.map((binding) => binding.resourceSubType).find((found) => found !== undefined);
131
+ const designer = DESIGNER_SLOT_NAMES.get(resource);
132
+ if (designer) {
133
+ const value = {};
134
+ if (folderPath !== undefined) {
135
+ value.folderPath = { defaultValue: folderPath, isExpression: false, displayName: designer.folderPath };
136
+ }
137
+ if (name !== undefined) {
138
+ value.name = { defaultValue: name, isExpression: false, displayName: designer.name };
139
+ }
140
+ const activityName = group.map((binding) => labels.get(binding.id)).find((found) => found !== undefined);
141
+ return {
142
+ resource,
143
+ key,
144
+ id: `${resource}${key}`,
145
+ value,
146
+ metadata: {
147
+ ...(activityName !== undefined && { ActivityName: activityName }),
148
+ BindingsVersion: RESOURCE_BINDINGS_VERSION,
149
+ DisplayLabel: 'name',
150
+ ...(designer.solutionsSupport && { SolutionsSupport: 'true' }),
151
+ ...(subType !== undefined && { subType }),
152
+ },
153
+ };
154
+ }
155
+ const value = {};
156
+ if (name !== undefined)
157
+ value.name = { defaultValue: name };
158
+ if (folderPath !== undefined)
159
+ value.folderPath = { defaultValue: folderPath };
160
+ // A solution-resource picker (such as a Data Fabric entity) carries its folder key as a slot of its own.
161
+ const folderKey = attributeDefault(group, FOLDER_KEY_ATTRIBUTE);
162
+ if (folderKey !== undefined)
163
+ value.folderKey = { defaultValue: folderKey };
164
+ return {
165
+ resource,
166
+ key,
167
+ value,
168
+ metadata: {
169
+ BindingsVersion: RESOURCE_BINDINGS_VERSION,
170
+ ...(subType !== undefined && { subType }),
171
+ },
172
+ };
173
+ }
174
+ /** A task's or condition's `displayName`, a trigger's `display.label`, or a legacy node's `label`. */
175
+ function ownLabel(node) {
176
+ const display = isRecord(node.display) ? node.display.label : undefined;
177
+ return [node.displayName, display, node.label].find((label) => typeof label === 'string' && label !== '');
178
+ }
179
+ /**
180
+ * Every value under `root`, each with the label of the nearest object above it
181
+ * that names itself ({@link ownLabel}) — the CLI's `walkLabelled`.
182
+ */
183
+ function walkLabelled(root, visit) {
184
+ const walk = (node, label) => {
185
+ visit(node, label);
186
+ if (Array.isArray(node)) {
187
+ for (const item of node)
188
+ walk(item, label);
189
+ return;
190
+ }
191
+ if (!isRecord(node))
192
+ return;
193
+ const scoped = ownLabel(node) ?? label;
194
+ for (const value of Object.values(node))
195
+ walk(value, scoped);
196
+ };
197
+ walk(root, undefined);
198
+ }
199
+ /** A `{ name, value }` context array that names a connector, by entry name; else undefined. */
200
+ function connectorContext(value) {
201
+ if (!Array.isArray(value))
202
+ return undefined;
203
+ const byName = new Map();
204
+ for (const entry of value) {
205
+ if (!isRecord(entry) || typeof entry.name !== 'string')
206
+ return undefined;
207
+ if (typeof entry.value === 'string')
208
+ byName.set(entry.name, entry.value);
209
+ }
210
+ return byName.has('connectorKey') ? byName : undefined;
211
+ }
212
+ function referencedBindingId(context) {
213
+ const connection = context?.get('connection');
214
+ return connection?.startsWith(BINDING_REFERENCE_PREFIX)
215
+ ? connection.slice(BINDING_REFERENCE_PREFIX.length)
216
+ : undefined;
217
+ }
218
+ /**
219
+ * The connector whose authentication an http-request task reuses: its `body`
220
+ * input's `targetConnector`. Its context names the generic HTTP connector
221
+ * whatever the request calls, while the connection belongs to the target.
222
+ */
223
+ function httpTargetConnector(node) {
224
+ if (!isRecord(node) || node.type !== 'http-request' || !isRecord(node.data) || !Array.isArray(node.data.inputs)) {
225
+ return undefined;
226
+ }
227
+ for (const input of node.data.inputs) {
228
+ if (!isRecord(input) || input.name !== 'body' || !isRecord(input.body))
229
+ continue;
230
+ const target = input.body.targetConnector;
231
+ if (typeof target === 'string' && target !== '')
232
+ return target;
233
+ }
234
+ return undefined;
235
+ }
236
+ /**
237
+ * The connector of each connection binding, keyed by binding id, from the
238
+ * connector contexts that reference it (`connection: "=bindings.<id>"`). The
239
+ * first use wins, as the CLI's `collectConnectorKeys` decides it.
240
+ */
241
+ function collectConnectorUses(caseplan) {
242
+ const byBindingId = new Map();
243
+ walkLabelled(caseplan, (node, label) => {
244
+ // Before the context array beneath it, which would otherwise claim the
245
+ // binding for the generic HTTP connector.
246
+ const target = httpTargetConnector(node);
247
+ if (target !== undefined && isRecord(node)) {
248
+ const data = node.data;
249
+ const bindingId = referencedBindingId(connectorContext(data.context));
250
+ if (bindingId !== undefined && !byBindingId.has(bindingId)) {
251
+ byBindingId.set(bindingId, { connectorKey: target, activityName: ownLabel(node) ?? label });
252
+ }
253
+ }
254
+ if (!Array.isArray(node))
255
+ return;
256
+ const context = connectorContext(node);
257
+ const connectorKey = context?.get('connectorKey');
258
+ const bindingId = referencedBindingId(context);
259
+ if (connectorKey !== undefined && bindingId !== undefined && !byBindingId.has(bindingId)) {
260
+ byBindingId.set(bindingId, { connectorKey, activityName: label });
261
+ }
262
+ });
263
+ return byBindingId;
264
+ }
265
+ /**
266
+ * The label of the first object that references each binding id
267
+ * (`=bindings.<id>`), in document order: the nearest enclosing `displayName`,
268
+ * `display.label` or `label`. The same walk as the CLI's
269
+ * `collectBindingLabels`, so both name an entry after the same task.
270
+ */
271
+ function bindingLabels(nodes) {
272
+ const found = new Map();
273
+ walkLabelled(nodes ?? [], (node, label) => {
274
+ if (typeof node !== 'string' || label === undefined || !node.startsWith(BINDING_REFERENCE_PREFIX))
275
+ return;
276
+ const id = node.slice(BINDING_REFERENCE_PREFIX.length);
277
+ if (!found.has(id))
278
+ found.set(id, label);
279
+ });
280
+ return found;
281
+ }
@@ -437,9 +437,10 @@ export type ConnectorSpecData = Extract<ActionSpec, {
437
437
  * and `hybrid` as `Async…Execution`, and writes the value itself into the task's
438
438
  * `executionType` context.
439
439
  *
440
- * `hybrid` is refused for now (EXTERNAL_TASK_HYBRID_UNSUPPORTED): the Designer
441
- * also writes an event pair for it (`operation`, `eventMode`) that the connector
442
- * library does not carry (#1387). Add a task on a hybrid object in Studio Web.
440
+ * An `async` or `hybrid` object also answers through an event, which the
441
+ * Designer saves next to it; pass that pair as {@link ExternalTaskOptions.event}.
442
+ * A `hybrid` task without it is refused (EXTERNAL_TASK_EVENT), since the task
443
+ * would have nothing to wait on.
443
444
  *
444
445
  * It does not decide whether the stage waits for the task. A task the stage must
445
446
  * not wait on is `.required(false)`, whatever its execution type. For example, a
@@ -447,6 +448,27 @@ export type ConnectorSpecData = Extract<ActionSpec, {
447
448
  * it.
448
449
  */
449
450
  export type ExternalExecutionMode = 'sync' | 'async' | 'hybrid';
451
+ /**
452
+ * The event an `async` or `hybrid` external object answers with, as Integration
453
+ * Service reports it for the connector object.
454
+ *
455
+ * @remarks
456
+ * Like the execution type, a property of the connector object, not a choice:
457
+ * the Case Designer reads it from the object's metadata (its first event mode
458
+ * and that mode's first event type) and saves it as the task's `operation` and
459
+ * `eventMode` context rows, and as `eventType` / `eventMode` in the metadata's
460
+ * `activityPropertyConfiguration`. Copy it from the same two rows of the
461
+ * activity's `case spec` result (`caseShape.context`). `decompile` lifts it from
462
+ * a saved task. For
463
+ * example, the Power Automate "Invoke a Flow" object (`hybrid`) answers with
464
+ * `{ operation: 'TRIGGER_CREATED', mode: 'polling' }`.
465
+ */
466
+ export interface ExternalEventPair {
467
+ /** The event operation, e.g. `TRIGGER_CREATED`: the task's `operation` context row. */
468
+ operation: string;
469
+ /** How the event is received: the task's `eventMode` context row. */
470
+ mode: 'polling' | 'webhooks';
471
+ }
450
472
  /** Required wiring and typed inputs for an external agent/workflow invocation. */
451
473
  export interface ExternalTaskOptions<I extends Record<string, unknown>> {
452
474
  /** Symbolic Integration Service connection name declared in `bindings.json`. */
@@ -460,6 +482,12 @@ export interface ExternalTaskOptions<I extends Record<string, unknown>> {
460
482
  * `.required(false)`. See {@link ExternalExecutionMode}.
461
483
  */
462
484
  mode: ExternalExecutionMode;
485
+ /**
486
+ * The event an `async` or `hybrid` object answers with. Required for `hybrid`;
487
+ * optional for `async`; refused for `sync`, which answers inline. See
488
+ * {@link ExternalEventPair}.
489
+ */
490
+ event?: ExternalEventPair;
463
491
  /** Inputs statically checked by the generated connector descriptor. */
464
492
  inputs: I;
465
493
  }
@@ -467,6 +495,8 @@ export interface ExternalTaskOptions<I extends Record<string, unknown>> {
467
495
  export interface ExternalTaskSpecData {
468
496
  /** The connector object's `executionType`. */
469
497
  mode: ExternalExecutionMode;
498
+ /** The event an `async` or `hybrid` object answers with, when given. */
499
+ event?: ExternalEventPair;
470
500
  /** Shared connector action identity, inputs, and symbolic bindings. */
471
501
  connector: ConnectorSpecData;
472
502
  }
@@ -334,6 +334,7 @@ class TaskBuilder {
334
334
  this.setKind(kind);
335
335
  this._external = {
336
336
  mode: opts.mode,
337
+ ...(opts.event !== undefined ? { event: { ...opts.event } } : {}),
337
338
  connector: makeConnector(descriptor, opts.inputs, {
338
339
  connection: opts.connection,
339
340
  folder: opts.folder,
@@ -6,7 +6,7 @@ import { schemaVersionRefusal } from '../schema-version.js';
6
6
  import { caseJsonPreservation, caseJsonPreservationErrors, preservedBindings } from './preserve.js';
7
7
  import { EQUALS_EXPR_DIALECT, checkBindingLabels, checkConnectorContract, checkConnectorLookups, checkConnectorSchema, } from '../core/connector-checks.js';
8
8
  import { checkEventResolution } from '../core/event-checks.js';
9
- import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, EXTERNAL_TASK_HYBRID_UNSUPPORTED, externalHybridRefusal, isExternalExecutionMode } from './external.js';
9
+ import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, EXTERNAL_TASK_EVENT, externalEventRefusal, isExternalExecutionMode } from './external.js';
10
10
  import { isResourceLabelRefusal, resolveResourceRef } from './resource-identity.js';
11
11
  import { taskShapeProblems } from './task-shape.js';
12
12
  import { isConnectorPlaceholder } from './connector-placeholder.js';
@@ -623,13 +623,11 @@ function checkStage(stage, diags, refStage, refPlacement, bindings) {
623
623
  message: `${family} task "${t.name}" mode must be the connector object's executionType: "sync", "async" or "hybrid".`,
624
624
  where: t.name,
625
625
  });
626
- else if (t.external.mode === 'hybrid')
627
- diags.push({
628
- level: 'error',
629
- code: EXTERNAL_TASK_HYBRID_UNSUPPORTED,
630
- message: externalHybridRefusal(`${family} task "${t.name}"`),
631
- where: t.name,
632
- });
626
+ else {
627
+ const refused = externalEventRefusal(`${family} task "${t.name}"`, t.external.mode, t.external.event);
628
+ if (refused)
629
+ diags.push({ level: 'error', code: EXTERNAL_TASK_EVENT, message: refused, where: t.name });
630
+ }
633
631
  if (!t.external.connector?.key?.trim() || !t.external.connector?.action?.trim())
634
632
  diags.push({
635
633
  level: 'error',