@uipath/maestro-builder-sdk 6.9.0 → 6.9.2
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.d.ts +7 -5
- package/dist/check.js +46 -9
- package/dist/cli/commands/registry.js +2 -2
- package/dist/compile-cli.js +21 -4
- package/dist/core/binding-messages.d.ts +60 -1
- package/dist/core/binding-messages.js +82 -7
- package/dist/registry/prepare/messages.d.ts +21 -0
- package/dist/registry/prepare/messages.js +62 -0
- package/dist/registry/prepare/pipeline.d.ts +5 -2
- package/dist/registry/prepare/pipeline.js +119 -11
- package/dist/registry/prepare/source-import.d.ts +2 -0
- package/dist/registry/prepare/source-import.js +1 -1
- package/dist/registry/prepare/source-labels.d.ts +24 -0
- package/dist/registry/prepare/source-labels.js +346 -0
- package/dist/serialize.js +67 -32
- package/package.json +2 -2
package/dist/check.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type LookupResolutions, type LookupSpec } from './core/lookups.js';
|
|
2
2
|
import type { SchemaReadiness } from './core/library.js';
|
|
3
3
|
import type { BuiltFlow } from './flow-sdk.js';
|
|
4
|
+
import { type BindingsView } from './core/binding-messages.js';
|
|
4
5
|
export interface Diagnostic {
|
|
5
6
|
level: 'error' | 'warning';
|
|
6
7
|
code: string;
|
|
@@ -140,12 +141,13 @@ export interface CheckOpts {
|
|
|
140
141
|
object?: string;
|
|
141
142
|
}, inputs: Record<string, unknown>) => string | undefined;
|
|
142
143
|
/**
|
|
143
|
-
* The project's `bindings.json`, when there is one — enough of it to detect
|
|
144
|
-
* entry that answers with its own name
|
|
144
|
+
* The project's `bindings.json`, when there is one — enough of it to detect the
|
|
145
|
+
* two labels `compile` refuses: an entry that answers with its own name
|
|
146
|
+
* (BINDING_SELF_NAME), and a label a populated file does not declare
|
|
147
|
+
* (BINDING_UNDECLARED). `declared` and `entry` are optional so a caller holding
|
|
148
|
+
* only `resolve` still gets the first rule; the `Bindings` class has all three.
|
|
145
149
|
*/
|
|
146
|
-
bindings?:
|
|
147
|
-
resolve(name?: string): string | undefined;
|
|
148
|
-
};
|
|
150
|
+
bindings?: BindingsView;
|
|
149
151
|
}
|
|
150
152
|
export declare function check(built: BuiltFlow, opts?: CheckOpts): Diagnostic[];
|
|
151
153
|
/**
|
package/dist/check.js
CHANGED
|
@@ -10,7 +10,7 @@ import { FlowNode } from './core/node-classes.js';
|
|
|
10
10
|
import { isLookupToken, resolvedValue, unresolvedLookupMessage, } from './core/lookups.js';
|
|
11
11
|
import { EVENT_FILTER_OPERATORS, readEventFilter, eventFilterProblem } from './event-filters.js';
|
|
12
12
|
import { extractRefs } from './core/expr-check.js';
|
|
13
|
-
import { bindingSelfNameMessage } from './core/binding-messages.js';
|
|
13
|
+
import { bindingSelfNameMessage, bindingUndeclaredMessage, undeclaredBinding } from './core/binding-messages.js';
|
|
14
14
|
import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from './core/hitl-routing.js';
|
|
15
15
|
import { declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
|
|
16
16
|
import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
|
|
@@ -357,6 +357,14 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
357
357
|
checkTransform(s.name, s.spec.inputs, diags, typeOf, triggerId);
|
|
358
358
|
if (s.spec.kind === 'http') {
|
|
359
359
|
checkHttp(s.name, s.spec.inputs, diags);
|
|
360
|
+
// Only a complete pair: an empty half is HTTP_MANAGED_BINDING_INCOMPLETE, which
|
|
361
|
+
// serialize refuses before it resolves either name.
|
|
362
|
+
const pair = s.spec.inputs;
|
|
363
|
+
const filled = (v) => typeof v === 'string' && v.trim() !== '';
|
|
364
|
+
if (pair?.managed && filled(pair.connection) && filled(pair.folder)) {
|
|
365
|
+
const httpStep = s.name;
|
|
366
|
+
checkBindingLabels(`managed http "${httpStep}"`, pair, opts, (code, message) => diags.push({ level: 'error', code, step: httpStep, message }));
|
|
367
|
+
}
|
|
360
368
|
const seen = new Set();
|
|
361
369
|
for (const b of s.spec.inputs.branches ?? []) {
|
|
362
370
|
addExpr(b.condition.js, s.name);
|
|
@@ -530,6 +538,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
530
538
|
});
|
|
531
539
|
checkEventSubscription(s.spec.subscription, `waitForEvent "${stepName}"`, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts));
|
|
532
540
|
checkEventResolution(s.spec.subscription, `waitForEvent "${stepName}"`, 'event', opts, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts));
|
|
541
|
+
checkBindingLabels(`waitForEvent "${stepName}"`, s.spec.subscription, opts, push('error'), eventTarget(s.spec.subscription));
|
|
533
542
|
}
|
|
534
543
|
if (s.spec.kind === 'subflow') {
|
|
535
544
|
// Only a real child goes in the map: a missing one is reported by
|
|
@@ -1470,14 +1479,41 @@ function checkConnectorContract(step, spec, opts, diags) {
|
|
|
1470
1479
|
if (inputErr) {
|
|
1471
1480
|
diags.push({ level: 'error', code: 'CONNECTOR_INPUT', step, message: `Connector step "${step}": ${inputErr}` });
|
|
1472
1481
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1482
|
+
checkBindingLabels(`Connector step "${step}"`, spec, opts, (code, message) => diags.push({ level: 'error', code, step, message }), { key: spec.key, action: spec.action });
|
|
1483
|
+
}
|
|
1484
|
+
/** What `registry prepare` takes for an event subscription: its connector key and curated event name. */
|
|
1485
|
+
function eventTarget(sub) {
|
|
1486
|
+
const { connector, event } = (sub ?? {});
|
|
1487
|
+
return typeof connector === 'string' && connector !== ''
|
|
1488
|
+
? { key: connector, ...(typeof event === 'string' && event !== '' ? { action: event } : {}) }
|
|
1489
|
+
: undefined;
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* The binding-label rules `serialize`'s `resolveBinding` enforces, for one
|
|
1493
|
+
* `{ connection, folder }` pair — reported at every site that resolves one
|
|
1494
|
+
* (connector step, event trigger and `.entryPoint` onEvent, waitForEvent, managed
|
|
1495
|
+
* HTTP), with the sentence compile throws. Without this at the event and HTTP
|
|
1496
|
+
* sites `check` printed `✓ no issues` for a source `compile` then refused.
|
|
1497
|
+
*
|
|
1498
|
+
* Silent without a bindings.json, and for a label that resolves to anything
|
|
1499
|
+
* other than its own name (stub values are compile's CONNECTION_STUB warning).
|
|
1500
|
+
*/
|
|
1501
|
+
function checkBindingLabels(where, labels, opts, report, target) {
|
|
1502
|
+
const bindings = opts.bindings;
|
|
1503
|
+
if (!bindings || !labels)
|
|
1504
|
+
return;
|
|
1505
|
+
for (const attribute of ['connection', 'folder']) {
|
|
1506
|
+
const name = labels[attribute];
|
|
1507
|
+
if (typeof name !== 'string')
|
|
1508
|
+
continue;
|
|
1509
|
+
const resolved = bindings.resolve(name);
|
|
1510
|
+
if (resolved === name) {
|
|
1511
|
+
report('BINDING_SELF_NAME', bindingSelfNameMessage(where, name, attribute, target));
|
|
1512
|
+
}
|
|
1513
|
+
else if (resolved === undefined) {
|
|
1514
|
+
const declared = undeclaredBinding(bindings, name);
|
|
1515
|
+
if (declared) {
|
|
1516
|
+
report('BINDING_UNDECLARED', bindingUndeclaredMessage(where, name, attribute, declared, target));
|
|
1481
1517
|
}
|
|
1482
1518
|
}
|
|
1483
1519
|
}
|
|
@@ -2278,6 +2314,7 @@ function checkTrigger(spec, diags, opts = {}) {
|
|
|
2278
2314
|
const sub = spec.subscription;
|
|
2279
2315
|
checkEventSubscription(sub, 'trigger', at, push('warning'), eventParamsFor(sub, opts));
|
|
2280
2316
|
checkEventResolution(sub, 'trigger', 'trigger', opts, at, push('warning'), eventParamsFor(sub, opts));
|
|
2317
|
+
checkBindingLabels('event trigger', sub, opts, at, eventTarget(sub));
|
|
2281
2318
|
return;
|
|
2282
2319
|
}
|
|
2283
2320
|
// manual() is the explicit spelling of the default — nothing to validate.
|
|
@@ -130,8 +130,8 @@ export function registerRegistryCommands(program) {
|
|
|
130
130
|
.option('--connection-id <id>', 'connection to read the schema through')
|
|
131
131
|
.option('--connection <name>', 'pick the connection by name when several match')
|
|
132
132
|
.option('--bindings <file>', 'bindings file to update (default: ./bindings.json)')
|
|
133
|
-
.option('--bind-connection <name>', 'symbolic connection name (default: the connector suffix)')
|
|
134
|
-
.option('--bind-folder <name>', 'symbolic folder name (default: shared)')
|
|
133
|
+
.option('--bind-connection <name>', 'symbolic connection name (default: the one label the source uses for this connector, else the connector suffix)')
|
|
134
|
+
.option('--bind-folder <name>', 'symbolic folder name (default: the one folder label the source uses, else shared)')
|
|
135
135
|
// A SEPARATE key, not commander's negation of `--bindings`: sharing one key
|
|
136
136
|
// made the result order-dependent, so `--no-bindings --bindings b.json` and
|
|
137
137
|
// the reverse disagreed. argparse had two independent options.
|
package/dist/compile-cli.js
CHANGED
|
@@ -27,6 +27,7 @@ import { Bindings } from './bindings.js';
|
|
|
27
27
|
import { runWhenInvokedDirectly } from './cli-run.js';
|
|
28
28
|
import { connectionResources, mergeBindingsV2, readBindingsV2 } from './bindings-v2.js';
|
|
29
29
|
import { ensureTypeScriptRuntime } from './node-runtime.js';
|
|
30
|
+
import { BindingRefusal } from './core/binding-messages.js';
|
|
30
31
|
function opt(argv, name) {
|
|
31
32
|
const i = argv.indexOf(name);
|
|
32
33
|
return i >= 0 ? argv[i + 1] : undefined;
|
|
@@ -106,11 +107,27 @@ export async function run(argv) {
|
|
|
106
107
|
}
|
|
107
108
|
// The serializer's own observations — a connection or folder value that can
|
|
108
109
|
// never be a tenant resource key — printed in the same `⚠ [CODE]` shape as
|
|
109
|
-
// check()'s
|
|
110
|
+
// check()'s. Warnings are never fatal: the .flow is still written, exit stays 0.
|
|
111
|
+
// A binding LABEL the serializer refuses (its own name as the resourceKey, or a
|
|
112
|
+
// label a populated bindings.json does not declare) is an error, not a
|
|
113
|
+
// warning: the check pass above reports it first, and this is the same refusal
|
|
114
|
+
// in the same `✗ [CODE]` shape when that pass was skipped (`--no-check`, or an
|
|
115
|
+
// emit-only project). Nothing is written.
|
|
110
116
|
const warnings = [];
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
let emitted;
|
|
118
|
+
try {
|
|
119
|
+
emitted = serializeWithSidecars(built, { library, bindings, warn: (w) => warnings.push(w) });
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (!(error instanceof BindingRefusal))
|
|
123
|
+
throw error;
|
|
124
|
+
for (const w of warnings)
|
|
125
|
+
console.error(` ⚠ [${w.code}] ${w.message}`);
|
|
126
|
+
console.error(` ✗ [${error.code}] ${error.message}`);
|
|
127
|
+
console.error('compile: 1 error(s); not emitting.');
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
const { flow: flowJson, sidecars } = emitted;
|
|
114
131
|
for (const w of warnings)
|
|
115
132
|
console.error(` ⚠ [${w.code}] ${w.message}`);
|
|
116
133
|
const out = opt(argv, '-o') ?? `${built.name || basename(file).replace(/\.flow\.ts$|\.ts$/, '')}.flow`;
|
|
@@ -1 +1,60 @@
|
|
|
1
|
-
|
|
1
|
+
/** A tenant resource key — an Integration Service connection id or an Orchestrator folder key — is a GUID. */
|
|
2
|
+
export declare const GUID_RE: RegExp;
|
|
3
|
+
/** The diagnostic codes a label refusal carries, in `check` and on the error `serialize` throws. */
|
|
4
|
+
export type BindingRefusalCode = 'BINDING_SELF_NAME' | 'BINDING_UNDECLARED';
|
|
5
|
+
/**
|
|
6
|
+
* What `serialize` throws for a label it refuses. It is an ordinary `Error` whose
|
|
7
|
+
* message is the sentence `check` prints; the `code` lets `compile --no-check`
|
|
8
|
+
* print it in the same `✗ [CODE]` shape as the check pass instead of a bare line.
|
|
9
|
+
*/
|
|
10
|
+
export declare class BindingRefusal extends Error {
|
|
11
|
+
readonly code: BindingRefusalCode;
|
|
12
|
+
constructor(code: BindingRefusalCode, message: string);
|
|
13
|
+
}
|
|
14
|
+
/** The part of a bindings.json the label rules read — `core/bindings.ts` `Bindings` has all three. */
|
|
15
|
+
export interface BindingsView {
|
|
16
|
+
resolve(name?: string): string | undefined;
|
|
17
|
+
declared?(): string[];
|
|
18
|
+
entry?(name?: string): unknown;
|
|
19
|
+
}
|
|
20
|
+
export declare function bindingSelfNameMessage(where: string, name: string, attribute: 'connection' | 'folder', target?: PrepareTarget): string;
|
|
21
|
+
/**
|
|
22
|
+
* The names a populated bindings.json declares when it does NOT declare `name`,
|
|
23
|
+
* else `undefined`.
|
|
24
|
+
*
|
|
25
|
+
* `undefined` — nothing to refuse — when no bindings.json is loaded, when it
|
|
26
|
+
* declares no entries (offline authoring: the symbolic name is the only honest
|
|
27
|
+
* placeholder there), when `name` is itself a GUID (an author may write the
|
|
28
|
+
* resource key directly), or when any entry answers to `name`. "Answers" is the
|
|
29
|
+
* union of `entry()`, `resolve()` and `declared()`, so an entry that is declared
|
|
30
|
+
* but carries no `resourceKey` is NOT refused here — it stays the
|
|
31
|
+
* `CONNECTION_STUB` warning, which is the route a simulated or validate-only task
|
|
32
|
+
* with nothing real to bind is meant to take.
|
|
33
|
+
*/
|
|
34
|
+
export declare function undeclaredBinding(bindings: BindingsView | undefined, name: string): string[] | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* The connector and operation a label belongs to, when the call site knows them:
|
|
37
|
+
* a connector step's `key` + `action`, an event's `connector` + `event`. With it
|
|
38
|
+
* the remedy is a command the author can paste; without it (managed HTTP) the
|
|
39
|
+
* command keeps its `<connector-key> <action>` placeholders.
|
|
40
|
+
*/
|
|
41
|
+
export interface PrepareTarget {
|
|
42
|
+
key: string;
|
|
43
|
+
action?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Why compile refuses `name`, and how to bind it.
|
|
47
|
+
*
|
|
48
|
+
* It names NO declared label as the one to use. bindings.json rows carry a label,
|
|
49
|
+
* a resource key and a kind (`ConnectionId` / `FolderKey`), never the connector
|
|
50
|
+
* they were bound for, so compile cannot tell which declared label — if any — is
|
|
51
|
+
* this connector's. Suggesting one sent a Data Service step to `connection:
|
|
52
|
+
* 'slack'` (the only `ConnectionId` row), and following that compiled the Slack
|
|
53
|
+
* connection id into the Data Service node with no warning at all: a real GUID of
|
|
54
|
+
* the wrong connector, which nothing downstream detects. The partial-prepare shape
|
|
55
|
+
* (ipe-drive-to-slack 09-23: `slack` bound, `drive` not yet) is exactly where
|
|
56
|
+
* that advice was on offer. So the message lists what is declared, says that a
|
|
57
|
+
* row is only reusable if it was bound for THIS connector, and gives the
|
|
58
|
+
* `prepare` command for this step's own connector and operation.
|
|
59
|
+
*/
|
|
60
|
+
export declare function bindingUndeclaredMessage(where: string, name: string, attribute: 'connection' | 'folder', declared: readonly string[], target?: PrepareTarget): string;
|
|
@@ -1,16 +1,91 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
* `serialize` (which throws
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 2026-09-01, turn 22 → 23).
|
|
2
|
+
* The refusals for a connection/folder label `compile` cannot turn into a tenant
|
|
3
|
+
* resource key — shared by `serialize` (which throws them) and `check`
|
|
4
|
+
* (BINDING_SELF_NAME, BINDING_UNDECLARED), so the author reads the same sentence
|
|
5
|
+
* from the inner loop as from compile. Before this, `check` printed `0 error(s)`
|
|
6
|
+
* for a source `compile` then refused (teams-decision, 2026-09-01, turn 22 → 23).
|
|
7
7
|
*/
|
|
8
8
|
import { prepareCommand } from './cli-spelling.js';
|
|
9
|
-
|
|
9
|
+
/** A tenant resource key — an Integration Service connection id or an Orchestrator folder key — is a GUID. */
|
|
10
|
+
export const GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
/**
|
|
12
|
+
* What `serialize` throws for a label it refuses. It is an ordinary `Error` whose
|
|
13
|
+
* message is the sentence `check` prints; the `code` lets `compile --no-check`
|
|
14
|
+
* print it in the same `✗ [CODE]` shape as the check pass instead of a bare line.
|
|
15
|
+
*/
|
|
16
|
+
export class BindingRefusal extends Error {
|
|
17
|
+
code;
|
|
18
|
+
constructor(code, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'BindingRefusal';
|
|
21
|
+
this.code = code;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function bindingSelfNameMessage(where, name, attribute, target) {
|
|
10
25
|
return `${where}: binding "${name}" resolves to its own name, so the ${attribute} would compile to the `
|
|
11
26
|
+ `literal string "${name}" and the run would fault with "'Connection' has an invalid GUID value: `
|
|
12
|
-
+ `'${name}'". Set its "resourceKey" to the real id: \`${
|
|
27
|
+
+ `'${name}'". Set its "resourceKey" to the real id: \`${prepareFor(target)}\` `
|
|
13
28
|
+ `discovers the connection and writes both the connection and the folder entry into bindings.json; by hand, `
|
|
14
29
|
+ `read Id and FolderKey from \`uip is connections list --all-folders --output json\` (the default listing `
|
|
15
30
|
+ `shows only your own folder, so a connection shared from another folder is invisible without --all-folders).`;
|
|
16
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The names a populated bindings.json declares when it does NOT declare `name`,
|
|
34
|
+
* else `undefined`.
|
|
35
|
+
*
|
|
36
|
+
* `undefined` — nothing to refuse — when no bindings.json is loaded, when it
|
|
37
|
+
* declares no entries (offline authoring: the symbolic name is the only honest
|
|
38
|
+
* placeholder there), when `name` is itself a GUID (an author may write the
|
|
39
|
+
* resource key directly), or when any entry answers to `name`. "Answers" is the
|
|
40
|
+
* union of `entry()`, `resolve()` and `declared()`, so an entry that is declared
|
|
41
|
+
* but carries no `resourceKey` is NOT refused here — it stays the
|
|
42
|
+
* `CONNECTION_STUB` warning, which is the route a simulated or validate-only task
|
|
43
|
+
* with nothing real to bind is meant to take.
|
|
44
|
+
*/
|
|
45
|
+
export function undeclaredBinding(bindings, name) {
|
|
46
|
+
if (!bindings || typeof bindings.declared !== 'function')
|
|
47
|
+
return undefined;
|
|
48
|
+
const declared = bindings.declared();
|
|
49
|
+
if (declared.length === 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (GUID_RE.test(name))
|
|
52
|
+
return undefined;
|
|
53
|
+
if (declared.includes(name))
|
|
54
|
+
return undefined;
|
|
55
|
+
if (typeof bindings.entry === 'function' && bindings.entry(name) !== undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (bindings.resolve(name) !== undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
return declared;
|
|
60
|
+
}
|
|
61
|
+
function prepareFor(target) {
|
|
62
|
+
return prepareCommand(target?.key ?? '<connector-key>', target?.action ?? '<action>');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Why compile refuses `name`, and how to bind it.
|
|
66
|
+
*
|
|
67
|
+
* It names NO declared label as the one to use. bindings.json rows carry a label,
|
|
68
|
+
* a resource key and a kind (`ConnectionId` / `FolderKey`), never the connector
|
|
69
|
+
* they were bound for, so compile cannot tell which declared label — if any — is
|
|
70
|
+
* this connector's. Suggesting one sent a Data Service step to `connection:
|
|
71
|
+
* 'slack'` (the only `ConnectionId` row), and following that compiled the Slack
|
|
72
|
+
* connection id into the Data Service node with no warning at all: a real GUID of
|
|
73
|
+
* the wrong connector, which nothing downstream detects. The partial-prepare shape
|
|
74
|
+
* (ipe-drive-to-slack 09-23: `slack` bound, `drive` not yet) is exactly where
|
|
75
|
+
* that advice was on offer. So the message lists what is declared, says that a
|
|
76
|
+
* row is only reusable if it was bound for THIS connector, and gives the
|
|
77
|
+
* `prepare` command for this step's own connector and operation.
|
|
78
|
+
*/
|
|
79
|
+
export function bindingUndeclaredMessage(where, name, attribute, declared, target) {
|
|
80
|
+
const key = attribute === 'connection' ? 'connection id' : 'folder key';
|
|
81
|
+
const flag = attribute === 'connection' ? '--bind-connection' : '--bind-folder';
|
|
82
|
+
return `${where}: ${attribute} "${name}" is not declared in bindings.json (it declares: ${declared.join(', ')}), `
|
|
83
|
+
+ `so the .flow would carry "${name}" as its ${key} and the run would fault with `
|
|
84
|
+
+ `"'Connection' has an invalid GUID value: '${name}'". Bind this one: `
|
|
85
|
+
+ `\`${prepareFor(target)} ${flag} ${name}\` (several candidates: add \`--connection-id <id>\` from the list it prints). `
|
|
86
|
+
+ `Switch to a declared label only if that row was bound for this same connector: bindings.json does not record `
|
|
87
|
+
+ `which connector a row belongs to, and another connector's ${key} compiles without any warning and faults only at run time. `
|
|
88
|
+
+ `A connector with nothing real to bind yet (a simulated or validate-only task, or one not prepared yet) `
|
|
89
|
+
+ `declares the label with a placeholder resourceKey instead, which compiles with a CONNECTION_STUB warning — `
|
|
90
|
+
+ `never an invented GUID.`;
|
|
91
|
+
}
|
|
@@ -44,6 +44,27 @@ export declare function exitText(error: unknown): string;
|
|
|
44
44
|
export declare function connectionLine(row: Record<string, unknown>, connectionId: string): string;
|
|
45
45
|
/** ` bindings: <symbolic name> -> <bindings.json>`, once per row this run changed. */
|
|
46
46
|
export declare function bindingsLine(symbolicName: string, bindingsPath: string): string;
|
|
47
|
+
/** ` (the label Demo.flow.ts uses)` — appended to a bindings line whose name came from the source. */
|
|
48
|
+
export declare function sourceLabelSuffix(files: readonly string[]): string;
|
|
49
|
+
/**
|
|
50
|
+
* The source uses several labels for this one connector, and one prepare binds one
|
|
51
|
+
* connection: refused before any tenant call or write, with the command for each.
|
|
52
|
+
*/
|
|
53
|
+
export declare function ambiguousLabelsMessage(attribute: "connection" | "folder", connectorKey: string, action: string, labels: ReadonlyMap<string, readonly string[]>, command: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* The source gives this connector's steps a connection label that steps of another
|
|
56
|
+
* connector use too. One label binds one connection id, so one of them would dial the
|
|
57
|
+
* other's connection — a mismatch compile cannot see, because the label IS declared.
|
|
58
|
+
*/
|
|
59
|
+
export declare function sharedLabelMessage(label: string, connectorKey: string, others: readonly string[], files: readonly string[]): string;
|
|
60
|
+
/** bindings.json already binds the label to a connection of ANOTHER connector: refused, the row is left as it is. */
|
|
61
|
+
export declare function labelTakenMessage(label: string, connectorKey: string, existingId: string, existingConnector: string, bindingsShown: string): string;
|
|
62
|
+
/** `--bind-connection`/`--bind-folder` names a label the source does not use for this connector. */
|
|
63
|
+
export declare function flagDiffersFromSourceLine(attribute: "connection" | "folder", flag: string, labels: readonly string[], files: readonly string[]): string;
|
|
64
|
+
/** One label was read and bound, but another call's could not be: the line names that call. */
|
|
65
|
+
export declare function partlyUnreadableLabelLine(attribute: "connection" | "folder", label: string, reasons: readonly string[]): string;
|
|
66
|
+
/** A label that could not be read statically: the default is bound, and the line says why. */
|
|
67
|
+
export declare function unreadableLabelLine(attribute: "connection" | "folder", fallback: string, reasons: readonly string[]): string;
|
|
47
68
|
/** `_missing_generic_schema_message`: a Generic action that expanded to nothing. */
|
|
48
69
|
export declare function missingGenericSchemaMessage(operation: string, objectName?: string | null): string;
|
|
49
70
|
/** One `registry get` probe that missed, as a refusal quotes it. */
|
|
@@ -62,6 +62,68 @@ export function connectionLine(row, connectionId) {
|
|
|
62
62
|
export function bindingsLine(symbolicName, bindingsPath) {
|
|
63
63
|
return ` bindings: ${symbolicName} -> ${bindingsPath}`;
|
|
64
64
|
}
|
|
65
|
+
/** ` (the label Demo.flow.ts uses)` — appended to a bindings line whose name came from the source. */
|
|
66
|
+
export function sourceLabelSuffix(files) {
|
|
67
|
+
return ` (the label ${files.join(", ")} ${files.length === 1 ? "uses" : "use"})`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The source uses several labels for this one connector, and one prepare binds one
|
|
71
|
+
* connection: refused before any tenant call or write, with the command for each.
|
|
72
|
+
*/
|
|
73
|
+
export function ambiguousLabelsMessage(attribute, connectorKey, action, labels, command) {
|
|
74
|
+
const flag = attribute === "connection" ? "--bind-connection" : "--bind-folder";
|
|
75
|
+
const files = [...new Set([...labels.values()].flat())];
|
|
76
|
+
const lines = [...labels.keys()].map((label) => ` ${command} ${connectorKey} ${action} ${flag} ${label} --connection-id <id>`);
|
|
77
|
+
return prefixed(`${files.join(", ")} ${files.length === 1 ? "uses" : "use"} ${labels.size} ${attribute} labels for ${connectorKey} `
|
|
78
|
+
+ `(${[...labels.keys()].join(", ")}), and one prepare binds one ${attribute === "connection" ? "connection" : "folder key"}. `
|
|
79
|
+
+ `Bind each label to the connection it means, one run per label:\n${lines.join("\n")}\n`
|
|
80
|
+
+ (files.length > 1
|
|
81
|
+
? `The labels come from different files; if only one of them is the flow you are preparing, `
|
|
82
|
+
+ `pass --source <file> to read just that one.\n`
|
|
83
|
+
: "")
|
|
84
|
+
+ "Nothing was written.");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The source gives this connector's steps a connection label that steps of another
|
|
88
|
+
* connector use too. One label binds one connection id, so one of them would dial the
|
|
89
|
+
* other's connection — a mismatch compile cannot see, because the label IS declared.
|
|
90
|
+
*/
|
|
91
|
+
export function sharedLabelMessage(label, connectorKey, others, files) {
|
|
92
|
+
return prefixed(`${files.join(", ")} ${files.length === 1 ? "uses" : "use"} connection label "${label}" for ${connectorKey} and for `
|
|
93
|
+
+ `${others.join(", ")}. One bindings.json label binds one connection, so one of those steps would run on the `
|
|
94
|
+
+ `other connector's connection. Give each connector its own label in the source (for example `
|
|
95
|
+
+ `connection: '${afterLastDash(connectorKey)}') and re-run, or pass --bind-connection <label> to choose the name `
|
|
96
|
+
+ "yourself. Nothing was written.");
|
|
97
|
+
}
|
|
98
|
+
/** bindings.json already binds the label to a connection of ANOTHER connector: refused, the row is left as it is. */
|
|
99
|
+
export function labelTakenMessage(label, connectorKey, existingId, existingConnector, bindingsShown) {
|
|
100
|
+
return prefixed(`${bindingsShown} already binds "${label}" to ${existingId}, a ${existingConnector} connection. Writing this `
|
|
101
|
+
+ `${connectorKey} connection under the same label would repoint every ${existingConnector} step that uses it. `
|
|
102
|
+
+ `Give the ${connectorKey} steps their own label in the source, or pass --bind-connection <label>. Nothing was written.`);
|
|
103
|
+
}
|
|
104
|
+
/** `--bind-connection`/`--bind-folder` names a label the source does not use for this connector. */
|
|
105
|
+
export function flagDiffersFromSourceLine(attribute, flag, labels, files) {
|
|
106
|
+
const option = attribute === "connection" ? "--bind-connection" : "--bind-folder";
|
|
107
|
+
return ` WARN: ${option} ${flag}, but ${files.join(", ")} ${files.length === 1 ? "uses" : "use"} ${attribute} `
|
|
108
|
+
+ `${labels.map((l) => `"${l}"`).join(", ")} for this connector; compile refuses a label bindings.json does not `
|
|
109
|
+
+ `declare (BINDING_UNDECLARED), so make the two match`;
|
|
110
|
+
}
|
|
111
|
+
/** One label was read and bound, but another call's could not be: the line names that call. */
|
|
112
|
+
export function partlyUnreadableLabelLine(attribute, label, reasons) {
|
|
113
|
+
return ` bindings: bound under "${label}", the ${attribute} label the source uses; could not read one more `
|
|
114
|
+
+ `(${reasons[0]}${reasons.length > 1 ? `, and ${reasons.length - 1} more` : ""}) — if that step is this `
|
|
115
|
+
+ `connector's under another label, compile refuses it (BINDING_UNDECLARED)`;
|
|
116
|
+
}
|
|
117
|
+
/** A label that could not be read statically: the default is bound, and the line says why. */
|
|
118
|
+
export function unreadableLabelLine(attribute, fallback, reasons) {
|
|
119
|
+
const option = attribute === "connection" ? "--bind-connection" : "--bind-folder";
|
|
120
|
+
return ` bindings: could not read the ${attribute} label the source uses (${reasons[0]}${reasons.length > 1 ? `, and ${reasons.length - 1} more` : ""}); `
|
|
121
|
+
+ `bound under "${fallback}" — pass ${option} <label> if the source uses another`;
|
|
122
|
+
}
|
|
123
|
+
function afterLastDash(key) {
|
|
124
|
+
const i = key.lastIndexOf("-");
|
|
125
|
+
return i >= 0 ? key.slice(i + 1) : key;
|
|
126
|
+
}
|
|
65
127
|
// ---------------------------------------------------------------------------
|
|
66
128
|
// Refusals of the action path
|
|
67
129
|
/** `_missing_generic_schema_message`: a Generic action that expanded to nothing. */
|
|
@@ -12,9 +12,12 @@ export interface PrepareOptions {
|
|
|
12
12
|
connection?: string;
|
|
13
13
|
/** `--bindings` (default `./bindings.json`). */
|
|
14
14
|
bindings?: string;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* `--bind-connection`. Default: the one `connection:` label the source already
|
|
17
|
+
* uses for this connector, else the connector's last dash-segment.
|
|
18
|
+
*/
|
|
16
19
|
bindConnection?: string;
|
|
17
|
-
/** `--bind-folder`
|
|
20
|
+
/** `--bind-folder`. Default: the one `folder:` label the source uses for this connector, else `shared`. */
|
|
18
21
|
bindFolder?: string;
|
|
19
22
|
noBindings?: boolean;
|
|
20
23
|
/** `--object`; mutually exclusive with `allObjects`. */
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
* into the lines verbatim, as `args.out` and `args.bindings` did — while the
|
|
25
25
|
* file system is reached through paths resolved against `cwd`.
|
|
26
26
|
*/
|
|
27
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
27
28
|
import { resolve } from "node:path";
|
|
29
|
+
import { GUID_RE } from "../../core/binding-messages.js";
|
|
28
30
|
import { PACKAGE_NAME } from "../../package-name.js";
|
|
29
31
|
import { afterLast } from "../compat/pystr.js";
|
|
30
32
|
import { extractCanonical, isHttpPassthrough, nodeOf } from "../library-build/canonical.js";
|
|
@@ -34,17 +36,18 @@ import { expandGenericForConnector, genericTemplatesByOp } from "../library-buil
|
|
|
34
36
|
import { planLibrary } from "../library-build/plan.js";
|
|
35
37
|
import { MemoryResponseCache } from "../uip/response-cache.js";
|
|
36
38
|
import { registryCatalog } from "./actions.js";
|
|
37
|
-
import { discoverConnection, lookupConnection, mergeBindings, restoreBindings, snapshotBindings, } from "./connections.js";
|
|
39
|
+
import { discoverConnection, listConnections, lookupConnection, mergeBindings, restoreBindings, snapshotBindings, } from "./connections.js";
|
|
38
40
|
import { prepareEvent } from "./event.js";
|
|
39
41
|
import { descriptorUsage } from "./handoff.js";
|
|
40
42
|
import { ResolutionsExhausted, allParentsResolved, resolvedParentValues, writeResolutions } from "./ledger.js";
|
|
41
|
-
import { PREPARE_COMMAND, PrepareRefusal, bindingsLine, candidateLabel, connectionLine, generatedModulesLine, genericActionNeedsObjectMessage, inputFieldNote, keptSchemaMessage, lookupVocabularyLines, missingGenericSchemaMessage, missingParentsNote, nextLine, noCanonicalEntryMessage, objectFlagsOnlyForGenericMessage, objectNoteLine, objectRecordedLine, parentFieldNamesMismatchMessage, parentOnlyMessage, prefixed, preparedSummaryLine, rejectedParentFieldsMessage, rerunAgainstLine, resolvedParentsNote, registryGetFailedMessage, tsNote, unresolvedOperationMessage, } from "./messages.js";
|
|
43
|
+
import { PREPARE_COMMAND, PrepareRefusal, ambiguousLabelsMessage, bindingsLine, candidateLabel, connectionLine, generatedModulesLine, genericActionNeedsObjectMessage, inputFieldNote, flagDiffersFromSourceLine, keptSchemaMessage, labelTakenMessage, lookupVocabularyLines, missingGenericSchemaMessage, missingParentsNote, nextLine, noCanonicalEntryMessage, objectFlagsOnlyForGenericMessage, objectNoteLine, objectRecordedLine, parentFieldNamesMismatchMessage, parentOnlyMessage, prefixed, preparedSummaryLine, rejectedParentFieldsMessage, rerunAgainstLine, resolvedParentsNote, registryGetFailedMessage, sharedLabelMessage, sourceLabelSuffix, tsNote, partlyUnreadableLabelLine, unreadableLabelLine, unresolvedOperationMessage, } from "./messages.js";
|
|
42
44
|
import { ObjectMatchRefusal, RESOLUTIONS_FILENAME, matchObject, recordObjectResolution } from "./objects.js";
|
|
43
45
|
import { countInputFields, keepPreparedSchema, lookupSpecsFromOverlay, mergeFilesInto, parentFields as overlayParentFields, schemaActionParents, } from "./overlay.js";
|
|
44
46
|
import { declaredParents, objectActionRequest, objectActionSpecs, parseFields } from "./parents.js";
|
|
45
47
|
import { regenerateDescriptors } from "./regenerate.js";
|
|
46
48
|
import { retryOtherConnections } from "./retry.js";
|
|
47
49
|
import { repointSource } from "./source-import.js";
|
|
50
|
+
import { sourceBindingLabels } from "./source-labels.js";
|
|
48
51
|
import { isPlainObject, pyGet, pyOr, pyPathJoin, pyPathlibStr, pyStrAny, pyTruthy } from "./support.js";
|
|
49
52
|
function memoryCaches() {
|
|
50
53
|
return { registry: new MemoryResponseCache(), is: new MemoryResponseCache(), connectors: new MemoryResponseCache() };
|
|
@@ -83,9 +86,111 @@ export async function matchCatalogObject(ctx, connectorKey, wanted, verb) {
|
|
|
83
86
|
}
|
|
84
87
|
return undefined;
|
|
85
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* The names to bind under, settled BEFORE any tenant call or write, so a refusal
|
|
91
|
+
* here leaves the tenant unasked and bindings.json as it was.
|
|
92
|
+
*
|
|
93
|
+
* Per attribute: the flag; else the ONE label the source already uses for this
|
|
94
|
+
* connector; else the old default (the key's last dash-segment, `shared`). The
|
|
95
|
+
* source is parsed, never run (source-labels.ts). Several labels for one
|
|
96
|
+
* connector are refused rather than picked — binding one would leave the others
|
|
97
|
+
* to compile's BINDING_UNDECLARED — and so is a connection label the source also
|
|
98
|
+
* gives another connector's steps, since one label binds one connection id. A
|
|
99
|
+
* label that cannot be read statically keeps the default and says why — unless
|
|
100
|
+
* one label WAS read, which is then bound, with a line naming the unread call.
|
|
101
|
+
*/
|
|
102
|
+
function chooseLabels(options, ports) {
|
|
103
|
+
const info = (line) => ports.reporter.info(line);
|
|
104
|
+
const warn = (line) => ports.reporter.warn(line);
|
|
105
|
+
const command = ports.prepareCommand ?? PREPARE_COMMAND;
|
|
106
|
+
const labels = sourceBindingLabels({
|
|
107
|
+
cwd: ports.cwd ?? process.cwd(),
|
|
108
|
+
out: options.out ?? "./connectors-local",
|
|
109
|
+
connectorKey: options.connectorKey,
|
|
110
|
+
source: options.source,
|
|
111
|
+
});
|
|
112
|
+
// `args.bind_connection or …`: an empty --bind-connection falls back, as Python's
|
|
113
|
+
// `or` did. --bind-folder was an argparse default, so an empty one STANDS.
|
|
114
|
+
const flags = {
|
|
115
|
+
connection: pyTruthy(options.bindConnection) ? options.bindConnection : undefined,
|
|
116
|
+
folder: options.bindFolder,
|
|
117
|
+
};
|
|
118
|
+
const defaults = { connection: afterLast(options.connectorKey, "-"), folder: "shared" };
|
|
119
|
+
const chosen = {};
|
|
120
|
+
for (const attribute of ["connection", "folder"]) {
|
|
121
|
+
const found = labels[attribute];
|
|
122
|
+
const files = [...new Set([...found.values()].flat())];
|
|
123
|
+
const flag = flags[attribute];
|
|
124
|
+
if (flag !== undefined) {
|
|
125
|
+
if (found.size > 0 && !found.has(flag))
|
|
126
|
+
warn(flagDiffersFromSourceLine(attribute, flag, [...found.keys()], files));
|
|
127
|
+
chosen[attribute] = flag;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (found.size > 1) {
|
|
131
|
+
throw new PrepareRefusal(ambiguousLabelsMessage(attribute, options.connectorKey, options.action, found, command), "BINDING_LABEL_AMBIGUOUS");
|
|
132
|
+
}
|
|
133
|
+
const unreadable = labels.unreadable[attribute];
|
|
134
|
+
if (found.size === 1) {
|
|
135
|
+
const [label] = found.keys();
|
|
136
|
+
const others = attribute === "connection" ? labels.otherConnectors.get(label) : undefined;
|
|
137
|
+
if (others && others.size > 0) {
|
|
138
|
+
throw new PrepareRefusal(sharedLabelMessage(label, options.connectorKey, [...others], files), "BINDING_LABEL_SHARED");
|
|
139
|
+
}
|
|
140
|
+
// One readable label wins over the default even when another call could not
|
|
141
|
+
// be read: the default is certain to miss the step that WAS read, while the
|
|
142
|
+
// unread call, if it is this connector's under another label, is compile's
|
|
143
|
+
// BINDING_UNDECLARED. The line says which call was not read.
|
|
144
|
+
if (unreadable.length > 0)
|
|
145
|
+
info(partlyUnreadableLabelLine(attribute, label, unreadable));
|
|
146
|
+
chosen[attribute] = label;
|
|
147
|
+
chosen[attribute === "connection" ? "connectionFrom" : "folderFrom"] = files;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (unreadable.length > 0) {
|
|
151
|
+
info(unreadableLabelLine(attribute, defaults[attribute], unreadable));
|
|
152
|
+
chosen[attribute] = defaults[attribute];
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
chosen[attribute] = defaults[attribute];
|
|
156
|
+
}
|
|
157
|
+
return chosen;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Refuse to overwrite a label that bindings.json already binds to ANOTHER
|
|
161
|
+
* connector's connection. Only for a label read from the source: that is the
|
|
162
|
+
* route that can collide by accident (two connectors named after one display
|
|
163
|
+
* name, `is-sandboxes`), and compile cannot see the result, because the label IS
|
|
164
|
+
* declared. The same connector's other connection under the same label is a
|
|
165
|
+
* reconfigure and is written as before; so is a stub or an unlisted id. The one
|
|
166
|
+
* extra listing happens only when the row exists with a different real id.
|
|
167
|
+
*/
|
|
168
|
+
async function refuseLabelOfAnotherConnector(client, connectorKey, label, connectionId, bindingsPath, bindingsShown) {
|
|
169
|
+
if (!existsSync(bindingsPath))
|
|
170
|
+
return;
|
|
171
|
+
let entries;
|
|
172
|
+
try {
|
|
173
|
+
entries = JSON.parse(readFileSync(bindingsPath, "utf8"))?.bindings;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return; // mergeBindings refuses an unreadable file with its own message
|
|
177
|
+
}
|
|
178
|
+
if (!Array.isArray(entries))
|
|
179
|
+
return;
|
|
180
|
+
const row = entries.find((e) => isPlainObject(e) && (e.name === label || e.id === label));
|
|
181
|
+
const existing = typeof row?.resourceKey === "string" ? row.resourceKey : undefined;
|
|
182
|
+
if (!existing || existing === connectionId || !GUID_RE.test(existing))
|
|
183
|
+
return;
|
|
184
|
+
const owner = (await listConnections(client, { allFolders: true })).find((r) => pyStrAny(r.Id) === existing);
|
|
185
|
+
const ownerKey = owner ? pyStrAny(owner.ConnectorKey) : "";
|
|
186
|
+
if (ownerKey && ownerKey !== connectorKey) {
|
|
187
|
+
throw new PrepareRefusal(labelTakenMessage(label, connectorKey, existing, ownerKey, bindingsShown), "BINDING_LABEL_TAKEN");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
86
190
|
/** Stage 0: the connection, discovered or checked, and its row in `bindings.json`. */
|
|
87
191
|
async function connect(options, ports) {
|
|
88
192
|
const info = (line) => ports.reporter.info(line);
|
|
193
|
+
const labels = options.noBindings ? undefined : chooseLabels(options, ports);
|
|
89
194
|
let row;
|
|
90
195
|
let connectionId;
|
|
91
196
|
if (!options.connectionId) {
|
|
@@ -102,25 +207,25 @@ async function connect(options, ports) {
|
|
|
102
207
|
row = await lookupConnection(ports.client, options.connectorKey, connectionId);
|
|
103
208
|
}
|
|
104
209
|
info(connectionLine(row, connectionId));
|
|
105
|
-
if (
|
|
210
|
+
if (!labels)
|
|
106
211
|
return { connectionId, row };
|
|
107
212
|
// Recorded on BOTH routes. Until 3.30.5 only discovery wrote it, and the
|
|
108
213
|
// explicit-id route left the author hand-writing the id and folder key.
|
|
109
214
|
const bindingsShown = options.bindings ?? "./bindings.json";
|
|
110
215
|
const bindingsPath = resolve(ports.cwd ?? process.cwd(), bindingsShown);
|
|
216
|
+
if (labels.connectionFrom) {
|
|
217
|
+
await refuseLabelOfAnotherConnector(ports.client, options.connectorKey, labels.connection, connectionId, bindingsPath, bindingsShown);
|
|
218
|
+
}
|
|
111
219
|
// Taken before the first write so a later refusal can undo exactly this
|
|
112
220
|
// command's changes, and only them.
|
|
113
221
|
const rollback = { path: bindingsPath, snapshot: snapshotBindings(bindingsPath), shown: bindingsShown };
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// argparse default, so an empty one STANDS, as it did there.
|
|
117
|
-
const symbolic = pyOr(options.bindConnection, afterLast(options.connectorKey, "-"));
|
|
118
|
-
const folder = options.bindFolder ?? "shared";
|
|
222
|
+
const symbolic = labels.connection;
|
|
223
|
+
const folder = labels.folder;
|
|
119
224
|
const [wroteConnection, wroteFolder] = mergeBindings(bindingsPath, symbolic, folder, row);
|
|
120
225
|
if (wroteConnection)
|
|
121
|
-
info(bindingsLine(symbolic, bindingsShown));
|
|
226
|
+
info(bindingsLine(symbolic, bindingsShown) + (labels.connectionFrom ? sourceLabelSuffix(labels.connectionFrom) : ""));
|
|
122
227
|
if (wroteFolder)
|
|
123
|
-
info(bindingsLine(folder, bindingsShown));
|
|
228
|
+
info(bindingsLine(folder, bindingsShown) + (labels.folderFrom ? sourceLabelSuffix(labels.folderFrom) : ""));
|
|
124
229
|
return { connectionId, row, bound: { connection: symbolic, folder }, rollback };
|
|
125
230
|
}
|
|
126
231
|
/**
|
|
@@ -188,7 +293,10 @@ export async function prepareConnector(options, ports) {
|
|
|
188
293
|
// per command is the whole contract: the probe has already proved this
|
|
189
294
|
// connection resolves every lookup, so a re-run that still cannot is a
|
|
190
295
|
// changed tenant, not another connection to go looking through.
|
|
191
|
-
|
|
296
|
+
// The labels this run chose ride along as flags, so the re-entry binds under
|
|
297
|
+
// the same names instead of deriving them again.
|
|
298
|
+
const labels = connected.bound ? { bindConnection: connected.bound.connection, bindFolder: connected.bound.folder } : {};
|
|
299
|
+
return prepareConnector({ ...options, ...labels, connectionId: chosenId, refreshLookups: true }, ports);
|
|
192
300
|
}
|
|
193
301
|
}
|
|
194
302
|
/** Everything after stage 0, against one settled connection. */
|
|
@@ -16,6 +16,8 @@ export declare function canonicalPath(p: string): string;
|
|
|
16
16
|
* A bare package specifier never does — it is not our overlay.
|
|
17
17
|
*/
|
|
18
18
|
export declare function resolvesTo(specifier: string, source: string, target: string): boolean;
|
|
19
|
+
/** Every `*.flow.ts` file under `root`, sorted, skipping the directories a source is never in. */
|
|
20
|
+
export declare function flowSources(root: string): string[];
|
|
19
21
|
/**
|
|
20
22
|
* `_sources_importing`: every `*.flow.ts` under `root` whose import resolves
|
|
21
23
|
* to `baked`. Selection is by CONTENT rather than "the single flow in this
|
|
@@ -67,7 +67,7 @@ function comparePaths(a, b) {
|
|
|
67
67
|
return a.length - b.length;
|
|
68
68
|
}
|
|
69
69
|
/** Every `*.flow.ts` file under `root`, sorted, skipping the directories a source is never in. */
|
|
70
|
-
function flowSources(root) {
|
|
70
|
+
export function flowSources(root) {
|
|
71
71
|
const found = [];
|
|
72
72
|
const walk = (dir, parts) => {
|
|
73
73
|
let children;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface SourceLabels {
|
|
2
|
+
/** Distinct connection labels this connector's steps use → the files (relative to cwd) that use each. */
|
|
3
|
+
connection: Map<string, string[]>;
|
|
4
|
+
/** Distinct folder labels this connector's steps use → the files that use each. */
|
|
5
|
+
folder: Map<string, string[]>;
|
|
6
|
+
/** Connection labels that steps of OTHER connectors use in the same sources → those connector keys. */
|
|
7
|
+
otherConnectors: Map<string, Set<string>>;
|
|
8
|
+
/** Why a label for this connector could not be read, one sentence each, per attribute. */
|
|
9
|
+
unreadable: {
|
|
10
|
+
connection: string[];
|
|
11
|
+
folder: string[];
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface SourceLabelOptions {
|
|
15
|
+
/** The project root (`prepare`'s cwd). */
|
|
16
|
+
cwd: string;
|
|
17
|
+
/** The overlay directory (`--out`), relative to `cwd` or absolute. */
|
|
18
|
+
out: string;
|
|
19
|
+
connectorKey: string;
|
|
20
|
+
/** `--source`: read only this file. Otherwise every `*.flow.ts` under `cwd`. */
|
|
21
|
+
source?: string | null;
|
|
22
|
+
}
|
|
23
|
+
/** Every label the sources under `cwd` (or `--source`) use for `connectorKey`. Reads files, writes nothing. */
|
|
24
|
+
export declare function sourceBindingLabels(options: SourceLabelOptions): SourceLabels;
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which `connection:` / `folder:` labels the author's `.flow.ts` already uses for
|
|
3
|
+
* ONE connector — read statically, so `prepare` can write its bindings.json
|
|
4
|
+
* entries under those names instead of inventing its own.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: `prepare` named the entries after the connector key alone
|
|
7
|
+
* (`slack`, `shared`), while the source it then repointed said
|
|
8
|
+
* `connection: 'is-sandboxes'`. Two authorities chose one name, and the mismatch
|
|
9
|
+
* reached the run as `'Connection' has an invalid GUID value: 'is-sandboxes'`
|
|
10
|
+
* (skill-flow-e2e-escalation-orchestrator-paths, 2026-09-23).
|
|
11
|
+
*
|
|
12
|
+
* The source is PARSED with the TypeScript compiler, never executed: at prepare
|
|
13
|
+
* time its other connector imports may not resolve yet, and running an author's
|
|
14
|
+
* module to learn a string is not this command's business. A label is attributed
|
|
15
|
+
* to a connector only through what the call names:
|
|
16
|
+
*
|
|
17
|
+
* - `connector('<key>', '<action>', inputs, opts)` — the stringly form, also
|
|
18
|
+
* written `connector(...KEY, inputs, opts)` over a same-file const tuple
|
|
19
|
+
* (`const KEY = ['<key>', '<action>'] as const`);
|
|
20
|
+
* - `connector(Descriptor, inputs, opts)` — `Descriptor` imported from
|
|
21
|
+
* `./connectors/<key>.ts` or the prepared `<out>/<key>.ts` (named or namespace
|
|
22
|
+
* import), so a source that a first `prepare` already repointed is still read;
|
|
23
|
+
* - `onEvent(...)` / `waitForEvent(...)` — a subscription object whose
|
|
24
|
+
* `connector` is `'<key>'`, or a descriptor imported as above with its options.
|
|
25
|
+
*
|
|
26
|
+
* `opts` and the labels in it may be literals or same-file `const`s, through
|
|
27
|
+
* `as const`, `satisfies`, parentheses, spreads of such consts, and property
|
|
28
|
+
* reads (`OUTLOOK.connection`). Anything else — a computed value, a spread of
|
|
29
|
+
* something that is not a same-file const, a call whose connector cannot be
|
|
30
|
+
* named (a descriptor from a module that is not a generated connector module, a
|
|
31
|
+
* spread of an imported tuple), a file that does not parse — is reported as
|
|
32
|
+
* UNREADABLE, never guessed, so the caller can fall back and say why. An
|
|
33
|
+
* unplaceable call is reported for every connector key, because it may be this
|
|
34
|
+
* one's step.
|
|
35
|
+
*
|
|
36
|
+
* Managed HTTP steps (`http({ managed: true, connection, folder })`) are not
|
|
37
|
+
* read: they name no connector key to attribute a label to. `check` and
|
|
38
|
+
* `compile` still refuse an undeclared label there (BINDING_UNDECLARED).
|
|
39
|
+
* GUID-shaped labels are dropped: they are resource keys written directly, not
|
|
40
|
+
* names to bind.
|
|
41
|
+
*/
|
|
42
|
+
import { readFileSync, statSync } from "node:fs";
|
|
43
|
+
import { basename, dirname, extname, resolve } from "node:path";
|
|
44
|
+
import * as ts from "typescript";
|
|
45
|
+
import { GUID_RE } from "../../core/binding-messages.js";
|
|
46
|
+
import { relativePosix } from "../compat/paths.js";
|
|
47
|
+
import { canonicalPath, flowSources } from "./source-import.js";
|
|
48
|
+
const CALLEES = new Set(["connector", "onEvent", "waitForEvent"]);
|
|
49
|
+
function isFile(path) {
|
|
50
|
+
try {
|
|
51
|
+
return statSync(path).isFile();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Every label the sources under `cwd` (or `--source`) use for `connectorKey`. Reads files, writes nothing. */
|
|
58
|
+
export function sourceBindingLabels(options) {
|
|
59
|
+
const result = { connection: new Map(), folder: new Map(), otherConnectors: new Map(), unreadable: { connection: [], folder: [] } };
|
|
60
|
+
const root = canonicalPath(options.cwd);
|
|
61
|
+
const moduleDirs = [canonicalPath(resolve(root, "connectors")), canonicalPath(resolve(options.cwd, options.out))];
|
|
62
|
+
let files;
|
|
63
|
+
if (options.source) {
|
|
64
|
+
const one = canonicalPath(resolve(options.cwd, options.source));
|
|
65
|
+
files = isFile(one) ? [one] : [];
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
files = flowSources(root);
|
|
69
|
+
}
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
let text;
|
|
72
|
+
try {
|
|
73
|
+
text = readFileSync(file, "utf8");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue; // unreadable: it cannot be the author's working source
|
|
77
|
+
}
|
|
78
|
+
readFile(file, relativePosix(root, file), text, options.connectorKey, moduleDirs, result);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
/** The connector key a module specifier names, when it is one of the generated connector modules. */
|
|
83
|
+
function connectorModuleKey(specifier, from, moduleDirs) {
|
|
84
|
+
if (!specifier.startsWith("."))
|
|
85
|
+
return undefined;
|
|
86
|
+
let target = canonicalPath(resolve(dirname(from), specifier));
|
|
87
|
+
if (extname(target) === "")
|
|
88
|
+
target = `${target}.ts`;
|
|
89
|
+
if (extname(target) !== ".ts" && extname(target) !== ".js")
|
|
90
|
+
return undefined;
|
|
91
|
+
if (!moduleDirs.includes(dirname(target)))
|
|
92
|
+
return undefined;
|
|
93
|
+
return basename(target).replace(/\.(ts|js)$/, "");
|
|
94
|
+
}
|
|
95
|
+
function unwrap(expr) {
|
|
96
|
+
let e = expr;
|
|
97
|
+
for (;;) {
|
|
98
|
+
if (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e)
|
|
99
|
+
|| ts.isNonNullExpression(e) || ts.isTypeAssertionExpression(e)) {
|
|
100
|
+
e = e.expression;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
return e;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function propertyName(name) {
|
|
108
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name))
|
|
109
|
+
return name.text;
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
function readFile(file, shown, text, connectorKey, moduleDirs, result) {
|
|
113
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
114
|
+
// `parseDiagnostics` is where the scanner records syntax problems (see core/expr-check.ts).
|
|
115
|
+
const problems = sf.parseDiagnostics;
|
|
116
|
+
if (problems && problems.length > 0) {
|
|
117
|
+
if (text.includes(connectorKey)) {
|
|
118
|
+
const why = `${shown} does not parse, so its labels could not be read`;
|
|
119
|
+
result.unreadable.connection.push(why);
|
|
120
|
+
result.unreadable.folder.push(why);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Imports of generated connector modules: local name → key, and namespace name → key.
|
|
125
|
+
const named = new Map();
|
|
126
|
+
const namespaces = new Map();
|
|
127
|
+
// Same-file consts: name → initializer, or `null` when declared more than once.
|
|
128
|
+
const consts = new Map();
|
|
129
|
+
const visitDecls = (node) => {
|
|
130
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
131
|
+
const key = connectorModuleKey(node.moduleSpecifier.text, file, moduleDirs);
|
|
132
|
+
const clause = node.importClause;
|
|
133
|
+
if (key && clause?.namedBindings) {
|
|
134
|
+
if (ts.isNamespaceImport(clause.namedBindings))
|
|
135
|
+
namespaces.set(clause.namedBindings.name.text, key);
|
|
136
|
+
else
|
|
137
|
+
for (const el of clause.namedBindings.elements)
|
|
138
|
+
named.set(el.name.text, key);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.Const) !== 0) {
|
|
142
|
+
for (const d of node.declarations) {
|
|
143
|
+
if (!ts.isIdentifier(d.name) || !d.initializer)
|
|
144
|
+
continue;
|
|
145
|
+
consts.set(d.name.text, consts.has(d.name.text) ? null : d.initializer);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
ts.forEachChild(node, visitDecls);
|
|
149
|
+
};
|
|
150
|
+
visitDecls(sf);
|
|
151
|
+
const constObject = (expr, depth = 0) => {
|
|
152
|
+
const e = unwrap(expr);
|
|
153
|
+
if (ts.isObjectLiteralExpression(e))
|
|
154
|
+
return e;
|
|
155
|
+
if (depth < 8 && ts.isIdentifier(e)) {
|
|
156
|
+
const init = consts.get(e.text);
|
|
157
|
+
if (init)
|
|
158
|
+
return constObject(init, depth + 1);
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
};
|
|
162
|
+
const prop = (obj, name, depth = 0) => {
|
|
163
|
+
let found = { kind: "absent" };
|
|
164
|
+
for (const p of obj.properties) {
|
|
165
|
+
if (ts.isPropertyAssignment(p) && propertyName(p.name) === name)
|
|
166
|
+
found = { kind: "found", expr: p.initializer };
|
|
167
|
+
else if (ts.isShorthandPropertyAssignment(p) && p.name.text === name)
|
|
168
|
+
found = { kind: "found", expr: p.name };
|
|
169
|
+
else if (ts.isSpreadAssignment(p)) {
|
|
170
|
+
const inner = depth < 8 ? constObject(p.expression) : undefined;
|
|
171
|
+
if (!inner)
|
|
172
|
+
found = { kind: "unknown", why: `a spread of \`${p.expression.getText(sf).slice(0, 60)}\`` };
|
|
173
|
+
else {
|
|
174
|
+
const got = prop(inner, name, depth + 1);
|
|
175
|
+
if (got.kind !== "absent")
|
|
176
|
+
found = got;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return found;
|
|
181
|
+
};
|
|
182
|
+
const constString = (expr, depth = 0) => {
|
|
183
|
+
const e = unwrap(expr);
|
|
184
|
+
if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e))
|
|
185
|
+
return e.text;
|
|
186
|
+
if (depth >= 8)
|
|
187
|
+
return undefined;
|
|
188
|
+
if (ts.isIdentifier(e)) {
|
|
189
|
+
const init = consts.get(e.text);
|
|
190
|
+
return init ? constString(init, depth + 1) : undefined;
|
|
191
|
+
}
|
|
192
|
+
if (ts.isPropertyAccessExpression(e)) {
|
|
193
|
+
const obj = constObject(e.expression);
|
|
194
|
+
if (!obj)
|
|
195
|
+
return undefined;
|
|
196
|
+
const got = prop(obj, e.name.text);
|
|
197
|
+
return got.kind === "found" ? constString(got.expr, depth + 1) : undefined;
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
};
|
|
201
|
+
/** The connector key a descriptor expression was imported for. */
|
|
202
|
+
const descriptorKey = (expr) => {
|
|
203
|
+
const e = unwrap(expr);
|
|
204
|
+
if (ts.isIdentifier(e))
|
|
205
|
+
return named.get(e.text);
|
|
206
|
+
if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression))
|
|
207
|
+
return namespaces.get(e.expression.text);
|
|
208
|
+
return undefined;
|
|
209
|
+
};
|
|
210
|
+
const add = (map, label) => {
|
|
211
|
+
const files = map.get(label) ?? [];
|
|
212
|
+
if (!files.includes(shown))
|
|
213
|
+
files.push(shown);
|
|
214
|
+
map.set(label, files);
|
|
215
|
+
};
|
|
216
|
+
const snippet = (node) => {
|
|
217
|
+
const t = node.getText(sf).replace(/\s+/g, " ");
|
|
218
|
+
return t.length > 60 ? `${t.slice(0, 57)}...` : t;
|
|
219
|
+
};
|
|
220
|
+
const where = (node, callee) => `${shown}:${sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1} ${callee}(…)`;
|
|
221
|
+
/** Record one call's labels, attributed to `key`. `opts` undefined = the call passes none. */
|
|
222
|
+
const record = (key, opts, at) => {
|
|
223
|
+
if (opts === undefined)
|
|
224
|
+
return;
|
|
225
|
+
const obj = constObject(opts);
|
|
226
|
+
if (key !== connectorKey) {
|
|
227
|
+
// Only the connection label matters across connectors: a folder is shared by design.
|
|
228
|
+
if (!obj)
|
|
229
|
+
return;
|
|
230
|
+
const got = prop(obj, "connection");
|
|
231
|
+
const label = got.kind === "found" ? constString(got.expr) : undefined;
|
|
232
|
+
if (label !== undefined && !GUID_RE.test(label)) {
|
|
233
|
+
const keys = result.otherConnectors.get(label) ?? new Set();
|
|
234
|
+
keys.add(key);
|
|
235
|
+
result.otherConnectors.set(label, keys);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (!obj) {
|
|
240
|
+
const why = `${at} passes its options as \`${snippet(unwrap(opts))}\`, which is not a same-file object`;
|
|
241
|
+
result.unreadable.connection.push(why);
|
|
242
|
+
result.unreadable.folder.push(why);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
for (const attribute of ["connection", "folder"]) {
|
|
246
|
+
const got = prop(obj, attribute);
|
|
247
|
+
if (got.kind === "absent")
|
|
248
|
+
continue;
|
|
249
|
+
if (got.kind === "unknown") {
|
|
250
|
+
result.unreadable[attribute].push(`${at} takes its ${attribute} from ${got.why}`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const label = constString(got.expr);
|
|
254
|
+
if (label === undefined) {
|
|
255
|
+
result.unreadable[attribute].push(`${at} computes its ${attribute} (\`${snippet(unwrap(got.expr))}\`)`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (GUID_RE.test(label))
|
|
259
|
+
continue;
|
|
260
|
+
add(result[attribute], label);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* The call's arguments with spreads of same-file const arrays expanded
|
|
265
|
+
* (`connector(...SLACK, inputs, opts)`), or why a spread could not be.
|
|
266
|
+
*/
|
|
267
|
+
const expandArgs = (args, depth = 0) => {
|
|
268
|
+
const out = [];
|
|
269
|
+
for (const a of args) {
|
|
270
|
+
if (!ts.isSpreadElement(a)) {
|
|
271
|
+
out.push(a);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const arr = depth < 8 ? constArray(a.expression) : undefined;
|
|
275
|
+
if (!arr)
|
|
276
|
+
return { why: `spreads \`${snippet(a.expression)}\`, which is not a same-file const array` };
|
|
277
|
+
const inner = expandArgs(arr.elements, depth + 1);
|
|
278
|
+
if (!Array.isArray(inner))
|
|
279
|
+
return inner;
|
|
280
|
+
out.push(...inner);
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
};
|
|
284
|
+
const constArray = (expr, depth = 0) => {
|
|
285
|
+
const e = unwrap(expr);
|
|
286
|
+
if (ts.isArrayLiteralExpression(e))
|
|
287
|
+
return e;
|
|
288
|
+
if (depth < 8 && ts.isIdentifier(e)) {
|
|
289
|
+
const init = consts.get(e.text);
|
|
290
|
+
if (init)
|
|
291
|
+
return constArray(init, depth + 1);
|
|
292
|
+
}
|
|
293
|
+
return undefined;
|
|
294
|
+
};
|
|
295
|
+
/** A call whose connector cannot be named may be this connector's: it makes every label unreadable. */
|
|
296
|
+
const unplaceable = (at, why) => {
|
|
297
|
+
const line = `${at} ${why}, so which connector it binds cannot be read`;
|
|
298
|
+
result.unreadable.connection.push(line);
|
|
299
|
+
result.unreadable.folder.push(line);
|
|
300
|
+
};
|
|
301
|
+
const visitCalls = (node) => {
|
|
302
|
+
if (ts.isCallExpression(node)) {
|
|
303
|
+
const callee = ts.isIdentifier(node.expression)
|
|
304
|
+
? node.expression.text
|
|
305
|
+
: ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text : undefined;
|
|
306
|
+
if (callee && CALLEES.has(callee) && node.arguments.length > 0) {
|
|
307
|
+
const at = where(node, callee);
|
|
308
|
+
const args = expandArgs(node.arguments);
|
|
309
|
+
if (!Array.isArray(args))
|
|
310
|
+
unplaceable(at, args.why);
|
|
311
|
+
else if (args.length === 0)
|
|
312
|
+
unplaceable(at, "spreads an empty array");
|
|
313
|
+
else if (callee === "connector") {
|
|
314
|
+
const [first] = args;
|
|
315
|
+
const literalKey = constString(first);
|
|
316
|
+
const key = literalKey ?? descriptorKey(first);
|
|
317
|
+
if (key === undefined)
|
|
318
|
+
unplaceable(at, `names its connector as \`${snippet(unwrap(first))}\`, which is neither a string nor a descriptor imported from a generated connector module`);
|
|
319
|
+
else
|
|
320
|
+
record(key, args[literalKey !== undefined ? 3 : 2], at);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
const [first] = args;
|
|
324
|
+
const subscription = constObject(first);
|
|
325
|
+
if (subscription) {
|
|
326
|
+
const got = prop(subscription, "connector");
|
|
327
|
+
const key = got.kind === "found" ? constString(got.expr) : undefined;
|
|
328
|
+
if (key)
|
|
329
|
+
record(key, first, at);
|
|
330
|
+
else
|
|
331
|
+
unplaceable(at, got.kind === "absent" ? "passes a subscription with no `connector`" : "computes its subscription's `connector`");
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
const key = descriptorKey(first);
|
|
335
|
+
if (key)
|
|
336
|
+
record(key, args[1], at);
|
|
337
|
+
else
|
|
338
|
+
unplaceable(at, `takes \`${snippet(unwrap(first))}\`, which is neither a same-file subscription object nor a descriptor imported from a generated connector module`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
ts.forEachChild(node, visitCalls);
|
|
344
|
+
};
|
|
345
|
+
visitCalls(sf);
|
|
346
|
+
}
|
package/dist/serialize.js
CHANGED
|
@@ -14,7 +14,7 @@ import { Expr, toExpr, SCHEDULE_PRESETS, DELAY_PRESETS, parseIxpProjectType, fla
|
|
|
14
14
|
import { FlowAction, FlowTrigger } from './core/node-classes.js';
|
|
15
15
|
import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
|
|
16
16
|
import { dataFabricUnknownInputs } from './core/typed-raw-node.js';
|
|
17
|
-
import { bindingSelfNameMessage } from './core/binding-messages.js';
|
|
17
|
+
import { BindingRefusal, GUID_RE, bindingSelfNameMessage, bindingUndeclaredMessage, undeclaredBinding } from './core/binding-messages.js';
|
|
18
18
|
import { OUTCOME_HANDLE_NODE_TYPES, assertHitlExitInvariant, hitlExitModel, hitlExitViolation, outcomeSlug } from './core/hitl-routing.js';
|
|
19
19
|
import { declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
|
|
20
20
|
import { buildConfiguration, transportHttpMethod } from './config.js';
|
|
@@ -3064,8 +3064,6 @@ function renderValue(val, rename = NO_RENAME) {
|
|
|
3064
3064
|
return val.literal ? val.value : '=js:' + renameRefs(val.js, rename);
|
|
3065
3065
|
return val;
|
|
3066
3066
|
}
|
|
3067
|
-
/** A tenant resource key — an Integration Service connection id or an Orchestrator folder key — is a GUID. */
|
|
3068
|
-
const GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3069
3067
|
/**
|
|
3070
3068
|
* The all-zero prefix authors reach for when they need "a GUID" and have none:
|
|
3071
3069
|
* `00000000-0000-0000-0000-000000000001`. No tenant issues these. The same
|
|
@@ -3087,6 +3085,12 @@ function connectionStubReason(value) {
|
|
|
3087
3085
|
return 'not a GUID';
|
|
3088
3086
|
return undefined;
|
|
3089
3087
|
}
|
|
3088
|
+
/** What `registry prepare` takes for an event subscription: its connector key and curated event name. */
|
|
3089
|
+
function eventTarget(sub) {
|
|
3090
|
+
return typeof sub.connector === 'string' && sub.connector !== ''
|
|
3091
|
+
? { key: sub.connector, ...(typeof sub.event === 'string' && sub.event !== '' ? { action: sub.event } : {}) }
|
|
3092
|
+
: undefined;
|
|
3093
|
+
}
|
|
3090
3094
|
/**
|
|
3091
3095
|
* Resolve a symbolic connection/folder name to the resource key that is inlined
|
|
3092
3096
|
* as `connectionId` / `connectionFolderKey`.
|
|
@@ -3100,30 +3104,55 @@ function connectionStubReason(value) {
|
|
|
3100
3104
|
* `bindings.json` that never declares the name, and one that declares it with
|
|
3101
3105
|
* its own name as the `resourceKey`.
|
|
3102
3106
|
*
|
|
3103
|
-
*
|
|
3104
|
-
*
|
|
3105
|
-
*
|
|
3106
|
-
*
|
|
3107
|
-
*
|
|
3108
|
-
* the
|
|
3109
|
-
*
|
|
3107
|
+
* Two cases are errors here, and they are the two where the author's intent to
|
|
3108
|
+
* bind is on record and the emitted value still cannot be a resource key:
|
|
3109
|
+
*
|
|
3110
|
+
* - SELF-REFERENTIAL (`BINDING_SELF_NAME`): an entry declares the name and then
|
|
3111
|
+
* answers with the name. Someone wrote the entry, and the one thing it cannot
|
|
3112
|
+
* mean is "the resource key is the string `is-sandboxes`".
|
|
3113
|
+
* - UNDECLARED IN A POPULATED FILE (`BINDING_UNDECLARED`): bindings.json declares
|
|
3114
|
+
* entries, none of them answers to this label, and the label is not itself a
|
|
3115
|
+
* GUID. The file is the author's statement of what is bound, so a label it
|
|
3116
|
+
* does not carry is a mismatch between two names for one connection, not
|
|
3117
|
+
* offline authoring. Measured: the 2026-09-23 orchestrator-paths run bound
|
|
3118
|
+
* `slack` (prepare's default label) while the source said `is-sandboxes`;
|
|
3119
|
+
* compile warned, exited 0, and the Slack node answered `400 'Connection' has
|
|
3120
|
+
* an invalid GUID value: 'is-sandboxes'`. Across the eval archive this shape
|
|
3121
|
+
* warned 15 times; 13 were fixed before the final compile and the 2 that were
|
|
3122
|
+
* not are that row's failures (09-18, 09-23) — no green run shipped it.
|
|
3110
3123
|
*
|
|
3111
3124
|
* Everything else that cannot be a resource key is a WARNING (`CONNECTION_STUB`),
|
|
3112
|
-
* never an error:
|
|
3113
|
-
*
|
|
3114
|
-
*
|
|
3115
|
-
*
|
|
3116
|
-
*
|
|
3117
|
-
*
|
|
3118
|
-
*
|
|
3119
|
-
* the
|
|
3125
|
+
* never an error: a symbol when NO bindings.json is loaded or it declares no
|
|
3126
|
+
* entries, an entry declared without a resourceKey, an all-zero GUID, an
|
|
3127
|
+
* `<angle>` placeholder, a label. Compiling against absent bindings is how a flow
|
|
3128
|
+
* is authored offline, and the name is the only honest placeholder available;
|
|
3129
|
+
* the archive shows those in green runs of simulated and validate-only tasks,
|
|
3130
|
+
* where no connection exists to bind. Refusing them would push an author with
|
|
3131
|
+
* nothing real to bind into inventing a plausible GUID instead, which is worse
|
|
3132
|
+
* because nothing can tell. That is also the route for a connector that is not
|
|
3133
|
+
* bound YET in a file that binds others (a two-connector flow with one prepared,
|
|
3134
|
+
* or a simulated connector next to real ones): declare its label with a
|
|
3135
|
+
* placeholder resourceKey and it stays a warning. Mid-loop compiles took that
|
|
3136
|
+
* state in three archived runs (ipe-drive-to-slack 09-23 `drive`, ipe-jira-
|
|
3137
|
+
* lifecycle 09-04 `entra`, customer-escalation-simulated 09-03 `zendesk`), and
|
|
3138
|
+
* each fixed it before the final compile, so what now costs one refused compile
|
|
3139
|
+
* there was never shipped. The 2026-09-01 orchestrator-paths run compiled
|
|
3140
|
+
* `00000000-0000-0000-0000-000000000001` with no signal at all and the Slack node
|
|
3141
|
+
* answered `401 Invalid Organization or User secret` at dispatch; the author
|
|
3142
|
+
* deserves to hear that BEFORE the run.
|
|
3120
3143
|
*/
|
|
3121
|
-
function resolveBinding(opts, name, attribute, where) {
|
|
3144
|
+
function resolveBinding(opts, name, attribute, where, target) {
|
|
3122
3145
|
if (name === undefined)
|
|
3123
3146
|
return undefined;
|
|
3124
3147
|
const resolved = opts.bindings?.resolve(name);
|
|
3125
3148
|
if (resolved === name) {
|
|
3126
|
-
throw new
|
|
3149
|
+
throw new BindingRefusal('BINDING_SELF_NAME', bindingSelfNameMessage(where, name, attribute, target));
|
|
3150
|
+
}
|
|
3151
|
+
if (resolved === undefined) {
|
|
3152
|
+
const declared = undeclaredBinding(opts.bindings, name);
|
|
3153
|
+
if (declared) {
|
|
3154
|
+
throw new BindingRefusal('BINDING_UNDECLARED', bindingUndeclaredMessage(where, name, attribute, declared, target));
|
|
3155
|
+
}
|
|
3127
3156
|
}
|
|
3128
3157
|
const value = resolved ?? name;
|
|
3129
3158
|
const reason = connectionStubReason(value);
|
|
@@ -3136,8 +3165,10 @@ function resolveBinding(opts, name, attribute, where) {
|
|
|
3136
3165
|
+ `and writes both entries into bindings.json (several candidates: pass \`--connection-id <id>\` from the list it `
|
|
3137
3166
|
+ `prints); by hand, read Id and FolderKey from \`uip is connections list --all-folders --output json\`.`;
|
|
3138
3167
|
const message = resolved === undefined
|
|
3139
|
-
? `${where}: ${
|
|
3140
|
-
|
|
3168
|
+
? `${where}: ${declaredWithoutKey(opts.bindings, name)
|
|
3169
|
+
? `binding "${name}" is declared in bindings.json with no resourceKey`
|
|
3170
|
+
: `${attribute} "${name}" has no bindings.json entry${describeDeclared(opts.bindings)}`}`
|
|
3171
|
+
+ `, so the .flow carries the symbolic name as its ${key} and the run `
|
|
3141
3172
|
+ `faults at dispatch with ${fault}. ${remedy}`
|
|
3142
3173
|
: `${where}: binding "${name}" resolves to "${value}", which is ${reason} and so never a tenant ${key}. `
|
|
3143
3174
|
+ `The .flow carries it and the run faults at dispatch with ${fault}. ${remedy}`;
|
|
@@ -3145,6 +3176,10 @@ function resolveBinding(opts, name, attribute, where) {
|
|
|
3145
3176
|
}
|
|
3146
3177
|
return value;
|
|
3147
3178
|
}
|
|
3179
|
+
/** An entry answers to `name` but carries no resourceKey or default — the placeholder route, still a warning. */
|
|
3180
|
+
function declaredWithoutKey(bindings, name) {
|
|
3181
|
+
return (bindings?.declared?.() ?? []).includes(name) || bindings?.entry?.(name) !== undefined;
|
|
3182
|
+
}
|
|
3148
3183
|
/** " (bindings.json declares: a, b)" / " (no bindings.json was loaded)" — what the author CAN refer to. */
|
|
3149
3184
|
function describeDeclared(bindings) {
|
|
3150
3185
|
const names = bindings?.declared?.() ?? [];
|
|
@@ -3282,8 +3317,8 @@ function flattenWhere(where, declared) {
|
|
|
3282
3317
|
return out;
|
|
3283
3318
|
}
|
|
3284
3319
|
function eventDetail(rc, sub, nodeId, opts) {
|
|
3285
|
-
const conn = resolveBinding(opts, sub.connection, 'connection', `event node "${nodeId}"
|
|
3286
|
-
const folder = resolveBinding(opts, sub.folder, 'folder', `event node "${nodeId}"
|
|
3320
|
+
const conn = resolveBinding(opts, sub.connection, 'connection', `event node "${nodeId}"`, eventTarget(sub));
|
|
3321
|
+
const folder = resolveBinding(opts, sub.folder, 'folder', `event node "${nodeId}"`, eventTarget(sub));
|
|
3287
3322
|
const op = (rc.entry?.operation ?? {});
|
|
3288
3323
|
const filters = sub.filters ?? [];
|
|
3289
3324
|
// A GENERIC event (`activityType: GenericTrigger` — Data Fabric, Salesforce,
|
|
@@ -4106,8 +4141,8 @@ export function serialize(built, opts = {}) {
|
|
|
4106
4141
|
const { entries } = connectionResourceBindings({
|
|
4107
4142
|
def: eventTriggerRc.v1def,
|
|
4108
4143
|
connectorKey: eventTriggerRc.connectorKey,
|
|
4109
|
-
connectionId: resolveBinding(opts, sub.connection, 'connection', 'event trigger'),
|
|
4110
|
-
folderKey: resolveBinding(opts, sub.folder, 'folder', 'event trigger'),
|
|
4144
|
+
connectionId: resolveBinding(opts, sub.connection, 'connection', 'event trigger', eventTarget(sub)),
|
|
4145
|
+
folderKey: resolveBinding(opts, sub.folder, 'folder', 'event trigger', eventTarget(sub)),
|
|
4111
4146
|
}, resourceBindings);
|
|
4112
4147
|
for (const b of entries)
|
|
4113
4148
|
resourceBindings.set(b.id, b);
|
|
@@ -4231,8 +4266,8 @@ export function serialize(built, opts = {}) {
|
|
|
4231
4266
|
const id = stepUid(scope, step.name);
|
|
4232
4267
|
const node = makeNode(rc.nodeType, id, step.name, { parentId, requestedVersion: step.options?.version, def: rc.v1def });
|
|
4233
4268
|
declareListOutputType(node, rc);
|
|
4234
|
-
const conn = resolveBinding(opts, spec.connection, 'connection', `connector "${step.name}"
|
|
4235
|
-
const folder = resolveBinding(opts, spec.folder, 'folder', `connector "${step.name}"
|
|
4269
|
+
const conn = resolveBinding(opts, spec.connection, 'connection', `connector "${step.name}"`, { key: spec.key, action: spec.action });
|
|
4270
|
+
const folder = resolveBinding(opts, spec.folder, 'folder', `connector "${step.name}"`, { key: spec.key, action: spec.action });
|
|
4236
4271
|
// The platform reaches the connection through the flow's `bindings[]`,
|
|
4237
4272
|
// never through `inputs.detail` — without this pair the run faults with
|
|
4238
4273
|
// `[102010]` (board G-19; see connectionResourceBindings).
|
|
@@ -5131,8 +5166,8 @@ export function serialize(built, opts = {}) {
|
|
|
5131
5166
|
const { entries: waitConnBindings } = connectionResourceBindings({
|
|
5132
5167
|
def: rc.v1def,
|
|
5133
5168
|
connectorKey: rc.connectorKey,
|
|
5134
|
-
connectionId: resolveBinding(opts, sub.connection, 'connection', `waitForEvent "${step.name}"
|
|
5135
|
-
folderKey: resolveBinding(opts, sub.folder, 'folder', `waitForEvent "${step.name}"
|
|
5169
|
+
connectionId: resolveBinding(opts, sub.connection, 'connection', `waitForEvent "${step.name}"`, eventTarget(sub)),
|
|
5170
|
+
folderKey: resolveBinding(opts, sub.folder, 'folder', `waitForEvent "${step.name}"`, eventTarget(sub)),
|
|
5136
5171
|
}, resourceBindings);
|
|
5137
5172
|
for (const b of waitConnBindings)
|
|
5138
5173
|
resourceBindings.set(b.id, b);
|
|
@@ -5986,8 +6021,8 @@ export function serialize(built, opts = {}) {
|
|
|
5986
6021
|
const { entries } = connectionResourceBindings({
|
|
5987
6022
|
def: rc.v1def,
|
|
5988
6023
|
connectorKey: rc.connectorKey,
|
|
5989
|
-
connectionId: resolveBinding(opts, sub.connection, 'connection', 'onEvent() entry point'),
|
|
5990
|
-
folderKey: resolveBinding(opts, sub.folder, 'folder', 'onEvent() entry point'),
|
|
6024
|
+
connectionId: resolveBinding(opts, sub.connection, 'connection', 'onEvent() entry point', eventTarget(sub)),
|
|
6025
|
+
folderKey: resolveBinding(opts, sub.folder, 'folder', 'onEvent() entry point', eventTarget(sub)),
|
|
5991
6026
|
}, resourceBindings);
|
|
5992
6027
|
for (const b of entries)
|
|
5993
6028
|
resourceBindings.set(b.id, b);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "6.9.
|
|
3
|
+
"version": "6.9.2",
|
|
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",
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"@types/node": "^22.7.0",
|
|
88
88
|
"esbuild": "^0.28.1"
|
|
89
89
|
},
|
|
90
|
-
"gitref": "
|
|
90
|
+
"gitref": "25a192425e3db19a8df8f2c9aafc897c051dac02"
|
|
91
91
|
}
|