@uipath/maestro-builder-sdk 6.18.10 → 6.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/api-index.md +185 -184
  2. package/dist/api-members.md +506 -473
  3. package/dist/check.js +358 -695
  4. package/dist/cli/commands/flow.js +3 -0
  5. package/dist/compile-cli.js +51 -0
  6. package/dist/core/actions.d.ts +132 -765
  7. package/dist/core/actions.js +1 -307
  8. package/dist/core/binding-identity.d.ts +22 -0
  9. package/dist/core/binding-identity.js +28 -0
  10. package/dist/core/context-index.d.ts +34 -0
  11. package/dist/core/context-index.js +38 -0
  12. package/dist/core/definitionless-node-types.d.ts +16 -0
  13. package/dist/core/definitionless-node-types.js +16 -0
  14. package/dist/core/error-port.d.ts +6 -0
  15. package/dist/core/error-port.js +9 -0
  16. package/dist/core/form-field-type.d.ts +33 -0
  17. package/dist/core/form-field-type.js +30 -0
  18. package/dist/core/hitl-routing.d.ts +8 -0
  19. package/dist/core/hitl-routing.js +17 -1
  20. package/dist/core/node-classes.d.ts +4 -63
  21. package/dist/core/node-classes.js +0 -112
  22. package/dist/core/nodes/family.d.ts +36 -0
  23. package/dist/core/nodes/family.js +1 -0
  24. package/dist/core/nodes/node-type.d.ts +136 -0
  25. package/dist/core/nodes/node-type.js +1 -0
  26. package/dist/core/nodes/registry.d.ts +57 -0
  27. package/dist/core/nodes/registry.js +122 -0
  28. package/dist/core/nodes/versioned.d.ts +50 -0
  29. package/dist/core/nodes/versioned.js +46 -0
  30. package/dist/core/process-resource-sort-order.d.ts +40 -0
  31. package/dist/core/process-resource-sort-order.js +44 -0
  32. package/dist/core/typed-raw-node.d.ts +13 -62
  33. package/dist/core/typed-raw-node.js +40 -154
  34. package/dist/core/wire-expr.d.ts +27 -0
  35. package/dist/core/wire-expr.js +34 -0
  36. package/dist/decompile-cli.js +11 -1
  37. package/dist/decompile.d.ts +8 -3
  38. package/dist/decompile.js +665 -354
  39. package/dist/flow-definitions.d.ts +10 -0
  40. package/dist/flow-definitions.js +35 -0
  41. package/dist/flow-expr-check.js +1 -1
  42. package/dist/flow-node-registry.d.ts +13 -0
  43. package/dist/flow-node-registry.js +19 -0
  44. package/dist/flow-node-type.d.ts +192 -0
  45. package/dist/flow-node-type.js +1 -0
  46. package/dist/flow-sdk.d.ts +40 -1
  47. package/dist/flow-sdk.js +29 -2
  48. package/dist/flow-step-tree.js +2 -1
  49. package/dist/index.d.ts +2 -1
  50. package/dist/index.js +1 -0
  51. package/dist/merge-cli.js +25 -8
  52. package/dist/merge-roundtrip.d.ts +81 -0
  53. package/dist/merge-roundtrip.js +404 -0
  54. package/dist/merge.d.ts +35 -6
  55. package/dist/merge.js +124 -24
  56. package/dist/node-placement.d.ts +93 -0
  57. package/dist/node-placement.js +163 -0
  58. package/dist/nodes/data-fabric/flow/check.d.ts +13 -0
  59. package/dist/nodes/data-fabric/flow/check.js +164 -0
  60. package/dist/nodes/data-fabric/flow/factories.d.ts +113 -0
  61. package/dist/nodes/data-fabric/flow/factories.js +134 -0
  62. package/dist/nodes/data-fabric/flow/family.d.ts +7 -0
  63. package/dist/nodes/data-fabric/flow/family.js +9 -0
  64. package/dist/nodes/data-fabric/flow/index.d.ts +8 -0
  65. package/dist/nodes/data-fabric/flow/index.js +8 -0
  66. package/dist/nodes/data-fabric/flow/raw.d.ts +32 -0
  67. package/dist/nodes/data-fabric/flow/raw.js +100 -0
  68. package/dist/nodes/data-fabric/flow/shared.d.ts +77 -0
  69. package/dist/nodes/data-fabric/flow/shared.js +174 -0
  70. package/dist/nodes/data-fabric/flow/spec.d.ts +238 -0
  71. package/dist/nodes/data-fabric/flow/spec.js +1 -0
  72. package/dist/nodes/data-fabric/flow/verbs.d.ts +29 -0
  73. package/dist/nodes/data-fabric/flow/verbs.js +239 -0
  74. package/dist/nodes/delay/flow/check.d.ts +16 -0
  75. package/dist/nodes/delay/flow/check.js +86 -0
  76. package/dist/nodes/delay/flow/factories.d.ts +28 -0
  77. package/dist/nodes/delay/flow/factories.js +38 -0
  78. package/dist/nodes/delay/flow/family.d.ts +3 -0
  79. package/dist/nodes/delay/flow/family.js +8 -0
  80. package/dist/nodes/delay/flow/index.d.ts +3 -0
  81. package/dist/nodes/delay/flow/index.js +3 -0
  82. package/dist/nodes/delay/flow/node-type.d.ts +13 -0
  83. package/dist/nodes/delay/flow/node-type.js +71 -0
  84. package/dist/nodes/delay/flow/spec.d.ts +43 -0
  85. package/dist/nodes/delay/flow/spec.js +16 -0
  86. package/dist/nodes/document/flow/factories.d.ts +27 -0
  87. package/dist/nodes/document/flow/factories.js +30 -0
  88. package/dist/nodes/document/flow/family.d.ts +3 -0
  89. package/dist/nodes/document/flow/family.js +7 -0
  90. package/dist/nodes/document/flow/index.d.ts +3 -0
  91. package/dist/nodes/document/flow/index.js +3 -0
  92. package/dist/nodes/document/flow/node-type.d.ts +14 -0
  93. package/dist/nodes/document/flow/node-type.js +150 -0
  94. package/dist/nodes/document/flow/spec.d.ts +65 -0
  95. package/dist/nodes/document/flow/spec.js +1 -0
  96. package/dist/nodes/mock/flow/factories.d.ts +51 -0
  97. package/dist/nodes/mock/flow/factories.js +47 -0
  98. package/dist/nodes/mock/flow/family.d.ts +3 -0
  99. package/dist/nodes/mock/flow/family.js +7 -0
  100. package/dist/nodes/mock/flow/index.d.ts +2 -0
  101. package/dist/nodes/mock/flow/index.js +2 -0
  102. package/dist/nodes/mock/flow/node-type.d.ts +11 -0
  103. package/dist/nodes/mock/flow/node-type.js +51 -0
  104. package/dist/nodes/script/flow/check.d.ts +19 -0
  105. package/dist/nodes/script/flow/check.js +49 -0
  106. package/dist/nodes/script/flow/factories.d.ts +14 -0
  107. package/dist/nodes/script/flow/factories.js +15 -0
  108. package/dist/nodes/script/flow/family.d.ts +3 -0
  109. package/dist/nodes/script/flow/family.js +8 -0
  110. package/dist/nodes/script/flow/index.d.ts +3 -0
  111. package/dist/nodes/script/flow/index.js +3 -0
  112. package/dist/nodes/script/flow/node-type.d.ts +7 -0
  113. package/dist/nodes/script/flow/node-type.js +125 -0
  114. package/dist/nodes/script/flow/spec.d.ts +52 -0
  115. package/dist/nodes/script/flow/spec.js +1 -0
  116. package/dist/nodes/transform/flow/check.d.ts +6 -0
  117. package/dist/nodes/transform/flow/check.js +229 -0
  118. package/dist/nodes/transform/flow/factories.d.ts +52 -0
  119. package/dist/nodes/transform/flow/factories.js +53 -0
  120. package/dist/nodes/transform/flow/family.d.ts +3 -0
  121. package/dist/nodes/transform/flow/family.js +8 -0
  122. package/dist/nodes/transform/flow/index.d.ts +3 -0
  123. package/dist/nodes/transform/flow/index.js +3 -0
  124. package/dist/nodes/transform/flow/node-type.d.ts +7 -0
  125. package/dist/nodes/transform/flow/node-type.js +245 -0
  126. package/dist/nodes/transform/flow/spec.d.ts +116 -0
  127. package/dist/nodes/transform/flow/spec.js +10 -0
  128. package/dist/raw-node-advice.d.ts +16 -0
  129. package/dist/raw-node-advice.js +36 -0
  130. package/dist/raw-node.d.ts +71 -0
  131. package/dist/raw-node.js +116 -0
  132. package/dist/registry/uip/pipeline-front-door.js +5 -3
  133. package/dist/serialize.js +246 -549
  134. package/package.json +2 -2
