@uipath/maestro-builder-sdk 6.14.2 → 6.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bindings-v2.d.ts +26 -0
- package/dist/bindings-v2.js +111 -7
- package/dist/check.js +60 -7
- package/dist/core/binding-messages.d.ts +16 -1
- package/dist/core/binding-messages.js +21 -0
- package/dist/core/node-envelope.d.ts +34 -0
- package/dist/core/node-envelope.js +83 -0
- package/dist/serialize.d.ts +28 -0
- package/dist/serialize.js +99 -5
- package/package.json +2 -2
package/dist/bindings-v2.d.ts
CHANGED
|
@@ -13,6 +13,22 @@ export interface BindingsV2Document {
|
|
|
13
13
|
resources: BindingsV2Resource[];
|
|
14
14
|
[k: string]: unknown;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Which connector each bound connection belongs to, keyed by connection id —
|
|
18
|
+
* decided by EVERY node on the connection, so the answer does not depend on
|
|
19
|
+
* which step comes first.
|
|
20
|
+
*
|
|
21
|
+
* `bindings.json` cannot answer this: `prepare --connection-id` writes a label,
|
|
22
|
+
* a resource key and a kind (`ConnectionId` / `FolderKey`), never the connector
|
|
23
|
+
* (see `core/binding-messages.ts`). The nodes are the only record of it, so
|
|
24
|
+
* all of them must agree. Two that do not — a Drive step and a Slack step on
|
|
25
|
+
* one connection, or managed HTTP whose `targetConnector` is not the connection's
|
|
26
|
+
* connector — are refused with `CONNECTION_CONNECTOR_CONFLICT`: at most one of
|
|
27
|
+
* them can run, and picking the first silently mislabels the solution resource.
|
|
28
|
+
*
|
|
29
|
+
* @throws BindingRefusal when two nodes on one connection name different connectors.
|
|
30
|
+
*/
|
|
31
|
+
export declare function connectionConnectors(flow: unknown): Map<string, string>;
|
|
16
32
|
/**
|
|
17
33
|
* The `connection` resources a compiled flow declares — one per distinct
|
|
18
34
|
* connection, in node order, attributed to the FIRST node that uses it.
|
|
@@ -20,6 +36,16 @@ export interface BindingsV2Document {
|
|
|
20
36
|
* Both halves of that are pack's behaviour, not a choice made here: a flow with
|
|
21
37
|
* two Outlook steps on one connection yields a single resource whose
|
|
22
38
|
* `ActivityName` is the earlier step's.
|
|
39
|
+
*
|
|
40
|
+
* What the resource IS does not follow node order: its `Connector` comes from
|
|
41
|
+
* {@link connectionConnectors} (every node on the connection, managed HTTP read
|
|
42
|
+
* through its `targetConnector`). Pack stamps the same value — measured with
|
|
43
|
+
* `uip maestro flow pack` on managed HTTP → Slack alone, before a native Slack
|
|
44
|
+
* step, and after one: `uipath-salesforce-slack` all three times, where this
|
|
45
|
+
* module used to stamp `uipath-uipath-http` for the first two (#866;
|
|
46
|
+
* uip 1.202.0-dev, 2026-09-30).
|
|
47
|
+
*
|
|
48
|
+
* @throws BindingRefusal when two nodes on one connection name different connectors.
|
|
23
49
|
*/
|
|
24
50
|
export declare function connectionResources(flow: unknown): BindingsV2Resource[];
|
|
25
51
|
/**
|
package/dist/bindings-v2.js
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* the connection rows.
|
|
43
43
|
*/
|
|
44
44
|
import { existsSync, readFileSync } from 'node:fs';
|
|
45
|
+
import { BindingRefusal, connectionConnectorConflictMessage } from './core/binding-messages.js';
|
|
45
46
|
/** The document version pack emits. */
|
|
46
47
|
const DOCUMENT_VERSION = '2.0';
|
|
47
48
|
/** The per-resource metadata version pack stamps. */
|
|
@@ -56,6 +57,93 @@ function str(value) {
|
|
|
56
57
|
function isConnection(resource) {
|
|
57
58
|
return typeof resource === 'string' && resource.toLowerCase() === 'connection';
|
|
58
59
|
}
|
|
60
|
+
/** The managed-HTTP wrapper's own connector key — never the connection's type when a target is named. */
|
|
61
|
+
const MANAGED_HTTP_CONNECTOR = 'uipath-uipath-http';
|
|
62
|
+
function nodeDetail(node) {
|
|
63
|
+
return isRecord(node.inputs) && isRecord(node.inputs.detail) ? node.inputs.detail : undefined;
|
|
64
|
+
}
|
|
65
|
+
/** The connection a node runs on: `connectionId`, else `connectionResourceId`. */
|
|
66
|
+
function nodeConnectionKey(detail) {
|
|
67
|
+
return str(detail.connectionId) ?? str(detail.connectionResourceId);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The connector a node's CONNECTION belongs to — not the connector the node is.
|
|
71
|
+
*
|
|
72
|
+
* The two differ for managed HTTP: its `detail.connector` is always the wrapper,
|
|
73
|
+
* `uipath-uipath-http`, and the connection it authenticates with is the one
|
|
74
|
+
* `bodyParameters.targetConnector` names (`uipath-google-drive`,
|
|
75
|
+
* `uipath-salesforce-slack`, …). Stamping the wrapper made `uip solution
|
|
76
|
+
* resources refresh` refuse the connection — "Requested type
|
|
77
|
+
* 'uipath-uipath-http'; the resource is registered as type
|
|
78
|
+
* 'uipath-google-drive'" (flow-builder-sdk#866). Every other family's
|
|
79
|
+
* `detail.connector` is its connection's connector.
|
|
80
|
+
*/
|
|
81
|
+
function nodeConnectionConnector(detail) {
|
|
82
|
+
const connector = str(detail.connector);
|
|
83
|
+
if (connector === MANAGED_HTTP_CONNECTOR && isRecord(detail.bodyParameters)) {
|
|
84
|
+
return str(detail.bodyParameters.targetConnector) ?? connector;
|
|
85
|
+
}
|
|
86
|
+
return connector;
|
|
87
|
+
}
|
|
88
|
+
/** Every node of the flow and of its inline subflows — they share one `bindings[]`. */
|
|
89
|
+
function allNodes(flow) {
|
|
90
|
+
const nodes = Array.isArray(flow.nodes) ? flow.nodes.filter(isRecord) : [];
|
|
91
|
+
if (isRecord(flow.subflows)) {
|
|
92
|
+
for (const sub of Object.values(flow.subflows)) {
|
|
93
|
+
if (isRecord(sub) && Array.isArray(sub.nodes))
|
|
94
|
+
nodes.push(...sub.nodes.filter(isRecord));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return nodes;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Which connector each bound connection belongs to, keyed by connection id —
|
|
101
|
+
* decided by EVERY node on the connection, so the answer does not depend on
|
|
102
|
+
* which step comes first.
|
|
103
|
+
*
|
|
104
|
+
* `bindings.json` cannot answer this: `prepare --connection-id` writes a label,
|
|
105
|
+
* a resource key and a kind (`ConnectionId` / `FolderKey`), never the connector
|
|
106
|
+
* (see `core/binding-messages.ts`). The nodes are the only record of it, so
|
|
107
|
+
* all of them must agree. Two that do not — a Drive step and a Slack step on
|
|
108
|
+
* one connection, or managed HTTP whose `targetConnector` is not the connection's
|
|
109
|
+
* connector — are refused with `CONNECTION_CONNECTOR_CONFLICT`: at most one of
|
|
110
|
+
* them can run, and picking the first silently mislabels the solution resource.
|
|
111
|
+
*
|
|
112
|
+
* @throws BindingRefusal when two nodes on one connection name different connectors.
|
|
113
|
+
*/
|
|
114
|
+
export function connectionConnectors(flow) {
|
|
115
|
+
const out = new Map();
|
|
116
|
+
if (!isRecord(flow))
|
|
117
|
+
return out;
|
|
118
|
+
const bound = new Set();
|
|
119
|
+
for (const b of Array.isArray(flow.bindings) ? flow.bindings.filter(isRecord) : []) {
|
|
120
|
+
const key = str(b.resourceKey);
|
|
121
|
+
if (key && isConnection(b.resource) && str(b.propertyAttribute)?.toLowerCase() === 'connectionid')
|
|
122
|
+
bound.add(key);
|
|
123
|
+
}
|
|
124
|
+
const users = new Map();
|
|
125
|
+
for (const node of allNodes(flow)) {
|
|
126
|
+
const detail = nodeDetail(node);
|
|
127
|
+
const key = detail && nodeConnectionKey(detail);
|
|
128
|
+
if (!detail || !key || !bound.has(key))
|
|
129
|
+
continue;
|
|
130
|
+
const connector = nodeConnectionConnector(detail);
|
|
131
|
+
if (!connector)
|
|
132
|
+
continue;
|
|
133
|
+
const byConnector = users.get(key) ?? new Map();
|
|
134
|
+
users.set(key, byConnector);
|
|
135
|
+
const steps = byConnector.get(connector) ?? [];
|
|
136
|
+
steps.push((isRecord(node.display) ? str(node.display.label) : undefined) ?? str(node.id) ?? '?');
|
|
137
|
+
byConnector.set(connector, steps);
|
|
138
|
+
}
|
|
139
|
+
for (const [key, byConnector] of users) {
|
|
140
|
+
if (byConnector.size > 1) {
|
|
141
|
+
throw new BindingRefusal('CONNECTION_CONNECTOR_CONFLICT', connectionConnectorConflictMessage(key, byConnector));
|
|
142
|
+
}
|
|
143
|
+
out.set(key, [...byConnector.keys()][0]);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
59
147
|
/**
|
|
60
148
|
* The `connection` resources a compiled flow declares — one per distinct
|
|
61
149
|
* connection, in node order, attributed to the FIRST node that uses it.
|
|
@@ -63,6 +151,16 @@ function isConnection(resource) {
|
|
|
63
151
|
* Both halves of that are pack's behaviour, not a choice made here: a flow with
|
|
64
152
|
* two Outlook steps on one connection yields a single resource whose
|
|
65
153
|
* `ActivityName` is the earlier step's.
|
|
154
|
+
*
|
|
155
|
+
* What the resource IS does not follow node order: its `Connector` comes from
|
|
156
|
+
* {@link connectionConnectors} (every node on the connection, managed HTTP read
|
|
157
|
+
* through its `targetConnector`). Pack stamps the same value — measured with
|
|
158
|
+
* `uip maestro flow pack` on managed HTTP → Slack alone, before a native Slack
|
|
159
|
+
* step, and after one: `uipath-salesforce-slack` all three times, where this
|
|
160
|
+
* module used to stamp `uipath-uipath-http` for the first two (#866;
|
|
161
|
+
* uip 1.202.0-dev, 2026-09-30).
|
|
162
|
+
*
|
|
163
|
+
* @throws BindingRefusal when two nodes on one connection name different connectors.
|
|
66
164
|
*/
|
|
67
165
|
export function connectionResources(flow) {
|
|
68
166
|
if (!isRecord(flow))
|
|
@@ -78,25 +176,31 @@ export function connectionResources(flow) {
|
|
|
78
176
|
continue;
|
|
79
177
|
if (str(binding.propertyAttribute)?.toLowerCase() !== 'connectionid')
|
|
80
178
|
continue;
|
|
81
|
-
|
|
82
|
-
byKey.set(key, binding);
|
|
179
|
+
byKey.set(key, [...(byKey.get(key) ?? []), binding]);
|
|
83
180
|
}
|
|
84
181
|
if (byKey.size === 0)
|
|
85
182
|
return [];
|
|
183
|
+
const connectors = connectionConnectors(flow);
|
|
86
184
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes.filter(isRecord) : [];
|
|
87
185
|
const resources = [];
|
|
88
186
|
const seen = new Set();
|
|
89
187
|
for (const node of nodes) {
|
|
90
|
-
const detail =
|
|
188
|
+
const detail = nodeDetail(node);
|
|
91
189
|
if (!detail)
|
|
92
190
|
continue;
|
|
93
|
-
const key =
|
|
191
|
+
const key = nodeConnectionKey(detail);
|
|
94
192
|
if (!key || seen.has(key))
|
|
95
193
|
continue;
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
194
|
+
const candidates = byKey.get(key);
|
|
195
|
+
if (!candidates)
|
|
98
196
|
continue;
|
|
99
197
|
seen.add(key);
|
|
198
|
+
const connector = connectors.get(key) ?? nodeConnectionConnector(detail) ?? '';
|
|
199
|
+
// The label is the FIRST ConnectionId binding on the connection, even when
|
|
200
|
+
// #865's fix emits one per referencing definition — measured against `uip
|
|
201
|
+
// maestro flow pack` (uip 1.202.0-dev) on both orders of the issue's repro: pack labels
|
|
202
|
+
// the resource after the earlier step's binding, like `ActivityName`.
|
|
203
|
+
const binding = candidates[0];
|
|
100
204
|
const displayLabel = str(binding.name) ?? key;
|
|
101
205
|
const label = isRecord(node.display) ? str(node.display.label) : undefined;
|
|
102
206
|
resources.push({
|
|
@@ -116,7 +220,7 @@ export function connectionResources(flow) {
|
|
|
116
220
|
BindingsVersion: BINDINGS_VERSION,
|
|
117
221
|
DisplayLabel: displayLabel,
|
|
118
222
|
UseConnectionService: 'true',
|
|
119
|
-
Connector:
|
|
223
|
+
Connector: connector,
|
|
120
224
|
},
|
|
121
225
|
});
|
|
122
226
|
}
|
package/dist/check.js
CHANGED
|
@@ -11,7 +11,7 @@ import { isLookupToken, resolvedValue, unresolvedLookupMessage, } from './core/l
|
|
|
11
11
|
import { QUEUE_ITEM_RECORD_SCHEMA } from './core/queue-record.js';
|
|
12
12
|
import { EVENT_FILTER_OPERATORS, readEventFilter, eventFilterProblem } from './event-filters.js';
|
|
13
13
|
import { extractRefs } from './core/expr-check.js';
|
|
14
|
-
import { bindingSelfNameMessage, bindingUndeclaredMessage, undeclaredBinding } from './core/binding-messages.js';
|
|
14
|
+
import { bindingSelfNameMessage, bindingUndeclaredMessage, connectionConnectorConflictMessage, undeclaredBinding } from './core/binding-messages.js';
|
|
15
15
|
import { isAnswerField } from './core/hitl-answers.js';
|
|
16
16
|
import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from './core/hitl-routing.js';
|
|
17
17
|
import { childStepLists, declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
|
|
@@ -20,7 +20,51 @@ import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js
|
|
|
20
20
|
import { prepareCommand, resolveFlagArg } from './core/cli-spelling.js';
|
|
21
21
|
import { EVENT_BYOA_UNVERIFIED, byoaUnverified } from './core/byoa.js';
|
|
22
22
|
export function check(built, opts = {}) {
|
|
23
|
-
|
|
23
|
+
const outer = connectionUsage;
|
|
24
|
+
connectionUsage = [];
|
|
25
|
+
try {
|
|
26
|
+
const diags = checkFlow(built, [], opts);
|
|
27
|
+
diags.push(...connectionConnectorConflicts(connectionUsage, opts));
|
|
28
|
+
return diags;
|
|
29
|
+
}
|
|
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;
|
|
24
68
|
}
|
|
25
69
|
/**
|
|
26
70
|
* `ancestors` is the chain of child flows currently being checked, innermost
|
|
@@ -358,7 +402,10 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
358
402
|
const filled = (v) => typeof v === 'string' && v.trim() !== '';
|
|
359
403
|
if (pair?.managed && filled(pair.connection) && filled(pair.folder)) {
|
|
360
404
|
const httpStep = s.name;
|
|
361
|
-
|
|
405
|
+
const target = pair.targetConnector;
|
|
406
|
+
checkBindingLabels(`managed http "${httpStep}"`, pair, opts, (code, message) => diags.push({ level: 'error', code, step: httpStep, message }), undefined,
|
|
407
|
+
// The connector its CONNECTION belongs to: the target, not the wrapper.
|
|
408
|
+
{ step: httpStep, connector: typeof target === 'string' && target !== '' ? target : 'uipath-uipath-http' });
|
|
362
409
|
}
|
|
363
410
|
const seen = new Set();
|
|
364
411
|
for (const b of s.spec.inputs.branches ?? []) {
|
|
@@ -537,7 +584,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
537
584
|
});
|
|
538
585
|
checkEventSubscription(s.spec.subscription, `waitForEvent "${stepName}"`, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts));
|
|
539
586
|
checkEventResolution(s.spec.subscription, `waitForEvent "${stepName}"`, 'event', opts, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts));
|
|
540
|
-
checkBindingLabels(`waitForEvent "${stepName}"`, s.spec.subscription, opts, push('error'), eventTarget(s.spec.subscription));
|
|
587
|
+
checkBindingLabels(`waitForEvent "${stepName}"`, s.spec.subscription, opts, push('error'), eventTarget(s.spec.subscription), { step: stepName, connector: eventTarget(s.spec.subscription)?.key });
|
|
541
588
|
}
|
|
542
589
|
if (s.spec.kind === 'subflow') {
|
|
543
590
|
// Only a real child goes in the map: a missing one is reported by
|
|
@@ -1453,7 +1500,7 @@ function checkConnectorContract(step, spec, opts, diags) {
|
|
|
1453
1500
|
if (inputErr) {
|
|
1454
1501
|
diags.push({ level: 'error', code: 'CONNECTOR_INPUT', step, message: `Connector step "${step}": ${inputErr}` });
|
|
1455
1502
|
}
|
|
1456
|
-
checkBindingLabels(`Connector step "${step}"`, spec, opts, (code, message) => diags.push({ level: 'error', code, step, message }), { key: spec.key, action: spec.action });
|
|
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 });
|
|
1457
1504
|
}
|
|
1458
1505
|
/** What `registry prepare` takes for an event subscription: its connector key and curated event name. */
|
|
1459
1506
|
function eventTarget(sub) {
|
|
@@ -1472,7 +1519,13 @@ function eventTarget(sub) {
|
|
|
1472
1519
|
* Silent without a bindings.json, and for a label that resolves to anything
|
|
1473
1520
|
* other than its own name (stub values are compile's CONNECTION_STUB warning).
|
|
1474
1521
|
*/
|
|
1475
|
-
function checkBindingLabels(where, labels, opts, report, target
|
|
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
|
+
}
|
|
1476
1529
|
const bindings = opts.bindings;
|
|
1477
1530
|
if (!bindings || !labels)
|
|
1478
1531
|
return;
|
|
@@ -2285,7 +2338,7 @@ function checkTrigger(spec, diags, opts = {}) {
|
|
|
2285
2338
|
const sub = spec.subscription;
|
|
2286
2339
|
checkEventSubscription(sub, 'trigger', at, push('warning'), eventParamsFor(sub, opts));
|
|
2287
2340
|
checkEventResolution(sub, 'trigger', 'trigger', opts, at, push('warning'), eventParamsFor(sub, opts));
|
|
2288
|
-
checkBindingLabels('event trigger', sub, opts, at, eventTarget(sub));
|
|
2341
|
+
checkBindingLabels('event trigger', sub, opts, at, eventTarget(sub), { step: 'trigger', connector: eventTarget(sub)?.key });
|
|
2289
2342
|
return;
|
|
2290
2343
|
}
|
|
2291
2344
|
// manual() is the explicit spelling of the default — nothing to validate.
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/** A tenant resource key — an Integration Service connection id or an Orchestrator folder key — is a GUID. */
|
|
2
2
|
export declare const GUID_RE: RegExp;
|
|
3
3
|
/** The diagnostic codes a label refusal carries, in `check` and on the error `serialize` throws. */
|
|
4
|
-
export type BindingRefusalCode = 'BINDING_SELF_NAME' | 'BINDING_UNDECLARED'
|
|
4
|
+
export type BindingRefusalCode = 'BINDING_SELF_NAME' | 'BINDING_UNDECLARED'
|
|
5
|
+
/** An emitted definition's `<bindings.NAME>` placeholder has no `bindings[]` entry named NAME (#865). */
|
|
6
|
+
| 'BINDING_PLACEHOLDER_UNRESOLVED'
|
|
7
|
+
/** Two steps on one connection disagree on which connector it belongs to (#866). */
|
|
8
|
+
| 'CONNECTION_CONNECTOR_CONFLICT';
|
|
5
9
|
/**
|
|
6
10
|
* What `serialize` throws for a label it refuses. It is an ordinary `Error` whose
|
|
7
11
|
* message is the sentence `check` prints; the `code` lets `compile --no-check`
|
|
@@ -58,3 +62,14 @@ export interface PrepareTarget {
|
|
|
58
62
|
* `prepare` command for this step's own connector and operation.
|
|
59
63
|
*/
|
|
60
64
|
export declare function bindingUndeclaredMessage(where: string, name: string, attribute: 'connection' | 'folder', declared: readonly string[], target?: PrepareTarget): string;
|
|
65
|
+
/**
|
|
66
|
+
* Why compile refuses one connection used by steps of different connectors —
|
|
67
|
+
* shared by `serialize` (which throws it, via `connectionConnectors` in
|
|
68
|
+
* `bindings-v2.ts`) and `check`, so both print the same sentence.
|
|
69
|
+
*
|
|
70
|
+
* `users` maps each connector to the steps that use the connection as that
|
|
71
|
+
* connector. For managed HTTP the connector is its `targetConnector` — the
|
|
72
|
+
* connection it authenticates with — not the `uipath-uipath-http` wrapper
|
|
73
|
+
* (flow-builder-sdk#866).
|
|
74
|
+
*/
|
|
75
|
+
export declare function connectionConnectorConflictMessage(connection: string, users: ReadonlyMap<string, readonly string[]>): string;
|
|
@@ -89,3 +89,24 @@ export function bindingUndeclaredMessage(where, name, attribute, declared, targe
|
|
|
89
89
|
+ `declares the label with a placeholder resourceKey instead, which compiles with a CONNECTION_STUB warning — `
|
|
90
90
|
+ `never an invented GUID.`;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Why compile refuses one connection used by steps of different connectors —
|
|
94
|
+
* shared by `serialize` (which throws it, via `connectionConnectors` in
|
|
95
|
+
* `bindings-v2.ts`) and `check`, so both print the same sentence.
|
|
96
|
+
*
|
|
97
|
+
* `users` maps each connector to the steps that use the connection as that
|
|
98
|
+
* connector. For managed HTTP the connector is its `targetConnector` — the
|
|
99
|
+
* connection it authenticates with — not the `uipath-uipath-http` wrapper
|
|
100
|
+
* (flow-builder-sdk#866).
|
|
101
|
+
*/
|
|
102
|
+
export function connectionConnectorConflictMessage(connection, users) {
|
|
103
|
+
const who = [...users]
|
|
104
|
+
.map(([connector, steps]) => `${steps.map((s) => `"${s}"`).join(', ')} as ${connector}`)
|
|
105
|
+
.join('; ');
|
|
106
|
+
return `connection "${connection}" is used by steps of different connectors (${who}). An Integration Service `
|
|
107
|
+
+ `connection belongs to exactly one connector, so at most one of these steps can run on it, and the solution `
|
|
108
|
+
+ `resource would be declared as whichever came first. Give each connector its own connection label in `
|
|
109
|
+
+ `bindings.json (\`${prepareCommand('<connector-key>', '<action>')}\` binds one per connector). A managed `
|
|
110
|
+
+ `\`http({ managed: true, … })\` step on another connector's connection must name that connector in `
|
|
111
|
+
+ `\`targetConnector\`.`;
|
|
112
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The node envelope: the fields `uip maestro flow validate` requires on EVERY
|
|
3
|
+
* entry of a `.flow` file's `nodes[]` (root and every `subflows.<id>.nodes[]`),
|
|
4
|
+
* whatever its type.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the platform's file-format node schema (`@uipath/flow-schema`
|
|
7
|
+
* `fileFormatNodeSchema`, bundled into the CLI's flow tool):
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* id: z.string().min(1)
|
|
11
|
+
* type: z.string().min(1)
|
|
12
|
+
* typeVersion: z.string().regex(/^\d+\.\d+(\.\d+)?$/)
|
|
13
|
+
* display: z.object({ … }) // required, may be {}
|
|
14
|
+
* ui: { position: { x: number, y: number }, size?: { width, height } } // optional in the file
|
|
15
|
+
* inputs?: Record<string, unknown>
|
|
16
|
+
* outputs?: Record<string, unknown>
|
|
17
|
+
* parentId?: z.string().min(1)
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* `makeNode` builds this shape for every definition-backed node, but a node
|
|
21
|
+
* built any other way (sticky notes were, issue #869) had nothing holding it
|
|
22
|
+
* to the contract: `check` passed, `compile` wrote the file, and only
|
|
23
|
+
* `flow validate` refused it. `serialize` runs this over every finished scope,
|
|
24
|
+
* so a missing field is an SDK emit bug caught before a file is written — on
|
|
25
|
+
* every path, `compile --no-check` and direct `serialize()` included.
|
|
26
|
+
*/
|
|
27
|
+
/** Every way `node` breaks the envelope, as `<field>: <problem>` strings. Empty when it conforms. */
|
|
28
|
+
export declare function nodeEnvelopeProblems(node: unknown): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Throw if any node in one scope breaks the envelope. `where` names the scope
|
|
31
|
+
* (`flow "x"`, `subflow "call"`) so the message points at the file section
|
|
32
|
+
* `flow validate` would have reported.
|
|
33
|
+
*/
|
|
34
|
+
export declare function assertNodeEnvelope(nodes: readonly unknown[], where: string): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The node envelope: the fields `uip maestro flow validate` requires on EVERY
|
|
3
|
+
* entry of a `.flow` file's `nodes[]` (root and every `subflows.<id>.nodes[]`),
|
|
4
|
+
* whatever its type.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the platform's file-format node schema (`@uipath/flow-schema`
|
|
7
|
+
* `fileFormatNodeSchema`, bundled into the CLI's flow tool):
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* id: z.string().min(1)
|
|
11
|
+
* type: z.string().min(1)
|
|
12
|
+
* typeVersion: z.string().regex(/^\d+\.\d+(\.\d+)?$/)
|
|
13
|
+
* display: z.object({ … }) // required, may be {}
|
|
14
|
+
* ui: { position: { x: number, y: number }, size?: { width, height } } // optional in the file
|
|
15
|
+
* inputs?: Record<string, unknown>
|
|
16
|
+
* outputs?: Record<string, unknown>
|
|
17
|
+
* parentId?: z.string().min(1)
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* `makeNode` builds this shape for every definition-backed node, but a node
|
|
21
|
+
* built any other way (sticky notes were, issue #869) had nothing holding it
|
|
22
|
+
* to the contract: `check` passed, `compile` wrote the file, and only
|
|
23
|
+
* `flow validate` refused it. `serialize` runs this over every finished scope,
|
|
24
|
+
* so a missing field is an SDK emit bug caught before a file is written — on
|
|
25
|
+
* every path, `compile --no-check` and direct `serialize()` included.
|
|
26
|
+
*/
|
|
27
|
+
const TYPE_VERSION_RE = /^\d+\.\d+(\.\d+)?$/;
|
|
28
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
29
|
+
const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
|
|
30
|
+
/** Every way `node` breaks the envelope, as `<field>: <problem>` strings. Empty when it conforms. */
|
|
31
|
+
export function nodeEnvelopeProblems(node) {
|
|
32
|
+
if (!isRecord(node))
|
|
33
|
+
return ['node: expected an object'];
|
|
34
|
+
const problems = [];
|
|
35
|
+
if (!isNonEmptyString(node.id))
|
|
36
|
+
problems.push('id: expected a non-empty string');
|
|
37
|
+
if (!isNonEmptyString(node.type))
|
|
38
|
+
problems.push('type: expected a non-empty string');
|
|
39
|
+
if (typeof node.typeVersion !== 'string' || !TYPE_VERSION_RE.test(node.typeVersion)) {
|
|
40
|
+
problems.push(`typeVersion: expected "x.y" or "x.y.z", got ${JSON.stringify(node.typeVersion)}`);
|
|
41
|
+
}
|
|
42
|
+
if (!isRecord(node.display))
|
|
43
|
+
problems.push('display: expected an object (may be empty)');
|
|
44
|
+
if (node.ui !== undefined) {
|
|
45
|
+
const pos = isRecord(node.ui) ? node.ui.position : undefined;
|
|
46
|
+
if (!isRecord(pos) || typeof pos.x !== 'number' || typeof pos.y !== 'number') {
|
|
47
|
+
problems.push('ui.position: expected { x: number, y: number }');
|
|
48
|
+
}
|
|
49
|
+
const size = isRecord(node.ui) ? node.ui.size : undefined;
|
|
50
|
+
if (size !== undefined && (!isRecord(size) || typeof size.width !== 'number' || typeof size.height !== 'number')) {
|
|
51
|
+
problems.push('ui.size: expected { width: number, height: number }');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (node.inputs !== undefined && !isRecord(node.inputs))
|
|
55
|
+
problems.push('inputs: expected an object');
|
|
56
|
+
if (node.outputs !== undefined && !isRecord(node.outputs))
|
|
57
|
+
problems.push('outputs: expected an object');
|
|
58
|
+
if (node.parentId !== undefined && !isNonEmptyString(node.parentId)) {
|
|
59
|
+
problems.push('parentId: expected a non-empty string');
|
|
60
|
+
}
|
|
61
|
+
return problems;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Throw if any node in one scope breaks the envelope. `where` names the scope
|
|
65
|
+
* (`flow "x"`, `subflow "call"`) so the message points at the file section
|
|
66
|
+
* `flow validate` would have reported.
|
|
67
|
+
*/
|
|
68
|
+
export function assertNodeEnvelope(nodes, where) {
|
|
69
|
+
const bad = [];
|
|
70
|
+
nodes.forEach((n, i) => {
|
|
71
|
+
const problems = nodeEnvelopeProblems(n);
|
|
72
|
+
if (!problems.length)
|
|
73
|
+
return;
|
|
74
|
+
const id = isRecord(n) && typeof n.id === 'string' ? n.id : `#${i}`;
|
|
75
|
+
const type = isRecord(n) && typeof n.type === 'string' ? n.type : '?';
|
|
76
|
+
bad.push(` node "${id}" (${type}): ${problems.join('; ')}`);
|
|
77
|
+
});
|
|
78
|
+
if (!bad.length)
|
|
79
|
+
return;
|
|
80
|
+
throw new Error(`internal SDK error: ${where} would emit ${bad.length} node(s) that \`uip maestro flow validate\` `
|
|
81
|
+
+ `rejects — every node needs id, type, typeVersion and display:\n${bad.join('\n')}\n`
|
|
82
|
+
+ `This is a serializer bug, not an authoring mistake; please report it.`);
|
|
83
|
+
}
|
package/dist/serialize.d.ts
CHANGED
|
@@ -54,6 +54,34 @@ export declare function serializeWithSidecars(built: BuiltFlow, opts?: Serialize
|
|
|
54
54
|
}>;
|
|
55
55
|
};
|
|
56
56
|
export declare function serialize(built: BuiltFlow, opts?: SerializeOpts): any;
|
|
57
|
+
/**
|
|
58
|
+
* Every `<bindings.NAME>` placeholder a node's definition carries in its
|
|
59
|
+
* `model.context` must resolve to a `bindings[]` entry for THAT node.
|
|
60
|
+
*
|
|
61
|
+
* "Resolve" is Studio Web's rule, not a looser one: flow-v1
|
|
62
|
+
* `matchContextBindings` + `getInstanceBindingKey`
|
|
63
|
+
* (flow-schema/src/packaging/resolve-context.ts) take the entries whose
|
|
64
|
+
* `resource` EQUALS the definition's `model.bindings.resource`, scope them to the
|
|
65
|
+
* node's `inputs.detail.connectionResourceId` when that resource is `connection`
|
|
66
|
+
* (or undeclared), and look the placeholder up there BY NAME. Any miss blocks
|
|
67
|
+
* publish (MST-15010), and one that slips through reaches Studio as the literal
|
|
68
|
+
* placeholder: `invalid GUID <bindings.uipath-salesforce-slack connection>`.
|
|
69
|
+
* `uip maestro flow validate` does not check this.
|
|
70
|
+
*
|
|
71
|
+
* That is how #865 shipped — a per-call-site dedupe that let the first step on a
|
|
72
|
+
* shared connection claim it — and UV-16774 before it (a `Connection` resource
|
|
73
|
+
* spelling). Checking the finished document catches that class for every family
|
|
74
|
+
* and every emission order, whichever call site produced it.
|
|
75
|
+
*
|
|
76
|
+
* It holds for real definitions: of the 5,102 in the local registry index
|
|
77
|
+
* (2026-09-30), 4,999 reference a `<bindings.…>` name and every one of them
|
|
78
|
+
* declares that name in `model.bindings.values`, which is what the emitters
|
|
79
|
+
* close. Agent resource definitions are the one exemption (see below).
|
|
80
|
+
*
|
|
81
|
+
* @throws BindingRefusal `BINDING_PLACEHOLDER_UNRESOLVED`, naming each dangling
|
|
82
|
+
* placeholder and the step that carries it.
|
|
83
|
+
*/
|
|
84
|
+
export declare function assertBindingPlaceholdersResolve(flow: any): void;
|
|
57
85
|
/**
|
|
58
86
|
* What `compile` would refuse these connector inputs with, or `undefined` when it
|
|
59
87
|
* would accept them — the SAME code path `buildConnectorInputs` runs at emit time,
|
package/dist/serialize.js
CHANGED
|
@@ -15,10 +15,12 @@ import { FlowAction, FlowTrigger } from './core/node-classes.js';
|
|
|
15
15
|
import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
|
|
16
16
|
import { dataFabricUnknownInputs } from './core/typed-raw-node.js';
|
|
17
17
|
import { isAnswerField } from './core/hitl-answers.js';
|
|
18
|
+
import { connectionConnectors } from './bindings-v2.js';
|
|
18
19
|
import { BindingRefusal, GUID_RE, bindingSelfNameMessage, bindingUndeclaredMessage, undeclaredBinding } from './core/binding-messages.js';
|
|
19
20
|
import { ByoaRefusal, byoaUnverified } from './core/byoa.js';
|
|
20
21
|
import { OUTCOME_HANDLE_NODE_TYPES, assertHitlExitInvariant, hitlExitModel, hitlExitViolation, outcomeSlug } from './core/hitl-routing.js';
|
|
21
22
|
import { declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
|
|
23
|
+
import { assertNodeEnvelope } from './core/node-envelope.js';
|
|
22
24
|
import { buildConfiguration, transportHttpMethod } from './config.js';
|
|
23
25
|
import { stableId } from './core/stable-id.js';
|
|
24
26
|
import { readEventFilter, eventFilterProblem, eventFilterJmes, eventFilterTreeLeaf } from './event-filters.js';
|
|
@@ -2305,9 +2307,15 @@ function connectionResourceBindings(spec, existing) {
|
|
|
2305
2307
|
if (values.length === 0)
|
|
2306
2308
|
return { entries: [] };
|
|
2307
2309
|
const resourceKey = spec.resourceKey ?? spec.connectionId ?? '';
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2310
|
+
// Keyed on the NAME as well (flow-builder-sdk#865). The name is the definition's
|
|
2311
|
+
// own `<bindings.NAME>` placeholder, and two definitions sharing one connection
|
|
2312
|
+
// can spell it differently — managed HTTP says `uipath-uipath-http connection`,
|
|
2313
|
+
// the Slack connector `uipath-salesforce-slack connection`. Keyed on
|
|
2314
|
+
// resourceKey + attribute alone, whichever step came first claimed the
|
|
2315
|
+
// connection and the other node's placeholder never resolved: Studio showed
|
|
2316
|
+
// `invalid GUID <bindings.uipath-salesforce-slack connection>`.
|
|
2317
|
+
const have = (name, attr) => [...existing.values()].some((b) => b.resource === 'connection' && b.name === name && b.resourceKey === resourceKey
|
|
2318
|
+
&& b.propertyAttribute === attr);
|
|
2311
2319
|
const slug = spec.connectorKey.replace(/[^A-Za-z0-9]/g, '') || 'Connector';
|
|
2312
2320
|
let base = `b${slug.charAt(0).toUpperCase()}${slug.slice(1)}`;
|
|
2313
2321
|
for (let n = 2; existing.has(`${base}Connection`) || existing.has(`${base}FolderKey`); n++) {
|
|
@@ -2316,7 +2324,7 @@ function connectionResourceBindings(spec, existing) {
|
|
|
2316
2324
|
const entries = values.flatMap((v) => {
|
|
2317
2325
|
const name = typeof v.name === 'string' ? v.name : '';
|
|
2318
2326
|
const attr = v.propertyAttribute;
|
|
2319
|
-
if (!name || (attr !== 'ConnectionId' && attr !== 'FolderKey') || have(attr))
|
|
2327
|
+
if (!name || (attr !== 'ConnectionId' && attr !== 'FolderKey') || have(name, attr))
|
|
2320
2328
|
return [];
|
|
2321
2329
|
const isConnection = attr === 'ConnectionId';
|
|
2322
2330
|
const dflt = isConnection ? spec.connectionId : spec.folderKey;
|
|
@@ -3063,6 +3071,8 @@ const ERROR_ENVELOPE_VAR = {
|
|
|
3063
3071
|
function errorEnvelopeVar(nodeType) {
|
|
3064
3072
|
return ERROR_ENVELOPE_VAR[nodeType];
|
|
3065
3073
|
}
|
|
3074
|
+
/** The `typeVersion` Studio Web stamps on a sticky note. The type has no manifest to take it from. */
|
|
3075
|
+
const STICKY_NOTE_TYPE_VERSION = '1.0.0';
|
|
3066
3076
|
/**
|
|
3067
3077
|
* Append the flow's `.stickyNotes()` as real `stickyNote` nodes.
|
|
3068
3078
|
*
|
|
@@ -3100,14 +3110,20 @@ function emitStickyNotes(built, scope) {
|
|
|
3100
3110
|
if ((note.width === undefined) !== (note.height === undefined)) {
|
|
3101
3111
|
throw new Error(`.stickyNotes(): note "${id}" needs both width and height, or neither.`);
|
|
3102
3112
|
}
|
|
3113
|
+
// The envelope Studio Web writes for a note (flow-v1 canvas: the add path
|
|
3114
|
+
// sets `typeVersion: '1.0.0'` and no display, and `nodeToInstance` persists
|
|
3115
|
+
// `display` as `{}`). Both fields are required on every node by
|
|
3116
|
+
// `flow validate`, manifest or not (#869).
|
|
3103
3117
|
scope.nodes.push({
|
|
3104
3118
|
id,
|
|
3105
3119
|
type: 'stickyNote',
|
|
3106
|
-
|
|
3120
|
+
typeVersion: STICKY_NOTE_TYPE_VERSION,
|
|
3107
3121
|
ui: {
|
|
3108
3122
|
position: { x: note.x, y: note.y },
|
|
3109
3123
|
...(note.width !== undefined ? { size: { width: note.width, height: note.height } } : {}),
|
|
3110
3124
|
},
|
|
3125
|
+
display: {},
|
|
3126
|
+
inputs: { color: note.color ?? 'yellow', content: note.content },
|
|
3111
3127
|
});
|
|
3112
3128
|
});
|
|
3113
3129
|
}
|
|
@@ -4184,6 +4200,7 @@ export function serialize(built, opts = {}) {
|
|
|
4184
4200
|
typeAgentInputSchemas(scope, globals);
|
|
4185
4201
|
emitStickyNotes(child, scope);
|
|
4186
4202
|
const layoutEdges = applyAuthorLayout(child.layout, scope, `subflow "${nodeId}"`);
|
|
4203
|
+
assertNodeEnvelope(scope.nodes, `subflow "${nodeId}"`);
|
|
4187
4204
|
return {
|
|
4188
4205
|
nodes: scope.nodes,
|
|
4189
4206
|
edges: scope.edges,
|
|
@@ -6241,6 +6258,7 @@ export function serialize(built, opts = {}) {
|
|
|
6241
6258
|
typeAgentInputSchemas(root, globals);
|
|
6242
6259
|
emitStickyNotes(built, root);
|
|
6243
6260
|
const rootLayoutEdges = applyAuthorLayout(built.layout, root, `flow "${built.id}"`);
|
|
6261
|
+
assertNodeEnvelope(root.nodes, `flow "${built.id}"`);
|
|
6244
6262
|
// The FILE-FORMAT version this document declares: the author's
|
|
6245
6263
|
// `.schemaVersion()` when they set one, else the profile's write floor. It is
|
|
6246
6264
|
// resolved HERE rather than in `check()` alone because the serializer must not
|
|
@@ -6269,8 +6287,84 @@ export function serialize(built, opts = {}) {
|
|
|
6269
6287
|
// Passing the target is what lets a version-conditional step exist at all —
|
|
6270
6288
|
// see migrate.ts's VERSION_CONDITIONAL ladder.
|
|
6271
6289
|
migrateToSchemaVersion(flow, schemaVersion);
|
|
6290
|
+
// Emitted-document invariants over connections, checked on the FINISHED file
|
|
6291
|
+
// so every family and every emission order is covered by one rule, not by
|
|
6292
|
+
// each call site remembering it (flow-builder-sdk#865/#866).
|
|
6293
|
+
assertBindingPlaceholdersResolve(flow);
|
|
6294
|
+
connectionConnectors(flow);
|
|
6272
6295
|
return flow;
|
|
6273
6296
|
}
|
|
6297
|
+
/**
|
|
6298
|
+
* Every `<bindings.NAME>` placeholder a node's definition carries in its
|
|
6299
|
+
* `model.context` must resolve to a `bindings[]` entry for THAT node.
|
|
6300
|
+
*
|
|
6301
|
+
* "Resolve" is Studio Web's rule, not a looser one: flow-v1
|
|
6302
|
+
* `matchContextBindings` + `getInstanceBindingKey`
|
|
6303
|
+
* (flow-schema/src/packaging/resolve-context.ts) take the entries whose
|
|
6304
|
+
* `resource` EQUALS the definition's `model.bindings.resource`, scope them to the
|
|
6305
|
+
* node's `inputs.detail.connectionResourceId` when that resource is `connection`
|
|
6306
|
+
* (or undeclared), and look the placeholder up there BY NAME. Any miss blocks
|
|
6307
|
+
* publish (MST-15010), and one that slips through reaches Studio as the literal
|
|
6308
|
+
* placeholder: `invalid GUID <bindings.uipath-salesforce-slack connection>`.
|
|
6309
|
+
* `uip maestro flow validate` does not check this.
|
|
6310
|
+
*
|
|
6311
|
+
* That is how #865 shipped — a per-call-site dedupe that let the first step on a
|
|
6312
|
+
* shared connection claim it — and UV-16774 before it (a `Connection` resource
|
|
6313
|
+
* spelling). Checking the finished document catches that class for every family
|
|
6314
|
+
* and every emission order, whichever call site produced it.
|
|
6315
|
+
*
|
|
6316
|
+
* It holds for real definitions: of the 5,102 in the local registry index
|
|
6317
|
+
* (2026-09-30), 4,999 reference a `<bindings.…>` name and every one of them
|
|
6318
|
+
* declares that name in `model.bindings.values`, which is what the emitters
|
|
6319
|
+
* close. Agent resource definitions are the one exemption (see below).
|
|
6320
|
+
*
|
|
6321
|
+
* @throws BindingRefusal `BINDING_PLACEHOLDER_UNRESOLVED`, naming each dangling
|
|
6322
|
+
* placeholder and the step that carries it.
|
|
6323
|
+
*/
|
|
6324
|
+
export function assertBindingPlaceholdersResolve(flow) {
|
|
6325
|
+
const bindings = Array.isArray(flow?.bindings) ? flow.bindings : [];
|
|
6326
|
+
const defs = new Map();
|
|
6327
|
+
for (const def of Array.isArray(flow?.definitions) ? flow.definitions : []) {
|
|
6328
|
+
defs.set(`${def?.nodeType}@${def?.version}`, def);
|
|
6329
|
+
}
|
|
6330
|
+
const nodes = [...(Array.isArray(flow?.nodes) ? flow.nodes : [])];
|
|
6331
|
+
for (const sub of Object.values(flow?.subflows ?? {})) {
|
|
6332
|
+
if (Array.isArray(sub?.nodes))
|
|
6333
|
+
nodes.push(...sub.nodes);
|
|
6334
|
+
}
|
|
6335
|
+
const dangling = [];
|
|
6336
|
+
for (const node of nodes) {
|
|
6337
|
+
// An agent's resource nodes (`uipath.agent.resource.tool.connector.…`) carry
|
|
6338
|
+
// the same placeholders, but they are resolved through the AGENT project —
|
|
6339
|
+
// `uip agent refresh` generates the connector tool's resource — and the
|
|
6340
|
+
// deployed corpus carries no flow `bindings[]` entry for them.
|
|
6341
|
+
if (typeof node?.type !== 'string' || node.type.startsWith('uipath.agent.resource.'))
|
|
6342
|
+
continue;
|
|
6343
|
+
const model = defs.get(`${node.type}@${node.typeVersion}`)?.model;
|
|
6344
|
+
if (!Array.isArray(model?.context))
|
|
6345
|
+
continue;
|
|
6346
|
+
const resource = model.bindings?.resource;
|
|
6347
|
+
const scope = resource === 'connection' || resource === undefined
|
|
6348
|
+
? node.inputs?.detail?.connectionResourceId ?? node.inputs?.connectionId ?? undefined
|
|
6349
|
+
: undefined;
|
|
6350
|
+
const candidates = resource === undefined ? bindings : bindings.filter((b) => b?.resource === resource);
|
|
6351
|
+
for (const row of model.context) {
|
|
6352
|
+
const name = typeof row?.value === 'string' ? /^<bindings\.(.+)>$/.exec(row.value)?.[1] : undefined;
|
|
6353
|
+
if (name === undefined)
|
|
6354
|
+
continue;
|
|
6355
|
+
if (candidates.some((b) => b?.name === name && (!scope || b.resourceKey === scope)))
|
|
6356
|
+
continue;
|
|
6357
|
+
dangling.push(`step "${node.display?.label ?? node.id}" (${node.type}): "${row.value}"`
|
|
6358
|
+
+ `${scope ? ` on connection ${scope}` : ''}`);
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
if (dangling.length === 0)
|
|
6362
|
+
return;
|
|
6363
|
+
throw new BindingRefusal('BINDING_PLACEHOLDER_UNRESOLVED', `serialize: ${dangling.join('; ')} — no bindings[] entry of that name${dangling.length === 1 ? '' : ' (each)'} `
|
|
6364
|
+
+ `is emitted for the step, so Studio Web reads the placeholder itself as the value (`
|
|
6365
|
+
+ `"invalid GUID <bindings.…>") and refuses to publish. This is a serializer defect, not an authoring `
|
|
6366
|
+
+ `error; report it with the flow source.`);
|
|
6367
|
+
}
|
|
6274
6368
|
/**
|
|
6275
6369
|
* Validate a connector's activity inputs against its declared field list and
|
|
6276
6370
|
* return them ROUTED to the wire slots the platform reads. Catches the three
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "6.14.
|
|
3
|
+
"version": "6.14.4",
|
|
4
4
|
"description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://docs.uipath.com/maestro",
|
|
@@ -92,5 +92,5 @@
|
|
|
92
92
|
"@types/node": "^22.7.0",
|
|
93
93
|
"esbuild": "^0.28.1"
|
|
94
94
|
},
|
|
95
|
-
"gitref": "
|
|
95
|
+
"gitref": "deacd660d0428eb3144e4b61a76bbfbbfc87cbe8"
|
|
96
96
|
}
|