@uipath/maestro-builder-sdk 6.44.2 → 6.44.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/check.js +13 -6
- package/dist/core/vars-read.d.ts +6 -0
- package/dist/core/vars-read.js +6 -0
- package/dist/decompile.js +37 -3
- package/dist/flow-node-type.d.ts +2 -0
- package/dist/merge-cli.js +28 -6
- package/dist/merge-compile.d.ts +71 -10
- package/dist/merge-compile.js +113 -16
- package/dist/merge.d.ts +10 -0
- package/dist/merge.js +104 -8
- package/dist/nodes/http/flow/node-type.js +22 -1
- package/package.json +2 -2
package/dist/check.js
CHANGED
|
@@ -22,6 +22,7 @@ import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
|
|
|
22
22
|
import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
|
|
23
23
|
import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-variables.js';
|
|
24
24
|
import { planOutputBacking } from './flow-output-backing.js';
|
|
25
|
+
import { VARS_READ_RE } from './core/vars-read.js';
|
|
25
26
|
import { terminateStepClashMessage, terminateTwiceMessage } from './flow-terminate-names.js';
|
|
26
27
|
import { endContentKey } from './flow-end-content.js';
|
|
27
28
|
import { FLOW_NODE_TYPES } from './flow-node-registry.js';
|
|
@@ -781,9 +782,12 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
781
782
|
}
|
|
782
783
|
}
|
|
783
784
|
else if (s.kind === 'return') {
|
|
785
|
+
// A named End (`.return({…}, { name })`) is a node merge can place a
|
|
786
|
+
// finding on, so its expressions report under that name (#1322); an
|
|
787
|
+
// unnamed one has no id until compile, so it stays `return`.
|
|
784
788
|
for (const [name, e] of Object.entries(s.values)) {
|
|
785
|
-
addExpr(e.js, 'return');
|
|
786
|
-
checkOutputCategory(name, e, outputType, typeOf, diags, triggerId);
|
|
789
|
+
addExpr(e.js, s.options?.name ?? 'return');
|
|
790
|
+
checkOutputCategory(name, e, outputType, typeOf, diags, triggerId, s.options?.name ?? 'return');
|
|
787
791
|
}
|
|
788
792
|
}
|
|
789
793
|
// AFTER the dispatch chain, not inside it: `s` was normalised to an action
|
|
@@ -1056,7 +1060,7 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
1056
1060
|
// subflow read, where the value lives one level deeper
|
|
1057
1061
|
// (`$vars.<call>.output.<childOutput>`). Every other rule reads only the
|
|
1058
1062
|
// first two segments, exactly as before.
|
|
1059
|
-
for (const m of js.matchAll(
|
|
1063
|
+
for (const m of js.matchAll(VARS_READ_RE)) {
|
|
1060
1064
|
const [, name, field] = m;
|
|
1061
1065
|
// `?.` reads the same path as `.`, and the rules below parse the tail as `.a.b`.
|
|
1062
1066
|
const rest = m[3].replace(/\?\./g, '.');
|
|
@@ -1157,7 +1161,10 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
|
|
|
1157
1161
|
// (`<step>.error`), which is what `err()` writes; on managed http it is the
|
|
1158
1162
|
// output itself, and `serialize` rewrites the read to match. Either way this
|
|
1159
1163
|
// spelling resolves to nothing, silently: the handler runs and reports empty.
|
|
1160
|
-
|
|
1164
|
+
// The FIELD `error`, not a prefix: `error_type`, `error_message`,
|
|
1165
|
+
// `errors` are success fields a function returns on its happy path, and
|
|
1166
|
+
// two designer-saved flows report outcomes that way (#1321).
|
|
1167
|
+
if (field === 'output' && /^\.error(?:\.|$)/.test(rest)) {
|
|
1161
1168
|
const wanted = /^\.error\.([A-Za-z0-9_]+)/.exec(rest);
|
|
1162
1169
|
diags.push({
|
|
1163
1170
|
level: 'error',
|
|
@@ -1734,7 +1741,7 @@ function typeCategory(t) {
|
|
|
1734
1741
|
* its declared type is known. A `tmpl`/`js` expression produces text or something
|
|
1735
1742
|
* unknowable, so it is left alone.
|
|
1736
1743
|
*/
|
|
1737
|
-
function checkOutputCategory(name, e, outputType, typeOf, diags, triggerId) {
|
|
1744
|
+
function checkOutputCategory(name, e, outputType, typeOf, diags, triggerId, step = 'return') {
|
|
1738
1745
|
const declaredOut = outputType.get(name);
|
|
1739
1746
|
if (!declaredOut || e.literal)
|
|
1740
1747
|
return;
|
|
@@ -1756,7 +1763,7 @@ function checkOutputCategory(name, e, outputType, typeOf, diags, triggerId) {
|
|
|
1756
1763
|
message: `Output "${name}" is declared ${declaredOut} but returns "${refName}", which is `
|
|
1757
1764
|
+ `${typeOf.get(refName)}. That is a type error, so the output would come back `
|
|
1758
1765
|
+ `empty at run time.`,
|
|
1759
|
-
step
|
|
1766
|
+
step,
|
|
1760
1767
|
suggestion: fix,
|
|
1761
1768
|
});
|
|
1762
1769
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dotted `$vars` read: `$vars.<name>`, an optional `.<field>`, then the dotted
|
|
3
|
+
* `rest`, each dot optionally `?.`. It stops at the first bracket. Check and
|
|
4
|
+
* decompile both scan with it, so decompile declares the body fields check reads.
|
|
5
|
+
*/
|
|
6
|
+
export declare const VARS_READ_RE: RegExp;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dotted `$vars` read: `$vars.<name>`, an optional `.<field>`, then the dotted
|
|
3
|
+
* `rest`, each dot optionally `?.`. It stops at the first bracket. Check and
|
|
4
|
+
* decompile both scan with it, so decompile declares the body fields check reads.
|
|
5
|
+
*/
|
|
6
|
+
export const VARS_READ_RE = /\$vars\.([A-Za-z0-9_]+)(?:\??\.([A-Za-z0-9_]+))?((?:\??\.[A-Za-z0-9_]+)*)/g;
|
package/dist/decompile.js
CHANGED
|
@@ -42,6 +42,7 @@ import { parseIxpProjectType } from './core/actions.js';
|
|
|
42
42
|
import { readFlowGlobals } from './flow-variables.js';
|
|
43
43
|
import { outputBackingGlobal, outputBackingName } from './flow-output-backing.js';
|
|
44
44
|
import { mapJsCode } from './core/js-code-spans.js';
|
|
45
|
+
import { VARS_READ_RE } from './core/vars-read.js';
|
|
45
46
|
import { missingBindingLabels } from './core/binding-messages.js';
|
|
46
47
|
import { prepareCommand } from './core/cli-spelling.js';
|
|
47
48
|
import { OUTCOME_HANDLE_NODE_TYPES } from './core/hitl-routing.js';
|
|
@@ -459,6 +460,30 @@ function emitValMap(m, imp, inputNames) {
|
|
|
459
460
|
*/
|
|
460
461
|
const ARTIFACT_PORTS = new Set(['tool', 'context', 'escalation', 'memory']);
|
|
461
462
|
// ─── Graph model ──────────────────────────────────────────────────────────────
|
|
463
|
+
/** An index right after a read: `[0]`, `[i]`, `?.["k"]`. */
|
|
464
|
+
const INDEX_NEXT_RE = /^\s*(?:\?\.)?\[/;
|
|
465
|
+
/** A read's tail when an index follows it, whatever the index: `.output.body.items[]`. */
|
|
466
|
+
const INDEX_MARK = '[]';
|
|
467
|
+
/** Every `$vars` read in `values`' strings, by id, with `?.` read as `.`. */
|
|
468
|
+
function scopeReads(values) {
|
|
469
|
+
const reads = new Map();
|
|
470
|
+
const visit = (v) => {
|
|
471
|
+
if (typeof v === 'string') {
|
|
472
|
+
for (const m of v.matchAll(VARS_READ_RE)) {
|
|
473
|
+
const [read, id, field, rest] = m;
|
|
474
|
+
const path = field === undefined ? '' : `.${field}${rest.replace(/\?\./g, '.')}`;
|
|
475
|
+
const indexed = INDEX_NEXT_RE.test(v.slice(m.index + read.length));
|
|
476
|
+
(reads.get(id) ?? reads.set(id, []).get(id)).push(indexed ? path + INDEX_MARK : path);
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (v && typeof v === 'object')
|
|
481
|
+
for (const x of Object.values(v))
|
|
482
|
+
visit(x);
|
|
483
|
+
};
|
|
484
|
+
visit(values);
|
|
485
|
+
return reads;
|
|
486
|
+
}
|
|
462
487
|
class Graph {
|
|
463
488
|
byId = new Map();
|
|
464
489
|
out = new Map();
|
|
@@ -474,8 +499,11 @@ class Graph {
|
|
|
474
499
|
*/
|
|
475
500
|
accounted = new Set();
|
|
476
501
|
edges;
|
|
477
|
-
|
|
502
|
+
variables;
|
|
503
|
+
reads;
|
|
504
|
+
constructor(nodes, edges, variables) {
|
|
478
505
|
this.edges = edges;
|
|
506
|
+
this.variables = variables;
|
|
479
507
|
for (const n of nodes)
|
|
480
508
|
this.byId.set(n.id, n);
|
|
481
509
|
for (const e of edges) {
|
|
@@ -483,6 +511,11 @@ class Graph {
|
|
|
483
511
|
(this.inc.get(e.targetNodeId) ?? this.inc.set(e.targetNodeId, []).get(e.targetNodeId)).push(e);
|
|
484
512
|
}
|
|
485
513
|
}
|
|
514
|
+
/** The tails this scope's inputs, outputs and updates read off `$vars.<id>`, e.g. `.output.body.current`. */
|
|
515
|
+
readsOf(id) {
|
|
516
|
+
this.reads ??= scopeReads([...[...this.byId.values()].flatMap((n) => [n.inputs, n.outputs]), this.variables?.variableUpdates]);
|
|
517
|
+
return this.reads.get(id) ?? [];
|
|
518
|
+
}
|
|
486
519
|
outEdges(id) {
|
|
487
520
|
return this.out.get(id) ?? [];
|
|
488
521
|
}
|
|
@@ -666,6 +699,7 @@ function liftRegistered(node, impl = registeredImpl(node), graph) {
|
|
|
666
699
|
absorb: (id, compiledId) => { runAbsorbed.set(id, compiledId); },
|
|
667
700
|
outPorts: () => (graph?.outEdges(node.id) ?? []).map((e) => e.sourcePort),
|
|
668
701
|
convergedFanOut: () => graph?.convergedFanOut(node.id) === true,
|
|
702
|
+
reads: () => graph?.readsOf(node.id) ?? [],
|
|
669
703
|
};
|
|
670
704
|
const spec = impl.deserialize(node, ctx);
|
|
671
705
|
return spec === undefined ? undefined : { impl, spec };
|
|
@@ -2556,7 +2590,7 @@ function readChildStartAsStart(child, startId) {
|
|
|
2556
2590
|
function emitChildFlowIn(name, child, trigger, ctx) {
|
|
2557
2591
|
const globals = readFlowGlobals(child.variables, runStrict);
|
|
2558
2592
|
const childCtx = {
|
|
2559
|
-
graph: new Graph(child.nodes ?? [], child.edges ?? []),
|
|
2593
|
+
graph: new Graph(child.nodes ?? [], child.edges ?? [], child.variables),
|
|
2560
2594
|
flow: child,
|
|
2561
2595
|
imp: ctx.imp,
|
|
2562
2596
|
inputNames: new Set(globals.filter((g) => g.direction === 'in').map((g) => g.name ?? g.id)),
|
|
@@ -2721,7 +2755,7 @@ export function decompile(flow, options = {}) {
|
|
|
2721
2755
|
}
|
|
2722
2756
|
runCarried = carriedPairs(flow);
|
|
2723
2757
|
const imp = new Imports();
|
|
2724
|
-
const graph = new Graph(flow.nodes ?? [], flow.edges ?? []);
|
|
2758
|
+
const graph = new Graph(flow.nodes ?? [], flow.edges ?? [], flow.variables);
|
|
2725
2759
|
const globals = readFlowGlobals(flow.variables, runStrict);
|
|
2726
2760
|
const inputNames = new Set(globals.filter((g) => g.direction === 'in').map((g) => g.name ?? g.id));
|
|
2727
2761
|
// Trigger-bound globals name the node they came from; fall back to the graph's
|
package/dist/flow-node-type.d.ts
CHANGED
|
@@ -281,6 +281,8 @@ export interface FlowDeserializeContext {
|
|
|
281
281
|
* the type's `exits()`, so it is `false` for any other node.
|
|
282
282
|
*/
|
|
283
283
|
convergedFanOut(): boolean;
|
|
284
|
+
/** The tails the node's scope reads off `$vars.<nodeId>`, e.g. `.output.body.current`, `.output.body.items[]` for an indexed read. */
|
|
285
|
+
reads(): readonly string[];
|
|
284
286
|
}
|
|
285
287
|
/** What `print` may ask of the decompiler. Every method returns source text. */
|
|
286
288
|
export interface FlowPrintContext {
|
package/dist/merge-cli.js
CHANGED
|
@@ -54,7 +54,7 @@ import { ensureTypeScriptRuntime } from './node-runtime.js';
|
|
|
54
54
|
import { flowCheckOpts, openCliLibrary } from './cli-library.js';
|
|
55
55
|
import { Bindings } from './bindings.js';
|
|
56
56
|
import { defaultBindingsFile } from './workdir.js';
|
|
57
|
-
import { baselineFromOriginal, checkEdited, classifyDrafts, compileSourceDraftTolerant } from './merge-compile.js';
|
|
57
|
+
import { baselineFromOriginal, checkEdited, classifyDrafts, compileSourceDraftTolerant, findingCounts, splitUnplaced } from './merge-compile.js';
|
|
58
58
|
const USAGE = 'usage: flow-merge <original.flow> <edited.flow | Edited.flow.ts> [-o merged.flow] '
|
|
59
59
|
+ '[--baseline <baseline.flow> | --baseline-from <original.flow>] [--library <dir>] [--bindings <file>] [--strict]';
|
|
60
60
|
/**
|
|
@@ -215,19 +215,41 @@ export async function run(argv) {
|
|
|
215
215
|
// acceptable only where merge restores the ORIGINAL node; one the author
|
|
216
216
|
// changed or added gets the strict compile's refusal (#1134).
|
|
217
217
|
// check()'s errors follow the same rule; its warnings are printed as compile prints them.
|
|
218
|
-
const { kept, refused } = classifyDrafts(original, edited, baseline, drafts);
|
|
218
|
+
const { kept, refused, unplaced } = classifyDrafts(original, edited, baseline, drafts);
|
|
219
219
|
const checked = classifyDrafts(original, edited, baseline, diagnostics.filter((d) => d.level === 'error'));
|
|
220
220
|
for (const d of diagnostics)
|
|
221
221
|
if (d.level === 'warning')
|
|
222
222
|
console.error(` ⚠ [${d.code}] ${d.message}`);
|
|
223
223
|
for (const d of [...kept, ...checked.kept]) {
|
|
224
|
-
console.error(
|
|
224
|
+
console.error(d.subflow !== undefined
|
|
225
|
+
? ` ⚠ [DRAFT_NODE_KEPT] "${d.nodeId}" is unfinished in the original (${d.code}); it is inside subflow "${d.subflow}", which the edit did not touch, so the original subflow is kept as is.`
|
|
226
|
+
: ` ⚠ [DRAFT_NODE_KEPT] "${d.nodeId}" is unfinished in the original (${d.code}); it is untouched, so the original node is kept as is.`);
|
|
225
227
|
}
|
|
226
|
-
|
|
228
|
+
// A finding merge cannot place on a node — an unnamed End's `.return`, a
|
|
229
|
+
// flow-level finding — is printed when the ORIGINAL's own round trip reports
|
|
230
|
+
// it too (the designer's file had it; check has already said it), and
|
|
231
|
+
// refused when it does not: then the edit introduced it (#1322). With a
|
|
232
|
+
// precompiled --baseline file there is nothing to compare against, so every
|
|
233
|
+
// one is refused, as before.
|
|
234
|
+
const baselineFindings = baselineCompile === undefined ? undefined : findingCounts([
|
|
235
|
+
...baselineCompile.drafts,
|
|
236
|
+
...checkEdited(baselineCompile.built, baselineCompile.flow, flowCheckOpts(library, bindings)).filter((d) => d.level === 'error'),
|
|
237
|
+
]);
|
|
238
|
+
const unplacedDrafts = splitUnplaced(unplaced, baselineFindings);
|
|
239
|
+
const unplacedChecks = splitUnplaced(checked.unplaced, baselineFindings);
|
|
240
|
+
for (const d of [...unplacedDrafts.known, ...unplacedChecks.known]) {
|
|
241
|
+
console.error(` ⚠ [CHECK_FINDING_UNPLACED] ${d.code} names no node, and the original's own round trip reports it too, so it does not stop the write: ${d.message}`);
|
|
242
|
+
}
|
|
243
|
+
const errors = [
|
|
244
|
+
...refused, ...checked.refused,
|
|
245
|
+
...[...unplacedDrafts.introduced, ...unplacedChecks.introduced].map((d) => ({ ...d, reason: 'unplaced' })),
|
|
246
|
+
];
|
|
227
247
|
if (errors.length) {
|
|
228
248
|
for (const d of errors) {
|
|
229
|
-
const where = d.
|
|
230
|
-
|
|
249
|
+
const where = d.reason === 'unplaced'
|
|
250
|
+
? 'the edited flow (this finding names no node, and the original\'s own round trip does not report it, so the edit introduced it)'
|
|
251
|
+
: d.reason === 'subflow'
|
|
252
|
+
? `"${d.nodeId}" is inside subflow "${d.subflow}", which the edit changed, so merge takes that subflow whole; finish it`
|
|
231
253
|
: `"${d.nodeId}" is changed or new in the edited flow`;
|
|
232
254
|
console.error(` ✗ [${d.code}] ${where}: ${d.message}`);
|
|
233
255
|
}
|
package/dist/merge-compile.d.ts
CHANGED
|
@@ -27,36 +27,97 @@ export declare function compileSourceDraftTolerant(file: string, opts?: Omit<Ser
|
|
|
27
27
|
* it, as `flow decompile` does), then compile the source with draft tolerance.
|
|
28
28
|
* The source goes to a temporary directory that is removed afterwards.
|
|
29
29
|
*/
|
|
30
|
-
export declare function baselineFromOriginal(originalPath: string, opts?: Omit<SerializeOpts, 'draft'>, warn?: (message: string) => void): Promise<DraftTolerantCompile
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/**
|
|
30
|
+
export declare function baselineFromOriginal(originalPath: string, opts?: Omit<SerializeOpts, 'draft'>, warn?: (message: string) => void): Promise<DraftTolerantCompile & {
|
|
31
|
+
built: BuiltFlow;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Why a finding is an error: `edited`, its node is one the author changed or
|
|
35
|
+
* added; `subflow`, it sits in a subflow the edit changed, which merge takes
|
|
36
|
+
* whole; `unplaced`, it names no node and the original's own round trip does
|
|
37
|
+
* not report it, so the edit introduced it.
|
|
38
|
+
*/
|
|
39
|
+
export type TakenReason = 'edited' | 'subflow' | 'unplaced';
|
|
40
|
+
/**
|
|
41
|
+
* A finding on a node merge takes from the edited flow, with the reason it
|
|
42
|
+
* takes it: `edited`, the author changed or added the node; `subflow`, the
|
|
43
|
+
* node sits in a subflow the edit changed, which merge takes whole (`subflow`
|
|
44
|
+
* names it).
|
|
45
|
+
*/
|
|
34
46
|
export type Refused<T> = T & {
|
|
35
47
|
reason: TakenReason;
|
|
48
|
+
subflow?: string;
|
|
49
|
+
};
|
|
50
|
+
/** A finding on a node merge restores from the original; `subflow` names the untouched subflow it sits in. */
|
|
51
|
+
export type Kept<T> = T & {
|
|
52
|
+
subflow?: string;
|
|
36
53
|
};
|
|
37
54
|
/**
|
|
38
55
|
* Sort each finding by what merge does with its node: `refused` when the node
|
|
39
56
|
* comes from the edited flow (reason `edited`: the author changed or added it;
|
|
40
|
-
* reason `subflow`: it sits in a subflow, which merge takes
|
|
41
|
-
* whole), `kept` when merge restores the original node
|
|
57
|
+
* reason `subflow`: it sits in a subflow the edit changed, which merge takes
|
|
58
|
+
* whole), `kept` when merge restores the original node — a root node the edit
|
|
59
|
+
* did not touch, or any node of a subflow the edit did not touch (#1322) — and
|
|
60
|
+
* `unplaced` when the finding names no node merge knows (an unnamed End's
|
|
61
|
+
* `.return`, a flow-level finding). The caller sorts those again with
|
|
62
|
+
* {@link splitUnplaced}: one the original's own round trip reports too is
|
|
63
|
+
* printed, one the edit introduced is refused.
|
|
42
64
|
*
|
|
43
65
|
* For instance a Slack node with no connection that the author did not touch
|
|
44
66
|
* is `kept`; the same node with a new message text is `refused` as `edited`.
|
|
45
67
|
*/
|
|
46
68
|
export declare function classifyDrafts<T extends {
|
|
47
69
|
nodeId: string;
|
|
70
|
+
subflow?: string;
|
|
48
71
|
}>(original: any, edited: any, baseline: any, findings: readonly T[]): {
|
|
49
|
-
kept: T[];
|
|
72
|
+
kept: Kept<T>[];
|
|
50
73
|
refused: Refused<T>[];
|
|
74
|
+
unplaced: T[];
|
|
51
75
|
};
|
|
52
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* A `check()` diagnostic, placed on the node its step emitted (`''` when it
|
|
78
|
+
* names no node); `subflow` is the calling step when the finding is inside a
|
|
79
|
+
* subflow, which then decides how it is classified.
|
|
80
|
+
*/
|
|
53
81
|
export type PlacedDiagnostic = Diagnostic & {
|
|
54
82
|
nodeId: string;
|
|
83
|
+
subflow?: string;
|
|
55
84
|
};
|
|
56
85
|
/**
|
|
57
86
|
* Run `check()` on the edited source, as `compile` does, and place each
|
|
58
87
|
* diagnostic on the node its step emitted in `editedFlow`, so
|
|
59
|
-
* {@link classifyDrafts} can sort it
|
|
60
|
-
*
|
|
88
|
+
* {@link classifyDrafts} can sort it: a root step on its node (by label, then
|
|
89
|
+
* id); a step inside a subflow (`check` reports it as `<caller> › <step>`, one
|
|
90
|
+
* segment per level) on the deepest step the path resolves to, with `subflow`
|
|
91
|
+
* set to the deepest subflow on that path, which is what decides; a named End
|
|
92
|
+
* on its End node (#1322). A diagnostic with no step, or one no node answers
|
|
93
|
+
* to, gets nodeId `''`.
|
|
61
94
|
*/
|
|
62
95
|
export declare function checkEdited(built: BuiltFlow, editedFlow: any, opts: CheckOpts): PlacedDiagnostic[];
|
|
96
|
+
/** The identity of a finding for the introduced-or-not comparison: its code and message. */
|
|
97
|
+
export declare function findingKey(f: {
|
|
98
|
+
code: string;
|
|
99
|
+
message: string;
|
|
100
|
+
}): string;
|
|
101
|
+
/** How many times each {@link findingKey} occurs, for {@link splitUnplaced}'s baseline. */
|
|
102
|
+
export declare function findingCounts(findings: ReadonlyArray<{
|
|
103
|
+
code: string;
|
|
104
|
+
message: string;
|
|
105
|
+
}>): Map<string, number>;
|
|
106
|
+
/**
|
|
107
|
+
* Sort findings merge could not place on a node by whether the edit introduced
|
|
108
|
+
* them: `known` when the ORIGINAL's own round trip (`baseline`, the decompiled
|
|
109
|
+
* original compiled and checked, as {@link findingCounts}) reports the same
|
|
110
|
+
* code and message — once per occurrence, so a second copy of a finding the
|
|
111
|
+
* original has once is `introduced` — so the finding predates the edit and
|
|
112
|
+
* only prints; `introduced` otherwise, which is refused like any error on an
|
|
113
|
+
* edited node (#1322). With no baseline findings to compare against (a
|
|
114
|
+
* precompiled `--baseline` file), every one is `introduced`, as merge treated
|
|
115
|
+
* them before.
|
|
116
|
+
*/
|
|
117
|
+
export declare function splitUnplaced<T extends {
|
|
118
|
+
code: string;
|
|
119
|
+
message: string;
|
|
120
|
+
}>(unplaced: readonly T[], baseline: ReadonlyMap<string, number> | undefined): {
|
|
121
|
+
known: T[];
|
|
122
|
+
introduced: T[];
|
|
123
|
+
};
|
package/dist/merge-compile.js
CHANGED
|
@@ -13,7 +13,7 @@ import { decompile } from './decompile.js';
|
|
|
13
13
|
import { loadBuiltFlow } from './load.js';
|
|
14
14
|
import { serialize } from './serialize.js';
|
|
15
15
|
import { check } from './check.js';
|
|
16
|
-
import { nodesTakenFromEdited } from './merge.js';
|
|
16
|
+
import { nodesTakenFromEdited, subflowsTakenFromEdited } from './merge.js';
|
|
17
17
|
import { PACKAGE_NAME } from './package-name.js';
|
|
18
18
|
/** Compile `built` with draft tolerance. Every other refusal still throws. */
|
|
19
19
|
export function compileDraftTolerant(built, opts = {}) {
|
|
@@ -63,32 +63,62 @@ export async function baselineFromOriginal(originalPath, opts = {}, warn) {
|
|
|
63
63
|
/**
|
|
64
64
|
* Sort each finding by what merge does with its node: `refused` when the node
|
|
65
65
|
* comes from the edited flow (reason `edited`: the author changed or added it;
|
|
66
|
-
* reason `subflow`: it sits in a subflow, which merge takes
|
|
67
|
-
* whole), `kept` when merge restores the original node
|
|
66
|
+
* reason `subflow`: it sits in a subflow the edit changed, which merge takes
|
|
67
|
+
* whole), `kept` when merge restores the original node — a root node the edit
|
|
68
|
+
* did not touch, or any node of a subflow the edit did not touch (#1322) — and
|
|
69
|
+
* `unplaced` when the finding names no node merge knows (an unnamed End's
|
|
70
|
+
* `.return`, a flow-level finding). The caller sorts those again with
|
|
71
|
+
* {@link splitUnplaced}: one the original's own round trip reports too is
|
|
72
|
+
* printed, one the edit introduced is refused.
|
|
68
73
|
*
|
|
69
74
|
* For instance a Slack node with no connection that the author did not touch
|
|
70
75
|
* is `kept`; the same node with a new message text is `refused` as `edited`.
|
|
71
76
|
*/
|
|
72
77
|
export function classifyDrafts(original, edited, baseline, findings) {
|
|
73
78
|
const fromEdited = new Set(nodesTakenFromEdited(original, edited, baseline));
|
|
79
|
+
const takenSubflows = new Set(subflowsTakenFromEdited(original, edited, baseline));
|
|
74
80
|
const rootIds = new Set((edited?.nodes ?? []).map((n) => n.id));
|
|
81
|
+
/** A subflow's own node ids → the key (the calling step) of the subflow holding them. */
|
|
82
|
+
const subflowOfNode = new Map();
|
|
83
|
+
for (const [key, sf] of Object.entries(edited?.subflows ?? {})) {
|
|
84
|
+
for (const n of sf?.nodes ?? [])
|
|
85
|
+
subflowOfNode.set(n.id, key);
|
|
86
|
+
}
|
|
75
87
|
const kept = [];
|
|
76
88
|
const refused = [];
|
|
89
|
+
const unplaced = [];
|
|
77
90
|
for (const f of findings) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
else
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
// A finding inside a subflow — placed on the calling step by `checkEdited`
|
|
92
|
+
// (`subflow` set), or a draft on one of the subflow's own nodes — is judged
|
|
93
|
+
// by the SUBFLOW: taken whole when the edit changed it, else the original's.
|
|
94
|
+
const subflow = f.subflow ?? subflowOfNode.get(f.nodeId);
|
|
95
|
+
if (subflow !== undefined) {
|
|
96
|
+
if (takenSubflows.has(subflow))
|
|
97
|
+
refused.push({ ...f, reason: 'subflow', subflow });
|
|
98
|
+
else
|
|
99
|
+
kept.push({ ...f, subflow });
|
|
100
|
+
}
|
|
101
|
+
else if (rootIds.has(f.nodeId)) {
|
|
102
|
+
if (fromEdited.has(f.nodeId))
|
|
103
|
+
refused.push({ ...f, reason: 'edited' });
|
|
104
|
+
else
|
|
105
|
+
kept.push(f);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
unplaced.push(f);
|
|
109
|
+
}
|
|
84
110
|
}
|
|
85
|
-
return { kept, refused };
|
|
111
|
+
return { kept, refused, unplaced };
|
|
86
112
|
}
|
|
87
113
|
/**
|
|
88
114
|
* Run `check()` on the edited source, as `compile` does, and place each
|
|
89
115
|
* diagnostic on the node its step emitted in `editedFlow`, so
|
|
90
|
-
* {@link classifyDrafts} can sort it
|
|
91
|
-
*
|
|
116
|
+
* {@link classifyDrafts} can sort it: a root step on its node (by label, then
|
|
117
|
+
* id); a step inside a subflow (`check` reports it as `<caller> › <step>`, one
|
|
118
|
+
* segment per level) on the deepest step the path resolves to, with `subflow`
|
|
119
|
+
* set to the deepest subflow on that path, which is what decides; a named End
|
|
120
|
+
* on its End node (#1322). A diagnostic with no step, or one no node answers
|
|
121
|
+
* to, gets nodeId `''`.
|
|
92
122
|
*/
|
|
93
123
|
export function checkEdited(built, editedFlow, opts) {
|
|
94
124
|
const byStep = new Map();
|
|
@@ -98,8 +128,75 @@ export function checkEdited(built, editedFlow, opts) {
|
|
|
98
128
|
byStep.set(label, n.id);
|
|
99
129
|
}
|
|
100
130
|
const ids = new Set((editedFlow?.nodes ?? []).map((n) => n.id));
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
131
|
+
/** `name` as a node id in `nodes`: by label, then by id; `''` when neither answers. */
|
|
132
|
+
const resolveIn = (nodes, name) => {
|
|
133
|
+
const byLabel = nodes.find((n) => n?.display?.label === name);
|
|
134
|
+
if (byLabel)
|
|
135
|
+
return byLabel.id;
|
|
136
|
+
return nodes.some((n) => n?.id === name) ? name : '';
|
|
137
|
+
};
|
|
138
|
+
// `subflows` is flat, keyed by each caller's id at every depth, and check
|
|
139
|
+
// prefixes a finding at every level (`outer › inner › x`): the subflow that
|
|
140
|
+
// decides is the DEEPEST one on the path, since merge keeps or takes each
|
|
141
|
+
// subflow on its own (an edit inside `inner` leaves `outer` kept).
|
|
142
|
+
const place = (step) => {
|
|
143
|
+
if (step === undefined)
|
|
144
|
+
return { nodeId: '' };
|
|
145
|
+
const segments = step.split(SUBFLOW_STEP_SEPARATOR);
|
|
146
|
+
let scope = editedFlow?.nodes ?? [];
|
|
147
|
+
let subflow;
|
|
148
|
+
let nodeId = '';
|
|
149
|
+
for (const [i, segment] of segments.entries()) {
|
|
150
|
+
const id = i === 0 ? (byStep.get(segment) ?? (ids.has(segment) ? segment : '')) : resolveIn(scope, segment);
|
|
151
|
+
if (id === '')
|
|
152
|
+
break;
|
|
153
|
+
nodeId = id;
|
|
154
|
+
const child = i < segments.length - 1 ? editedFlow?.subflows?.[id] : undefined;
|
|
155
|
+
if (child === undefined)
|
|
156
|
+
break;
|
|
157
|
+
subflow = id;
|
|
158
|
+
scope = child.nodes ?? [];
|
|
159
|
+
}
|
|
160
|
+
return subflow !== undefined ? { nodeId: nodeId === '' ? subflow : nodeId, subflow } : { nodeId };
|
|
161
|
+
};
|
|
162
|
+
return check(built, opts).map((d) => ({ ...d, ...place(d.step) }));
|
|
163
|
+
}
|
|
164
|
+
/** The identity of a finding for the introduced-or-not comparison: its code and message. */
|
|
165
|
+
export function findingKey(f) {
|
|
166
|
+
return `${f.code}\n${f.message}`;
|
|
167
|
+
}
|
|
168
|
+
/** How many times each {@link findingKey} occurs, for {@link splitUnplaced}'s baseline. */
|
|
169
|
+
export function findingCounts(findings) {
|
|
170
|
+
const counts = new Map();
|
|
171
|
+
for (const f of findings)
|
|
172
|
+
counts.set(findingKey(f), (counts.get(findingKey(f)) ?? 0) + 1);
|
|
173
|
+
return counts;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Sort findings merge could not place on a node by whether the edit introduced
|
|
177
|
+
* them: `known` when the ORIGINAL's own round trip (`baseline`, the decompiled
|
|
178
|
+
* original compiled and checked, as {@link findingCounts}) reports the same
|
|
179
|
+
* code and message — once per occurrence, so a second copy of a finding the
|
|
180
|
+
* original has once is `introduced` — so the finding predates the edit and
|
|
181
|
+
* only prints; `introduced` otherwise, which is refused like any error on an
|
|
182
|
+
* edited node (#1322). With no baseline findings to compare against (a
|
|
183
|
+
* precompiled `--baseline` file), every one is `introduced`, as merge treated
|
|
184
|
+
* them before.
|
|
185
|
+
*/
|
|
186
|
+
export function splitUnplaced(unplaced, baseline) {
|
|
187
|
+
const known = [];
|
|
188
|
+
const introduced = [];
|
|
189
|
+
const left = new Map(baseline ?? []);
|
|
190
|
+
for (const f of unplaced) {
|
|
191
|
+
const n = left.get(findingKey(f)) ?? 0;
|
|
192
|
+
if (n > 0) {
|
|
193
|
+
left.set(findingKey(f), n - 1);
|
|
194
|
+
known.push(f);
|
|
195
|
+
}
|
|
196
|
+
else
|
|
197
|
+
introduced.push(f);
|
|
198
|
+
}
|
|
199
|
+
return { known, introduced };
|
|
105
200
|
}
|
|
201
|
+
/** How `check` joins a subflow's calling step and the step inside it in a diagnostic's `step`. */
|
|
202
|
+
const SUBFLOW_STEP_SEPARATOR = ' › ';
|
package/dist/merge.d.ts
CHANGED
|
@@ -181,6 +181,16 @@ export interface MergeOptions {
|
|
|
181
181
|
* `original`, the answer is `[]` — a no-edit merge keeps them all.
|
|
182
182
|
*/
|
|
183
183
|
export declare function nodesTakenFromEdited(original: AnyFlow, edited: AnyFlow, baseline?: AnyFlow): string[];
|
|
184
|
+
/**
|
|
185
|
+
* The keys of the subflows {@link mergeFlow} takes from `edited` rather than
|
|
186
|
+
* restoring from `original`: every subflow that is new, that the original does
|
|
187
|
+
* not have, or that differs from its `baseline` counterpart in any way the
|
|
188
|
+
* author can edit — a node's inputs, output sources or label, an edge, the
|
|
189
|
+
* scope's `variables` or `layout` (`subflowUnchanged`, #1322). With no
|
|
190
|
+
* baseline, that is every subflow. A kept subflow is the original's whole: its
|
|
191
|
+
* nodes, edges, variables and layout.
|
|
192
|
+
*/
|
|
193
|
+
export declare function subflowsTakenFromEdited(original: AnyFlow, edited: AnyFlow, baseline?: AnyFlow): string[];
|
|
184
194
|
export declare function mergeFlow(original: AnyFlow, edited: AnyFlow, options?: MergeOptions): AnyFlow;
|
|
185
195
|
/**
|
|
186
196
|
* `mergeFlow`, plus the agent sidecar files the merged flow needs written beside
|
package/dist/merge.js
CHANGED
|
@@ -138,6 +138,68 @@ export function nodesTakenFromEdited(original, edited, baseline) {
|
|
|
138
138
|
const baseNodes = byId(baseline?.nodes);
|
|
139
139
|
return (edited.nodes ?? []).filter((e) => !isKept(e, origNodes, baseNodes, baseline)).map((e) => e.id);
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* The keys of the subflows {@link mergeFlow} takes from `edited` rather than
|
|
143
|
+
* restoring from `original`: every subflow that is new, that the original does
|
|
144
|
+
* not have, or that differs from its `baseline` counterpart in any way the
|
|
145
|
+
* author can edit — a node's inputs, output sources or label, an edge, the
|
|
146
|
+
* scope's `variables` or `layout` (`subflowUnchanged`, #1322). With no
|
|
147
|
+
* baseline, that is every subflow. A kept subflow is the original's whole: its
|
|
148
|
+
* nodes, edges, variables and layout.
|
|
149
|
+
*/
|
|
150
|
+
export function subflowsTakenFromEdited(original, edited, baseline) {
|
|
151
|
+
return Object.keys(edited.subflows ?? {}).filter((key) => {
|
|
152
|
+
const orig = original.subflows?.[key];
|
|
153
|
+
const base = baseline?.subflows?.[key];
|
|
154
|
+
return !baseline || orig === undefined || base === undefined
|
|
155
|
+
|| !subflowUnchanged(edited.subflows[key], base);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/** A node's canvas label, when it has one. */
|
|
159
|
+
const labelOf = (n) => n?.display?.label;
|
|
160
|
+
/**
|
|
161
|
+
* Whether an edited subflow equals its baseline: the same node ids, each node
|
|
162
|
+
* unchanged (`nodeUnchanged`) and under the same label, the same edges by
|
|
163
|
+
* their endpoints and handles (ids are compile's own), and the same
|
|
164
|
+
* `variables` and `layout` byte for byte. The scope's variables carry what the
|
|
165
|
+
* nodes do not — a step's `{ updates }` row, a child `.input()` type, a `.var()`
|
|
166
|
+
* default — and a kept subflow is the original's whole, so any of them changing
|
|
167
|
+
* has to make the subflow a taken one.
|
|
168
|
+
*/
|
|
169
|
+
function subflowUnchanged(e, b) {
|
|
170
|
+
const eNodes = byId(e.nodes);
|
|
171
|
+
const bNodes = byId(b.nodes);
|
|
172
|
+
if (eNodes.size !== bNodes.size)
|
|
173
|
+
return false;
|
|
174
|
+
for (const [id, n] of eNodes) {
|
|
175
|
+
const base = bNodes.get(id);
|
|
176
|
+
if (!nodeUnchanged(n, base) || labelOf(n) !== labelOf(base))
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if (JSON.stringify(e.variables ?? null) !== JSON.stringify(b.variables ?? null))
|
|
180
|
+
return false;
|
|
181
|
+
// `layout.edges` is keyed by edge id, and ids come from one file-wide
|
|
182
|
+
// counter: a step added before the call shifts every id in the subflow
|
|
183
|
+
// without touching it. So routes are compared by the endpoints the id names
|
|
184
|
+
// (as the edges themselves are), and `layout.nodes` as it is.
|
|
185
|
+
if (JSON.stringify(layoutByEndpoints(e)) !== JSON.stringify(layoutByEndpoints(b)))
|
|
186
|
+
return false;
|
|
187
|
+
const eEdges = (e.edges ?? []).map(edgeKey).sort();
|
|
188
|
+
const bEdges = (b.edges ?? []).map(edgeKey).sort();
|
|
189
|
+
return eEdges.length === bEdges.length && eEdges.every((k, i) => k === bEdges[i]);
|
|
190
|
+
}
|
|
191
|
+
/** `<source>@<port>-><target>@<port>`: an edge's identity apart from its id. */
|
|
192
|
+
const edgeKey = (x) => `${x.sourceNodeId}@${x.sourcePort}->${x.targetNodeId}@${x.targetPort}`;
|
|
193
|
+
/** A scope's layout with its `edges` re-keyed by endpoints, so an id shift does not read as a change. */
|
|
194
|
+
function layoutByEndpoints(sf) {
|
|
195
|
+
const layout = sf.layout;
|
|
196
|
+
if (!layout || typeof layout !== 'object')
|
|
197
|
+
return layout ?? null;
|
|
198
|
+
const byId = new Map((sf.edges ?? []).map((x) => [x.id, edgeKey(x)]));
|
|
199
|
+
const { edges, ...rest } = layout;
|
|
200
|
+
const routes = Object.entries(edges ?? {}).map(([id, route]) => [byId.get(id) ?? id, route]).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
201
|
+
return { ...rest, edges: Object.fromEntries(routes) };
|
|
202
|
+
}
|
|
141
203
|
/**
|
|
142
204
|
* Whether merge restores `e`'s ORIGINAL node instead of taking `e`: there is a
|
|
143
205
|
* baseline, the original has the node, and `e` equals its baseline counterpart.
|
|
@@ -343,15 +405,44 @@ function mergeFlowFindings(original, edited, options) {
|
|
|
343
405
|
result.nodes = finalNodes;
|
|
344
406
|
// Nodes present in the original but absent from `edited` were deleted by the
|
|
345
407
|
// user; they are simply not carried over.
|
|
346
|
-
// ── Subflows: the edited flow is authoritative (the user's graph)
|
|
408
|
+
// ── Subflows: the edited flow is authoritative (the user's graph) — except a
|
|
409
|
+
// subflow the edit never touched, which is the original's, whole: the same
|
|
410
|
+
// `kept` rule as a root node, one level down (#1322). Before, every subflow
|
|
411
|
+
// came from the edited compile, so a no-edit round trip rewrote each one
|
|
412
|
+
// in the SDK's shape, and a check error inside one refused the write. ──
|
|
347
413
|
result.subflows = jsonClone(edited.subflows ?? {});
|
|
348
|
-
|
|
349
|
-
|
|
414
|
+
const takenSubflows = new Set(subflowsTakenFromEdited(original, edited, options.baseline));
|
|
415
|
+
for (const key of Object.keys(result.subflows)) {
|
|
416
|
+
if (!takenSubflows.has(key))
|
|
417
|
+
result.subflows[key] = jsonClone(original.subflows[key]);
|
|
418
|
+
}
|
|
419
|
+
// A kept subflow carries the original's edge ids and a taken one the
|
|
420
|
+
// recompile's, from two different counters, so a taken subflow's edges are
|
|
421
|
+
// renumbered against every id a kept one holds (and against the taken ones
|
|
422
|
+
// before it), the way the root's are below.
|
|
423
|
+
const idScopes = Object.entries(result.subflows).filter(([k]) => !takenSubflows.has(k)).map(([, sf]) => sf);
|
|
424
|
+
for (const key of Object.keys(result.subflows)) {
|
|
425
|
+
if (!takenSubflows.has(key))
|
|
426
|
+
continue;
|
|
427
|
+
const sf = result.subflows[key];
|
|
428
|
+
const { edges: renumbered, renamed } = withFileUniqueEdgeIds(sf.edges ?? [], idScopes);
|
|
429
|
+
sf.edges = renumbered;
|
|
430
|
+
// A route in `layout.edges` is keyed by the edge's id, so it follows the rename.
|
|
431
|
+
const layout = sf.layout;
|
|
432
|
+
if (renamed.size > 0 && layout?.edges) {
|
|
433
|
+
layout.edges = Object.fromEntries(Object.entries(layout.edges).map(([id, route]) => [renamed.get(id) ?? id, route]));
|
|
434
|
+
}
|
|
435
|
+
idScopes.push(sf);
|
|
436
|
+
}
|
|
437
|
+
// A subflow taken from `edited` whole would flip an embedded agent inside it
|
|
438
|
+
// to the edited shell even with no edit to that node. Keep it embedded too.
|
|
350
439
|
// This runs before the source fix-up below: it compares each node with the
|
|
351
|
-
// baseline to tell which ones the author edited.
|
|
352
|
-
|
|
440
|
+
// baseline to tell which ones the author edited. A kept subflow is the
|
|
441
|
+
// original's already, so neither fix-up looks at it.
|
|
442
|
+
const takenEntries = Object.fromEntries(Object.entries(result.subflows).filter(([key]) => takenSubflows.has(key)));
|
|
443
|
+
const subflowEdited = keepEmbeddedAgentsInSubflows(original, takenEntries, options.baseline);
|
|
353
444
|
if (options.baseline)
|
|
354
|
-
keepOriginalSourcesInSubflows(original,
|
|
445
|
+
keepOriginalSourcesInSubflows(original, takenEntries, options.baseline);
|
|
355
446
|
const subflowScopes = Object.values(result.subflows);
|
|
356
447
|
// ── Variables: the same delta as nodes, per entry — what the author did not
|
|
357
448
|
// change stays the original's, so compile's own spellings (an added
|
|
@@ -380,7 +471,7 @@ function mergeFlowFindings(original, edited, options) {
|
|
|
380
471
|
// document (it drops one element or crashes). Only an edge with no id, or
|
|
381
472
|
// one already taken elsewhere in the file, gets a fresh id, from ids unused
|
|
382
473
|
// anywhere in the file. ──
|
|
383
|
-
result.edges = withFileUniqueEdgeIds(edited.edges ?? [], subflowScopes);
|
|
474
|
+
result.edges = withFileUniqueEdgeIds(edited.edges ?? [], subflowScopes).edges;
|
|
384
475
|
// ── Placement: a node the edit ADDED has no original position. Unless the
|
|
385
476
|
// author set one, it has compile's placeholder, an emission-index slot
|
|
386
477
|
// that can land on an original node. Put it next to its predecessor,
|
|
@@ -511,11 +602,16 @@ function withFileUniqueEdgeIds(edges, subflows) {
|
|
|
511
602
|
n++;
|
|
512
603
|
return `edge_${n++}`;
|
|
513
604
|
};
|
|
514
|
-
|
|
605
|
+
/** Old id → new id, for every edge that had to move (a route keyed by the old id follows it). */
|
|
606
|
+
const renamed = new Map();
|
|
607
|
+
const out = edges.map((e) => {
|
|
515
608
|
const id = typeof e.id === 'string' && !taken.has(e.id) ? e.id : fresh();
|
|
609
|
+
if (typeof e.id === 'string' && id !== e.id)
|
|
610
|
+
renamed.set(e.id, id);
|
|
516
611
|
taken.add(id);
|
|
517
612
|
return { ...jsonClone(e), id };
|
|
518
613
|
});
|
|
614
|
+
return { edges: out, renamed };
|
|
519
615
|
}
|
|
520
616
|
/** The `nodeType@version` identity of a definition row or of a node's type pin. */
|
|
521
617
|
function defKey(nodeType, version) {
|
|
@@ -245,6 +245,27 @@ function liftReturns(node) {
|
|
|
245
245
|
.map(([k, v]) => [k, v.type]);
|
|
246
246
|
return entries.length ? Object.fromEntries(entries) : undefined;
|
|
247
247
|
}
|
|
248
|
+
/** A read of a body field: the field, then whatever the read goes on to. */
|
|
249
|
+
const BODY_READ_RE = /^\.output\.body\.([A-Za-z0-9_]+)(.*)$/;
|
|
250
|
+
/**
|
|
251
|
+
* The `returns` a node with no body schema needs for the body fields its flow
|
|
252
|
+
* already reads, so the source decompile prints compiles. A field read further
|
|
253
|
+
* in (`body.current.time`) is an `object`; a field read whole is a `string`, the
|
|
254
|
+
* type the check suggests. Undefined when nothing reads the body.
|
|
255
|
+
*/
|
|
256
|
+
function returnsFromReads(reads) {
|
|
257
|
+
const fields = new Map();
|
|
258
|
+
for (const read of reads) {
|
|
259
|
+
const m = BODY_READ_RE.exec(read);
|
|
260
|
+
if (!m)
|
|
261
|
+
continue;
|
|
262
|
+
if (m[2] !== '')
|
|
263
|
+
fields.set(m[1], 'object');
|
|
264
|
+
else if (!fields.has(m[1]))
|
|
265
|
+
fields.set(m[1], 'string');
|
|
266
|
+
}
|
|
267
|
+
return fields.size ? Object.fromEntries(fields) : undefined;
|
|
268
|
+
}
|
|
248
269
|
/**
|
|
249
270
|
* Invert an HTTP node. The plain node keeps its request at the top level
|
|
250
271
|
* (`url` / `method` / `headers` / `queryParams` / `body` / `contentType`); the
|
|
@@ -275,7 +296,7 @@ function liftHttp(node, ctx, managed) {
|
|
|
275
296
|
throw new Error(`decompile: expected a number, got ${JSON.stringify(inputs.retryCount)}.`);
|
|
276
297
|
}
|
|
277
298
|
const branchRows = Array.isArray(inputs.branches) ? inputs.branches : [];
|
|
278
|
-
const returns = liftReturns(node);
|
|
299
|
+
const returns = liftReturns(node) ?? returnsFromReads(ctx.reads());
|
|
279
300
|
const spec = {
|
|
280
301
|
url: value(req.url ?? ''),
|
|
281
302
|
...(text(req.method) !== undefined && req.method !== 'GET' ? { method: req.method } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "6.44.
|
|
3
|
+
"version": "6.44.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",
|
|
@@ -93,5 +93,5 @@
|
|
|
93
93
|
"@types/node": "^22.7.0",
|
|
94
94
|
"esbuild": "^0.28.1"
|
|
95
95
|
},
|
|
96
|
-
"gitref": "
|
|
96
|
+
"gitref": "cbc51e1db2b0ffabe85f841dee73f5a6301192d2"
|
|
97
97
|
}
|