@uipath/maestro-builder-sdk 6.27.1 → 6.28.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.
- package/dist/cli/commands/flow.js +8 -2
- package/dist/core/connector-inputs.d.ts +6 -0
- package/dist/core/connector-inputs.js +7 -2
- package/dist/merge-cli.d.ts +1 -1
- package/dist/merge-cli.js +75 -8
- package/dist/merge-compile.d.ts +55 -0
- package/dist/merge-compile.js +104 -0
- package/dist/merge.d.ts +9 -0
- package/dist/merge.js +22 -1
- package/dist/serialize.d.ts +26 -0
- package/dist/serialize.js +54 -4
- package/package.json +2 -2
|
@@ -104,11 +104,14 @@ export function registerFlowCommands(program) {
|
|
|
104
104
|
});
|
|
105
105
|
flow.command('merge')
|
|
106
106
|
.description('Merge an edited flow with its original artifact.')
|
|
107
|
-
.argument('<edited>', 'edited .flow path')
|
|
107
|
+
.argument('<edited>', 'edited .flow path, or the edited .flow.ts source (merge compiles it, tolerating untouched draft nodes)')
|
|
108
108
|
.option('--original <file>', '(required) original .flow path, the artifact to merge into')
|
|
109
109
|
.option('-o, --output <file>', 'merged output path')
|
|
110
110
|
.option('--baseline <file>', 'compiled pristine decompile')
|
|
111
|
-
.option('--
|
|
111
|
+
.option('--baseline-from <file>', 'build the baseline from this original .flow (decompile + compile, tolerating draft nodes) instead of --baseline')
|
|
112
|
+
.option('--library <dir>', 'connector library, for compiling with --baseline-from or a .flow.ts edited source')
|
|
113
|
+
.option('--bindings <file>', 'bindings.json, for the same compiles')
|
|
114
|
+
.option('--strict', 'refuse when the no-edit round trip (the baseline) differs from the original')
|
|
112
115
|
.allowExcessArguments()
|
|
113
116
|
.action(async (_edited, options, command) => {
|
|
114
117
|
const pair = namedOrLegacySecond(command, {
|
|
@@ -121,6 +124,9 @@ export function registerFlowCommands(program) {
|
|
|
121
124
|
const argv = [original, edited];
|
|
122
125
|
addValue(argv, '-o', options.output);
|
|
123
126
|
addValue(argv, '--baseline', options.baseline);
|
|
127
|
+
addValue(argv, '--baseline-from', options.baselineFrom);
|
|
128
|
+
addValue(argv, '--library', options.library);
|
|
129
|
+
addValue(argv, '--bindings', options.bindings);
|
|
124
130
|
if (options.strict)
|
|
125
131
|
argv.push('--strict');
|
|
126
132
|
await invoke('flow merge', () => import('../../merge-cli.js'), argv);
|
|
@@ -37,6 +37,12 @@ export interface ConnectorInputOpts {
|
|
|
37
37
|
lookups?: Readonly<Record<string, LookupSpec>>;
|
|
38
38
|
/** `ignore` skips an unresolved lookup instead of refusing it — what `check` wants. */
|
|
39
39
|
unresolvedLookups?: 'error' | 'ignore';
|
|
40
|
+
/**
|
|
41
|
+
* Receives a missing REQUIRED input instead of refusing it — the draft
|
|
42
|
+
* tolerance of a merge-owned compile (#1134). Every other input error still
|
|
43
|
+
* refuses.
|
|
44
|
+
*/
|
|
45
|
+
onMissingRequired?: (message: string) => void;
|
|
40
46
|
}
|
|
41
47
|
/** Flow's dialect, re-exported so `serialize.ts` need not reach past this module. */
|
|
42
48
|
export { FLOW_EXPR_DIALECT } from './connector-checks.js';
|
|
@@ -116,8 +116,13 @@ export function buildConnectorInputs(rc, inputs, opts) {
|
|
|
116
116
|
// not know what to put in it than because they forgot it exists: name the
|
|
117
117
|
// id it sends and the helper that finds one, so "(string)" is not read as
|
|
118
118
|
// "any string" — an e-mail into `fields.reporter.id` is exactly that misreading.
|
|
119
|
-
if (f.required)
|
|
120
|
-
|
|
119
|
+
if (f.required) {
|
|
120
|
+
const message = `missing required input "${f.name}" (${f.type ?? 'value'})${missingLookupHint(f.name, lookups[f.name], rc, dialect.descriptors)}`;
|
|
121
|
+
if (opts.onMissingRequired)
|
|
122
|
+
opts.onMissingRequired(message);
|
|
123
|
+
else
|
|
124
|
+
errs.push(message);
|
|
125
|
+
}
|
|
121
126
|
continue;
|
|
122
127
|
}
|
|
123
128
|
// An unresolved lookup is swapped for its recorded id, or reported. It is
|
package/dist/merge-cli.d.ts
CHANGED
package/dist/merge-cli.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* merge-cli — merge an edited flow back into the original `.flow` (bin `flow-merge`)
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* flow-merge <original.flow> <edited.flow> [-o merged.flow]
|
|
6
|
+
* flow-merge <original.flow> <edited.flow | Edited.flow.ts> [-o merged.flow]
|
|
7
|
+
* [--baseline <baseline.flow> | --baseline-from <original.flow>] [--strict]
|
|
8
|
+
*
|
|
9
|
+
* `--baseline-from <original.flow>` makes merge build the baseline itself
|
|
10
|
+
* (decompile + compile, nothing written), and an edited `.flow.ts` makes merge
|
|
11
|
+
* compile it: both compiles tolerate DRAFT nodes, and merge refuses a draft
|
|
12
|
+
* only where it would take the node from the edited flow. See merge-compile.ts.
|
|
7
13
|
*
|
|
8
14
|
* `edited.flow` is compile(decompile(original) after your edits). `--baseline`
|
|
9
15
|
* is compile(decompile(original)) BEFORE any edit — supplying it enables the
|
|
@@ -19,27 +25,88 @@ import { readFileSync } from 'node:fs';
|
|
|
19
25
|
import { basename, resolve } from 'node:path';
|
|
20
26
|
import { MergeRefusedError, mergeFlow } from './merge.js';
|
|
21
27
|
import { flagValue, runWhenInvokedDirectly } from './cli-run.js';
|
|
22
|
-
|
|
28
|
+
import { ensureTypeScriptRuntime } from './node-runtime.js';
|
|
29
|
+
import { flowCheckOpts, openCliLibrary } from './cli-library.js';
|
|
30
|
+
import { Bindings } from './bindings.js';
|
|
31
|
+
import { defaultBindingsFile } from './workdir.js';
|
|
32
|
+
import { baselineFromOriginal, checkEdited, classifyDrafts, compileSourceDraftTolerant } from './merge-compile.js';
|
|
33
|
+
const USAGE = 'usage: flow-merge <original.flow> <edited.flow | Edited.flow.ts> [-o merged.flow] '
|
|
34
|
+
+ '[--baseline <baseline.flow> | --baseline-from <original.flow>] [--library <dir>] [--bindings <file>] [--strict]';
|
|
35
|
+
export async function run(argv) {
|
|
23
36
|
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
24
|
-
console.error(
|
|
37
|
+
console.error(USAGE);
|
|
25
38
|
return 2;
|
|
26
39
|
}
|
|
27
|
-
const flags = new Set(['-o', '--baseline']);
|
|
40
|
+
const flags = new Set(['-o', '--baseline', '--baseline-from', '--library', '--bindings', '--connectors-local']);
|
|
28
41
|
const positionals = argv.filter((a, i) => !flags.has(a) && !flags.has(argv[i - 1]) && !a.startsWith('-'));
|
|
29
42
|
const [originalPath, editedPath] = positionals;
|
|
30
43
|
if (!originalPath || !editedPath) {
|
|
31
44
|
console.error('flow-merge: need <original.flow> and <edited.flow>');
|
|
32
45
|
return 2;
|
|
33
46
|
}
|
|
34
|
-
const original = JSON.parse(readFileSync(originalPath, 'utf8'));
|
|
35
|
-
const edited = JSON.parse(readFileSync(editedPath, 'utf8'));
|
|
36
47
|
const baselinePath = flagValue(argv, '--baseline');
|
|
37
|
-
const
|
|
48
|
+
const baselineFrom = flagValue(argv, '--baseline-from');
|
|
49
|
+
if (baselinePath && baselineFrom) {
|
|
50
|
+
console.error('flow-merge: pass --baseline or --baseline-from, not both');
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
const editedIsSource = /\.ts$/.test(editedPath);
|
|
54
|
+
// Merge compiles a side itself only for --baseline-from or a .flow.ts edited
|
|
55
|
+
// source; both load TypeScript, so both need the type-stripping runtime.
|
|
56
|
+
if (baselineFrom || editedIsSource)
|
|
57
|
+
ensureTypeScriptRuntime();
|
|
58
|
+
const library = baselineFrom || editedIsSource ? openCliLibrary(argv) : undefined;
|
|
59
|
+
const bindings = baselineFrom || editedIsSource ? new Bindings(flagValue(argv, '--bindings') ?? defaultBindingsFile()) : undefined;
|
|
60
|
+
const compileOpts = { library, bindings };
|
|
61
|
+
const original = JSON.parse(readFileSync(originalPath, 'utf8'));
|
|
62
|
+
let drafts = [];
|
|
63
|
+
let diagnostics = [];
|
|
64
|
+
let edited;
|
|
65
|
+
if (editedIsSource) {
|
|
66
|
+
// The edited source gets what `compile` gives it: check() with the same
|
|
67
|
+
// options, and the serializer's warnings (CONNECTION_STUB, ...) printed.
|
|
68
|
+
const compiled = await compileSourceDraftTolerant(resolve(editedPath), {
|
|
69
|
+
...compileOpts,
|
|
70
|
+
warn: (w) => console.error(` ⚠ [${w.code}] ${w.message}`),
|
|
71
|
+
});
|
|
72
|
+
edited = compiled.flow;
|
|
73
|
+
drafts = compiled.drafts;
|
|
74
|
+
diagnostics = checkEdited(compiled.built, edited, flowCheckOpts(library, bindings));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
edited = JSON.parse(readFileSync(editedPath, 'utf8'));
|
|
78
|
+
}
|
|
79
|
+
const baseline = baselineFrom
|
|
80
|
+
? (await baselineFromOriginal(baselineFrom, compileOpts, (m) => console.error(`flow-merge: decompile warning: ${m}`))).flow
|
|
81
|
+
: baselinePath ? JSON.parse(readFileSync(baselinePath, 'utf8')) : undefined;
|
|
38
82
|
const strict = argv.includes('--strict');
|
|
39
83
|
if (strict && !baseline) {
|
|
40
|
-
console.error('flow-merge: --strict compares the no-edit round trip with the original; pass --baseline <baseline.flow>');
|
|
84
|
+
console.error('flow-merge: --strict compares the no-edit round trip with the original; pass --baseline <baseline.flow> or --baseline-from <original.flow>');
|
|
41
85
|
return 2;
|
|
42
86
|
}
|
|
87
|
+
// A draft node (no connection, an empty required input, no object) is
|
|
88
|
+
// acceptable only where merge restores the ORIGINAL node; one the author
|
|
89
|
+
// changed or added gets the strict compile's refusal (#1134).
|
|
90
|
+
// check()'s errors follow the same rule; its warnings are printed as compile prints them.
|
|
91
|
+
const { kept, refused } = classifyDrafts(original, edited, baseline, drafts);
|
|
92
|
+
const checked = classifyDrafts(original, edited, baseline, diagnostics.filter((d) => d.level === 'error'));
|
|
93
|
+
for (const d of diagnostics)
|
|
94
|
+
if (d.level === 'warning')
|
|
95
|
+
console.error(` ⚠ [${d.code}] ${d.message}`);
|
|
96
|
+
for (const d of [...kept, ...checked.kept]) {
|
|
97
|
+
console.error(` ⚠ [DRAFT_NODE_KEPT] "${d.nodeId}" is unfinished in the original (${d.code}); it is untouched, so the original node is kept as is.`);
|
|
98
|
+
}
|
|
99
|
+
const errors = [...refused, ...checked.refused];
|
|
100
|
+
if (errors.length) {
|
|
101
|
+
for (const d of errors) {
|
|
102
|
+
const where = d.nodeId === '' ? 'the edited flow'
|
|
103
|
+
: d.reason === 'subflow' ? `"${d.nodeId}" is inside a subflow, which merge takes whole; finish it`
|
|
104
|
+
: `"${d.nodeId}" is changed or new in the edited flow`;
|
|
105
|
+
console.error(` ✗ [${d.code}] ${where}: ${d.message}`);
|
|
106
|
+
}
|
|
107
|
+
console.error(`flow-merge: ${errors.length} error(s) in nodes merge takes from the edited flow; finish them or revert the edit. Nothing written.`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
43
110
|
let merged;
|
|
44
111
|
try {
|
|
45
112
|
merged = mergeFlow(original, edited, {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type DraftFinding, type SerializeOpts } from './serialize.js';
|
|
2
|
+
import { type CheckOpts, type Diagnostic } from './check.js';
|
|
3
|
+
import type { BuiltFlow } from './flow-sdk.js';
|
|
4
|
+
/** A compile's flow and the draft refusals it reported instead of throwing. */
|
|
5
|
+
export interface DraftTolerantCompile {
|
|
6
|
+
flow: any;
|
|
7
|
+
drafts: DraftFinding[];
|
|
8
|
+
}
|
|
9
|
+
/** Compile `built` with draft tolerance. Every other refusal still throws. */
|
|
10
|
+
export declare function compileDraftTolerant(built: BuiltFlow, opts?: Omit<SerializeOpts, 'draft'>): DraftTolerantCompile;
|
|
11
|
+
/**
|
|
12
|
+
* Load a `.flow.ts` source and compile it with draft tolerance.
|
|
13
|
+
* The caller has already called `ensureTypeScriptRuntime()`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function compileSourceDraftTolerant(file: string, opts?: Omit<SerializeOpts, 'draft'>): Promise<DraftTolerantCompile & {
|
|
16
|
+
built: BuiltFlow;
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* The baseline of `originalPath`: decompile it (reading agent sidecars beside
|
|
20
|
+
* it, as `flow decompile` does), then compile the source with draft tolerance.
|
|
21
|
+
* The source goes to a temporary directory that is removed afterwards.
|
|
22
|
+
*/
|
|
23
|
+
export declare function baselineFromOriginal(originalPath: string, opts?: Omit<SerializeOpts, 'draft'>, warn?: (message: string) => void): Promise<DraftTolerantCompile>;
|
|
24
|
+
/** Why merge takes a node from the edited flow, so a finding on it is an error. */
|
|
25
|
+
export type TakenReason = 'edited' | 'subflow';
|
|
26
|
+
/** A finding on a node merge takes from the edited flow, with the reason it takes it. */
|
|
27
|
+
export type Refused<T> = T & {
|
|
28
|
+
reason: TakenReason;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Sort each finding by what merge does with its node: `refused` when the node
|
|
32
|
+
* comes from the edited flow (reason `edited`: the author changed or added it;
|
|
33
|
+
* reason `subflow`: it sits in a subflow, which merge takes from `edited`
|
|
34
|
+
* whole), `kept` when merge restores the original node.
|
|
35
|
+
*
|
|
36
|
+
* For instance a Slack node with no connection that the author did not touch
|
|
37
|
+
* is `kept`; the same node with a new message text is `refused` as `edited`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function classifyDrafts<T extends {
|
|
40
|
+
nodeId: string;
|
|
41
|
+
}>(original: any, edited: any, baseline: any, findings: readonly T[]): {
|
|
42
|
+
kept: T[];
|
|
43
|
+
refused: Refused<T>[];
|
|
44
|
+
};
|
|
45
|
+
/** A `check()` diagnostic, placed on the root node its step emitted (`''` when it names no root step). */
|
|
46
|
+
export type PlacedDiagnostic = Diagnostic & {
|
|
47
|
+
nodeId: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Run `check()` on the edited source, as `compile` does, and place each
|
|
51
|
+
* diagnostic on the node its step emitted in `editedFlow`, so
|
|
52
|
+
* {@link classifyDrafts} can sort it. A diagnostic with no step, or a step
|
|
53
|
+
* with no root node, gets nodeId `''`, which is always refused.
|
|
54
|
+
*/
|
|
55
|
+
export declare function checkEdited(built: BuiltFlow, editedFlow: any, opts: CheckOpts): PlacedDiagnostic[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* merge-compile — the compiles `flow merge --baseline-from` runs itself (#1134).
|
|
3
|
+
*
|
|
4
|
+
* Both compiles tolerate draft nodes (`SerializeOpts.draft`). A draft on a node
|
|
5
|
+
* merge keeps from the original is a warning; a draft or `check()` error on a
|
|
6
|
+
* node merge takes from the edited flow is an error. Nothing draft-tolerant is
|
|
7
|
+
* written to disk.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { decompile } from './decompile.js';
|
|
13
|
+
import { loadBuiltFlow } from './load.js';
|
|
14
|
+
import { serialize } from './serialize.js';
|
|
15
|
+
import { check } from './check.js';
|
|
16
|
+
import { nodesTakenFromEdited } from './merge.js';
|
|
17
|
+
import { PACKAGE_NAME } from './package-name.js';
|
|
18
|
+
/** Compile `built` with draft tolerance. Every other refusal still throws. */
|
|
19
|
+
export function compileDraftTolerant(built, opts = {}) {
|
|
20
|
+
const drafts = [];
|
|
21
|
+
const flow = serialize(built, { ...opts, draft: (f) => drafts.push(f) });
|
|
22
|
+
return { flow, drafts };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Load a `.flow.ts` source and compile it with draft tolerance.
|
|
26
|
+
* The caller has already called `ensureTypeScriptRuntime()`.
|
|
27
|
+
*/
|
|
28
|
+
export async function compileSourceDraftTolerant(file, opts = {}) {
|
|
29
|
+
const built = await loadBuiltFlow(file);
|
|
30
|
+
return { ...compileDraftTolerant(built, opts), built };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The baseline of `originalPath`: decompile it (reading agent sidecars beside
|
|
34
|
+
* it, as `flow decompile` does), then compile the source with draft tolerance.
|
|
35
|
+
* The source goes to a temporary directory that is removed afterwards.
|
|
36
|
+
*/
|
|
37
|
+
export async function baselineFromOriginal(originalPath, opts = {}, warn) {
|
|
38
|
+
const original = JSON.parse(readFileSync(originalPath, 'utf8'));
|
|
39
|
+
const source = decompile(original, {
|
|
40
|
+
importSpecifier: PACKAGE_NAME,
|
|
41
|
+
warn,
|
|
42
|
+
readSidecar: (relativePath) => {
|
|
43
|
+
try {
|
|
44
|
+
return readFileSync(join(dirname(resolve(originalPath)), relativePath), 'utf8');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const dir = mkdtempSync(join(tmpdir(), 'flow-merge-baseline-'));
|
|
52
|
+
try {
|
|
53
|
+
writeFileSync(join(dir, 'package.json'), '{"type":"module"}\n');
|
|
54
|
+
const file = join(dir, 'Baseline.flow.ts');
|
|
55
|
+
writeFileSync(file, source);
|
|
56
|
+
return await compileSourceDraftTolerant(file, opts);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
rmSync(dir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Sort each finding by what merge does with its node: `refused` when the node
|
|
64
|
+
* comes from the edited flow (reason `edited`: the author changed or added it;
|
|
65
|
+
* reason `subflow`: it sits in a subflow, which merge takes from `edited`
|
|
66
|
+
* whole), `kept` when merge restores the original node.
|
|
67
|
+
*
|
|
68
|
+
* For instance a Slack node with no connection that the author did not touch
|
|
69
|
+
* is `kept`; the same node with a new message text is `refused` as `edited`.
|
|
70
|
+
*/
|
|
71
|
+
export function classifyDrafts(original, edited, baseline, findings) {
|
|
72
|
+
const fromEdited = new Set(nodesTakenFromEdited(original, edited, baseline));
|
|
73
|
+
const rootIds = new Set((edited?.nodes ?? []).map((n) => n.id));
|
|
74
|
+
const kept = [];
|
|
75
|
+
const refused = [];
|
|
76
|
+
for (const f of findings) {
|
|
77
|
+
if (!rootIds.has(f.nodeId))
|
|
78
|
+
refused.push({ ...f, reason: 'subflow' });
|
|
79
|
+
else if (fromEdited.has(f.nodeId))
|
|
80
|
+
refused.push({ ...f, reason: 'edited' });
|
|
81
|
+
else
|
|
82
|
+
kept.push(f);
|
|
83
|
+
}
|
|
84
|
+
return { kept, refused };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Run `check()` on the edited source, as `compile` does, and place each
|
|
88
|
+
* diagnostic on the node its step emitted in `editedFlow`, so
|
|
89
|
+
* {@link classifyDrafts} can sort it. A diagnostic with no step, or a step
|
|
90
|
+
* with no root node, gets nodeId `''`, which is always refused.
|
|
91
|
+
*/
|
|
92
|
+
export function checkEdited(built, editedFlow, opts) {
|
|
93
|
+
const byStep = new Map();
|
|
94
|
+
for (const n of editedFlow?.nodes ?? []) {
|
|
95
|
+
const label = n?.display?.label;
|
|
96
|
+
if (typeof label === 'string' && !byStep.has(label))
|
|
97
|
+
byStep.set(label, n.id);
|
|
98
|
+
}
|
|
99
|
+
const ids = new Set((editedFlow?.nodes ?? []).map((n) => n.id));
|
|
100
|
+
return check(built, opts).map((d) => ({
|
|
101
|
+
...d,
|
|
102
|
+
nodeId: d.step === undefined ? '' : byStep.get(d.step) ?? (ids.has(d.step) ? d.step : ''),
|
|
103
|
+
}));
|
|
104
|
+
}
|
package/dist/merge.d.ts
CHANGED
|
@@ -152,4 +152,13 @@ export interface MergeOptions {
|
|
|
152
152
|
*/
|
|
153
153
|
strict?: boolean;
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* The ids of the root nodes {@link mergeFlow} takes from `edited` rather than
|
|
157
|
+
* restoring from `original`: every node that is new, or that differs from its
|
|
158
|
+
* `baseline` counterpart. With no baseline, that is every node.
|
|
159
|
+
*
|
|
160
|
+
* For instance, with a baseline equal to `edited` and every node present in
|
|
161
|
+
* `original`, the answer is `[]` — a no-edit merge keeps them all.
|
|
162
|
+
*/
|
|
163
|
+
export declare function nodesTakenFromEdited(original: AnyFlow, edited: AnyFlow, baseline?: AnyFlow): string[];
|
|
155
164
|
export declare function mergeFlow(original: AnyFlow, edited: AnyFlow, options?: MergeOptions): AnyFlow;
|
package/dist/merge.js
CHANGED
|
@@ -97,6 +97,27 @@ function nodeUnchanged(a, b) {
|
|
|
97
97
|
return false;
|
|
98
98
|
return JSON.stringify(a.inputs ?? {}) === JSON.stringify(b.inputs ?? {});
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* The ids of the root nodes {@link mergeFlow} takes from `edited` rather than
|
|
102
|
+
* restoring from `original`: every node that is new, or that differs from its
|
|
103
|
+
* `baseline` counterpart. With no baseline, that is every node.
|
|
104
|
+
*
|
|
105
|
+
* For instance, with a baseline equal to `edited` and every node present in
|
|
106
|
+
* `original`, the answer is `[]` — a no-edit merge keeps them all.
|
|
107
|
+
*/
|
|
108
|
+
export function nodesTakenFromEdited(original, edited, baseline) {
|
|
109
|
+
const origNodes = byId(original.nodes);
|
|
110
|
+
const baseNodes = byId(baseline?.nodes);
|
|
111
|
+
return (edited.nodes ?? []).filter((e) => !isKept(e, origNodes, baseNodes, baseline)).map((e) => e.id);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Whether merge restores `e`'s ORIGINAL node instead of taking `e`: there is a
|
|
115
|
+
* baseline, the original has the node, and `e` equals its baseline counterpart.
|
|
116
|
+
* {@link mergeFlow}'s node loop and {@link nodesTakenFromEdited} both ask this.
|
|
117
|
+
*/
|
|
118
|
+
function isKept(e, origNodes, baseNodes, baseline) {
|
|
119
|
+
return !!baseline && origNodes.has(e.id) && nodeUnchanged(e, baseNodes.get(e.id));
|
|
120
|
+
}
|
|
100
121
|
/**
|
|
101
122
|
* Give an edited node the original's position, by id.
|
|
102
123
|
*
|
|
@@ -132,7 +153,7 @@ export function mergeFlow(original, edited, options = {}) {
|
|
|
132
153
|
const finalNodes = [];
|
|
133
154
|
for (const e of edited.nodes ?? []) {
|
|
134
155
|
const orig = origNodes.get(e.id);
|
|
135
|
-
if (options.baseline && orig
|
|
156
|
+
if (isKept(e, origNodes, baseNodes, options.baseline) && orig) {
|
|
136
157
|
// Untouched by the user (equal to the faithful baseline) → keep the
|
|
137
158
|
// original node exactly, placeholder-safe.
|
|
138
159
|
finalNodes.push(jsonClone(orig));
|
package/dist/serialize.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DefinitionReference } from './core/node-classes.js';
|
|
2
2
|
import type { BuiltFlow } from './flow-sdk.js';
|
|
3
3
|
import type { Library } from './library.js';
|
|
4
|
+
import { type BindingRefusalCode } from './core/binding-messages.js';
|
|
4
5
|
import type { Bindings } from './bindings.js';
|
|
5
6
|
import { connectorInputErrors as coreConnectorInputErrors } from './core/connector-inputs.js';
|
|
6
7
|
/**
|
|
@@ -47,6 +48,18 @@ export interface SerializeWarning {
|
|
|
47
48
|
code: string;
|
|
48
49
|
message: string;
|
|
49
50
|
}
|
|
51
|
+
/** A required connector input left empty — the one draft code with no error class of its own. */
|
|
52
|
+
export declare const CONNECTOR_INPUT_MISSING = "CONNECTOR_INPUT_MISSING";
|
|
53
|
+
/** The refusals a draft-tolerant compile reports instead of throwing. */
|
|
54
|
+
export type DraftCode = BindingRefusalCode | typeof CONNECTOR_INPUT_MISSING | 'CONNECTOR_OBJECT_REQUIRED';
|
|
55
|
+
/** A refusal a draft-tolerant compile reported instead of throwing (see {@link SerializeOpts.draft}). */
|
|
56
|
+
export interface DraftFinding {
|
|
57
|
+
/** The emitted node's id. */
|
|
58
|
+
nodeId: string;
|
|
59
|
+
code: DraftCode;
|
|
60
|
+
/** The sentence the strict compile refuses with. */
|
|
61
|
+
message: string;
|
|
62
|
+
}
|
|
50
63
|
export interface SerializeOpts {
|
|
51
64
|
library?: Library;
|
|
52
65
|
bindings?: Bindings;
|
|
@@ -56,6 +69,19 @@ export interface SerializeOpts {
|
|
|
56
69
|
* hears nothing, which keeps `serialize()` silent in tests and libraries.
|
|
57
70
|
*/
|
|
58
71
|
warn?: (warning: SerializeWarning) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Draft tolerance, for a compile whose output only `merge` reads (#1134).
|
|
74
|
+
*
|
|
75
|
+
* A canvas flow often holds DRAFT nodes — a connector with no connection, an
|
|
76
|
+
* empty required input, a generic operation with no object — which the
|
|
77
|
+
* canvas saves and compile refuses. In a brownfield merge an untouched node
|
|
78
|
+
* is restored from the original anyway, so refusing it only blocks the edit.
|
|
79
|
+
* When this callback is given, those three refusals are reported here, with
|
|
80
|
+
* the node's id, and the node is emitted with what it has; `merge` then
|
|
81
|
+
* refuses any such node it would take from the edited flow. Absent (every
|
|
82
|
+
* ordinary compile), behaviour is unchanged.
|
|
83
|
+
*/
|
|
84
|
+
draft?: (finding: DraftFinding) => void;
|
|
59
85
|
/**
|
|
60
86
|
* Filled in by `serialize` with the extra files the flow needs beside the
|
|
61
87
|
* `.flow`, relative to it: one `<source>/agent.json` per inline agent, plus a
|
package/dist/serialize.js
CHANGED
|
@@ -20,6 +20,7 @@ import { stepExits } from './flow-step-exits.js';
|
|
|
20
20
|
import { resolveVersionCarried } from './core/nodes/versioned.js';
|
|
21
21
|
import { indexCarriedDefinitions } from './core/carried-definitions.js';
|
|
22
22
|
import { FORM_FIELD_JSON_TYPE } from './core/form-field-type.js';
|
|
23
|
+
import { ConnectorResolveError } from './core/library.js';
|
|
23
24
|
import { connectionConnectors } from './bindings-v2.js';
|
|
24
25
|
import { BindingRefusal, GUID_RE, bindingLabelRefusal, bindingMissingMessage, missingBindingLabels } from './core/binding-messages.js';
|
|
25
26
|
import { ByoaRefusal, byoaUnverified } from './core/byoa.js';
|
|
@@ -2999,6 +3000,8 @@ function scheduleInputs(inputs, nodeId) {
|
|
|
2999
3000
|
function outcomeId(name, index) {
|
|
3000
3001
|
return outcomeSlug(name, index);
|
|
3001
3002
|
}
|
|
3003
|
+
/** A required connector input left empty — the one draft code with no error class of its own. */
|
|
3004
|
+
export const CONNECTOR_INPUT_MISSING = 'CONNECTOR_INPUT_MISSING';
|
|
3002
3005
|
/**
|
|
3003
3006
|
* `serialize`, plus the extra files the flow needs on disk beside it.
|
|
3004
3007
|
*
|
|
@@ -4958,20 +4961,67 @@ export function serialize(built, opts = {}) {
|
|
|
4958
4961
|
if (spec.kind === 'connector') {
|
|
4959
4962
|
if (!opts.library)
|
|
4960
4963
|
throw new Error(`connector "${step.name}" needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`);
|
|
4961
|
-
|
|
4964
|
+
let rc;
|
|
4965
|
+
try {
|
|
4966
|
+
rc = opts.library.resolve(spec.key, spec.action, spec.version, spec.object);
|
|
4967
|
+
}
|
|
4968
|
+
catch (error) {
|
|
4969
|
+
if (!opts.draft || !(error instanceof ConnectorResolveError) || error.code !== 'CONNECTOR_OBJECT_REQUIRED')
|
|
4970
|
+
throw error;
|
|
4971
|
+
// A generic operation saved before its object was picked has no
|
|
4972
|
+
// definition to emit against. Draft tolerance emits a stable stand-in
|
|
4973
|
+
// under the node's id; merge restores the original when it is untouched
|
|
4974
|
+
// and refuses it otherwise.
|
|
4975
|
+
const id = stepUid(scope, step.name);
|
|
4976
|
+
opts.draft({ nodeId: id, code: error.code, message: error.message });
|
|
4977
|
+
// It leaves on the port every connector action leaves on, so the
|
|
4978
|
+
// no-edit round trip rewires nothing. Its inputs carry what the author
|
|
4979
|
+
// wrote, so an edit to the node (an input, a connection) shows up as a
|
|
4980
|
+
// change and merge refuses it instead of restoring the original.
|
|
4981
|
+
const type = `uipath.connector.${spec.key}.${spec.action}`;
|
|
4982
|
+
scope.nodes.push({
|
|
4983
|
+
id, type, typeVersion: spec.version ?? '1.0.0',
|
|
4984
|
+
...(parentId ? { parentId } : {}), display: { label: step.name },
|
|
4985
|
+
inputs: {
|
|
4986
|
+
draft: true,
|
|
4987
|
+
spec: renderDeep(spec.inputs ?? {}, scope.rename),
|
|
4988
|
+
...(spec.connection !== undefined ? { connection: spec.connection } : {}),
|
|
4989
|
+
...(spec.folder !== undefined ? { folder: spec.folder } : {}),
|
|
4990
|
+
},
|
|
4991
|
+
});
|
|
4992
|
+
for (const t of tails)
|
|
4993
|
+
addEdge(scope, t, id);
|
|
4994
|
+
return [{ node: id, port: successPort(type) }];
|
|
4995
|
+
}
|
|
4962
4996
|
const id = stepUid(scope, step.name);
|
|
4963
4997
|
const node = makeNode(rc.nodeType, id, step.name, { parentId, requestedVersion: step.options?.version, def: rc.v1def });
|
|
4964
4998
|
declareListOutputType(node, rc);
|
|
4965
|
-
|
|
4999
|
+
// A draft that names no connection gets no binding rows either: there
|
|
5000
|
+
// is nothing to bind, and rows with an empty resourceKey would leak into
|
|
5001
|
+
// the merged file through merge's bindings union.
|
|
5002
|
+
let unbound = false;
|
|
5003
|
+
try {
|
|
5004
|
+
requireBindingPair(`connector "${step.name}"`, spec, { key: spec.key, action: spec.action });
|
|
5005
|
+
}
|
|
5006
|
+
catch (error) {
|
|
5007
|
+
if (!opts.draft || !(error instanceof BindingRefusal))
|
|
5008
|
+
throw error;
|
|
5009
|
+
opts.draft({ nodeId: id, code: error.code, message: error.message });
|
|
5010
|
+
unbound = true;
|
|
5011
|
+
}
|
|
4966
5012
|
const conn = resolveBinding(opts, spec.connection, 'connection', `connector "${step.name}"`, { key: spec.key, action: spec.action });
|
|
4967
5013
|
const folder = resolveBinding(opts, spec.folder, 'folder', `connector "${step.name}"`, { key: spec.key, action: spec.action });
|
|
4968
5014
|
// The platform reaches the connection through the flow's `bindings[]`,
|
|
4969
5015
|
// never through `inputs.detail` — without this pair the run faults with
|
|
4970
5016
|
// `[102010]` (board G-19; see connectionResourceBindings).
|
|
4971
5017
|
const { entries: connBindings } = connectionResourceBindings({ def: rc.v1def, connectorKey: rc.connectorKey, connectionId: conn, folderKey: folder }, resourceBindings);
|
|
4972
|
-
|
|
4973
|
-
|
|
5018
|
+
if (!unbound)
|
|
5019
|
+
for (const b of connBindings)
|
|
5020
|
+
resourceBindings.set(b.id, b);
|
|
4974
5021
|
const connectorInputs = buildConnectorInputs(rc, spec.inputs, {
|
|
5022
|
+
...(opts.draft
|
|
5023
|
+
? { onMissingRequired: (m) => opts.draft({ nodeId: id, code: CONNECTOR_INPUT_MISSING, message: `connector ${rc.nodeType}: ${m}` }) }
|
|
5024
|
+
: {}),
|
|
4975
5025
|
dialect: FLOW_EXPR_DIALECT,
|
|
4976
5026
|
render: (value) => renderDeep(value, scope.rename),
|
|
4977
5027
|
resolutions: opts.library.lookupResolutions ?? {},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.28.0",
|
|
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": "2129535cf7f13d90345db00a5d72d096c69fce04"
|
|
96
96
|
}
|