@uipath/maestro-builder-sdk 6.8.4 → 6.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/check.js +20 -4
- package/dist/core/cli-spelling.d.ts +10 -0
- package/dist/core/cli-spelling.js +24 -0
- package/dist/core/lookups.js +4 -2
- package/dist/registry/generators/markdown/sections/lookups.js +20 -5
- package/dist/registry/prepare/ledger.d.ts +3 -1
- package/dist/registry/prepare/ledger.js +55 -5
- package/dist/registry/prepare/messages.d.ts +8 -0
- package/dist/registry/prepare/messages.js +17 -0
- package/dist/registry/prepare/pipeline.js +4 -1
- package/dist/registry/prepare/resolve.d.ts +27 -2
- package/dist/registry/prepare/resolve.js +69 -7
- package/dist/serialize.js +67 -8
- package/lib/library-json.samples/index.json +18 -0
- package/lib/library-json.samples/uipath-getresponse-getresponse/send-newsletter@1.0.0.json +159 -0
- package/lib/library-json.samples/uipath-getresponse-getresponse/send-newsletter@1.0.0.v1def.json +323 -0
- package/lib/library-json.samples/uipath-salesforce-slack/create-group-direct-message@1.0.0.json +149 -0
- package/lib/library-json.samples/uipath-salesforce-slack/create-group-direct-message@1.0.0.v1def.json +237 -0
- package/package.json +2 -2
package/dist/check.js
CHANGED
|
@@ -15,7 +15,7 @@ import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from '
|
|
|
15
15
|
import { declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
|
|
16
16
|
import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
|
|
17
17
|
import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
|
|
18
|
-
import { prepareCommand } from './core/cli-spelling.js';
|
|
18
|
+
import { prepareCommand, resolveFlagArg } from './core/cli-spelling.js';
|
|
19
19
|
export function check(built, opts = {}) {
|
|
20
20
|
return checkFlow(built, [], opts);
|
|
21
21
|
}
|
|
@@ -1264,6 +1264,9 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
1264
1264
|
function checkConnectorLookups(step, spec, opts, diags) {
|
|
1265
1265
|
const specs = opts.lookups?.(spec.key, spec.action, spec.version, spec.object);
|
|
1266
1266
|
const resolutions = opts.resolutions ?? {};
|
|
1267
|
+
// One LOOKUP_LITERAL_ID per field: a multiselect of three pasted ids is one
|
|
1268
|
+
// mistake with one remedy, not three.
|
|
1269
|
+
const literalWarned = new Set();
|
|
1267
1270
|
const visit = (value, path) => {
|
|
1268
1271
|
if (isLookupToken(value)) {
|
|
1269
1272
|
// A match key the field does not declare is certainly wrong, and the
|
|
@@ -1304,7 +1307,17 @@ function checkConnectorLookups(step, spec, opts, diags) {
|
|
|
1304
1307
|
return;
|
|
1305
1308
|
}
|
|
1306
1309
|
if (Array.isArray(value)) {
|
|
1307
|
-
|
|
1310
|
+
// An element is named by the WIRE name the lookup table is keyed on —
|
|
1311
|
+
// `users[*]`, `contacts[*].id` — not by its index. `users.0` matched no
|
|
1312
|
+
// spec, so a pasted id, a token with a bad match key, or a runtime value
|
|
1313
|
+
// inside a multiselect passed every rule below unseen. A name that already
|
|
1314
|
+
// is the array's (a flat `'users[*]'` key, or a field typed as an array)
|
|
1315
|
+
// stays as it is.
|
|
1316
|
+
const last = path[path.length - 1] ?? '';
|
|
1317
|
+
const element = last.endsWith('[*]') || specs?.[path.join('.')]
|
|
1318
|
+
? path
|
|
1319
|
+
: [...path.slice(0, -1), `${last}[*]`];
|
|
1320
|
+
value.forEach((v) => visit(v, element));
|
|
1308
1321
|
return;
|
|
1309
1322
|
}
|
|
1310
1323
|
if (value && typeof value === 'object' && !(value instanceof Expr)) {
|
|
@@ -1329,6 +1342,9 @@ function checkConnectorLookups(step, spec, opts, diags) {
|
|
|
1329
1342
|
const literal = value instanceof Expr ? value.value : value;
|
|
1330
1343
|
if (typeof literal !== 'string')
|
|
1331
1344
|
return;
|
|
1345
|
+
if (literalWarned.has(field))
|
|
1346
|
+
return;
|
|
1347
|
+
literalWarned.add(field);
|
|
1332
1348
|
const { target, call } = lookupPhrases(field, lookup, spec);
|
|
1333
1349
|
diags.push({
|
|
1334
1350
|
level: 'warning',
|
|
@@ -1340,7 +1356,7 @@ function checkConnectorLookups(step, spec, opts, diags) {
|
|
|
1340
1356
|
+ `compile nor deploy can tell. Write what you mean and resolve it late:\n`
|
|
1341
1357
|
+ ` ${call}\n`
|
|
1342
1358
|
+ `then record the value once with \`${prepareCommand(spec.key)} `
|
|
1343
|
-
+ `${spec.action} --resolve ${field
|
|
1359
|
+
+ `${spec.action} --resolve ${resolveFlagArg(field, lookup.by[0] ?? 'id', '<value>')}\`.`,
|
|
1344
1360
|
step,
|
|
1345
1361
|
});
|
|
1346
1362
|
};
|
|
@@ -1557,7 +1573,7 @@ function runtimeLookupDiagnostic(step, spec, field, lookup, value) {
|
|
|
1557
1573
|
+ `neither compile nor validate can tell. Resolve the value you know instead:\n`
|
|
1558
1574
|
+ ` ${call}\n`
|
|
1559
1575
|
+ `then record it once with \`${prepareCommand(spec.key, spec.action)} `
|
|
1560
|
-
+ `--resolve ${field
|
|
1576
|
+
+ `--resolve ${resolveFlagArg(field, lookup.by[0] ?? 'id', '<value>')}\`; or read the id from a step whose `
|
|
1561
1577
|
+ `output returned one (\`${lookup.value}\`).`
|
|
1562
1578
|
+ (isEmail
|
|
1563
1579
|
? `\n\`${single.raw}\` reads \`${leaf}\`, an e-mail address, not a \`${target}\`.`
|
|
@@ -37,3 +37,13 @@ export declare function cliSpelling(env?: CliSpellingEnv): string;
|
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
39
|
export declare function prepareCommand(...args: string[]): string;
|
|
40
|
+
/**
|
|
41
|
+
* The argument of one `--resolve <field>:<by>=<value>`, single-quoted when the
|
|
42
|
+
* shell would otherwise mangle it.
|
|
43
|
+
*
|
|
44
|
+
* @param field - The lookup field's wire name, e.g. `channel` or `users[*]`.
|
|
45
|
+
* @param by - The match key, e.g. `profile.email`.
|
|
46
|
+
* @param value - The value to match, or the `<value>` placeholder.
|
|
47
|
+
* @returns `channel:profile.email=d@x.com` as is; `'users[*]:profile.real_name=Coder Evals'` quoted.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveFlagArg(field: string, by: string, value: string): string;
|
|
@@ -60,3 +60,27 @@ export function cliSpelling(env = ambientEnv()) {
|
|
|
60
60
|
export function prepareCommand(...args) {
|
|
61
61
|
return [cliSpelling(), 'registry', 'prepare', ...args.filter(Boolean)].join(' ');
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Characters that make a `--resolve` argument unsafe to paste unquoted. `[*]`
|
|
65
|
+
* is a shell glob (every multiselect wire name, `users[*]`), whitespace splits
|
|
66
|
+
* a real name (`Coder Evals`) into two arguments, and quotes, `$`, backslash
|
|
67
|
+
* and the control operators change what the shell hands over. The `<value>`
|
|
68
|
+
* placeholder a template prints is stripped before the test, so a template
|
|
69
|
+
* with nothing else unsafe keeps its bytes.
|
|
70
|
+
*/
|
|
71
|
+
const SHELL_UNSAFE = /[\s'"`$\\[\]*?;&|(){}!#~<>]/;
|
|
72
|
+
/**
|
|
73
|
+
* The argument of one `--resolve <field>:<by>=<value>`, single-quoted when the
|
|
74
|
+
* shell would otherwise mangle it.
|
|
75
|
+
*
|
|
76
|
+
* @param field - The lookup field's wire name, e.g. `channel` or `users[*]`.
|
|
77
|
+
* @param by - The match key, e.g. `profile.email`.
|
|
78
|
+
* @param value - The value to match, or the `<value>` placeholder.
|
|
79
|
+
* @returns `channel:profile.email=d@x.com` as is; `'users[*]:profile.real_name=Coder Evals'` quoted.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveFlagArg(field, by, value) {
|
|
82
|
+
const arg = `${field}:${by}=${value}`;
|
|
83
|
+
if (!SHELL_UNSAFE.test(arg.replaceAll('<value>', '')))
|
|
84
|
+
return arg;
|
|
85
|
+
return `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
86
|
+
}
|
package/dist/core/lookups.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* degenerate classes the generator excludes and why generating for them would
|
|
28
28
|
* make the surface worse rather than more complete.
|
|
29
29
|
*/
|
|
30
|
-
import { prepareCommand } from './cli-spelling.js';
|
|
30
|
+
import { prepareCommand, resolveFlagArg } from './cli-spelling.js';
|
|
31
31
|
/** True when `v` is an unresolved {@link LookupToken}. */
|
|
32
32
|
export function isLookupToken(v) {
|
|
33
33
|
return (typeof v === 'object' && v !== null &&
|
|
@@ -192,7 +192,9 @@ export function unresolvedLookupMessage(token) {
|
|
|
192
192
|
return (`input "${field}" is an unresolved lookup (${by}=${JSON.stringify(value)}). ` +
|
|
193
193
|
`Compile does not make network calls, so the value has to be resolved first:\n` +
|
|
194
194
|
` ${prepareCommand(key, action)} \\\n` +
|
|
195
|
-
|
|
195
|
+
// Quoted only when the shell would mangle it: a multiselect's `users[*]`
|
|
196
|
+
// is a glob and a real name has spaces, and the last line is meant to paste.
|
|
197
|
+
` --resolve ${resolveFlagArg(field, by, String(value))}`);
|
|
196
198
|
}
|
|
197
199
|
// ─── Classification, mirroring `_lookups.py` ─────────────────────────────────
|
|
198
200
|
//
|
|
@@ -11,6 +11,7 @@ import { sortedStrings } from "../../../compat/compare.js";
|
|
|
11
11
|
import { pyStr, splitOnce } from "../../../compat/pystr.js";
|
|
12
12
|
import { classify, referenceOf } from "../../../lookup-specs.js";
|
|
13
13
|
import { byAliases } from "../../../naming.js";
|
|
14
|
+
import { resolveFlagArg } from "../../../../core/cli-spelling.js";
|
|
14
15
|
import { authoringField, referenceCollection, referenceQuery } from "../common.js";
|
|
15
16
|
import { firstValue, IDENT, pyOr, pyTruthy, strList } from "../pyvalue.js";
|
|
16
17
|
/** The `resources run list` call that shows what an identity-lookup field accepts. */
|
|
@@ -85,9 +86,21 @@ export function lookupLines(connectorKey, action, fields, symbol, invariant) {
|
|
|
85
86
|
"different connection. Resolve it late:");
|
|
86
87
|
out.push("");
|
|
87
88
|
out.push("```ts");
|
|
88
|
-
// A
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// A plain multiselect (`users[*]`) is authored as an array of lookups under
|
|
90
|
+
// its authoring name, the same form `## Example` prints and the one that
|
|
91
|
+
// compiles to one id per element. Any other wire name is written as a key —
|
|
92
|
+
// a dotted one is a legal object key only when quoted — and one ending in
|
|
93
|
+
// `[*]` (`recipients.listIds[*]`) still names ONE list, so its value is an
|
|
94
|
+
// array too: the flat key compiles to the same list the nested
|
|
95
|
+
// `recipients: { listIds: [...] }` of `## Example` does.
|
|
96
|
+
const [authoringName, , multiselectWire] = authoringField(firstField);
|
|
97
|
+
if (multiselectWire) {
|
|
98
|
+
out.push(`${authoringName}: [${call}${sample}]`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const key = IDENT.test(firstName) ? firstName : pyJsonString(firstName);
|
|
102
|
+
out.push(firstName.endsWith("[*]") ? `${key}: [${call}${sample}]` : `${key}: ${call}${sample}`);
|
|
103
|
+
}
|
|
91
104
|
out.push("```");
|
|
92
105
|
out.push("");
|
|
93
106
|
out.push("| field | helpers | matches on | sends |");
|
|
@@ -116,10 +129,12 @@ export function lookupLines(connectorKey, action, fields, symbol, invariant) {
|
|
|
116
129
|
// ONE command carrying every --resolve, and no `--connection-id`.
|
|
117
130
|
out.push("```bash");
|
|
118
131
|
out.push(`npx flow-sdk registry prepare ${connectorKey} ${action} \\`);
|
|
132
|
+
// The WIRE name (`users[*]`), which is what prepare's lookup table is keyed
|
|
133
|
+
// on — the authoring name (`users`) is refused as RESOLVE_UNKNOWN_FIELD.
|
|
119
134
|
const resolves = resolvable.map(([f, reference]) => {
|
|
120
|
-
const
|
|
135
|
+
const wire = pyStr(pyOr(f.name, authoringField(f)[0]));
|
|
121
136
|
const names = pyOr(strList(reference.lookupNames), ["id"]);
|
|
122
|
-
return ` --resolve ${
|
|
137
|
+
return ` --resolve ${resolveFlagArg(wire, names[0], "<value>")}`;
|
|
123
138
|
});
|
|
124
139
|
for (const line of resolves.slice(0, -1))
|
|
125
140
|
out.push(`${line} \\`);
|
|
@@ -55,7 +55,9 @@ export interface ResolutionPlan {
|
|
|
55
55
|
export declare class ResolutionsExhausted extends LookupExhausted {
|
|
56
56
|
readonly first: LookupExhausted;
|
|
57
57
|
readonly plan: ResolutionPlan;
|
|
58
|
-
constructor(first: LookupExhausted, plan: ResolutionPlan
|
|
58
|
+
constructor(first: LookupExhausted, plan: ResolutionPlan,
|
|
59
|
+
/** Every miss of the run, when there was more to say than `first`'s own message. */
|
|
60
|
+
message?: string);
|
|
59
61
|
}
|
|
60
62
|
/** What one `--resolve` run needs; `objectName` narrows the overlay entry the specs are read from. */
|
|
61
63
|
export interface ResolveRun {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { lookupKey } from "../../core/lookups.js";
|
|
21
21
|
import { pyJsonStringify } from "../codecs/pyjson.js";
|
|
22
22
|
import { sortedStrings } from "../compat/compare.js";
|
|
23
|
-
import { PrepareRefusal, resolutionLine, unknownLookupMessage, wroteResolutionsLine } from "./messages.js";
|
|
23
|
+
import { PrepareRefusal, incompleteRunMessage, notTriedLine, resolutionLine, unknownLookupMessage, wroteResolutionsLine, } from "./messages.js";
|
|
24
24
|
import { LedgerRefusal, RESOLUTIONS_FILENAME, ledgerPath, readLedger, writeLedger } from "./objects.js";
|
|
25
25
|
import { lookupSpecsFromOverlay } from "./overlay.js";
|
|
26
26
|
import { LookupExhausted, orderResolutions, parseResolveFlag, resolveOne } from "./resolve.js";
|
|
@@ -87,8 +87,10 @@ export function findResolution(existing, nodeType, field, by, value) {
|
|
|
87
87
|
export class ResolutionsExhausted extends LookupExhausted {
|
|
88
88
|
first;
|
|
89
89
|
plan;
|
|
90
|
-
constructor(first, plan
|
|
91
|
-
|
|
90
|
+
constructor(first, plan,
|
|
91
|
+
/** Every miss of the run, when there was more to say than `first`'s own message. */
|
|
92
|
+
message = first.message) {
|
|
93
|
+
super(message, first);
|
|
92
94
|
this.first = first;
|
|
93
95
|
this.plan = plan;
|
|
94
96
|
this.name = "ResolutionsExhausted";
|
|
@@ -117,13 +119,29 @@ export async function writeResolutions(run, specs) {
|
|
|
117
119
|
const recorded = pyGet(ledger, "resolutions", null);
|
|
118
120
|
const existing = isPlainObject(recorded) ? { ...recorded } : {};
|
|
119
121
|
const resolved = new Map();
|
|
122
|
+
// An exhaustive miss no longer ends the run: the rest are still tried, so ONE
|
|
123
|
+
// run reports every value this connection does not hold — a multiselect of
|
|
124
|
+
// three names used to cost three runs to learn about three misses. A lookup
|
|
125
|
+
// addressed through a field that missed is skipped, since its path cannot be
|
|
126
|
+
// built. The WRITE stays all-or-nothing; see the throw below.
|
|
127
|
+
const misses = [];
|
|
128
|
+
const unresolved = new Set();
|
|
129
|
+
const notTried = [];
|
|
130
|
+
const answered = [];
|
|
120
131
|
for (const [field, by, value] of ordered) {
|
|
132
|
+
const blockedBy = (table.get(field)?.dependsOn ?? []).find((prerequisite) => unresolved.has(prerequisite));
|
|
133
|
+
if (blockedBy !== undefined) {
|
|
134
|
+
unresolved.add(field);
|
|
135
|
+
notTried.push(notTriedLine(field, by, value, blockedBy));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
121
138
|
const key = resolutionKey(run.nodeType, field, by, value);
|
|
122
139
|
const found = findResolution(existing, run.nodeType, field, by, value);
|
|
123
140
|
if (found && !run.refreshLookups) {
|
|
124
141
|
const kept = pyStrAny(pyGet(found.row, "value", null));
|
|
125
142
|
resolved.set(field, kept);
|
|
126
143
|
run.reporter.info(resolutionLine(field, by, value, kept, true));
|
|
144
|
+
answered.push(resolutionLine(field, by, value, kept, true));
|
|
127
145
|
continue;
|
|
128
146
|
}
|
|
129
147
|
let got;
|
|
@@ -152,8 +170,25 @@ export async function writeResolutions(run, specs) {
|
|
|
152
170
|
// refusal, whose text already names the remedy ("the value may be
|
|
153
171
|
// further in"). Upstream's own tests never covered the row, which is
|
|
154
172
|
// how it shipped unimplemented.
|
|
155
|
-
|
|
156
|
-
|
|
173
|
+
//
|
|
174
|
+
// Only that exhaustive miss is collected; every other refusal — capped,
|
|
175
|
+
// transport, unsearchable `by` — still ends the run at once, as before,
|
|
176
|
+
// so none of them can reach the cross-connection retry.
|
|
177
|
+
if (error instanceof LookupExhausted && !error.capped) {
|
|
178
|
+
misses.push(error);
|
|
179
|
+
unresolved.add(field);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// An exhaustive miss collected EARLIER still decides the run: it is
|
|
183
|
+
// evidence against this connection, and before the misses were collected
|
|
184
|
+
// it would have ended the run and started the cross-connection retry
|
|
185
|
+
// before this lookup was ever tried. So that retry still runs, and its
|
|
186
|
+
// refusal names the collected misses and then this error.
|
|
187
|
+
if (misses.length > 0) {
|
|
188
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
189
|
+
const lines = [...misses.map((miss) => miss.message), ...notTried, text.startsWith(`${field}:`) ? text : `${field}: ${text}`];
|
|
190
|
+
throw new ResolutionsExhausted(misses[0], { specs: table, ordered }, incompleteRunMessage(lines, [], answered, ordered.length));
|
|
191
|
+
}
|
|
157
192
|
throw error;
|
|
158
193
|
}
|
|
159
194
|
resolved.set(field, got);
|
|
@@ -163,6 +198,21 @@ export async function writeResolutions(run, specs) {
|
|
|
163
198
|
delete existing[found.key];
|
|
164
199
|
existing[key] = { nodeType: run.nodeType, field, by, match: value, value: got };
|
|
165
200
|
run.reporter.info(resolutionLine(field, by, value, got));
|
|
201
|
+
answered.push(resolutionLine(field, by, value, got));
|
|
202
|
+
}
|
|
203
|
+
// ALL-OR-NOTHING, deliberately, even though the rows that DID resolve are
|
|
204
|
+
// correct for this connection. A ledger row carries no connection: it is
|
|
205
|
+
// keyed `[nodeType, field, by, value]`, and a later run REUSES a recorded row
|
|
206
|
+
// without asking again unless `--refresh-lookups` is passed (above). So a
|
|
207
|
+
// partial write from connection A would hand A's workspace ids to the next
|
|
208
|
+
// run bound to connection B — the 2026-09-23 run resolved one recipient on
|
|
209
|
+
// one Slack workspace and then re-pinned to another. Until rows are keyed by
|
|
210
|
+
// connection, a run that misses records nothing and says what it found.
|
|
211
|
+
if (misses.length > 0) {
|
|
212
|
+
const message = misses.length === 1 && notTried.length === 0 && answered.length === 0
|
|
213
|
+
? misses[0].message
|
|
214
|
+
: incompleteRunMessage(misses.map((miss) => miss.message), notTried, answered, ordered.length);
|
|
215
|
+
throw new ResolutionsExhausted(misses[0], { specs: table, ordered }, message);
|
|
166
216
|
}
|
|
167
217
|
const written = { ...ledger, schemaVersion: "1", resolutions: existing };
|
|
168
218
|
writeLedger(path, written);
|
|
@@ -111,6 +111,14 @@ export declare function lookupVocabularyLines(connectorKey: string, action: stri
|
|
|
111
111
|
export declare function unknownLookupMessage(unknown: readonly string[], nodeType: string, known: readonly string[]): string;
|
|
112
112
|
/** ` <field>: <by>=<value> -> <id>`, with ` (already resolved)` when the ledger answered. */
|
|
113
113
|
export declare function resolutionLine(field: string, by: string, value: string, got: unknown, alreadyResolved?: boolean): string;
|
|
114
|
+
/** A lookup skipped because the field its collection path is built from did not resolve. */
|
|
115
|
+
export declare function notTriedLine(field: string, by: string, value: string, prerequisite: string): string;
|
|
116
|
+
/**
|
|
117
|
+
* Every miss of one `--resolve` run, then what DID resolve. Nothing is recorded
|
|
118
|
+
* when anything misses, and the reader needs both halves to fix every value in
|
|
119
|
+
* one pass rather than one per run.
|
|
120
|
+
*/
|
|
121
|
+
export declare function incompleteRunMessage(misses: readonly string[], notTried: readonly string[], answered: readonly string[], requested: number): string;
|
|
114
122
|
export declare function wroteResolutionsLine(count: number, ledgerPath: string): string;
|
|
115
123
|
/**
|
|
116
124
|
* `generated N connector module(s) → <out>` — the descriptor generator's own
|
|
@@ -262,6 +262,23 @@ export function unknownLookupMessage(unknown, nodeType, known) {
|
|
|
262
262
|
export function resolutionLine(field, by, value, got, alreadyResolved = false) {
|
|
263
263
|
return ` ${field}: ${by}=${value} -> ${pyStrAny(got)}${alreadyResolved ? " (already resolved)" : ""}`;
|
|
264
264
|
}
|
|
265
|
+
/** A lookup skipped because the field its collection path is built from did not resolve. */
|
|
266
|
+
export function notTriedLine(field, by, value, prerequisite) {
|
|
267
|
+
return `${field}: ${by}=${pyRepr(value)} not tried — its collection is addressed through ${pyRepr(prerequisite)}, which did not resolve.`;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Every miss of one `--resolve` run, then what DID resolve. Nothing is recorded
|
|
271
|
+
* when anything misses, and the reader needs both halves to fix every value in
|
|
272
|
+
* one pass rather than one per run.
|
|
273
|
+
*/
|
|
274
|
+
export function incompleteRunMessage(misses, notTried, answered, requested) {
|
|
275
|
+
const failed = misses.length + notTried.length;
|
|
276
|
+
return (`${failed} of ${requested} lookup(s) did not resolve, so nothing was recorded:\n` +
|
|
277
|
+
[...misses, ...notTried].map((line) => ` ${line}`).join("\n") +
|
|
278
|
+
(answered.length > 0
|
|
279
|
+
? `\nThese resolved, and are recorded only once every lookup of the run does:\n${answered.join("\n")}`
|
|
280
|
+
: ""));
|
|
281
|
+
}
|
|
265
282
|
export function wroteResolutionsLine(count, ledgerPath) {
|
|
266
283
|
return ` wrote ${count} resolution(s) to ${ledgerPath}`;
|
|
267
284
|
}
|
|
@@ -163,7 +163,10 @@ export async function prepareConnector(options, ports) {
|
|
|
163
163
|
connectionId: connected.connectionId,
|
|
164
164
|
connection: options.connection,
|
|
165
165
|
pinned,
|
|
166
|
-
}, error.plan,
|
|
166
|
+
}, error.plan,
|
|
167
|
+
// The wrapper, not `error.first`: same facts, and its message names every
|
|
168
|
+
// miss of the run rather than only the first.
|
|
169
|
+
error);
|
|
167
170
|
// Undo the bindings this run wrote for the connection that cannot serve
|
|
168
171
|
// the lookups, then start over pinned to the one that can. Re-running
|
|
169
172
|
// rather than switching in place is deliberate: the overlay and its
|
|
@@ -36,7 +36,21 @@ export interface LookupExhaustedFacts {
|
|
|
36
36
|
pages: number;
|
|
37
37
|
/** True when a cap, not the collection's end, stopped the scan. */
|
|
38
38
|
capped: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Every distinct value the scan saw at `by`, when the whole collection was
|
|
41
|
+
* read and holds at most {@link CANDIDATE_LIMIT} records — what to pick from.
|
|
42
|
+
*/
|
|
43
|
+
candidates?: readonly string[];
|
|
44
|
+
/** A value the scan saw that equals the wanted one ignoring case; the match itself is exact. */
|
|
45
|
+
near?: string;
|
|
39
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A collection this small is listed in full when a value is not in it. The
|
|
49
|
+
* 2026-09-23 run guessed `send_as` 'Bot token' from the field's description
|
|
50
|
+
* while `available_tokens` held two records, 'Bot' and 'User', and the refusal
|
|
51
|
+
* said only "scanned 2 record(s)".
|
|
52
|
+
*/
|
|
53
|
+
export declare const CANDIDATE_LIMIT = 20;
|
|
40
54
|
/**
|
|
41
55
|
* The collection was read to the end on THIS connection and had no match
|
|
42
56
|
* (`_resolve.LookupExhausted`).
|
|
@@ -63,6 +77,8 @@ export declare class LookupExhausted extends ResolutionRefusal {
|
|
|
63
77
|
readonly scanned: number;
|
|
64
78
|
readonly pages: number;
|
|
65
79
|
readonly capped: boolean;
|
|
80
|
+
readonly candidates?: readonly string[];
|
|
81
|
+
readonly near?: string;
|
|
66
82
|
constructor(message: string, facts: LookupExhaustedFacts);
|
|
67
83
|
}
|
|
68
84
|
/** One `--resolve <field>:<by>=<value>`, parsed. */
|
|
@@ -97,8 +113,17 @@ type SpecTable = ReadonlyMap<string, ResolveSpec> | Readonly<Record<string, Reso
|
|
|
97
113
|
* unsubstituted `{placeholder}` in a URL, and the error the connector returns
|
|
98
114
|
* for that names neither field.
|
|
99
115
|
*
|
|
100
|
-
* A field requested
|
|
101
|
-
*
|
|
116
|
+
* A field requested more than once keeps its first position and EVERY distinct
|
|
117
|
+
* `by`/`value`, in the order given; an exact repeat collapses. DELIBERATE
|
|
118
|
+
* DIVERGENCE FROM THE PYTHON, whose dict kept one request per field — its LAST
|
|
119
|
+
* `by`/`value` — and dropped the rest without a word. That is fatal for a
|
|
120
|
+
* multiselect: `--resolve users[*]:…=A --resolve users[*]:…=B --resolve
|
|
121
|
+
* users[*]:…=C`, one command as the library page prints it, resolved and
|
|
122
|
+
* recorded only C (2026-09-23, skill-flow-ipe-multiselect), and the probe of
|
|
123
|
+
* the other connections asked only about C too.
|
|
124
|
+
*
|
|
125
|
+
* A PREREQUISITE given more than one value is refused: its id is substituted
|
|
126
|
+
* into the dependent's collection path, and one placeholder takes one id.
|
|
102
127
|
*/
|
|
103
128
|
export declare function orderResolutions(requested: readonly ResolveRequest[], specs: SpecTable): ResolveRequest[];
|
|
104
129
|
/**
|
|
@@ -50,6 +50,13 @@ export class ResolutionRefusal extends Refusal {
|
|
|
50
50
|
this.name = "ResolutionRefusal";
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* A collection this small is listed in full when a value is not in it. The
|
|
55
|
+
* 2026-09-23 run guessed `send_as` 'Bot token' from the field's description
|
|
56
|
+
* while `available_tokens` held two records, 'Bot' and 'User', and the refusal
|
|
57
|
+
* said only "scanned 2 record(s)".
|
|
58
|
+
*/
|
|
59
|
+
export const CANDIDATE_LIMIT = 20;
|
|
53
60
|
/**
|
|
54
61
|
* The collection was read to the end on THIS connection and had no match
|
|
55
62
|
* (`_resolve.LookupExhausted`).
|
|
@@ -76,6 +83,8 @@ export class LookupExhausted extends ResolutionRefusal {
|
|
|
76
83
|
scanned;
|
|
77
84
|
pages;
|
|
78
85
|
capped;
|
|
86
|
+
candidates;
|
|
87
|
+
near;
|
|
79
88
|
constructor(message, facts) {
|
|
80
89
|
super(message, "RESOLVE_NO_MATCH");
|
|
81
90
|
this.name = "LookupExhausted";
|
|
@@ -86,6 +95,10 @@ export class LookupExhausted extends ResolutionRefusal {
|
|
|
86
95
|
this.scanned = facts.scanned;
|
|
87
96
|
this.pages = facts.pages;
|
|
88
97
|
this.capped = facts.capped;
|
|
98
|
+
if (facts.candidates !== undefined)
|
|
99
|
+
this.candidates = facts.candidates;
|
|
100
|
+
if (facts.near !== undefined)
|
|
101
|
+
this.near = facts.near;
|
|
89
102
|
}
|
|
90
103
|
}
|
|
91
104
|
/**
|
|
@@ -123,13 +136,27 @@ function specOf(specs, field) {
|
|
|
123
136
|
* unsubstituted `{placeholder}` in a URL, and the error the connector returns
|
|
124
137
|
* for that names neither field.
|
|
125
138
|
*
|
|
126
|
-
* A field requested
|
|
127
|
-
*
|
|
139
|
+
* A field requested more than once keeps its first position and EVERY distinct
|
|
140
|
+
* `by`/`value`, in the order given; an exact repeat collapses. DELIBERATE
|
|
141
|
+
* DIVERGENCE FROM THE PYTHON, whose dict kept one request per field — its LAST
|
|
142
|
+
* `by`/`value` — and dropped the rest without a word. That is fatal for a
|
|
143
|
+
* multiselect: `--resolve users[*]:…=A --resolve users[*]:…=B --resolve
|
|
144
|
+
* users[*]:…=C`, one command as the library page prints it, resolved and
|
|
145
|
+
* recorded only C (2026-09-23, skill-flow-ipe-multiselect), and the probe of
|
|
146
|
+
* the other connections asked only about C too.
|
|
147
|
+
*
|
|
148
|
+
* A PREREQUISITE given more than one value is refused: its id is substituted
|
|
149
|
+
* into the dependent's collection path, and one placeholder takes one id.
|
|
128
150
|
*/
|
|
129
151
|
export function orderResolutions(requested, specs) {
|
|
130
152
|
const byField = new Map();
|
|
131
|
-
for (const request of requested)
|
|
132
|
-
byField.
|
|
153
|
+
for (const request of requested) {
|
|
154
|
+
const same = byField.get(request[0]);
|
|
155
|
+
if (same === undefined)
|
|
156
|
+
byField.set(request[0], [request]);
|
|
157
|
+
else if (!same.some(([, by, value]) => by === request[1] && value === request[2]))
|
|
158
|
+
same.push(request);
|
|
159
|
+
}
|
|
133
160
|
const ordered = [];
|
|
134
161
|
const placed = new Set();
|
|
135
162
|
const place = (field, trail) => {
|
|
@@ -141,7 +168,14 @@ export function orderResolutions(requested, specs) {
|
|
|
141
168
|
}
|
|
142
169
|
const dependsOn = specOf(specs, field)?.dependsOn;
|
|
143
170
|
for (const prerequisite of Array.isArray(dependsOn) ? dependsOn : []) {
|
|
144
|
-
|
|
171
|
+
const given = byField.get(prerequisite);
|
|
172
|
+
if (given !== undefined && given.length > 1) {
|
|
173
|
+
throw new ResolutionRefusal(`cannot resolve ${pyRepr(field)}: its collection is addressed through ` +
|
|
174
|
+
`${pyRepr(prerequisite)}, which was given ${given.length} values ` +
|
|
175
|
+
`(${given.map(([, by, value]) => `${by}=${pyRepr(value)}`).join(", ")}). ` +
|
|
176
|
+
`The path takes one id; keep one --resolve ${prerequisite}:<by>=<value> and re-run.`, "RESOLVE_AMBIGUOUS_PREREQUISITE");
|
|
177
|
+
}
|
|
178
|
+
if (given !== undefined) {
|
|
145
179
|
place(prerequisite, [...trail, field]);
|
|
146
180
|
}
|
|
147
181
|
else {
|
|
@@ -151,7 +185,7 @@ export function orderResolutions(requested, specs) {
|
|
|
151
185
|
}
|
|
152
186
|
}
|
|
153
187
|
placed.add(field);
|
|
154
|
-
ordered.push(byField.get(field));
|
|
188
|
+
ordered.push(...byField.get(field));
|
|
155
189
|
};
|
|
156
190
|
for (const [field] of requested)
|
|
157
191
|
place(field, []);
|
|
@@ -433,9 +467,21 @@ export async function resolveOne(client, connectorKey, spec, field, by, value, c
|
|
|
433
467
|
query = query ? `${query}&${filtered}` : filtered;
|
|
434
468
|
}
|
|
435
469
|
const scan = scanCollection(client, connectorKey, collection, connectionId, query);
|
|
470
|
+
// What the collection DOES hold at `by`, for the refusal: kept only while it
|
|
471
|
+
// could still be listed in full, plus the first case-insensitive equal.
|
|
472
|
+
const held = [];
|
|
473
|
+
let near;
|
|
436
474
|
let step = await scan.next();
|
|
437
475
|
while (!step.done) {
|
|
438
476
|
for (const record of step.value) {
|
|
477
|
+
const at = readPath(record, by);
|
|
478
|
+
if (at !== null && at !== undefined) {
|
|
479
|
+
const text = pyStrAny(at);
|
|
480
|
+
if (held.length <= CANDIDATE_LIMIT && !held.includes(text))
|
|
481
|
+
held.push(text);
|
|
482
|
+
if (near === undefined && text !== value && text.toLowerCase() === value.toLowerCase())
|
|
483
|
+
near = text;
|
|
484
|
+
}
|
|
439
485
|
if (matchRecord(record, by, value)) {
|
|
440
486
|
const got = readPath(record, send);
|
|
441
487
|
if (got === null) {
|
|
@@ -463,8 +509,24 @@ export async function resolveOne(client, connectorKey, spec, field, by, value, c
|
|
|
463
509
|
// The listing command is still offered, once, by the caller's terminal
|
|
464
510
|
// report — where every connection has been tried and seeing what IS there
|
|
465
511
|
// is finally the next step rather than a detour.
|
|
512
|
+
//
|
|
513
|
+
// What it DOES carry is what the collection holds, when that is small enough
|
|
514
|
+
// to read in one line, and a case-only near miss: facts about this
|
|
515
|
+
// connection, not a remedy. `matchRecord` stays exact — nothing is picked.
|
|
516
|
+
//
|
|
517
|
+
// Never under a server-side filter: the scan then read only the records the
|
|
518
|
+
// filter returned (`name like '%Coder Eval%'` gives 'Coder Evals' and 'Coder
|
|
519
|
+
// Eval Test'), and listing that subset as what the collection "holds" would
|
|
520
|
+
// be false. 693 library operations declare a `filterPattern`.
|
|
521
|
+
const candidates = !capped && !pyTruthy(filterPattern) && scanned <= CANDIDATE_LIMIT && held.length > 0 ? held : undefined;
|
|
466
522
|
throw new LookupExhausted(`${field}: no record in ${pyRepr(collection)} has ${by}=${pyRepr(value)} ` +
|
|
467
523
|
`(scanned ${scanned} record(s) across ${pages + 1} page(s)` +
|
|
468
524
|
(capped ? `; STOPPED AT THE ${MAX_SCANNED}-record cap, so the value may be further in` : "; collection exhausted") +
|
|
469
|
-
`) on connection ${connectionId}
|
|
525
|
+
`) on connection ${connectionId}.` +
|
|
526
|
+
(near !== undefined ? ` Did you mean ${pyRepr(near)}? The match is exact, case included.` : "") +
|
|
527
|
+
(candidates !== undefined ? ` It holds: ${candidates.map(pyRepr).join(", ")}.` : ""), {
|
|
528
|
+
field, collection, by, value, scanned, pages: pages + 1, capped,
|
|
529
|
+
...(candidates !== undefined ? { candidates } : {}),
|
|
530
|
+
...(near !== undefined ? { near } : {}),
|
|
531
|
+
});
|
|
470
532
|
}
|
package/dist/serialize.js
CHANGED
|
@@ -3474,8 +3474,18 @@ function eventDetail(rc, sub, nodeId, opts) {
|
|
|
3474
3474
|
errorState: { issues: [] },
|
|
3475
3475
|
};
|
|
3476
3476
|
}
|
|
3477
|
-
/**
|
|
3477
|
+
/**
|
|
3478
|
+
* Recursively render Exprs nested inside objects/arrays (for connector inputs).
|
|
3479
|
+
*
|
|
3480
|
+
* A lookup token is REFUSED, never rendered. `buildConnectorInputs` swaps every
|
|
3481
|
+
* token it can place — a field's whole value, or an element of an array — so a
|
|
3482
|
+
* token that reaches here sits where nothing resolves it: inside an object value,
|
|
3483
|
+
* or in a custom node's inputs. Recursing into it would put `{"__lookup": …}` on
|
|
3484
|
+
* the wire where the connector expects an id.
|
|
3485
|
+
*/
|
|
3478
3486
|
function renderDeep(val, rename = NO_RENAME) {
|
|
3487
|
+
if (isLookupToken(val))
|
|
3488
|
+
throw new Error(misplacedLookupMessage(val));
|
|
3479
3489
|
if (val instanceof Expr)
|
|
3480
3490
|
return renderValue(val, rename);
|
|
3481
3491
|
if (Array.isArray(val))
|
|
@@ -3488,6 +3498,13 @@ function renderDeep(val, rename = NO_RENAME) {
|
|
|
3488
3498
|
}
|
|
3489
3499
|
return val;
|
|
3490
3500
|
}
|
|
3501
|
+
/** Why {@link renderDeep} refused a token: where a lookup CAN go, and what it would have emitted. */
|
|
3502
|
+
function misplacedLookupMessage(token) {
|
|
3503
|
+
const { field, by, value } = token.__lookup;
|
|
3504
|
+
return (`lookup('${field}') (${by}=${JSON.stringify(value)}) is in a position no lookup is resolved from: ` +
|
|
3505
|
+
`a lookup() must be the whole value of a connector input, or an element of an array input. ` +
|
|
3506
|
+
`Emitting it here would send a raw {"__lookup": …} object where the connector expects an id.`);
|
|
3507
|
+
}
|
|
3491
3508
|
function newScope(outputs, vars = []) {
|
|
3492
3509
|
return {
|
|
3493
3510
|
nodes: [],
|
|
@@ -6118,16 +6135,35 @@ function buildConnectorInputs(rc, inputs, rename = NO_RENAME, resolutions = {},
|
|
|
6118
6135
|
// NOT rendered as a placeholder: emitting something for a value nobody
|
|
6119
6136
|
// resolved would produce a flow that compiles and then talks to the wrong
|
|
6120
6137
|
// record, which is exactly the failure the lookup surface exists to remove.
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6138
|
+
//
|
|
6139
|
+
// A token is swapped wherever the field's value can hold one: the whole
|
|
6140
|
+
// value, or an element of an array at any depth. A multiselect is
|
|
6141
|
+
// `users: [lookup(…), lookup(…)]` — the form `ConnectorLookupValue`'s array
|
|
6142
|
+
// branch types and the generated `## Example` prints — and until this swapped
|
|
6143
|
+
// only the whole-value case, so every token inside the array reached
|
|
6144
|
+
// validation as an object ("expected string, got object") with every lookup
|
|
6145
|
+
// recorded. An unresolved element under 'ignore' becomes `null`, which
|
|
6146
|
+
// validation skips, so `check` still type-checks the resolved siblings.
|
|
6147
|
+
const unresolved = [];
|
|
6148
|
+
const swapped = swapLookupTokens(nested.value, resolutions, unresolved);
|
|
6149
|
+
if (unresolved.length) {
|
|
6150
|
+
if (policy.unresolvedLookups !== 'ignore') {
|
|
6151
|
+
for (const token of unresolved)
|
|
6152
|
+
errs.push(unresolvedLookupMessage(token));
|
|
6126
6153
|
continue;
|
|
6127
6154
|
}
|
|
6128
|
-
|
|
6155
|
+
if (isLookupToken(nested.value))
|
|
6156
|
+
continue;
|
|
6129
6157
|
}
|
|
6130
|
-
|
|
6158
|
+
nested = { ...nested, value: swapped };
|
|
6159
|
+
// A FLAT key whose wire name ends in `[*]` names one array: an array value
|
|
6160
|
+
// there is checked element by element, as the nested form is. A flat scalar
|
|
6161
|
+
// keeps depth 0 and is emitted as the scalar it is — the product wraps it
|
|
6162
|
+
// into a one-element list (flow-v1 `convertDotNotationToNested`).
|
|
6163
|
+
const depth = flat
|
|
6164
|
+
? (f.name.endsWith('[*]') && Array.isArray(nested.value) ? 1 : 0)
|
|
6165
|
+
: nested.projectedArrays;
|
|
6166
|
+
validateConnectorFieldValue(f, nested.value, depth, errs);
|
|
6131
6167
|
if (f.location === 'multipart') {
|
|
6132
6168
|
partValues.set(f.name, renderDeep(nested.value, rename));
|
|
6133
6169
|
continue;
|
|
@@ -6195,6 +6231,29 @@ function buildConnectorInputs(rc, inputs, rename = NO_RENAME, resolutions = {},
|
|
|
6195
6231
|
}
|
|
6196
6232
|
return slots;
|
|
6197
6233
|
}
|
|
6234
|
+
/**
|
|
6235
|
+
* `value` with every lookup token swapped for its recorded id: the value itself,
|
|
6236
|
+
* or the elements of an array at any depth. An unrecorded token is pushed to
|
|
6237
|
+
* `unresolved` and stands as `null`, so an array keeps its length.
|
|
6238
|
+
*
|
|
6239
|
+
* Objects are NOT descended: a lookup's place is a field's value or an element
|
|
6240
|
+
* of an array field (`ConnectorLookupValue`), and a nested path such as
|
|
6241
|
+
* `contacts[*].id` reaches here already projected to its leaf array. A token
|
|
6242
|
+
* anywhere else is refused by {@link renderDeep}.
|
|
6243
|
+
*/
|
|
6244
|
+
function swapLookupTokens(value, resolutions, unresolved) {
|
|
6245
|
+
if (isLookupToken(value)) {
|
|
6246
|
+
const resolved = resolvedValue(value, resolutions);
|
|
6247
|
+
if (resolved === undefined) {
|
|
6248
|
+
unresolved.push(value);
|
|
6249
|
+
return null;
|
|
6250
|
+
}
|
|
6251
|
+
return resolved;
|
|
6252
|
+
}
|
|
6253
|
+
if (Array.isArray(value))
|
|
6254
|
+
return value.map((item) => swapLookupTokens(item, resolutions, unresolved));
|
|
6255
|
+
return value;
|
|
6256
|
+
}
|
|
6198
6257
|
/**
|
|
6199
6258
|
* `inputMetadata.multipart.bodyFieldName` names the CONTAINER part — the one the
|
|
6200
6259
|
* runtime composes from `bodyParameters`. Measured on the designer's own nodes in
|