package/dist/merge.js CHANGED
@@ -28,7 +28,7 @@
28
28
  * preserved from the original; `subflows` and edges (with their ids) come from
29
29
  * `edited`; `definitions` and `bindings` are unioned
30
30
  * (original wins on conflict, since the original's are the tenant-resolved
31
- * authority). Node ids are stable by construction (decompile names every step by
31
+ * authority). A binding row's identity is what it binds, not its row id. Node ids are stable by construction (decompile names every step by
32
32
  * its node id), which is what lets every re-attach key on id.
33
33
  *
34
34
  * ## Definitions are keyed by `nodeType@version`, not by `nodeType`
@@ -63,8 +63,23 @@
63
63
  * A node whose pair is in NEITHER file cannot be given a definition here, and a
64
64
  * merge that stayed silent about it wrote a file it already knew would fail
65
65
  * `validate`. Those are reported through {@link MergeOptions.warn}.
66
+ *
67
+ * ## The baseline is also compared with the original
68
+ *
69
+ * `baseline` is the no-edit round trip, so wherever it differs from the original
70
+ * in wire identity (a node, an edge, a switch case id, a human-task outcome id,
71
+ * a binding row), the merged file inherits that change unless the author's edit
72
+ * undid it. Each difference the merged file still carries is reported through
73
+ * `warn`, and `strict` refuses the merge over it; the merged file is also
74
+ * checked for an edge that leaves a handle its kept source node does not
75
+ * declare. See merge-roundtrip.ts.
66
76
  */
77
+ import { bindingIdentity } from './core/binding-identity.js';
67
78
  import { jsonClone } from './core/json-file.js';
79
+ import { DEFINITIONLESS_NODE_TYPES } from './core/definitionless-node-types.js';
80
+ import { MergeRefusedError, mergedFileFindings, roundTripFindings } from './merge-roundtrip.js';
81
+ import { placeAddedNodes } from './node-placement.js';
82
+ export { MergeRefusedError } from './merge-roundtrip.js';
68
83
  function byId(nodes) {
69
84
  return new Map((nodes ?? []).map((n) => [n.id, n]));
70
85
  }
@@ -82,11 +97,31 @@ function nodeUnchanged(a, b) {
82
97
  return false;
83
98
  return JSON.stringify(a.inputs ?? {}) === JSON.stringify(b.inputs ?? {});
84
99
  }
