@uipath/maestro-builder-sdk 6.14.4 → 6.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-index.md +129 -129
- package/dist/api-members.md +420 -416
- package/dist/bpmn/bpmn-sdk.d.ts +16 -0
- package/dist/bpmn/bpmn-sdk.js +2 -0
- package/dist/bpmn/check-cli.js +22 -5
- package/dist/bpmn/check.d.ts +15 -1
- package/dist/bpmn/check.js +69 -2
- package/dist/bpmn/compile-cli.js +24 -14
- package/dist/bpmn/connector.js +29 -14
- package/dist/bpmn/intsvc.js +5 -2
- package/dist/bpmn/serialize.d.ts +2 -0
- package/dist/bpmn/serialize.js +1 -1
- package/dist/case/compile-cli.js +6 -9
- package/dist/case/serialize.js +1 -1
- package/dist/check-cli.js +6 -35
- package/dist/check.d.ts +16 -148
- package/dist/check.js +53 -711
- package/dist/cli/commands/bpmn.js +15 -0
- package/dist/cli-library.d.ts +6 -25
- package/dist/cli-library.js +6 -73
- package/dist/compile-cli.js +14 -8
- package/dist/core/actions.d.ts +19 -0
- package/dist/core/cli-library.d.ts +47 -0
- package/dist/core/cli-library.js +150 -0
- package/dist/core/connector-checks.d.ts +226 -0
- package/dist/core/connector-checks.js +518 -0
- package/dist/core/connector-inputs.d.ts +80 -0
- package/dist/core/connector-inputs.js +453 -0
- package/dist/core/event-checks.d.ts +137 -0
- package/dist/core/event-checks.js +321 -0
- package/dist/decompile.js +1 -1
- package/dist/registry/generators/markdown/sections/example.d.ts +7 -1
- package/dist/registry/generators/markdown/sections/example.js +70 -5
- package/dist/registry/generators/markdown/sections/lookups.js +29 -8
- package/dist/registry/prepare/messages.js +6 -1
- package/dist/registry/prepare/source-import.d.ts +3 -3
- package/dist/registry/prepare/source-import.js +15 -4
- package/dist/registry/prepare/source-labels.js +31 -4
- package/dist/serialize.d.ts +11 -11
- package/dist/serialize.js +285 -428
- package/lib/README.md +1 -0
- package/lib/library-json.samples/index.json +9 -0
- package/lib/library-json.samples/uipath-uipath-airdk/web-search@1.0.0.json +118 -0
- package/lib/library-json.samples/uipath-uipath-airdk/web-search@1.0.0.v1def.json +258 -0
- package/package.json +2 -2
- /package/dist/{event-filters.d.ts → core/event-filters.d.ts} +0 -0
- /package/dist/{event-filters.js → core/event-filters.js} +0 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/connector-checks — the check rules whose facts live in the connector
|
|
3
|
+
* library, shared by every family that authors a connector activity.
|
|
4
|
+
*
|
|
5
|
+
* These were Flow's, and they are the reason Flow's `check` can say "run THIS
|
|
6
|
+
* prepare" in the loop the author is already in. BPMN had none of them: its
|
|
7
|
+
* `check` took no options at all, so a BPMN connector node reached `compile`
|
|
8
|
+
* with an unresolved lookup, an unprepared object or a self-naming binding and
|
|
9
|
+
* learned about it from `uip maestro bpmn validate`, a tenant, or a faulted run.
|
|
10
|
+
* (`flow-v2-run-8`: `skill-bpmn-e2e-customer-escalation-triage` packed
|
|
11
|
+
* `bindings_v2.json` carrying the literal strings `jiraConnection` and
|
|
12
|
+
* `slackConnection` where connection ids belong.)
|
|
13
|
+
*
|
|
14
|
+
* Nothing here knows what a step or a task is. A caller supplies:
|
|
15
|
+
*
|
|
16
|
+
* - `where` — how the message opens (`Connector step "post"`,
|
|
17
|
+
* `Connector task "post"`), so each family keeps its own vocabulary;
|
|
18
|
+
* - `report` — where the diagnostic goes, so each family adds its own locator
|
|
19
|
+
* field (Flow's `step`, BPMN's `where`);
|
|
20
|
+
* - `dialect` — how an authored value reads, because Flow writes expressions as
|
|
21
|
+
* `Expr` objects over `$vars` and BPMN writes them as `=`-prefixed strings
|
|
22
|
+
* over `vars`. Without this a BPMN `'=vars.channelId'` would read as a pasted
|
|
23
|
+
* literal id and warn about itself.
|
|
24
|
+
*/
|
|
25
|
+
import { isLookupToken, resolvedValue, } from './lookups.js';
|
|
26
|
+
import { Expr } from './expr.js';
|
|
27
|
+
import { extractRefs } from './expr-check.js';
|
|
28
|
+
import { prepareCommand, resolveFlagArg } from './cli-spelling.js';
|
|
29
|
+
import { bindingSelfNameMessage, bindingUndeclaredMessage, connectionConnectorConflictMessage, undeclaredBinding, } from './binding-messages.js';
|
|
30
|
+
/**
|
|
31
|
+
* Flow: an input is an {@link Expr} object. A non-literal one is runtime; a
|
|
32
|
+
* literal one (`lit(…)`) carries the constant it was built from.
|
|
33
|
+
*/
|
|
34
|
+
export const FLOW_EXPR_DIALECT = {
|
|
35
|
+
namespace: '$vars',
|
|
36
|
+
isOpaque: (value) => value instanceof Expr,
|
|
37
|
+
descriptors: true,
|
|
38
|
+
classify(value) {
|
|
39
|
+
if (value instanceof Expr) {
|
|
40
|
+
return value.literal ? { kind: 'literal', value: value.value } : { kind: 'runtime', js: value.js };
|
|
41
|
+
}
|
|
42
|
+
if (value !== null && typeof value === 'object')
|
|
43
|
+
return { kind: 'container' };
|
|
44
|
+
return { kind: 'literal', value };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* BPMN and Case: an expression is a string that starts with `=`.
|
|
49
|
+
*
|
|
50
|
+
* `=js:…` is the same thing with an evaluator prefix; both reduce to the source
|
|
51
|
+
* `extractRefs` reads. A string that does NOT start with `=` is a constant, which
|
|
52
|
+
* is exactly what `LOOKUP_LITERAL_ID` is about.
|
|
53
|
+
*/
|
|
54
|
+
export const EQUALS_EXPR_DIALECT = {
|
|
55
|
+
namespace: 'vars',
|
|
56
|
+
isOpaque: (value) => typeof value === 'string' && value.startsWith('='),
|
|
57
|
+
descriptors: false,
|
|
58
|
+
classify(value) {
|
|
59
|
+
if (typeof value === 'string' && value.startsWith('=')) {
|
|
60
|
+
return { kind: 'runtime', js: value.slice(1).replace(/^js:/, '') };
|
|
61
|
+
}
|
|
62
|
+
if (value !== null && typeof value === 'object')
|
|
63
|
+
return { kind: 'container' };
|
|
64
|
+
return { kind: 'literal', value };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The lookup rules: a pasted id, and an unresolved token.
|
|
69
|
+
*
|
|
70
|
+
* These are the strongest discoverability route in the design, because they
|
|
71
|
+
* need no prior knowledge and arrive in the loop the author is already in. An
|
|
72
|
+
* author who pastes `WT9RC72KU` into a lookup field is not being careless, they
|
|
73
|
+
* are being uninformed — nothing anywhere told them the field was resolvable —
|
|
74
|
+
* and the fix is to tell them at the point of the mistake.
|
|
75
|
+
*
|
|
76
|
+
* Both are WARNINGS, not errors. A literal id is legal and sometimes correct
|
|
77
|
+
* (a value that genuinely does not move), and the unresolved case is already a
|
|
78
|
+
* hard failure at compile, where the same message is repeated. Making it an
|
|
79
|
+
* error here would stop a `check` that is otherwise clean from telling the
|
|
80
|
+
* author about everything ELSE that is wrong, which is the whole point of
|
|
81
|
+
* running the cheap pass first.
|
|
82
|
+
*/
|
|
83
|
+
export function checkConnectorLookups(where, spec, opts, dialect, report) {
|
|
84
|
+
const specs = opts.lookups?.(spec.key, spec.action, spec.version, spec.object);
|
|
85
|
+
const resolutions = opts.resolutions ?? {};
|
|
86
|
+
// One LOOKUP_LITERAL_ID per field: a multiselect of three pasted ids is one
|
|
87
|
+
// mistake with one remedy, not three.
|
|
88
|
+
const literalWarned = new Set();
|
|
89
|
+
// ...and ONE diagnostic per NODE for both families, not one per field.
|
|
90
|
+
//
|
|
91
|
+
// The remedy is identical for every field of a node — the same `registry
|
|
92
|
+
// prepare`, which takes every `--resolve` in one invocation, exactly as the
|
|
93
|
+
// operation's own reference page prints it. Emitting it per field repeated a
|
|
94
|
+
// five-line block with two full commands for each one: a Slack send with a
|
|
95
|
+
// channel and a `send_as` produced ten such blocks across a run, and the
|
|
96
|
+
// `integration_service_boundary` eval card went from 70 turns to 86 and from
|
|
97
|
+
// 0/3 to 3/3 `max_turns_exhausted` when BPMN started reporting them at all.
|
|
98
|
+
const unresolved = [];
|
|
99
|
+
const literals = [];
|
|
100
|
+
const visit = (value, path) => {
|
|
101
|
+
if (isLookupToken(value)) {
|
|
102
|
+
// A match key the field does not declare is certainly wrong, and the
|
|
103
|
+
// spec that says so is already in hand. `fields.reporter.id` is searchable
|
|
104
|
+
// by `displayName | accountId`; `.by('name', …)` used to pass here as an
|
|
105
|
+
// ordinary unresolved lookup, and the prepare command this warning prints
|
|
106
|
+
// would then be REFUSED by the resolver — the mistake surviving both the
|
|
107
|
+
// check and its own suggested fix. `spec.by` is the field's own
|
|
108
|
+
// declaration, so it is complete by construction and may hard-error.
|
|
109
|
+
const named = path.join('.');
|
|
110
|
+
const declared = specs?.[named];
|
|
111
|
+
const by = value.__lookup.by;
|
|
112
|
+
if (declared && typeof by === 'string' && !declared.by.includes(by)) {
|
|
113
|
+
const { target, call } = lookupPhrases(named, declared, spec, dialect.descriptors);
|
|
114
|
+
report({
|
|
115
|
+
level: 'error',
|
|
116
|
+
code: 'LOOKUP_UNKNOWN_MATCH_KEY',
|
|
117
|
+
message: `${where}: \`${named}\` has no match key \`${by}\`. It resolves `
|
|
118
|
+
+ `against \`${target}\`, which is searchable by `
|
|
119
|
+
+ `${declared.by.map((b) => `\`${b}\``).join(' or ')}. Resolving on `
|
|
120
|
+
+ `\`${by}\` is refused, so recording it would fail too:\n`
|
|
121
|
+
+ ` ${call}`,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (resolvedValue(value, resolutions) === undefined)
|
|
126
|
+
unresolved.push(value);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const reading = dialect.classify(value);
|
|
130
|
+
if (reading.kind === 'container') {
|
|
131
|
+
if (Array.isArray(value)) {
|
|
132
|
+
// An element is named by the WIRE name the lookup table is keyed on —
|
|
133
|
+
// `users[*]`, `contacts[*].id` — not by its index. `users.0` matched no
|
|
134
|
+
// spec, so a pasted id, a token with a bad match key, or a runtime value
|
|
135
|
+
// inside a multiselect passed every rule below unseen. A name that already
|
|
136
|
+
// is the array's (a flat `'users[*]'` key, or a field typed as an array)
|
|
137
|
+
// stays as it is.
|
|
138
|
+
const last = path[path.length - 1] ?? '';
|
|
139
|
+
const element = last.endsWith('[*]') || specs?.[path.join('.')]
|
|
140
|
+
? path
|
|
141
|
+
: [...path.slice(0, -1), `${last}[*]`];
|
|
142
|
+
value.forEach((v) => visit(v, element));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (const [k, v] of Object.entries(value)) {
|
|
146
|
+
visit(v, [...path, k]);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!specs)
|
|
151
|
+
return;
|
|
152
|
+
const field = path.join('.');
|
|
153
|
+
const lookup = specs[field];
|
|
154
|
+
if (!lookup)
|
|
155
|
+
return;
|
|
156
|
+
if (reading.kind === 'runtime') {
|
|
157
|
+
const d = runtimeLookupDiagnostic(where, spec, field, lookup, reading.js, dialect);
|
|
158
|
+
if (d)
|
|
159
|
+
report(d);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// A pasted id, whether bare or wrapped in `lit()`.
|
|
163
|
+
const literal = reading.value;
|
|
164
|
+
if (typeof literal !== 'string')
|
|
165
|
+
return;
|
|
166
|
+
if (literalWarned.has(field))
|
|
167
|
+
return;
|
|
168
|
+
literalWarned.add(field);
|
|
169
|
+
literals.push({ field, lookup });
|
|
170
|
+
};
|
|
171
|
+
for (const [name, value] of Object.entries(spec.inputs ?? {}))
|
|
172
|
+
visit(value, [name]);
|
|
173
|
+
if (unresolved.length)
|
|
174
|
+
report(unresolvedDiagnostic(where, spec, unresolved));
|
|
175
|
+
if (literals.length)
|
|
176
|
+
report(literalIdDiagnostic(where, spec, literals, dialect.descriptors));
|
|
177
|
+
}
|
|
178
|
+
/** `--resolve a:by=<v> \` … — every field of one node, in one pasteable command. */
|
|
179
|
+
function resolveBlock(spec, args) {
|
|
180
|
+
const head = ` ${prepareCommand(spec.key, spec.action)}`;
|
|
181
|
+
if (args.length === 1)
|
|
182
|
+
return `${head} \\\n --resolve ${args[0]}`;
|
|
183
|
+
return [head + ' \\', ...args.map((a, i) => ` --resolve ${a}${i < args.length - 1 ? ' \\' : ''}`)].join('\n');
|
|
184
|
+
}
|
|
185
|
+
/** Every unresolved token on one node, with the single prepare that records them all. */
|
|
186
|
+
function unresolvedDiagnostic(where, spec, tokens) {
|
|
187
|
+
const named = tokens.map((t) => `\`${t.__lookup.field}\` (${t.__lookup.by}=${JSON.stringify(t.__lookup.value)})`);
|
|
188
|
+
const args = tokens.map((t) => resolveFlagArg(t.__lookup.field, t.__lookup.by, String(t.__lookup.value)));
|
|
189
|
+
return {
|
|
190
|
+
level: 'warning',
|
|
191
|
+
code: 'LOOKUP_UNRESOLVED',
|
|
192
|
+
message: `${where}: ${named.length === 1 ? 'input' : 'inputs'} ${named.join(', ')} `
|
|
193
|
+
+ `${named.length === 1 ? 'is an' : 'are'} unresolved lookup${named.length === 1 ? '' : 's'}. `
|
|
194
|
+
+ `Compile does not make network calls, so the value has to be resolved first:\n`
|
|
195
|
+
+ resolveBlock(spec, args)
|
|
196
|
+
+ `\nCompile will fail until ${named.length === 1 ? 'this is' : 'these are'} recorded. Running it now, `
|
|
197
|
+
+ `before compiling, is the cheap order: this pass has already found everything else that is wrong.`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Every pasted lookup value on one node.
|
|
202
|
+
*
|
|
203
|
+
* The JUSTIFICATION here is the one the operation's reference page settled on
|
|
204
|
+
* (#706) and this message had not caught up with. It used to assert the value
|
|
205
|
+
* is an opaque, connection-specific id — false for 320 of the library's 2,563
|
|
206
|
+
* lookup rows, and false for a field it fires on constantly: Slack's `send_as`
|
|
207
|
+
* sends the word `bot`, which is the same word on every connection. Nothing in
|
|
208
|
+
* the metadata says which case a field is in, so the id claim is stated as the
|
|
209
|
+
* CONDITION it always was, and the value each field sends is named so the
|
|
210
|
+
* reader can tell which case theirs is.
|
|
211
|
+
*/
|
|
212
|
+
function literalIdDiagnostic(where, spec, rows, descriptors) {
|
|
213
|
+
const sends = rows.map(({ field, lookup }) => `\`${field}\` sends \`${lookup.objectName ?? lookup.path}.${lookup.value}\``);
|
|
214
|
+
const calls = rows.map(({ field, lookup }) => ` ${lookupPhrases(field, lookup, spec, descriptors).call}`);
|
|
215
|
+
const args = rows.map(({ field, lookup }) => resolveFlagArg(field, lookup.by[0] ?? 'id', '<value>'));
|
|
216
|
+
return {
|
|
217
|
+
level: 'warning',
|
|
218
|
+
code: 'LOOKUP_LITERAL_ID',
|
|
219
|
+
message: `${where}: ${sends.join(', ')} — the value the connector defines, not the label `
|
|
220
|
+
+ `you know it by. A literal is CORRECT where that value is a fixed vocabulary `
|
|
221
|
+
+ `(\`bot\`, a country code), and where you have no connection to resolve against: `
|
|
222
|
+
+ `resolving is a live call, so a draft authored offline writes the literal and says so. `
|
|
223
|
+
+ `It is wrong only where the value is an opaque id, which is also connection-specific — `
|
|
224
|
+
+ `a pasted one resolves to nobody, or to somebody else, against a different connection. `
|
|
225
|
+
+ `If this is that case, resolve ${rows.length === 1 ? 'it' : 'them'} late:\n`
|
|
226
|
+
+ calls.join('\n')
|
|
227
|
+
+ `\nand record ${rows.length === 1 ? 'the value' : 'the values'} once, against a live connection:\n`
|
|
228
|
+
+ resolveBlock(spec, args),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The schema rules: an unprepared object, and unprepared custom fields.
|
|
233
|
+
*
|
|
234
|
+
* Same reasoning as the lookup rules above — the author needs no prior
|
|
235
|
+
* knowledge, and the diagnostic arrives in the loop they are already in, with
|
|
236
|
+
* the one command that discharges it. Both are WARNINGS: compile accepts a
|
|
237
|
+
* connection-scoped object (stamped `objectNameUnverified`) and passes the
|
|
238
|
+
* inputs through, so `check` is the one place the gap is visible BEFORE a
|
|
239
|
+
* process dispatches against a schema nothing validated.
|
|
240
|
+
*
|
|
241
|
+
* The custom-fields rule is deliberately narrower than "unprepared op": a step
|
|
242
|
+
* that uses only baked fields owes nothing, and warning on it anyway teaches
|
|
243
|
+
* the author that the warning is noise. It fires only for inputs OUTSIDE the
|
|
244
|
+
* tenant-agnostic snapshot — the moment hand-guessing actually starts.
|
|
245
|
+
*/
|
|
246
|
+
export function checkConnectorSchema(where, spec, opts, dialect, report) {
|
|
247
|
+
const readiness = opts.schemaReadiness?.(spec.key, spec.action, spec.version, spec.object);
|
|
248
|
+
if (!readiness || readiness.prepared)
|
|
249
|
+
return;
|
|
250
|
+
const prepare = prepareCommand(spec.key, spec.action);
|
|
251
|
+
if (readiness.kind === 'object') {
|
|
252
|
+
report({
|
|
253
|
+
level: 'warning',
|
|
254
|
+
code: 'OBJECT_UNPREPARED',
|
|
255
|
+
message: `${where}: object "${spec.object}" has no locally materialized schema — `
|
|
256
|
+
+ `this operation resolves its object against the connection, so its fields cannot be `
|
|
257
|
+
+ `validated here and compile will emit it unverified. Prepare it once:\n`
|
|
258
|
+
+ ` ${prepare} --object ${spec.object}\n`
|
|
259
|
+
+ `The name is matched against the connected catalog (exact, then case-insensitive on `
|
|
260
|
+
+ `name and display name); several matches fail with the candidates to choose from, and `
|
|
261
|
+
+ `the prepared descriptor is the import to switch to.`,
|
|
262
|
+
...(readiness.canonical ? { suggestion: readiness.canonical } : {}),
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (readiness.kind === 'customFields') {
|
|
267
|
+
// Match by full dotted path, the same descent `checkConnectorLookups`
|
|
268
|
+
// uses above — a top-level comparison would let `contact:
|
|
269
|
+
// { cf_custom: … }` slide because `contact` is a baked head, and a nested
|
|
270
|
+
// hand-guessed custom field is precisely the moment this warning is for.
|
|
271
|
+
// Wire-array markers are stripped from the baked names (`users[*]` is
|
|
272
|
+
// authored as `users`).
|
|
273
|
+
const baked = (readiness.bakedFields ?? []).map((name) => name.replace(/\[\*\]/g, ''));
|
|
274
|
+
const unknown = [];
|
|
275
|
+
const visit = (value, path) => {
|
|
276
|
+
const dotted = path.join('.');
|
|
277
|
+
// A baked field, or deeper than one — the name is in the vocabulary;
|
|
278
|
+
// whether the VALUE fits is compile's business, not this warning's.
|
|
279
|
+
if (baked.some((b) => b === dotted || dotted.startsWith(b + '.')))
|
|
280
|
+
return;
|
|
281
|
+
// The head of baked fields (`contact` when `contact.email` is baked):
|
|
282
|
+
// descend to find the precise gap underneath it. Array elements keep
|
|
283
|
+
// their parent's path — the baked names index arrays as `items[*].sku`,
|
|
284
|
+
// normalized above to `items.sku` — so a guessed field inside a list
|
|
285
|
+
// element (`lineItems: [{ cf_custom }]`) warns exactly like one under
|
|
286
|
+
// an object head.
|
|
287
|
+
if (baked.some((b) => b.startsWith(dotted + '.'))) {
|
|
288
|
+
if (Array.isArray(value)) {
|
|
289
|
+
for (const element of value)
|
|
290
|
+
visit(element, path);
|
|
291
|
+
}
|
|
292
|
+
else if (!isLookupToken(value) && dialect.classify(value).kind === 'container') {
|
|
293
|
+
for (const [k, v] of Object.entries(value)) {
|
|
294
|
+
visit(v, [...path, k]);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// Neither a baked name nor on the way to one — this IS the gap,
|
|
300
|
+
// reported once at its head.
|
|
301
|
+
unknown.push(dotted);
|
|
302
|
+
};
|
|
303
|
+
for (const [name, value] of Object.entries(spec.inputs ?? {}))
|
|
304
|
+
visit(value, [name]);
|
|
305
|
+
// Array elements share their parent's path, so one guessed field across
|
|
306
|
+
// many list rows is one gap, not one per row.
|
|
307
|
+
const distinct = [...new Set(unknown)];
|
|
308
|
+
if (distinct.length === 0)
|
|
309
|
+
return;
|
|
310
|
+
report({
|
|
311
|
+
level: 'warning',
|
|
312
|
+
code: 'CUSTOM_FIELDS_UNPREPARED',
|
|
313
|
+
message: `${where}: ${distinct.map((n) => `\`${n}\``).join(', ')} `
|
|
314
|
+
+ `${distinct.length === 1 ? 'is' : 'are'} not in the tenant-agnostic snapshot, and this `
|
|
315
|
+
+ `operation resolves its real field set — custom fields included — from a live `
|
|
316
|
+
+ `connection. Do not hand-guess the names; materialize the schema once:\n`
|
|
317
|
+
+ ` ${prepare}\n`
|
|
318
|
+
+ `then author against the prepared descriptor (its field names and doc comments are `
|
|
319
|
+
+ `the vocabulary) and re-run check.`,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The field contract and the binding names `compile` enforces, reported here first.
|
|
325
|
+
*
|
|
326
|
+
* Both rules run only with the facts in hand (a library for the fields, a
|
|
327
|
+
* bindings.json for the names); without them `check` stays a pure function of the
|
|
328
|
+
* built model and says nothing, as before.
|
|
329
|
+
*/
|
|
330
|
+
export function checkConnectorContract(where, spec, opts, report,
|
|
331
|
+
/** The activity's own id, for {@link connectionConnectorConflicts}; omit to record nothing. */
|
|
332
|
+
id) {
|
|
333
|
+
const inputErr = opts.connectorInputs?.({ key: spec.key, action: spec.action, version: spec.version, object: spec.object }, spec.inputs ?? {});
|
|
334
|
+
if (inputErr) {
|
|
335
|
+
report({ level: 'error', code: 'CONNECTOR_INPUT', message: `${where}: ${inputErr}` });
|
|
336
|
+
}
|
|
337
|
+
checkBindingLabels(where, spec, opts, (code, message) => report({ level: 'error', code, message }), { key: spec.key, action: spec.action }, id === undefined ? undefined : { id, connector: spec.key });
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The binding-label rules `serialize`'s `resolveBinding` enforces, for one
|
|
341
|
+
* `{ connection, folder }` pair — reported at every site that resolves one
|
|
342
|
+
* (connector step, event trigger and `.entryPoint` onEvent, waitForEvent, managed
|
|
343
|
+
* HTTP), with the sentence compile throws. Without this at the event and HTTP
|
|
344
|
+
* sites `check` printed `✓ no issues` for a source `compile` then refused.
|
|
345
|
+
*
|
|
346
|
+
* Silent without a bindings.json, and for a label that resolves to anything
|
|
347
|
+
* other than its own name (stub values are compile's CONNECTION_STUB warning).
|
|
348
|
+
*/
|
|
349
|
+
export function checkBindingLabels(where, labels, opts, report, target,
|
|
350
|
+
/** The activity, and the connector its connection must belong to, for {@link connectionConnectorConflicts}. */
|
|
351
|
+
use) {
|
|
352
|
+
const label = labels?.connection;
|
|
353
|
+
if (use?.connector && typeof label === 'string' && label !== '') {
|
|
354
|
+
connectionUsage?.push({ id: use.id, label, connector: use.connector });
|
|
355
|
+
}
|
|
356
|
+
const bindings = opts.bindings;
|
|
357
|
+
if (!bindings || !labels)
|
|
358
|
+
return;
|
|
359
|
+
for (const attribute of ['connection', 'folder']) {
|
|
360
|
+
const name = labels[attribute];
|
|
361
|
+
if (typeof name !== 'string')
|
|
362
|
+
continue;
|
|
363
|
+
const resolved = bindings.resolve(name);
|
|
364
|
+
if (resolved === name) {
|
|
365
|
+
report('BINDING_SELF_NAME', bindingSelfNameMessage(where, name, attribute, target));
|
|
366
|
+
}
|
|
367
|
+
else if (resolved === undefined) {
|
|
368
|
+
const declared = undeclaredBinding(bindings, name);
|
|
369
|
+
if (declared) {
|
|
370
|
+
report('BINDING_UNDECLARED', bindingUndeclaredMessage(where, name, attribute, declared, target));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The two phrases every lookup diagnostic needs: what the field sends, and how
|
|
377
|
+
* to resolve it.
|
|
378
|
+
*
|
|
379
|
+
* The `call` names BOTH authoring forms, and names which one the alias belongs
|
|
380
|
+
* to. Printing `lookup(<Descriptor>, 'entityName').byDisplayName('<value>')`
|
|
381
|
+
* alone — the old text — was a remedy with an unstated precondition: aliases
|
|
382
|
+
* are generated onto the descriptor, and `<Descriptor>` is a placeholder the
|
|
383
|
+
* reader has to fill from somewhere. An author holding the connector key and
|
|
384
|
+
* action instead (the form the library's Generic CRUD half is authored in, and
|
|
385
|
+
* the majority of its lookup fields) substitutes those for the placeholder,
|
|
386
|
+
* keeps the alias, and gets `lookup(...).byDisplayName is not a function`. Six
|
|
387
|
+
* flow-eval tasks did precisely that, each losing a check-edit-check round to
|
|
388
|
+
* a message that had told them to.
|
|
389
|
+
*/
|
|
390
|
+
export function lookupPhrases(field, lookup, spec, descriptors = true) {
|
|
391
|
+
const target = `${lookup.objectName ?? lookup.path}.${lookup.value}`;
|
|
392
|
+
const by = lookup.by[0] ?? 'id';
|
|
393
|
+
const alias = Object.keys(lookup.aliases)[0];
|
|
394
|
+
// The key/action form first: it needs no import, so it is the one line that
|
|
395
|
+
// is paste-ready from the message alone.
|
|
396
|
+
const generic = `lookup('${spec.key}', '${spec.action}', '${field}').by('${by}', '<value>')`;
|
|
397
|
+
const call = alias && descriptors
|
|
398
|
+
? `${generic}\n or \`lookup(D, '${field}').${alias}('<value>')\`, where D is the `
|
|
399
|
+
+ `descriptor \`${prepareCommand(spec.key, spec.action)}\` generates — the aliases exist only there`
|
|
400
|
+
: generic;
|
|
401
|
+
return { target, call };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* `senderEmail`, `emailAddress`, `e_mail`, `userPrincipalName` — an address. Applied
|
|
405
|
+
* to the LEAF an expression reads, and to the lookup's own `value` / `by` names,
|
|
406
|
+
* because whether an address is wrong depends on what the field sends: SendGrid's
|
|
407
|
+
* `from` sends `from_email`, PagerDuty's `From` sends `email` — there an e-mail IS
|
|
408
|
+
* the id. `emailId` is left to the id rule.
|
|
409
|
+
*/
|
|
410
|
+
const EMAIL_LEAF = /e[-_]?mail|principal/i;
|
|
411
|
+
/**
|
|
412
|
+
* `id`, `accountId`, `channelKey`, `record_id`, `CHANNEL_KEY` — the author is
|
|
413
|
+
* already naming an id. Case-sensitive on purpose: `valid`, `paid` and `monkey`
|
|
414
|
+
* end in the same letters and are not ids.
|
|
415
|
+
*/
|
|
416
|
+
const ID_LEAF = /(?:^|[a-z0-9])(?:Id|ID|Key|KEY)$|(?:^|_)(?:id|key|ID|KEY|Id|Key)$/;
|
|
417
|
+
/**
|
|
418
|
+
* The lookup rule for a RUNTIME value.
|
|
419
|
+
*
|
|
420
|
+
* `check` cannot evaluate `$vars.start.output.senderEmail`, but it does know what
|
|
421
|
+
* the field it lands in must carry — `reporter.accountId` — because the descriptor
|
|
422
|
+
* says so. That is the fact the author of the 09-01 escalation flow never saw: the
|
|
423
|
+
* generated type read `id: ConnectorLookupValue<string>`, the field was required,
|
|
424
|
+
* the only person-shaped input in scope was an e-mail, and nothing between the
|
|
425
|
+
* source and the tenant said an address is not an account id. Jira did, at run
|
|
426
|
+
* time, as `[102003] Integration Services bad request`.
|
|
427
|
+
*
|
|
428
|
+
* Three tiers, decided by the LEAF the expression reads, because that name is
|
|
429
|
+
* the only signal available before the process runs:
|
|
430
|
+
* - id-shaped (`…Id`, `…Key`, or the lookup's own `value`): silence. The author
|
|
431
|
+
* has said it is an id; a mistaken id is `LOOKUP_LITERAL_ID`'s move-between-
|
|
432
|
+
* connections problem, not this one.
|
|
433
|
+
* - e-mail-shaped, into a lookup that neither SENDS an e-mail (`value`) nor
|
|
434
|
+
* SEARCHES by one (`by`): an ERROR. Jira's reporter (`accountId`, by
|
|
435
|
+
* displayName/accountId) is the measured case — the provider rejects the
|
|
436
|
+
* address, and stopping here is cheaper than a faulted run. Into a lookup
|
|
437
|
+
* whose `value` is itself an e-mail (SendGrid `from` → `from_email`, PagerDuty
|
|
438
|
+
* `From` → `email`, 19 fields in the library): silence — the address is the
|
|
439
|
+
* id. Into one that only searches by e-mail (ActiveCampaign `contact` → `id`,
|
|
440
|
+
* by `email`, 139 fields): a WARNING — the provider indexes people by address,
|
|
441
|
+
* so some accept it in the id slot and some do not; the reader decides.
|
|
442
|
+
* - anything else (a name, a whole output object, a concatenation): a WARNING
|
|
443
|
+
* that states the identity, so the reader can decide.
|
|
444
|
+
*/
|
|
445
|
+
function runtimeLookupDiagnostic(where, spec, field, lookup, js, dialect) {
|
|
446
|
+
const refs = extractRefs(js, dialect.namespace);
|
|
447
|
+
const single = refs.length === 1 && js.trim() === refs[0].raw ? refs[0] : undefined;
|
|
448
|
+
const leaf = single ? (single.path.length ? single.path[single.path.length - 1] : single.root) : undefined;
|
|
449
|
+
// The lookup's value may be dotted (`properties.email`): compare against its last segment too.
|
|
450
|
+
const valueLeaf = lookup.value.split('.').pop() ?? lookup.value;
|
|
451
|
+
const valueIsEmail = EMAIL_LEAF.test(lookup.value);
|
|
452
|
+
if (leaf && (ID_LEAF.test(leaf)
|
|
453
|
+
|| leaf.toLowerCase() === lookup.value.toLowerCase()
|
|
454
|
+
|| leaf.toLowerCase() === valueLeaf.toLowerCase()
|
|
455
|
+
|| (valueIsEmail && EMAIL_LEAF.test(leaf))))
|
|
456
|
+
return undefined;
|
|
457
|
+
const { target, call } = lookupPhrases(field, lookup, spec, dialect.descriptors);
|
|
458
|
+
const searchesByEmail = lookup.by.some((b) => EMAIL_LEAF.test(b));
|
|
459
|
+
const isEmail = !!leaf && EMAIL_LEAF.test(leaf) && !valueIsEmail && !searchesByEmail;
|
|
460
|
+
const message = `${where}: \`${field}\` is a lookup field bound to the runtime expression `
|
|
461
|
+
+ `\`${js}\`. At run time it must carry a \`${target}\` — the id the connector stores — `
|
|
462
|
+
+ `not a name or an address: the connector rejects anything else when the flow runs, and `
|
|
463
|
+
+ `neither compile nor validate can tell. Resolve the value you know instead:\n`
|
|
464
|
+
+ ` ${call}\n`
|
|
465
|
+
+ `then record it once with \`${prepareCommand(spec.key, spec.action)} `
|
|
466
|
+
+ `--resolve ${resolveFlagArg(field, lookup.by[0] ?? 'id', '<value>')}\`; or read the id from a step whose `
|
|
467
|
+
+ `output returned one (\`${lookup.value}\`).`
|
|
468
|
+
+ (isEmail
|
|
469
|
+
? `\n\`${single.raw}\` reads \`${leaf}\`, an e-mail address, not a \`${target}\`.`
|
|
470
|
+
: '');
|
|
471
|
+
return { level: isEmail ? 'error' : 'warning', code: 'LOOKUP_RUNTIME_VALUE', message };
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* The connection uses {@link checkBindingLabels} saw during the current check —
|
|
475
|
+
* the model and every child model, which share one `bindings[]`. Module state
|
|
476
|
+
* rather than a `ConnectorCheckOpts` field because a check is synchronous and
|
|
477
|
+
* this is not an input; {@link collectConnectionUses} saves and restores it, so
|
|
478
|
+
* a nested check cannot mix two models.
|
|
479
|
+
*/
|
|
480
|
+
let connectionUsage;
|
|
481
|
+
/** Run `check`, and hand back the connection uses its binding-label rules saw. */
|
|
482
|
+
export function collectConnectionUses(check) {
|
|
483
|
+
const outer = connectionUsage;
|
|
484
|
+
connectionUsage = [];
|
|
485
|
+
try {
|
|
486
|
+
check();
|
|
487
|
+
return connectionUsage;
|
|
488
|
+
}
|
|
489
|
+
finally {
|
|
490
|
+
connectionUsage = outer;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* CONNECTION_CONNECTOR_CONFLICT — the rule `serialize` enforces on the emitted
|
|
495
|
+
* document (`connectionConnectors`, bindings-v2.ts), reported here with the same
|
|
496
|
+
* sentence so `check` does not print `✓ no issues` for a source `compile` then
|
|
497
|
+
* refuses. Labels are grouped by what they resolve to, so two labels bound to
|
|
498
|
+
* one connection id are one connection, as they are in the artifact.
|
|
499
|
+
*/
|
|
500
|
+
export function connectionConnectorConflicts(uses, opts) {
|
|
501
|
+
const byConnection = new Map();
|
|
502
|
+
for (const use of uses) {
|
|
503
|
+
const key = opts.bindings?.resolve(use.label) ?? use.label;
|
|
504
|
+
const byConnector = byConnection.get(key) ?? new Map();
|
|
505
|
+
byConnection.set(key, byConnector);
|
|
506
|
+
byConnector.set(use.connector, [...(byConnector.get(use.connector) ?? []), use.id]);
|
|
507
|
+
}
|
|
508
|
+
const out = [];
|
|
509
|
+
for (const [key, byConnector] of byConnection) {
|
|
510
|
+
if (byConnector.size < 2)
|
|
511
|
+
continue;
|
|
512
|
+
out.push({
|
|
513
|
+
id: [...byConnector.values()][1][0],
|
|
514
|
+
message: connectionConnectorConflictMessage(key, byConnector),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/connector-inputs — a connector operation's authored inputs, validated
|
|
3
|
+
* against its declared field list and ROUTED to the wire slots the platform reads.
|
|
4
|
+
*
|
|
5
|
+
* Shared by every family that authors a connector activity. It was Flow's, inside
|
|
6
|
+
* `serialize.ts`, and BPMN had a fourteen-line substitute: an input the library
|
|
7
|
+
* did not declare went silently to the body, a missing required field was never
|
|
8
|
+
* reported, and a `lookup()` token was refused outright because nothing here
|
|
9
|
+
* read `connectors-local/resolutions.json`. That last one is why
|
|
10
|
+
* `skill-bpmn-paginated-reference-lookup` could resolve a Slack channel id and
|
|
11
|
+
* then emit a process that did not carry it.
|
|
12
|
+
*
|
|
13
|
+
* Two things differ between families and are injected rather than assumed:
|
|
14
|
+
*
|
|
15
|
+
* - `dialect` — which values are opaque (a Flow `Expr`, a BPMN `=`-expression).
|
|
16
|
+
* An opaque value is not type-checked against the field's declared type,
|
|
17
|
+
* because nothing here can evaluate it.
|
|
18
|
+
* - `render` — how an authored value becomes a wire value. Flow runs `renderDeep`
|
|
19
|
+
* to lower `Expr` objects into `$vars.…` source; BPMN's values are already
|
|
20
|
+
* plain, so it passes them through.
|
|
21
|
+
*
|
|
22
|
+
* The ROUTING is the library's business either way: a field's location is a
|
|
23
|
+
* property of the operation, not of the family emitting it.
|
|
24
|
+
*/
|
|
25
|
+
import { type LookupResolutions, type LookupSpec } from './lookups.js';
|
|
26
|
+
import type { MultipartPart, ResolvedConnector } from './library.js';
|
|
27
|
+
import type { ExprDialect } from './connector-checks.js';
|
|
28
|
+
/** How one family's values reach the wire. */
|
|
29
|
+
export interface ConnectorInputOpts {
|
|
30
|
+
/** Which values are opaque, and so exempt from the declared-type check. */
|
|
31
|
+
dialect: ExprDialect;
|
|
32
|
+
/** An authored value → its wire value. Identity when the family needs no lowering. */
|
|
33
|
+
render?: (value: unknown) => unknown;
|
|
34
|
+
/** Everything `registry prepare --resolve` has recorded in the local overlay. */
|
|
35
|
+
resolutions?: LookupResolutions;
|
|
36
|
+
/** The operation's generated lookup table, keyed by field — for the missing-required hint. */
|
|
37
|
+
lookups?: Readonly<Record<string, LookupSpec>>;
|
|
38
|
+
/** `ignore` skips an unresolved lookup instead of refusing it — what `check` wants. */
|
|
39
|
+
unresolvedLookups?: 'error' | 'ignore';
|
|
40
|
+
}
|
|
41
|
+
/** Flow's dialect, re-exported so `serialize.ts` need not reach past this module. */
|
|
42
|
+
export { FLOW_EXPR_DIALECT } from './connector-checks.js';
|
|
43
|
+
/**
|
|
44
|
+
* What `compile` would refuse these connector inputs with, or `undefined` when it
|
|
45
|
+
* would accept them — the SAME code path `buildConnectorInputs` runs at emit time,
|
|
46
|
+
* so `check` and `compile` cannot disagree about a missing required field, an
|
|
47
|
+
* unknown or mistyped input, or a container part passed as a value. Unresolved
|
|
48
|
+
* lookups are the one thing skipped: `check` already reports them as
|
|
49
|
+
* LOOKUP_UNRESOLVED with the resolution recipe.
|
|
50
|
+
*/
|
|
51
|
+
export declare function connectorInputErrors(rc: ResolvedConnector, inputs: Record<string, unknown>, resolutions?: LookupResolutions, lookups?: Readonly<Record<string, LookupSpec>>, wire?: Pick<ConnectorInputOpts, 'dialect' | 'render'>): string | undefined;
|
|
52
|
+
export declare function buildConnectorInputs(rc: any, inputs: Record<string, unknown>, opts: ConnectorInputOpts): ConnectorSlots;
|
|
53
|
+
/**
|
|
54
|
+
* `inputMetadata.multipart.bodyFieldName` names the CONTAINER part — the one the
|
|
55
|
+
* runtime composes from `bodyParameters`. Measured on the designer's own nodes in
|
|
56
|
+
* the eval archive: `body` ×413, `RagRequest` ×1 (GenAI `content-generation`, whose
|
|
57
|
+
* string part is named that), and `body` on 91 of 93 file-only operations. A
|
|
58
|
+
* mis-named container is fatal at dispatch — hardcoding `body` for GenAI made
|
|
59
|
+
* Integration Service answer `400 The request body is empty` (verified live
|
|
60
|
+
* 2026-09-02) — so it is the string part's own name, with `body` only as the
|
|
61
|
+
* default for an operation that has no string part.
|
|
62
|
+
*/
|
|
63
|
+
export declare function multipartBodyFieldName(parts: MultipartPart[]): string;
|
|
64
|
+
/** The `inputs.detail` slots a connector's inputs are routed to, by field location. */
|
|
65
|
+
interface ConnectorSlots {
|
|
66
|
+
bodyParameters?: Record<string, unknown>;
|
|
67
|
+
pathParameters?: Record<string, unknown>;
|
|
68
|
+
queryParameters?: Record<string, unknown>;
|
|
69
|
+
multipartParameters?: Array<{
|
|
70
|
+
name: string;
|
|
71
|
+
dataType: string;
|
|
72
|
+
value?: unknown;
|
|
73
|
+
}>;
|
|
74
|
+
inputMetadata?: {
|
|
75
|
+
type: 'multipart';
|
|
76
|
+
multipart: {
|
|
77
|
+
bodyFieldName: string;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
}
|