85
- /** Graft the original node's layout (`ui`) onto an edited node, by id. */
86
- function withOriginalLayout(edited, orig) {
87
- if (!orig || orig.ui === undefined)
100
+ /**
101
+ * Give an edited node the original's position, by id.
102
+ *
103
+ * The original stores it in one of two places: `node.ui` (an SDK-compiled file)
104
+ * or `layout.nodes[id]` (every canvas-saved file — the designer's save moves
105
+ * `ui` into `layout.nodes`). From `node.ui` it is grafted onto the node. With a
106
+ * `layout.nodes` entry, the merged file keeps that entry (it is carried over
107
+ * with the original's `layout`) and the edited node's compile placeholder `ui`
108
+ * is dropped, so the node has one position, not two.
109
+ */
110
+ function withOriginalLayout(edited, orig, originalLayout) {
111
+ if (!orig)
88
112
  return edited;
89
- return { ...edited, ui: jsonClone(orig.ui) };
113
+ if (orig.ui !== undefined)
114
+ return { ...edited, ui: jsonClone(orig.ui) };
115
+ if (originalLayout?.[orig.id] !== undefined) {
116
+ const { ui: _placeholder, ...rest } = edited;
117
+ return rest;
118
+ }
119
+ return edited;
120
+ }
121
+ /** The original's `layout.nodes` map, when it has one. */
122
+ function layoutNodesOf(f) {
123
+ const nodes = f.layout?.nodes;
124
+ return nodes && typeof nodes === 'object' && !Array.isArray(nodes) ? nodes : undefined;
90
125
  }
91
126
  export function mergeFlow(original, edited, options = {}) {
92
127
  const result = jsonClone(original);
@@ -104,7 +139,11 @@ export function mergeFlow(original, edited, options = {}) {
104
139
  }
105
140
  else {
106
141
  // Added or edited → take the edited node, re-attaching original layout.
107
- finalNodes.push(withOriginalLayout(jsonClone(e), orig));
142
+ finalNodes.push(withOriginalLayout(jsonClone(e), orig, layoutNodesOf(original)));
143
+ if (orig && orig.type === e.type && orig.typeVersion !== e.typeVersion) {
144
+ options.warn?.(`node ${JSON.stringify(e.id)} moved ${e.type}@${String(orig.typeVersion)} -> ${String(e.typeVersion)} `
145
+ + `(the edited node takes the SDK's version; flow-builder-sdk#1167).`);
146
+ }
108
147
  }
109
148
  }
110
149
  result.nodes = finalNodes;
@@ -124,15 +163,25 @@ export function mergeFlow(original, edited, options = {}) {
124
163
  // one already taken elsewhere in the file, gets a fresh id, from ids unused
125
164
  // anywhere in the file. ──
126
165
  result.edges = withFileUniqueEdgeIds(edited.edges ?? [], subflowScopes);
166
+ // ── Placement: a node the edit ADDED has only compile's placeholder, an
167
+ // emission-index slot that can land on an original node. Put it next to
168
+ // its predecessor, collision-free, in the store the
169
+ // original uses — see node-placement.ts. Root scope only: a subflow is
170
+ // taken from `edited` whole, layout included. ──
171
+ placeAddedNodes(finalNodes, result.edges, layoutNodesOf(result), (id) => !origNodes.has(id));
127
172
  // ── Definitions: the (type, version) pairs used by EVERY node in the file —
128
173
  // root and subflows — original winning on conflict (its defs are the
129
174
  // tenant-resolved authority). Selecting by root nodes only dropped the
130
175
  // definitions a subflow-only node needs, and validate refused the result
131
176
  // ("has no matching definition"). ──
132
177
  const allNodes = [...finalNodes, ...subflowScopes.flatMap((sf) => sf?.nodes ?? [])];
133
- result.definitions = mergeDefinitions(original.definitions, edited.definitions, allNodes, options.warn);
134
- // ── Bindings: union by id (original wins), so resources for newly-added nodes
135
- // come from `edited` while the original's stay intact. ──
178
+ const originalNodes = [
179
+ ...(original.nodes ?? []),
180
+ ...Object.values(original.subflows ?? {}).flatMap((sf) => sf?.nodes ?? []),
181
+ ];
182
+ result.definitions = mergeDefinitions(original.definitions, edited.definitions, allNodes, originalNodes, options.warn);
183
+ // ── Bindings: union by what each row binds (original wins), so resources for
184
+ // newly-added nodes come from `edited` while the original's stay intact. ──
136
185
  result.bindings = mergeBindings(original.bindings, edited.bindings);
137
186
  // metadata / solutionId / projectId / layout / runtime were carried by the
138
187
  // initial clone of `original` and are intentionally left untouched.
@@ -147,6 +196,19 @@ export function mergeFlow(original, edited, options = {}) {
147
196
  if (options.baseline && edited.version !== undefined && edited.version !== options.baseline.version) {
148
197
  result.version = edited.version;
149
198
  }
199
+ // ── What the no-edit round trip changed, and what mixing sources broke. ──
200
+ // Under strict, the findings that do not refuse are still warned, so a
201
+ // refused run shows them too.
202
+ const findings = [
203
+ ...(options.baseline ? roundTripFindings(original, options.baseline, result) : []),
204
+ ...mergedFileFindings(result, original),
205
+ ];
206
+ const refusing = options.strict ? findings.filter((f) => f.refuses) : [];
207
+ for (const f of findings)
208
+ if (!refusing.includes(f))
209
+ options.warn?.(f.message);
210
+ if (refusing.length > 0)
211
+ throw new MergeRefusedError(refusing);
150
212
  return result;
151
213
  }
152
214
  /**
@@ -186,8 +248,9 @@ function indexDefinitions(defs) {
186
248
  }
187
249
  return ix;
188
250
  }
189
- function mergeDefinitions(orig, edited, nodes, warn) {
251
+ function mergeDefinitions(orig, edited, nodes, originalNodes, warn) {
190
252
  const origIx = indexDefinitions(orig);
253
+ const originalPairs = new Set(originalNodes.filter((n) => n.type !== undefined && n.typeVersion !== undefined).map((n) => defKey(n.type, n.typeVersion)));
191
254
  const editedIx = indexDefinitions(edited);
192
255
  const out = new Map();
193
256
  const missing = [];
@@ -207,25 +270,62 @@ function mergeDefinitions(orig, edited, nodes, warn) {
207
270
  const def = origIx.get(key) ?? editedIx.get(key);
208
271
  if (def)
209
272
  out.set(key, def);
210
- else if (!missing.includes(key))
273
+ // A definitionless type is not missing anything.
274
+ else if (!DEFINITIONLESS_NODE_TYPES.has(n.type) && !missing.includes(key))
211
275
  missing.push(key);
212
276
  }
213
277
  for (const key of missing) {
214
- warn?.(`node type ${key} is pinned by a merged node but neither the original nor the edited flow `
215
- + `carries a definition for that exact pair, so the merged file has none; `
216
- + `\`validate\` will report "has no matching definition". `
217
- + `Recompile the edited source against an SDK that bundles ${key}, or align the node's typeVersion `
218
- + `with a definition one of the inputs does carry.`);
278
+ // A pair the original already pinned with no definition still fails
279
+ // `validate`; the warning says the merge inherited it rather than caused it.
280
+ warn?.(originalPairs.has(key)
281
+ ? `node type ${key} is pinned by a merged node and already had no definition in the original, `
282
+ + `and the edited flow carries none either, so the merged file has none; `
283
+ + `\`validate\` will report "has no matching definition", as it does for the original. `
284
+ + `Recompile the edited source against an SDK that bundles ${key} to add one.`
285
+ : `node type ${key} is pinned by a merged node but neither the original nor the edited flow `
286
+ + `carries a definition for that exact pair, so the merged file has none; `
287
+ + `\`validate\` will report "has no matching definition". `
288
+ + `Recompile the edited source against an SDK that bundles ${key}, or align the node's typeVersion `
289
+ + `with a definition one of the inputs does carry.`);
219
290
  }
220
291
  return [...out.values()].map((d) => jsonClone(d));
221
292
  }
293
+ /**
294
+ * The original's rows, then each edited row that binds something the original
295
+ * does not (a resource of a node the user added, or one an edit re-pointed).
296
+ *
297
+ * Original wins on the same identity: its rows are the tenant-resolved
298
+ * authority, and a canvas row can carry values (a `default`, a folder) the
299
+ * compile does not. An added row keeps its id unless an original row already
300
+ * holds it, in which case it gets a fresh one — ids are unreferenced labels, but
301
+ * two rows with one id are not a valid file.
302
+ *
303
+ * Rows no surviving node uses are NOT pruned. The file has no uniform
304
+ * node-to-row reference to decide "unused" by: an in-solution Flow is named by
305
+ * the node TYPE's suffix, a connection by an id in the node's inputs, a published
306
+ * process by a folder-qualified key that no node field spells. A wrong prune
307
+ * drops a row a kept node needs, and the run fails to resolve its resource; an
308
+ * orphan row is accepted by `validate` and ignored at run time.
309
+ */
222
310
  function mergeBindings(orig, edited) {
223
- const byId2 = new Map();
224
- for (const b of edited ?? [])
225
- if (b.id)
226
- byId2.set(b.id, b);
227
- for (const b of orig ?? [])
228
- if (b.id)
229
- byId2.set(b.id, b); // original wins
230
- return [...byId2.values()].map((b) => jsonClone(b));
311
+ const out = (orig ?? []).map((b) => jsonClone(b));
312
+ const identities = new Set(out.map(bindingIdentity));
313
+ const ids = new Set(out.map((b) => b.id).filter((id) => typeof id === 'string'));
314
+ for (const b of edited ?? []) {
315
+ const identity = bindingIdentity(b);
316
+ if (identities.has(identity))
317
+ continue;
318
+ identities.add(identity);
319
+ const row = jsonClone(b);
320
+ if (typeof row.id === 'string' && ids.has(row.id)) {
321
+ let n = 2;
322
+ while (ids.has(`${row.id}${n}`))
323
+ n++;
324
+ row.id = `${row.id}${n}`;
325
+ }
326
+ if (typeof row.id === 'string')
327
+ ids.add(row.id);
328
+ out.push(row);
329
+ }
330
+ return out;
231
331
  }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * node-placement — where compile puts a node it has no position for, and where
3
+ * merge moves one the edit added.
4
+ *
5
+ * Compile gives every node a placeholder from its emission index,
6
+ * `x = 256 + 288 × index, y = 144` (`+160` inside a container). Merge replaces
7
+ * that placeholder on each node the edit added:
8
+ *
9
+ * - next to a positioned predecessor in the same container: one slot right;
10
+ * - else, when the incoming edge comes from the node's own container (the first
11
+ * node of a loop body): at the body origin, inside the container's padding;
12
+ * - else, one slot left of a positioned successor in the same container;
13
+ * - then the nearest free spot around that candidate.
14
+ *
15
+ * Example: the edit inserts `auditLog` between `extractInvoice (448,144)` and
16
+ * `routeInvoice (640,144)`. The candidate `(640,144)` is taken, so `auditLog`
17
+ * goes to the first free spot, `(640,264)`.
18
+ *
19
+ * A child's position is relative to its container, as on the canvas.
20
+ *
21
+ * The position goes where the original stores positions: `layout.nodes[id]`
22
+ * when it has a `layout.nodes` map, otherwise `node.ui`. Positions are read the
23
+ * way the designer reads them (flow-workbench `flow-schema`
24
+ * `workflow-format-conversion.ts` `mergeLayout`): a `layout.nodes[id]` entry
25
+ * wins over `node.ui`, field by field.
26
+ *
27
+ * `findFreePosition` is a copy of flow-workbench `flow-core`
28
+ * `layout/collision-detection.ts` (same 24 px padding, 40 px step, 50 rings,
29
+ * offset order, 96 px default size, sticky notes excluded), because the SDK may
30
+ * not import `@uipath/flow-core` (lint FB015).
31
+ */
32
+ /** A position on the canvas. */
33
+ export interface Point {
34
+ x: number;
35
+ y: number;
36
+ }
37
+ /**
38
+ * Compile's placeholder for the `index`-th node it emits.
39
+ *
40
+ * @param index - The node's emission index in the file.
41
+ * @param nested - Whether the node sits inside a container.
42
+ * @returns The placeholder position.
43
+ */
44
+ export declare function placeholderPosition(index: number, nested: boolean): Point;
45
+ /**
46
+ * Whether `p` is a compile placeholder rather than a position someone chose.
47
+ * An author `.layout()` entry that happens to hit the same grid point reads as
48
+ * a placeholder; that is the one ambiguity.
49
+ */
50
+ export declare function isPlaceholderPosition(p: Point | undefined): boolean;
51
+ /** A node's `ui` (or its `layout.nodes` entry): the fields placement reads. */
52
+ interface Ui {
53
+ position?: Point;
54
+ size?: {
55
+ width?: number;
56
+ height?: number;
57
+ };
58
+ [k: string]: unknown;
59
+ }
60
+ /** A node as merge holds it; `ui` is untyped in the file model. */
61
+ interface PlacedNode {
62
+ id: string;
63
+ type?: string;
64
+ parentId?: string;
65
+ ui?: unknown;
66
+ [k: string]: unknown;
67
+ }
68
+ /**
69
+ * Give each node the edit added a position next to its predecessor, in place.
70
+ *
71
+ * A node is placed when it is not in the original and its position is a
72
+ * compile placeholder (an author `.layout()` position is kept). Its candidate,
73
+ * in order: one slot right of the first predecessor in the same container that
74
+ * already has a position (an original node, or an added node placed before it);
75
+ * the body origin, when an edge enters it from its own container; one slot left
76
+ * of a positioned successor in the same container. With none, the node keeps
77
+ * its placeholder, and placed nodes avoid it.
78
+ *
79
+ * Where the result is written follows the file: when the original stores
80
+ * positions in `layout.nodes` (every canvas-saved file does), the position goes
81
+ * there and the placeholder `node.ui` is dropped, so the file has one store;
82
+ * otherwise `node.ui.position` is updated.
83
+ *
84
+ * @param nodes - The merged root nodes (mutated).
85
+ * @param edges - The merged root edges.
86
+ * @param layoutNodes - The merged file's `layout.nodes`, or undefined when the original has none (mutated).
87
+ * @param isAdded - Whether a node id is new in this merge.
88
+ */
89
+ export declare function placeAddedNodes(nodes: PlacedNode[], edges: ReadonlyArray<{
90
+ sourceNodeId: string;
91
+ targetNodeId: string;
92
+ }>, layoutNodes: Record<string, Ui> | undefined, isAdded: (id: string) => boolean): void;
93
+ export {};
@@ -0,0 +1,163 @@
1
+ /**
2
+ * node-placement — where compile puts a node it has no position for, and where
3
+ * merge moves one the edit added.
4
+ *
5
+ * Compile gives every node a placeholder from its emission index,
6
+ * `x = 256 + 288 × index, y = 144` (`+160` inside a container). Merge replaces
7
+ * that placeholder on each node the edit added:
8
+ *
9
+ * - next to a positioned predecessor in the same container: one slot right;
10
+ * - else, when the incoming edge comes from the node's own container (the first
11
+ * node of a loop body): at the body origin, inside the container's padding;
12
+ * - else, one slot left of a positioned successor in the same container;
13
+ * - then the nearest free spot around that candidate.
14
+ *
15
+ * Example: the edit inserts `auditLog` between `extractInvoice (448,144)` and
16
+ * `routeInvoice (640,144)`. The candidate `(640,144)` is taken, so `auditLog`
17
+ * goes to the first free spot, `(640,264)`.
18
+ *
19
+ * A child's position is relative to its container, as on the canvas.
20
+ *
21
+ * The position goes where the original stores positions: `layout.nodes[id]`
22
+ * when it has a `layout.nodes` map, otherwise `node.ui`. Positions are read the
23
+ * way the designer reads them (flow-workbench `flow-schema`
24
+ * `workflow-format-conversion.ts` `mergeLayout`): a `layout.nodes[id]` entry
25
+ * wins over `node.ui`, field by field.
26
+ *
27
+ * `findFreePosition` is a copy of flow-workbench `flow-core`
28
+ * `layout/collision-detection.ts` (same 24 px padding, 40 px step, 50 rings,
29
+ * offset order, 96 px default size, sticky notes excluded), because the SDK may
30
+ * not import `@uipath/flow-core` (lint FB015).
31
+ */
32
+ // Sticky notes may overlap anything (flow-core skips them in collisions).
33
+ import { STICKY_NOTE_TYPE } from './core/definitionless-node-types.js';
34
+ /** Where compile's placeholder row starts, and its spacing. */
35
+ const PLACEHOLDER = { x0: 256, dx: 288, y0: 144, nestedDy: 160 };
36
+ /** The default node box the canvas assumes (flow-core `resolveWidth`/`resolveHeight`). */
37
+ const DEFAULT_SIZE = 96;
38
+ /** One slot to the right of a node: its width plus an equal gap, the spacing of a canvas row. */
39
+ const SLOT_DX = 192;
40
+ /** flow-core `findFreePosition` defaults. */
41
+ const PADDING = 24;
42
+ const STEP = 40;
43
+ const MAX_RINGS = 50;
44
+ /**
45
+ * Where a container's first body node goes, relative to the container: inside
46
+ * its left and top padding (flow-core `defaultContainerFit`: 144 left, 96 top).
47
+ */
48
+ const BODY_ORIGIN = { x: 144, y: 96 };
49
+ /**
50
+ * Compile's placeholder for the `index`-th node it emits.
51
+ *
52
+ * @param index - The node's emission index in the file.
53
+ * @param nested - Whether the node sits inside a container.
54
+ * @returns The placeholder position.
55
+ */
56
+ export function placeholderPosition(index, nested) {
57
+ return { x: PLACEHOLDER.x0 + index * PLACEHOLDER.dx, y: PLACEHOLDER.y0 + (nested ? PLACEHOLDER.nestedDy : 0) };
58
+ }
59
+ /**
60
+ * Whether `p` is a compile placeholder rather than a position someone chose.
61
+ * An author `.layout()` entry that happens to hit the same grid point reads as
62
+ * a placeholder; that is the one ambiguity.
63
+ */
64
+ export function isPlaceholderPosition(p) {
65
+ if (!p)
66
+ return false;
67
+ const k = (p.x - PLACEHOLDER.x0) / PLACEHOLDER.dx;
68
+ return Number.isInteger(k) && k >= 0 && (p.y === PLACEHOLDER.y0 || p.y === PLACEHOLDER.y0 + PLACEHOLDER.nestedDy);
69
+ }
70
+ const overlaps = (a, b) => !(a.x + a.width + PADDING <= b.x || b.x + b.width + PADDING <= a.x
71
+ || a.y + a.height + PADDING <= b.y || b.y + b.height + PADDING <= a.y);
72
+ /** flow-core `findFreePosition`: the candidate, else the first free spot on rings around it. */
73
+ function findFreePosition(candidate, size, others) {
74
+ const free = (p) => !others.some((o) => overlaps({ ...p, ...size }, o));
75
+ if (free(candidate))
76
+ return candidate;
77
+ for (let d = STEP; d <= STEP * MAX_RINGS; d += STEP) {
78
+ for (const [dx, dy] of [[d, 0], [0, d], [d, d], [-d, 0], [0, -d], [-d, -d]]) {
79
+ const p = { x: candidate.x + dx, y: candidate.y + dy };
80
+ if (free(p))
81
+ return p;
82
+ }
83
+ }
84
+ return candidate;
85
+ }
86
+ /**
87
+ * Give each node the edit added a position next to its predecessor, in place.
88
+ *
89
+ * A node is placed when it is not in the original and its position is a
90
+ * compile placeholder (an author `.layout()` position is kept). Its candidate,
91
+ * in order: one slot right of the first predecessor in the same container that
92
+ * already has a position (an original node, or an added node placed before it);
93
+ * the body origin, when an edge enters it from its own container; one slot left
94
+ * of a positioned successor in the same container. With none, the node keeps
95
+ * its placeholder, and placed nodes avoid it.
96
+ *
97
+ * Where the result is written follows the file: when the original stores
98
+ * positions in `layout.nodes` (every canvas-saved file does), the position goes
99
+ * there and the placeholder `node.ui` is dropped, so the file has one store;
100
+ * otherwise `node.ui.position` is updated.
101
+ *
102
+ * @param nodes - The merged root nodes (mutated).
103
+ * @param edges - The merged root edges.
104
+ * @param layoutNodes - The merged file's `layout.nodes`, or undefined when the original has none (mutated).
105
+ * @param isAdded - Whether a node id is new in this merge.
106
+ */
107
+ export function placeAddedNodes(nodes, edges, layoutNodes, isAdded) {
108
+ const byId = new Map(nodes.map((n) => [n.id, n]));
109
+ const uiOf = (n) => {
110
+ const entry = layoutNodes?.[n.id];
111
+ const own = n.ui;
112
+ return entry ? { ...own, ...entry } : own;
113
+ };
114
+ const sizeOf = (n) => ({
115
+ width: uiOf(n)?.size?.width ?? DEFAULT_SIZE,
116
+ height: uiOf(n)?.size?.height ?? DEFAULT_SIZE,
117
+ });
118
+ const pending = new Set(nodes.filter((n) => isAdded(n.id) && n.type !== STICKY_NOTE_TYPE && isPlaceholderPosition(uiOf(n)?.position)).map((n) => n.id));
119
+ const positioned = (id) => {
120
+ const n = byId.get(id);
121
+ return n !== undefined && !pending.has(id) && uiOf(n)?.position !== undefined;
122
+ };
123
+ // Place in waves, so a run of added nodes chains off the first one placed.
124
+ for (let progress = true; progress && pending.size > 0;) {
125
+ progress = false;
126
+ for (const id of [...pending]) {
127
+ const n = byId.get(id);
128
+ const sameScope = (other) => byId.get(other)?.parentId === n.parentId;
129
+ const pred = edges.find((e) => e.targetNodeId === id && sameScope(e.sourceNodeId) && positioned(e.sourceNodeId));
130
+ const entersBody = !pred && n.parentId !== undefined && byId.has(n.parentId)
131
+ && edges.some((e) => e.targetNodeId === id && e.sourceNodeId === n.parentId);
132
+ const succ = pred || entersBody ? undefined
133
+ : edges.find((e) => e.sourceNodeId === id && sameScope(e.targetNodeId) && positioned(e.targetNodeId));
134
+ let candidate;
135
+ if (pred || succ) {
136
+ const anchor = uiOf(byId.get(pred?.sourceNodeId ?? succ.targetNodeId)).position;
137
+ candidate = { x: anchor.x + (pred ? SLOT_DX : -SLOT_DX), y: anchor.y };
138
+ }
139
+ else if (entersBody) {
140
+ candidate = { ...BODY_ORIGIN };
141
+ }
142
+ else {
143
+ continue;
144
+ }
145
+ // Still-pending nodes count at their placeholder: one that never finds an
146
+ // anchor stays there, and nothing may land on it.
147
+ const others = nodes
148
+ .filter((o) => o.id !== id && o.parentId === n.parentId && o.type !== STICKY_NOTE_TYPE && uiOf(o)?.position !== undefined)
149
+ .map((o) => ({ ...uiOf(o).position, ...sizeOf(o) }));
150
+ const position = findFreePosition(candidate, sizeOf(n), others);
151
+ const ui = { ...uiOf(n), position };
152
+ if (layoutNodes) {
153
+ layoutNodes[id] = ui;
154
+ delete n.ui;
155
+ }
156
+ else {
157
+ n.ui = ui;
158
+ }
159
+ pending.delete(id);
160
+ progress = true;
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,13 @@
1
+ import type { DataFabricCreateInputs, DataFabricDeleteInputs, DataFabricReadInputs, DataFabricUpdateInputs } from './spec.js';
2
+ import type { DiagnosticCatalog, Placement } from '../../../core/nodes/family.js';
3
+ import type { FlowCheckContext, FlowFamilyCheckContext } from '../../../flow-node-type.js';
4
+ export declare const DATA_FABRIC_DIAGNOSTICS: DiagnosticCatalog;
5
+ export declare function checkRead(inputs: DataFabricReadInputs, ctx: FlowCheckContext): void;
6
+ export declare function checkCreate(inputs: DataFabricCreateInputs, ctx: FlowCheckContext): void;
7
+ export declare function checkUpdate(inputs: DataFabricUpdateInputs, ctx: FlowCheckContext): void;
8
+ export declare function checkWrite(inputs: DataFabricDeleteInputs, ctx: FlowCheckContext): void;
9
+ /**
10
+ * Update and delete target a record `{ fromRead: '<step>' }` with the same
11
+ * selector, so they share the check: the step must be a read, and not a list.
12
+ */
13
+ export declare function checkFromRead(placements: readonly Placement[], ctx: FlowFamilyCheckContext): void;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * The Flow Data Fabric family's rules: the per-step checks each verb runs, and
3
+ * the family-level `{ fromRead }` rules that need every placement at once. Moved
4
+ * verbatim from check.ts (Phase 2 of docs/NODE_TYPE_ARCHITECTURE_DESIGN.md);
5
+ * codes, messages and order are unchanged.
6
+ */
7
+ import { Expr } from '../../../core/expr.js';
8
+ export const DATA_FABRIC_DIAGNOSTICS = {
9
+ DATAFABRIC_NO_ENTITY: { severity: 'error', summary: 'a verb names no entity' },
10
+ DATAFABRIC_EMPTY_VALUES: { severity: 'error', summary: 'a create writes no non-blank column' },
11
+ DATAFABRIC_EMPTY_SET: { severity: 'error', summary: 'an update writes no column' },
12
+ DATAFABRIC_SYSTEM_COLUMN: { severity: 'error', summary: 'a write targets a system column' },
13
+ DATAFABRIC_FOLDER_NO_RESOURCE_KEY: { severity: 'error', summary: 'folderKey without resourceKey' },
14
+ DATAFABRIC_RESOURCE_KEY_NO_FOLDER: { severity: 'error', summary: 'resourceKey without folderKey' },
15
+ DATAFABRIC_QUERY_OPTS_WITHOUT_MULTIPLE: { severity: 'error', summary: 'list-query options on a single read' },
16
+ DATAFABRIC_LIMIT_RANGE: { severity: 'error', summary: 'limit outside 1–1000' },
17
+ DATAFABRIC_IN_REQUIRES_MULTIPLE: { severity: 'error', summary: "an 'in' filter on a single read" },
18
+ DATAFABRIC_FILTER_BLANK_VALUE: { severity: 'error', summary: 'a filter with a blank value' },
19
+ DATAFABRIC_FROMREAD_UNKNOWN: { severity: 'error', summary: '{ fromRead } names no read step' },
20
+ DATAFABRIC_FROMREAD_MULTIPLE: { severity: 'error', summary: "{ fromRead } names a resultMode: 'multiple' query" },
21
+ DATAFABRIC_DELETE_HAS_NO_OUTPUT: { severity: 'error', summary: "a read of a delete's output" },
22
+ };
23
+ function checkEntity(inputs, ctx) {
24
+ if (!String(inputs.entity ?? '').trim()) {
25
+ ctx.report('DATAFABRIC_NO_ENTITY', `"${ctx.step}" names no Data Fabric entity. Set { entity: '<entity name>' }.`);
26
+ }
27
+ }
28
+ export function checkRead(inputs, ctx) {
29
+ checkEntity(inputs, ctx);
30
+ checkQuery(inputs, ctx);
31
+ checkScope(inputs, ctx);
32
+ }
33
+ export function checkCreate(inputs, ctx) {
34
+ checkEntity(inputs, ctx);
35
+ // Not the same failure as an empty update: a BLANK value is omitted from the
36
+ // insert so the column default applies, so an all-blank `fieldValues`
37
+ // serializes to an empty body and runs green having inserted nothing. The
38
+ // node's schema rejects that — and the fix is a real value, never a
39
+ // placeholder to satisfy the rule.
40
+ const written = Object.values(inputs.values ?? {})
41
+ .filter((v) => (v instanceof Expr ? true : String(v ?? '').trim() !== ''));
42
+ if (written.length === 0) {
43
+ ctx.report('DATAFABRIC_EMPTY_VALUES', `"${ctx.step}" sets no column to a non-blank value, so it would insert nothing and run green. `
44
+ + 'The node\'s schema rejects that — give a real column a real value rather than a placeholder.');
45
+ }
46
+ checkColumns(Object.keys(inputs.values ?? {}), 'values', ctx);
47
+ checkScope(inputs, ctx);
48
+ }
49
+ export function checkUpdate(inputs, ctx) {
50
+ checkEntity(inputs, ctx);
51
+ if (Object.keys(inputs.set ?? {}).length === 0) {
52
+ ctx.report('DATAFABRIC_EMPTY_SET', `"${ctx.step}" writes no columns — { set } is empty, so the update would no-op green.`);
53
+ }
54
+ checkColumns(Object.keys(inputs.set ?? {}), 'set', ctx);
55
+ checkScope(inputs, ctx);
56
+ }
57
+ export function checkWrite(inputs, ctx) {
58
+ checkEntity(inputs, ctx);
59
+ checkScope(inputs, ctx);
60
+ }
61
+ /**
62
+ * Columns Data Fabric assigns and never accepts a write for.
63
+ *
64
+ * The canvas hides them from both write editors, but the serializer has no
65
+ * guard: a hand-authored `Id` row satisfies the node's own "at least one field"
66
+ * rule, ships in the body, and is rejected — and the rejection is only logged,
67
+ * so the row comes back unchanged and the run goes green.
68
+ */
69
+ const SYSTEM_COLUMNS = new Set(['Id', 'CreateTime', 'CreatedBy', 'UpdateTime', 'UpdatedBy']);
70
+ function checkColumns(columns, option, ctx) {
71
+ const system = columns.filter((c) => SYSTEM_COLUMNS.has(c));
72
+ if (system.length) {
73
+ ctx.report('DATAFABRIC_SYSTEM_COLUMN', `"${ctx.step}" writes ${system.map((c) => JSON.stringify(c)).join(', ')} in { ${option} }. `
74
+ + 'Data Fabric assigns its system columns (Id, CreateTime, CreatedBy, UpdateTime, UpdatedBy) and rejects a '
75
+ + 'write to them — and it only LOGS the rejection, so the row comes back unchanged and the run goes green.');
76
+ }
77
+ }
78
+ /**
79
+ * A folder-scoped entity is portable only with its `Entity` bindings.
80
+ *
81
+ * `_folderKey` on its own still serializes — it falls back to a source-org
82
+ * literal and logs a warning — so the flow looks fine until it is deployed to
83
+ * another org. Refusing the half-authored pair here is the whole point: the
84
+ * alternative is a defect that only appears at deploy time, in a different org,
85
+ * to somebody else.
86
+ */
87
+ function checkScope(inputs, ctx) {
88
+ if (inputs.folderKey !== undefined && inputs.resourceKey === undefined) {
89
+ ctx.report('DATAFABRIC_FOLDER_NO_RESOURCE_KEY', `"${ctx.step}" sets { folderKey } without { resourceKey }. A folder-scoped entity needs both — with only `
90
+ + 'the folder key the entity serializes as a source-org literal, which validates, runs in this org, and '
91
+ + 'breaks on deploy to another one.', 'resolve the key with `uip solution resources list --kind Entity`, or drop { folderKey } and keep '
92
+ + 'the entity tenant-scoped');
93
+ }
94
+ if (inputs.resourceKey !== undefined && inputs.folderKey === undefined) {
95
+ ctx.report('DATAFABRIC_RESOURCE_KEY_NO_FOLDER', `"${ctx.step}" sets { resourceKey } without { folderKey }. The pair exists to scope a FOLDER entity; a `
96
+ + 'tenant-scoped entity needs neither and serializes as a dotted literal.');
97
+ }
98
+ }
99
+ /**
100
+ * The query options, none of which the node's `inputDefinition` declares — they
101
+ * are `_`-prefixed extras, so `uip maestro flow validate` checks none of them
102
+ * and every mistake here is silent at run time.
103
+ */
104
+ function checkQuery(inputs, ctx) {
105
+ const step = ctx.step;
106
+ const listOnly = ['limit', 'skip', 'sort', 'fields'].filter((k) => inputs[k] !== undefined);
107
+ if (listOnly.length && inputs.resultMode !== 'multiple') {
108
+ ctx.report('DATAFABRIC_QUERY_OPTS_WITHOUT_MULTIPLE', `"${step}" sets { ${listOnly.join(', ')} }, which only a list query uses, but its resultMode is `
109
+ + `${inputs.resultMode ? JSON.stringify(inputs.resultMode) : 'unset (a single-record read)'}.`, "resultMode: 'multiple'");
110
+ }
111
+ // The emitted query ALWAYS carries a limit — omitted it resolves to 100 — and
112
+ // 1000 is a hard ceiling that TRUNCATES rather than erroring above it.
113
+ if (inputs.limit !== undefined && (!Number.isInteger(inputs.limit) || inputs.limit < 1 || inputs.limit > 1000)) {
114
+ ctx.report('DATAFABRIC_LIMIT_RANGE', `"${step}": limit must be a whole number between 1 and 1000 (got ${inputs.limit}). 1000 is a hard `
115
+ + 'ceiling — a larger value truncates silently, so page with { skip } instead.');
116
+ }
117
+ // `in` is honoured only on a list query; on a single read the row compiles
118
+ // away, and a read whose filters compiled away returns EVERY record.
119
+ if (inputs.resultMode !== 'multiple' && (inputs.filters ?? []).some((f) => f.operator === 'in')) {
120
+ ctx.report('DATAFABRIC_IN_REQUIRES_MULTIPLE', `"${step}" filters with the 'in' operator, which is honoured only when resultMode is 'multiple'. `
121
+ + 'On a single-record read that row compiles away — and a read whose filters compiled away matches every record.');
122
+ }
123
+ // A filter expression switched on and left blank is refused by the serializer,
124
+ // and it refuses the WHOLE query rather than dropping the row, which strands
125
+ // every downstream reference to this step.
126
+ for (const f of inputs.filters ?? []) {
127
+ const blank = f.value instanceof Expr ? f.value.js.trim() === '' : String(f.value ?? '').trim() === '';
128
+ if (blank) {
129
+ ctx.report('DATAFABRIC_FILTER_BLANK_VALUE', `"${step}" has a filter on ${JSON.stringify(f.field)} with a blank value. The serializer refuses the `
130
+ + 'ENTIRE query rather than dropping the row, so the step emits nothing and every downstream $vars read of '
131
+ + 'it breaks.');
132
+ }
133
+ }
134
+ }
135
+ /**
136
+ * Update and delete target a record `{ fromRead: '<step>' }` with the same
137
+ * selector, so they share the check: the step must be a read, and not a list.
138
+ */
139
+ export function checkFromRead(placements, ctx) {
140
+ for (const p of placements) {
141
+ if (p.kind !== 'dataFabricUpdate' && p.kind !== 'dataFabricDelete')
142
+ continue;
143
+ const fromRead = p.spec.inputs?.record?.fromRead;
144
+ if (typeof fromRead !== 'string')
145
+ continue;
146
+ const factory = p.kind;
147
+ // A step whose version did not resolve is already an error; saying it does
148
+ // not exist would send the author to the selector instead of the pin.
149
+ if (ctx.versionUnresolved(fromRead))
150
+ continue;
151
+ const read = ctx.placementOf(fromRead);
152
+ if (read?.kind !== 'dataFabricRead') {
153
+ ctx.report(p.step, 'DATAFABRIC_FROMREAD_UNKNOWN', `"${p.step}" targets record: { fromRead: ${JSON.stringify(fromRead)} }, but no dataFabricRead step has that name.`, `${factory}({ …, record: { byId: '<record id>' } })`);
154
+ continue;
155
+ }
156
+ // A LIST is not a write target: the serializer refuses it and emits a plain
157
+ // no-op task, so the flow validates, runs green, and writes nothing.
158
+ if (read.spec.inputs?.resultMode === 'multiple') {
159
+ ctx.report(p.step, 'DATAFABRIC_FROMREAD_MULTIPLE', `"${p.step}" targets record: { fromRead: ${JSON.stringify(fromRead)} }, but "${fromRead}" is a `
160
+ + "resultMode: 'multiple' query. A multi-record read is refused as a write target — the step compiles to "
161
+ + 'a no-op and the run goes green having written nothing.', `loop the results and target each record with ${factory}({ …, record: { byId: … } })`);
162
+ }
163
+ }
164
+ }